From a6bf2169a8a09ec115610b4f9199c029f125d2eb Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Sat, 3 Feb 2024 15:22:35 -0500 Subject: [PATCH 01/10] feat(authenticator): Added initial client authenticator generation --- Cargo.toml | 3 + src/webauthn/authenticator/mod.rs | 267 ++++++++++++++++++++++++ src/webauthn/authenticator/responses.rs | 6 + src/webauthn/mod.rs | 1 + src/webauthn/proto/constants.rs | 2 + src/webauthn/proto/raw_message.rs | 197 ++++++++++++++++- src/webauthn/proto/web_message.rs | 3 +- src/webauthn/server/mod.rs | 28 ++- 8 files changed, 497 insertions(+), 10 deletions(-) create mode 100644 src/webauthn/authenticator/mod.rs create mode 100644 src/webauthn/authenticator/responses.rs diff --git a/Cargo.toml b/Cargo.toml index 9486c43..b740457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ base32 = "0.4" hex = "0.4" rsa = "0.9.2" x509-parser = "0.15.0" +ed25519-dalek = { version = "2.1.0", features = ["rand_core"] } +p256 = "0.13.2" +rand = "0.8.5" base64 = { version = "0.13", optional = true } byteorder = { version = "1.4", optional = true } diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs new file mode 100644 index 0000000..41fa948 --- /dev/null +++ b/src/webauthn/authenticator/mod.rs @@ -0,0 +1,267 @@ +mod responses; + +use crate::webauthn::{ + authenticator::responses::AuthenticatorCredentialCreationResponse, + error::Error, + proto::{ + constants::{ECDAA_CURVE_ED25519, ECDSA_CURVE_P256, WEBAUTHN_FORMAT_NONE, WEBAUTHN_REQUEST_TYPE_CREATE}, + raw_message::{ + AttestationFlags, AttestationObject, AttestedCredentialData, AuthenticatorData, Coordinates, CoseAlgorithmIdentifier, + CoseKeyInfo, CredentialPublicKey, Message, Rsa, EC2, OKP, + }, + web_message::{ + AuthenticatorAttestationResponse, CollectedClientData, PublicKeyCredential, PublicKeyCredentialCreationOptions, + PublicKeyCredentialParameters, UserVerificationRequirement, + }, + }, +}; +use hmac::digest::Digest; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use rand::rngs::OsRng; +use rsa::traits::{PrivateKeyParts, PublicKeyParts}; +use serde_cbor::Value; +use sha2::Sha256; +use uuid::Uuid; + +#[cfg(test)] +use crate::webauthn::{ + proto::web_message::{PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity}, + server::CredentialCreationVerifier, +}; + +//String Reprensentation of the AAGUID: DE503f9-c21a-4f76-b4b7-558eb55c6f89 +pub const AAGUID: Uuid = Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89); + +#[derive(Debug)] +pub enum WebauthnCredentialCreationError { + UserVerificationRequired, + AlgorithmNotSupported, + CouldNotGenerateKey, + RpIdOrOriginRequired, + RpIdHashInvalidLength(usize), + SerdeError(serde_json::Error), + WebauthnError(Error), +} + +impl From for WebauthnCredentialCreationError { + fn from(e: serde_json::Error) -> Self { + WebauthnCredentialCreationError::SerdeError(e) + } +} + +impl From for WebauthnCredentialCreationError { + fn from(e: Error) -> Self { + WebauthnCredentialCreationError::WebauthnError(e) + } +} + +pub struct WebauthnAuthenticator; + +impl WebauthnAuthenticator { + pub fn generate_credential_response( + credential_creation_options: PublicKeyCredentialCreationOptions, + connection_id: String, + origin: Option, + attestation_flags: Vec, + ) -> Result { + if credential_creation_options + .authenticator_selection + .as_ref() + .and_then(|auth_selection| auth_selection.user_verification.as_ref()) + .filter(|user_verif| *user_verif == &UserVerificationRequirement::Required) + .is_some() + && !attestation_flags.contains(&AttestationFlags::UserVerified) + { + return Err(WebauthnCredentialCreationError::UserVerificationRequired); + } + + let rp_id = credential_creation_options + .rp + .id + .as_ref() + .or(origin.as_ref()) + .ok_or(WebauthnCredentialCreationError::RpIdOrOriginRequired)?; + let mut hasher = Sha256::new(); + hasher.update(rp_id); + + let alg = Self::find_best_supported_algorithm(&credential_creation_options.pub_key_cred_params)?; + let (key_info, private_key) = match alg { + CoseAlgorithmIdentifier::Ed25519 => { + let keypair = ed25519_dalek::SigningKey::generate(&mut OsRng); + let bytes = keypair.verifying_key().to_bytes(); + ( + CoseKeyInfo::OKP(OKP { + curve: ECDAA_CURVE_ED25519, + coords: Coordinates::Compressed { + y: if bytes[31] & 1 == 0 { 0x02 } else { 0x03 }, + x: bytes, + }, + }), + base64::encode(keypair.to_bytes()), + ) + } + + CoseAlgorithmIdentifier::EC2 => { + let secret_key = p256::SecretKey::random(&mut OsRng); + let keypair = secret_key.public_key().to_encoded_point(false); + let y = keypair.y().ok_or(WebauthnCredentialCreationError::CouldNotGenerateKey)?; + let x = keypair.x().ok_or(WebauthnCredentialCreationError::CouldNotGenerateKey)?; + ( + CoseKeyInfo::EC2(EC2 { + curve: ECDSA_CURVE_P256, + coords: Coordinates::Uncompressed { + y: (*y).into(), + x: (*x).into(), + }, + }), + base64::encode(keypair.to_bytes()), + ) + } + + CoseAlgorithmIdentifier::RSA | CoseAlgorithmIdentifier::RS1 => { + let key = rsa::RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| WebauthnCredentialCreationError::CouldNotGenerateKey)?; + ( + CoseKeyInfo::RSA(Rsa { + n: key.n().to_bytes_be(), + e: key.e().to_bytes_be(), + }), + base64::encode(key.d().to_bytes_be()), + ) + } + + _ => return Err(WebauthnCredentialCreationError::AlgorithmNotSupported), + }; + + let attested_credential_data = if attestation_flags.contains(&AttestationFlags::AttestedCredentialDataIncluded) { + Some(AttestedCredentialData { + aaguid: AAGUID.into_bytes(), + credential_id: connection_id.clone().into_bytes(), + credential_public_key: CredentialPublicKey { + key_type: key_info.key_type(), + alg: alg.into(), + key_info, + }, + }) + } else { + None + }; + + let attestation_object = AttestationObject { + auth_data: AuthenticatorData { + rp_id_hash: hasher + .finalize_reset() + .to_vec() + .try_into() + .map_err(|e: Vec| WebauthnCredentialCreationError::RpIdHashInvalidLength(e.len()))?, + flags: attestation_flags.into_iter().map(|f| f as u8).sum(), + sign_count: 0, + attested_credential_data, + extensions: Value::Null, + }, + raw_auth_data: vec![], + fmt: WEBAUTHN_FORMAT_NONE.to_owned(), + att_stmt: None, + } + .to_bytes()?; + + let collected_client_data = CollectedClientData { + request_type: WEBAUTHN_REQUEST_TYPE_CREATE.to_owned(), + challenge: credential_creation_options.challenge, + origin: origin.as_ref().unwrap_or_else(|| &rp_id).clone(), + cross_origin: false, + token_binding: None, + }; + + let credential = PublicKeyCredential { + id: connection_id, + response: Some(AuthenticatorAttestationResponse { + attestation_object: Some(base64::encode(attestation_object)), + client_data_json: base64::encode(serde_json::to_string(&collected_client_data)?.into_bytes()), + authenticator_data: None, + signature: None, + user_handle: None, + }), + }; + + Ok(AuthenticatorCredentialCreationResponse { + credential_response: credential, + private_key, + }) + } + + fn find_best_supported_algorithm( + pub_key_cred_params: &Vec, + ) -> Result { + //Order of preference for credential type is: Ed25519 > EC2 > RSA > RS1 + let mut possible_credential_types = vec![ + CoseAlgorithmIdentifier::RS1, + CoseAlgorithmIdentifier::RSA, + CoseAlgorithmIdentifier::EC2, + CoseAlgorithmIdentifier::Ed25519, + ]; + + let mut best_alg_index = None; + while let Some(param) = pub_key_cred_params.iter().next() { + if let Some(alg_index) = possible_credential_types + .iter() + .position(|r| *r == CoseAlgorithmIdentifier::from(param.alg)) + { + if best_alg_index.filter(|x| x < &alg_index).is_none() { + best_alg_index = Some(alg_index); + } + + if alg_index == possible_credential_types.len() - 1 { + break; + } + } + } + + match best_alg_index { + None => Err(WebauthnCredentialCreationError::AlgorithmNotSupported), + Some(index) => Ok(possible_credential_types.remove(index)), + } + } +} + +#[test] +fn test_credential_generation() { + let option = PublicKeyCredentialCreationOptions { + challenge: "test".to_owned(), + rp: PublicKeyCredentialRpEntity { + id: Some("localhost".to_owned()), + name: "localhost".to_owned(), + icon: None, + }, + user: PublicKeyCredentialUserEntity { + id: Uuid::new_v4().to_string(), + name: "test".to_owned(), + display_name: "test".to_owned(), + icon: None, + }, + pub_key_cred_params: vec![PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::Ed25519.into(), + }], + timeout: None, + exclude_credentials: vec![], + authenticator_selection: None, + attestation: None, + extensions: None, + }; + let credential = WebauthnAuthenticator::generate_credential_response( + option.clone(), + Uuid::new_v4().to_string(), + Some("http://localhost".to_owned()), + vec![AttestationFlags::AttestedCredentialDataIncluded, AttestationFlags::UserPresent], + ); + + match credential { + Ok(cred) => { + let mut verifier = CredentialCreationVerifier::new(cred.credential_response, option, "http://localhost"); + assert_eq!(dbg!(verifier.verify()).is_ok(), true) + } + Err(e) => { + panic!("{e:?}") + } + } +} diff --git a/src/webauthn/authenticator/responses.rs b/src/webauthn/authenticator/responses.rs new file mode 100644 index 0000000..425309e --- /dev/null +++ b/src/webauthn/authenticator/responses.rs @@ -0,0 +1,6 @@ +use crate::webauthn::proto::web_message::PublicKeyCredential; + +pub struct AuthenticatorCredentialCreationResponse { + pub credential_response: PublicKeyCredential, + pub private_key: String, +} diff --git a/src/webauthn/mod.rs b/src/webauthn/mod.rs index f95fcf6..9ae7f07 100644 --- a/src/webauthn/mod.rs +++ b/src/webauthn/mod.rs @@ -1,3 +1,4 @@ +pub mod authenticator; pub mod error; pub mod proto; pub mod server; diff --git a/src/webauthn/proto/constants.rs b/src/webauthn/proto/constants.rs index b58aa74..06fd771 100644 --- a/src/webauthn/proto/constants.rs +++ b/src/webauthn/proto/constants.rs @@ -15,6 +15,7 @@ pub const WEBAUTHN_FORMAT_ANDROID_SAFETYNET: &str = "android-safetynet"; pub const WEBAUTHN_FORMAT_ANDROID_KEY: &str = "android-key"; pub const WEBAUTHN_FORMAT_TPM: &str = "tpm"; +pub const WEBAUTH_PUBLIC_KEY_TYPE_OKP: i64 = 1; pub const WEBAUTH_PUBLIC_KEY_TYPE_EC2: i64 = 2; pub const WEBAUTH_PUBLIC_KEY_TYPE_RSA: i64 = 3; @@ -28,6 +29,7 @@ pub const ECDSA_Y_PREFIX_UNCOMPRESSED: u8 = 4; pub const ECDSA_CURVE_P256: i64 = 1; pub const ECDSA_CURVE_P384: i64 = 2; pub const ECDSA_CURVE_P521: i64 = 3; +pub const ECDAA_CURVE_ED25519: i64 = 6; pub const TPM_GENERATED_VALUE: u32 = 0xff544347; // https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation diff --git a/src/webauthn/proto/raw_message.rs b/src/webauthn/proto/raw_message.rs index 879bbee..95809b5 100644 --- a/src/webauthn/proto/raw_message.rs +++ b/src/webauthn/proto/raw_message.rs @@ -4,18 +4,18 @@ use crate::webauthn::{ constants::{ ECDSA_Y_PREFIX_NEGATIVE, ECDSA_Y_PREFIX_POSITIVE, ECDSA_Y_PREFIX_UNCOMPRESSED, WEBAUTHN_FORMAT_ANDROID_KEY, WEBAUTHN_FORMAT_ANDROID_SAFETYNET, WEBAUTHN_FORMAT_FIDO_U2F, WEBAUTHN_FORMAT_NONE, WEBAUTHN_FORMAT_PACKED, WEBAUTHN_FORMAT_TPM, - WEBAUTH_PUBLIC_KEY_TYPE_EC2, WEBAUTH_PUBLIC_KEY_TYPE_RSA, + WEBAUTH_PUBLIC_KEY_TYPE_EC2, WEBAUTH_PUBLIC_KEY_TYPE_OKP, WEBAUTH_PUBLIC_KEY_TYPE_RSA, }, tpm::TPM, }, }; use byteorder::{BigEndian, ReadBytesExt}; use bytes::Buf; -use serde_cbor::Value; +use serde_cbor::{to_vec, Value}; use serde_derive::*; use std::{ collections::BTreeMap, - io::{Cursor, Read}, + io::{Cursor, Read, Write}, str::FromStr, }; @@ -83,6 +83,19 @@ pub enum AttestationStatement { None, } +impl AttestationStatement { + pub fn to_cbor(self) -> Result { + match self { + AttestationStatement::Packed(value) => serde_cbor::value::to_value(&value).map_err(Error::CborError), + AttestationStatement::TPM(value) => serde_cbor::value::to_value(&value).map_err(Error::CborError), + AttestationStatement::FidoU2F(value) => serde_cbor::value::to_value(&value).map_err(Error::CborError), + AttestationStatement::AndroidKey(value) => serde_cbor::value::to_value(&value).map_err(Error::CborError), + AttestationStatement::AndroidSafetynet(value) => serde_cbor::value::to_value(&value).map_err(Error::CborError), + AttestationStatement::None => Ok(Value::Map(BTreeMap::new())), + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct AuthenticatorData { @@ -102,7 +115,7 @@ impl AuthenticatorData { let flags = cursor.read_u8()?; - let sign_count = cursor.read_u32::()?; + let sign_count = dbg!(cursor.read_u32::())?; let attested_credential_data = if cursor.remaining() > 16 { let mut aaguid = [0u8; 16]; @@ -140,6 +153,22 @@ impl AuthenticatorData { cursor.into_inner(), )) } + + pub fn to_vec(self) -> Result, Error> { + let mut vec = vec![]; + vec.write(&self.rp_id_hash)?; + vec.push(self.flags); + vec.write(&self.sign_count.to_be_bytes())?; + + if let Some(att_cred_data) = self.attested_credential_data { + vec.write(&att_cred_data.aaguid)?; + vec.write(&(att_cred_data.credential_id.len() as u16).to_be_bytes())?; + vec.write(&att_cred_data.credential_id)?; + vec.write(&att_cred_data.credential_public_key.to_bytes()?)?; + } + + Ok(vec) + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -169,12 +198,29 @@ pub struct EC2 { pub coords: Coordinates, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct OKP { + pub curve: i64, + pub coords: Coordinates, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub enum CoseKeyInfo { + OKP(OKP), EC2(EC2), RSA(Rsa), } +impl CoseKeyInfo { + pub fn key_type(&self) -> i64 { + match self { + CoseKeyInfo::OKP(_) => 1, + CoseKeyInfo::EC2(_) => 2, + CoseKeyInfo::RSA(_) => 3, + } + } +} + impl CredentialPublicKey { pub fn from_value(value: serde_cbor::Value) -> Result { let map = match value { @@ -243,6 +289,50 @@ impl CredentialPublicKey { key_info: CoseKeyInfo::EC2(EC2 { curve, coords }), }) } + (WEBAUTH_PUBLIC_KEY_TYPE_OKP, CoseAlgorithmIdentifier::Ed25519) => { + let curve = map + .get(&Value::Integer(-1)) + .map(|val| match val { + Value::Integer(i) => *i as i64, + _ => 0i64, + }) + .ok_or_else(|| Error::Other("curve missing".to_string()))?; + + let x = map + .get(&Value::Integer(-2)) + .and_then(|val| match val { + Value::Bytes(i) => { + let mut array = [0u8; 32]; + array.copy_from_slice(&i[0..32]); + Some(array) + } + _ => None, + }) + .ok_or_else(|| Error::Other("x coordinate missing".to_string()))?; + + let coords = map + .get(&Value::Integer(-3)) + .and_then(|val| match val { + Value::Bytes(i) => { + let mut array = [0u8; 32]; + array.copy_from_slice(&i[0..32]); + Some(Coordinates::Uncompressed { x, y: array }) + } + + Value::Bool(b) => Some(Coordinates::Compressed { + x, + y: if *b { ECDSA_Y_PREFIX_NEGATIVE } else { ECDSA_Y_PREFIX_POSITIVE }, + }), + _ => None, + }) + .ok_or_else(|| Error::Other("y coordinate missing".to_string()))?; + + Ok(CredentialPublicKey { + key_type, + alg, + key_info: CoseKeyInfo::EC2(EC2 { curve, coords }), + }) + } (WEBAUTH_PUBLIC_KEY_TYPE_RSA, CoseAlgorithmIdentifier::RSA) => { let n = map .get(&Value::Integer(-1)) @@ -281,6 +371,59 @@ impl CredentialPublicKey { _ => Err(Error::Other("Cose key type not supported".to_owned())), } } + + pub fn to_bytes(self) -> Result, Error> { + let mut map = BTreeMap::new(); + match self.key_info { + CoseKeyInfo::EC2(value) => { + map.insert(Value::Integer(1), Value::Integer(WEBAUTH_PUBLIC_KEY_TYPE_EC2 as i128)); + map.insert(Value::Integer(3), Value::Integer(CoseAlgorithmIdentifier::EC2 as i128)); + map.insert(Value::Integer(-1), Value::Integer(value.curve as i128)); + match value.coords { + Coordinates::Compressed { x, y } => { + map.insert(Value::Integer(-2), Value::Bytes(x.to_vec())); + map.insert(Value::Integer(-3), Value::Bool(y == ECDSA_Y_PREFIX_NEGATIVE)); + } + + Coordinates::Uncompressed { x, y } => { + map.insert(Value::Integer(-2), Value::Bytes(x.to_vec())); + map.insert(Value::Integer(-3), Value::Bytes(y.to_vec())); + } + + Coordinates::None => { + return Err(Error::Other("Invalid coordinates".to_string())); + } + } + } + + CoseKeyInfo::OKP(value) => { + map.insert(Value::Integer(1), Value::Integer(WEBAUTH_PUBLIC_KEY_TYPE_OKP as i128)); + map.insert(Value::Integer(3), Value::Integer(CoseAlgorithmIdentifier::Ed25519 as i128)); + map.insert(Value::Integer(-1), Value::Integer(value.curve as i128)); + match value.coords { + Coordinates::Compressed { x, y } => { + map.insert(Value::Integer(-2), Value::Bytes(x.to_vec())); + map.insert(Value::Integer(-3), Value::Bool(y == ECDSA_Y_PREFIX_NEGATIVE)); + } + + Coordinates::Uncompressed { x, y } => { + map.insert(Value::Integer(-2), Value::Bytes(x.to_vec())); + map.insert(Value::Integer(-3), Value::Bytes(y.to_vec())); + } + + Coordinates::None => { + return Err(Error::Other("Invalid coordinates".to_string())); + } + } + } + + CoseKeyInfo::RSA(value) => { + map.insert(Value::Integer(-1), Value::Bytes(value.n)); + map.insert(Value::Integer(-2), Value::Bytes(value.e)); + } + }; + to_vec(&map).map_err(Error::CborError) + } } pub trait Message { @@ -290,6 +433,14 @@ pub trait Message { fn from_bytes(raw_values: &[u8]) -> Result where Self: Sized; + + fn to_bytes(self) -> Result, Error> + where + Self: Sized; + + fn to_base64(self) -> Result + where + Self: Sized; } impl Message for AttestationObject { @@ -347,6 +498,29 @@ impl Message for AttestationObject { att_stmt, }) } + + fn to_bytes(self) -> Result, Error> + where + Self: Sized, + { + let att_stmt = match self.att_stmt { + Some(v) => v.to_cbor()?, + None => Value::Null, + }; + + let mut att_obj = BTreeMap::new(); + att_obj.insert("authData".to_string(), Value::Bytes(self.auth_data.to_vec()?)); + att_obj.insert("fmt".to_string(), Value::Text(self.fmt)); + att_obj.insert("attStmt".to_string(), att_stmt); + to_vec(&att_obj).map_err(Error::CborError) + } + + fn to_base64(self) -> Result + where + Self: Sized, + { + Ok(base64::encode(Self::to_bytes(self)?)) + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -456,6 +630,7 @@ impl FromStr for Coordinates { #[derive(PartialEq, Debug)] pub enum CoseAlgorithmIdentifier { + Ed25519 = -8, EC2 = -7, RSA = -257, RS1 = -65535, @@ -468,6 +643,7 @@ impl From for CoseAlgorithmIdentifier { -65535 => CoseAlgorithmIdentifier::RS1, -257 => CoseAlgorithmIdentifier::RSA, -7 => CoseAlgorithmIdentifier::EC2, + -8 => CoseAlgorithmIdentifier::Ed25519, _ => CoseAlgorithmIdentifier::NotSupported, } } @@ -479,7 +655,20 @@ impl From for i64 { CoseAlgorithmIdentifier::RS1 => -65535, CoseAlgorithmIdentifier::RSA => -257, CoseAlgorithmIdentifier::EC2 => -7, + CoseAlgorithmIdentifier::Ed25519 => -8, _ => -65536, //Unassigned } } } + +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum AttestationFlags { + UserPresent = 1, + //Reserved for future = 2 + UserVerified = 4, + BackupEligible = 8, + BackedUp = 16, + //Reserved for future = 32 + AttestedCredentialDataIncluded = 64, + ExtensionDataIncluded = 128, +} diff --git a/src/webauthn/proto/web_message.rs b/src/webauthn/proto/web_message.rs index d499f60..d8e2590 100644 --- a/src/webauthn/proto/web_message.rs +++ b/src/webauthn/proto/web_message.rs @@ -161,8 +161,9 @@ pub struct CollectedClientData { pub request_type: String, pub challenge: String, pub origin: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub cross_origin: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub token_binding: Option, } diff --git a/src/webauthn/server/mod.rs b/src/webauthn/server/mod.rs index a3e1df8..4424e64 100644 --- a/src/webauthn/server/mod.rs +++ b/src/webauthn/server/mod.rs @@ -1,3 +1,4 @@ +use http::Uri; use ring::{ signature, signature::{UnparsedPublicKey, VerificationAlgorithm}, @@ -5,14 +6,14 @@ use ring::{ use rsa::pkcs1::{der::Encode, RsaPublicKey, UintRef}; use sha2::{Digest, Sha256}; use webpki::{EndEntityCert, SignatureAlgorithm}; -use http::Uri; use crate::webauthn::{ error::{CredentialError, Error}, proto::{ constants::{ ECDSA_CURVE_P256, ECDSA_CURVE_P384, ECDSA_Y_PREFIX_UNCOMPRESSED, WEBAUTHN_REQUEST_TYPE_CREATE, WEBAUTHN_REQUEST_TYPE_GET, - WEBAUTHN_USER_PRESENT_FLAG, WEBAUTHN_USER_VERIFIED_FLAG, WEBAUTH_PUBLIC_KEY_TYPE_EC2, WEBAUTH_PUBLIC_KEY_TYPE_RSA, + WEBAUTHN_USER_PRESENT_FLAG, WEBAUTHN_USER_VERIFIED_FLAG, WEBAUTH_PUBLIC_KEY_TYPE_EC2, WEBAUTH_PUBLIC_KEY_TYPE_OKP, + WEBAUTH_PUBLIC_KEY_TYPE_RSA, }, raw_message::{ AttestationObject, AttestationStatement, AuthenticatorData, Coordinates, CoseAlgorithmIdentifier, CoseKeyInfo, @@ -148,6 +149,7 @@ struct Rp { pub id: Option, } +#[derive(Debug)] pub struct CredentialCreationResult { pub public_key: CredentialPublicKey, pub sign_count: u32, @@ -250,7 +252,7 @@ impl CredentialCreationVerifier { if !matches!( attested_credential_data.credential_public_key.key_type, - WEBAUTH_PUBLIC_KEY_TYPE_EC2 | WEBAUTH_PUBLIC_KEY_TYPE_RSA + WEBAUTH_PUBLIC_KEY_TYPE_EC2 | WEBAUTH_PUBLIC_KEY_TYPE_RSA | WEBAUTH_PUBLIC_KEY_TYPE_OKP ) { return Err(Error::CredentialError(CredentialError::KeyType)); } @@ -557,6 +559,21 @@ impl CredentialRequestVerifier { let mut key = Vec::new(); match &self.credential_pub.key_info { + CoseKeyInfo::OKP(ed25519) => match ed25519.coords { + Coordinates::Compressed { x, y } => { + key.push(y); + key.append(&mut x.to_vec()); + } + + Coordinates::Uncompressed { x, y } => { + key.push(ECDSA_Y_PREFIX_UNCOMPRESSED); + key.append(&mut x.to_vec()); + key.append(&mut y.to_vec()); + } + + _ => return Err(Error::Other("Expected coordinates found nothing".to_owned())), + }, + CoseKeyInfo::EC2(ec2) => match ec2.coords { Coordinates::Compressed { x, y } => { key.push(y); @@ -629,7 +646,8 @@ fn guid_bytes_to_string(guid: &[u8; 16]) -> Option { } fn get_default_rp_id(origin: &str) -> String { - origin.parse::() + origin + .parse::() .ok() .and_then(|u| u.authority().map(|a| a.host().to_string())) .unwrap_or(origin.to_string()) @@ -643,4 +661,4 @@ fn test_default_rp_id() { assert_eq!(get_default_rp_id("http://login.example.com"), "login.example.com"); assert_eq!(get_default_rp_id("login.example.com:1337"), "login.example.com"); assert_eq!(get_default_rp_id("login.example.com"), "login.example.com"); -} \ No newline at end of file +} From 9c63990658ec2ae2e2e696dcee9e8f8d2b278d04 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Fri, 9 Feb 2024 14:47:30 -0500 Subject: [PATCH 02/10] WIP: Registration is now working --- Cargo.toml | 8 +- src/lib.rs | 2 +- src/wasm.rs | 44 +++- src/webauthn/authenticator/mod.rs | 275 +++++++++++++++++++----- src/webauthn/authenticator/responses.rs | 14 +- src/webauthn/error.rs | 8 + src/webauthn/mod.rs | 5 + src/webauthn/proto/raw_message.rs | 8 +- src/webauthn/proto/web_message.rs | 58 +++++ src/webauthn/server/mod.rs | 21 +- wasm/build.sh | 2 +- 11 files changed, 360 insertions(+), 85 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b740457..c8001c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slauth" -version = "0.7.4" +version = "0.7.5-beta.0" authors = ["richer ", "LucFauvel "] edition = "2021" description = "oath HOTP and TOTP complient implementation" @@ -20,7 +20,8 @@ default = ["u2f-server", "webauthn-server", "native-bindings"] native-bindings = [] u2f-server = ["u2f", "webpki"] u2f = ["auth-base", "untrusted", "serde_repr", ] -webauthn-server = [ "auth-base", "bytes", "serde_cbor", "http", "uuid" ] +webauthn-server = [ "webauthn", "webpki" ] +webauthn = [ "auth-base", "bytes", "serde_cbor", "uuid", "http" ] auth-base = ["base64", "byteorder", "ring", "serde", "serde_derive", "serde_json", "serde_bytes"] android = ["jni"] @@ -56,7 +57,7 @@ uuid = { version = "1.6", optional = true } jni = { version = "0.20", default-features = false, optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] -wasm-bindgen = { version = "0.2.60" } +wasm-bindgen = { version = "0.2.91" } js-sys = "0.3.37" # FIXME: https://docs.rs/getrandom/0.2.2/getrandom/#webassembly-support # let `getrandom` know that JavaScript is available for our targets @@ -64,6 +65,7 @@ js-sys = "0.3.37" # it will be compiled with it in our dependencies as well (since union of # all the features selected is used when building a Cargo project) getrandom = { version = "0.2", features = ["js"] } +serde-wasm-bindgen = "0.6.3" [target.'cfg(target_arch="wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.10" diff --git a/src/lib.rs b/src/lib.rs index 1b0ae48..b162f31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ pub mod oath; #[cfg(feature = "u2f")] pub mod u2f; -#[cfg(feature = "webauthn-server")] +#[cfg(feature = "webauthn")] pub mod webauthn; #[cfg(target_arch = "wasm32")] diff --git a/src/wasm.rs b/src/wasm.rs index a26fa0c..6998b3f 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,9 +1,13 @@ +use uuid::Uuid; use wasm_bindgen::prelude::*; -use crate::oath::{ - decode_hex_or_base_32, - totp::{TOTPBuilder, TOTPContext}, - HashesAlgorithm, OtpAuth, +use crate::{ + oath::{ + decode_hex_or_base_32, + totp::{TOTPBuilder, TOTPContext}, + HashesAlgorithm, OtpAuth, + }, + webauthn::{authenticator::WebauthnAuthenticator, proto::web_message::PublicKeyCredentialCreationOptions}, }; #[wasm_bindgen] @@ -74,3 +78,35 @@ impl Totp { self.inner.gen() } } + +#[cfg(feature = "webauthn")] +#[wasm_bindgen] +#[derive(Clone)] +pub struct PasskeyAuthenticator { + aaguid: Uuid, +} + +#[cfg(feature = "webauthn")] +#[wasm_bindgen] +impl PasskeyAuthenticator { + #[wasm_bindgen(constructor)] + pub fn new(aaguid: String) -> PasskeyAuthenticator { + let aaguid = Uuid::parse_str(aaguid.as_str()).expect("Failed to parse aaguid from string"); + PasskeyAuthenticator { aaguid } + } + + #[wasm_bindgen(js_name = "generateCredentialCreationResponse")] + pub fn generate_credential_creation_response( + &self, + options: JsValue, + connection_id: Vec, + attestation_flags: u8, + origin: Option, + ) -> Result { + let options: PublicKeyCredentialCreationOptions = serde_wasm_bindgen::from_value(options).map_err(|e| format!("{e:?}"))?; + let cred = + WebauthnAuthenticator::generate_credential_creation_response(options, self.aaguid, connection_id, origin, attestation_flags) + .map_err(|e| format!("{e:?}"))?; + serde_wasm_bindgen::to_value(&cred).map_err(|e| format!("{e:?}")) + } +} diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index 41fa948..5e61fc1 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -1,4 +1,4 @@ -mod responses; +pub(crate) mod responses; use crate::webauthn::{ authenticator::responses::AuthenticatorCredentialCreationResponse, @@ -10,85 +10,124 @@ use crate::webauthn::{ CoseKeyInfo, CredentialPublicKey, Message, Rsa, EC2, OKP, }, web_message::{ - AuthenticatorAttestationResponse, CollectedClientData, PublicKeyCredential, PublicKeyCredentialCreationOptions, - PublicKeyCredentialParameters, UserVerificationRequirement, + CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialParameters, UserVerificationRequirement, }, }, }; +use base64::URL_SAFE_NO_PAD; +use ed25519_dalek::{SignatureError, Signer}; use hmac::digest::Digest; use p256::elliptic_curve::sec1::ToEncodedPoint; use rand::rngs::OsRng; -use rsa::traits::{PrivateKeyParts, PublicKeyParts}; +use rsa::{pkcs1::EncodeRsaPrivateKey, traits::PublicKeyParts}; use serde_cbor::Value; use sha2::Sha256; use uuid::Uuid; +use crate::webauthn::{ + authenticator::responses::PrivateKeyResponse, + proto::{ + constants::WEBAUTHN_REQUEST_TYPE_GET, + web_message::{get_default_rp_id, AuthenticatorAttestationResponseRaw, PublicKeyCredentialRaw, PublicKeyCredentialRequestOptions}, + }, +}; #[cfg(test)] use crate::webauthn::{ proto::web_message::{PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity}, server::CredentialCreationVerifier, }; -//String Reprensentation of the AAGUID: DE503f9-c21a-4f76-b4b7-558eb55c6f89 -pub const AAGUID: Uuid = Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89); - #[derive(Debug)] -pub enum WebauthnCredentialCreationError { +pub enum WebauthnCredentialRequestError { UserVerificationRequired, AlgorithmNotSupported, CouldNotGenerateKey, RpIdOrOriginRequired, RpIdHashInvalidLength(usize), - SerdeError(serde_json::Error), + SerdeJsonError(serde_json::Error), + SerdeCborError(serde_cbor::Error), WebauthnError(Error), + RsaError(rsa::pkcs1::Error), + Base64Error(base64::DecodeError), + Ed25519Error(ed25519_dalek::SignatureError), } -impl From for WebauthnCredentialCreationError { +impl From for WebauthnCredentialRequestError { fn from(e: serde_json::Error) -> Self { - WebauthnCredentialCreationError::SerdeError(e) + WebauthnCredentialRequestError::SerdeJsonError(e) } } -impl From for WebauthnCredentialCreationError { +impl From for WebauthnCredentialRequestError { + fn from(e: serde_cbor::Error) -> Self { + WebauthnCredentialRequestError::SerdeCborError(e) + } +} + +impl From for WebauthnCredentialRequestError { fn from(e: Error) -> Self { - WebauthnCredentialCreationError::WebauthnError(e) + WebauthnCredentialRequestError::WebauthnError(e) + } +} + +impl From for WebauthnCredentialRequestError { + fn from(value: rsa::pkcs1::Error) -> Self { + WebauthnCredentialRequestError::RsaError(value) + } +} + +impl From for WebauthnCredentialRequestError { + fn from(value: base64::DecodeError) -> Self { + WebauthnCredentialRequestError::Base64Error(value) + } +} + +impl From for WebauthnCredentialRequestError { + fn from(value: SignatureError) -> Self { + WebauthnCredentialRequestError::Ed25519Error(value) } } pub struct WebauthnAuthenticator; impl WebauthnAuthenticator { - pub fn generate_credential_response( + pub fn generate_credential_creation_response( credential_creation_options: PublicKeyCredentialCreationOptions, - connection_id: String, + aaguid: Uuid, + connection_id: Vec, origin: Option, - attestation_flags: Vec, - ) -> Result { + attestation_flags: u8, + ) -> Result { if credential_creation_options .authenticator_selection .as_ref() .and_then(|auth_selection| auth_selection.user_verification.as_ref()) .filter(|user_verif| *user_verif == &UserVerificationRequirement::Required) .is_some() - && !attestation_flags.contains(&AttestationFlags::UserVerified) + && (attestation_flags & AttestationFlags::UserVerified as u8 == 0) { - return Err(WebauthnCredentialCreationError::UserVerificationRequired); + return Err(WebauthnCredentialRequestError::UserVerificationRequired); } + let binding = origin.as_ref().map(|o| get_default_rp_id(o.as_str())); let rp_id = credential_creation_options .rp .id .as_ref() - .or(origin.as_ref()) - .ok_or(WebauthnCredentialCreationError::RpIdOrOriginRequired)?; + .or(binding.as_ref()) + .ok_or(WebauthnCredentialRequestError::RpIdOrOriginRequired)?; let mut hasher = Sha256::new(); hasher.update(rp_id); let alg = Self::find_best_supported_algorithm(&credential_creation_options.pub_key_cred_params)?; - let (key_info, private_key) = match alg { + let (key_info, private_key_response) = match alg { CoseAlgorithmIdentifier::Ed25519 => { let keypair = ed25519_dalek::SigningKey::generate(&mut OsRng); let bytes = keypair.verifying_key().to_bytes(); + let private_key = PrivateKeyResponse { + private_key: keypair.to_bytes().to_vec(), + key_alg: alg.clone(), + }; ( CoseKeyInfo::OKP(OKP { curve: ECDAA_CURVE_ED25519, @@ -97,15 +136,19 @@ impl WebauthnAuthenticator { x: bytes, }, }), - base64::encode(keypair.to_bytes()), + base64::encode(serde_cbor::to_vec(&private_key)?), ) } CoseAlgorithmIdentifier::EC2 => { let secret_key = p256::SecretKey::random(&mut OsRng); let keypair = secret_key.public_key().to_encoded_point(false); - let y = keypair.y().ok_or(WebauthnCredentialCreationError::CouldNotGenerateKey)?; - let x = keypair.x().ok_or(WebauthnCredentialCreationError::CouldNotGenerateKey)?; + let y = keypair.y().ok_or(WebauthnCredentialRequestError::CouldNotGenerateKey)?; + let x = keypair.x().ok_or(WebauthnCredentialRequestError::CouldNotGenerateKey)?; + let private_key = PrivateKeyResponse { + private_key: secret_key.to_bytes().to_vec(), + key_alg: alg.clone(), + }; ( CoseKeyInfo::EC2(EC2 { curve: ECDSA_CURVE_P256, @@ -114,28 +157,32 @@ impl WebauthnAuthenticator { x: (*x).into(), }, }), - base64::encode(keypair.to_bytes()), + base64::encode(serde_cbor::to_vec(&private_key)?), ) } CoseAlgorithmIdentifier::RSA | CoseAlgorithmIdentifier::RS1 => { - let key = rsa::RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| WebauthnCredentialCreationError::CouldNotGenerateKey)?; + let key = rsa::RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| WebauthnCredentialRequestError::CouldNotGenerateKey)?; + let private_key = PrivateKeyResponse { + private_key: key.to_pkcs1_der()?.to_bytes().to_vec(), + key_alg: alg.clone(), + }; ( CoseKeyInfo::RSA(Rsa { n: key.n().to_bytes_be(), e: key.e().to_bytes_be(), }), - base64::encode(key.d().to_bytes_be()), + base64::encode(serde_cbor::to_vec(&private_key)?), ) } - _ => return Err(WebauthnCredentialCreationError::AlgorithmNotSupported), + _ => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), }; - let attested_credential_data = if attestation_flags.contains(&AttestationFlags::AttestedCredentialDataIncluded) { + let attested_credential_data = if attestation_flags & AttestationFlags::AttestedCredentialDataIncluded as u8 != 0 { Some(AttestedCredentialData { - aaguid: AAGUID.into_bytes(), - credential_id: connection_id.clone().into_bytes(), + aaguid: aaguid.into_bytes(), + credential_id: connection_id.clone(), credential_public_key: CredentialPublicKey { key_type: key_info.key_type(), alg: alg.into(), @@ -152,8 +199,8 @@ impl WebauthnAuthenticator { .finalize_reset() .to_vec() .try_into() - .map_err(|e: Vec| WebauthnCredentialCreationError::RpIdHashInvalidLength(e.len()))?, - flags: attestation_flags.into_iter().map(|f| f as u8).sum(), + .map_err(|e: Vec| WebauthnCredentialRequestError::RpIdHashInvalidLength(e.len()))?, + flags: attestation_flags, sign_count: 0, attested_credential_data, extensions: Value::Null, @@ -164,19 +211,21 @@ impl WebauthnAuthenticator { } .to_bytes()?; + let challenge = base64::decode(credential_creation_options.challenge)?; let collected_client_data = CollectedClientData { request_type: WEBAUTHN_REQUEST_TYPE_CREATE.to_owned(), - challenge: credential_creation_options.challenge, + challenge: base64::encode_config(challenge, URL_SAFE_NO_PAD), origin: origin.as_ref().unwrap_or_else(|| &rp_id).clone(), cross_origin: false, token_binding: None, }; - let credential = PublicKeyCredential { - id: connection_id, - response: Some(AuthenticatorAttestationResponse { - attestation_object: Some(base64::encode(attestation_object)), - client_data_json: base64::encode(serde_json::to_string(&collected_client_data)?.into_bytes()), + let credential = PublicKeyCredentialRaw { + id: base64::encode_config(connection_id.clone(), URL_SAFE_NO_PAD), + raw_id: connection_id, + response: Some(AuthenticatorAttestationResponseRaw { + attestation_object: Some(attestation_object), + client_data_json: serde_json::to_string(&collected_client_data)?.into_bytes(), authenticator_data: None, signature: None, user_handle: None, @@ -185,13 +234,93 @@ impl WebauthnAuthenticator { Ok(AuthenticatorCredentialCreationResponse { credential_response: credential, - private_key, + private_key_response, + }) + } + + pub fn generate_credential_request_response( + connection_id: Vec, + attestation_flags: u8, + credential_request_options: PublicKeyCredentialRequestOptions, + origin: Option, + private_key: String, + ) -> Result { + if credential_request_options + .user_verification + .as_ref() + .filter(|user_verif| *user_verif == &UserVerificationRequirement::Required) + .is_some() + && (attestation_flags & AttestationFlags::UserVerified as u8 == 0) + { + return Err(WebauthnCredentialRequestError::UserVerificationRequired); + } + + let binding = origin.as_ref().map(|o| get_default_rp_id(o.as_str())); + let rp_id = credential_request_options + .rp_id + .as_ref() + .or(binding.as_ref()) + .ok_or(WebauthnCredentialRequestError::RpIdOrOriginRequired)?; + let mut hasher = Sha256::new(); + hasher.update(rp_id); + + let auth_data = AuthenticatorData { + rp_id_hash: hasher + .finalize_reset() + .to_vec() + .try_into() + .map_err(|e: Vec| WebauthnCredentialRequestError::RpIdHashInvalidLength(e.len()))?, + flags: attestation_flags, + sign_count: 0, + attested_credential_data: None, + extensions: Value::Null, + }; + let auth_data_bytes = auth_data.to_vec()?; + + let challenge = base64::decode(credential_request_options.challenge)?; + let collected_client_data = CollectedClientData { + request_type: WEBAUTHN_REQUEST_TYPE_GET.to_owned(), + challenge: base64::encode_config(challenge, URL_SAFE_NO_PAD), + origin: origin.as_ref().unwrap_or_else(|| &rp_id).clone(), + cross_origin: false, + token_binding: None, + }; + let client_data_bytes = serde_json::to_string(&collected_client_data)?.into_bytes(); + + let private_key_response: PrivateKeyResponse = serde_cbor::from_slice(&base64::decode(private_key)?)?; + + let signature = match private_key_response.key_alg { + CoseAlgorithmIdentifier::Ed25519 => { + let key = ed25519_dalek::SigningKey::try_from(private_key_response.private_key.as_slice())?; + key.sign( + [auth_data_bytes.as_slice(), "||".as_bytes(), client_data_bytes.as_slice()] + .concat() + .as_slice(), + ) + .to_vec() + } + CoseAlgorithmIdentifier::EC2 => Vec::new(), + CoseAlgorithmIdentifier::RSA => Vec::new(), + CoseAlgorithmIdentifier::RS1 => Vec::new(), + CoseAlgorithmIdentifier::NotSupported => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), + }; + + Ok(PublicKeyCredentialRaw { + id: base64::encode_config(connection_id.clone(), URL_SAFE_NO_PAD), + raw_id: connection_id, + response: Some(AuthenticatorAttestationResponseRaw { + attestation_object: None, + client_data_json: client_data_bytes, + authenticator_data: Some(auth_data_bytes), + signature: Some(signature), + user_handle: None, + }), }) } fn find_best_supported_algorithm( pub_key_cred_params: &Vec, - ) -> Result { + ) -> Result { //Order of preference for credential type is: Ed25519 > EC2 > RSA > RS1 let mut possible_credential_types = vec![ CoseAlgorithmIdentifier::RS1, @@ -201,12 +330,13 @@ impl WebauthnAuthenticator { ]; let mut best_alg_index = None; - while let Some(param) = pub_key_cred_params.iter().next() { + let mut iterator = pub_key_cred_params.iter(); + while let Some(param) = iterator.next() { if let Some(alg_index) = possible_credential_types .iter() .position(|r| *r == CoseAlgorithmIdentifier::from(param.alg)) { - if best_alg_index.filter(|x| x < &alg_index).is_none() { + if best_alg_index.filter(|x| x > &alg_index).is_none() { best_alg_index = Some(alg_index); } @@ -217,12 +347,55 @@ impl WebauthnAuthenticator { } match best_alg_index { - None => Err(WebauthnCredentialCreationError::AlgorithmNotSupported), + None => Err(WebauthnCredentialRequestError::AlgorithmNotSupported), Some(index) => Ok(possible_credential_types.remove(index)), } } } +#[test] +fn test_best_alg() { + let params = vec![ + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::Ed25519.into(), + }, + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::EC2.into(), + }, + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::RS1.into(), + }, + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::RSA.into(), + }, + ]; + + let alg = WebauthnAuthenticator::find_best_supported_algorithm(¶ms).unwrap(); + assert_eq!(alg, CoseAlgorithmIdentifier::Ed25519); + + let params2 = vec![ + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::EC2.into(), + }, + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::RS1.into(), + }, + PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: CoseAlgorithmIdentifier::RSA.into(), + }, + ]; + + let alg = WebauthnAuthenticator::find_best_supported_algorithm(¶ms2).unwrap(); + assert_eq!(alg, CoseAlgorithmIdentifier::EC2); +} + #[test] fn test_credential_generation() { let option = PublicKeyCredentialCreationOptions { @@ -248,17 +421,19 @@ fn test_credential_generation() { attestation: None, extensions: None, }; - let credential = WebauthnAuthenticator::generate_credential_response( + + let credential = WebauthnAuthenticator::generate_credential_creation_response( option.clone(), - Uuid::new_v4().to_string(), + Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89), + Uuid::new_v4().into_bytes().to_vec(), Some("http://localhost".to_owned()), - vec![AttestationFlags::AttestedCredentialDataIncluded, AttestationFlags::UserPresent], + AttestationFlags::AttestedCredentialDataIncluded as u8 + AttestationFlags::UserPresent as u8, ); match credential { Ok(cred) => { - let mut verifier = CredentialCreationVerifier::new(cred.credential_response, option, "http://localhost"); - assert_eq!(dbg!(verifier.verify()).is_ok(), true) + let mut verifier = CredentialCreationVerifier::new(cred.credential_response.into(), option, "http://localhost"); + assert_eq!(verifier.verify().is_ok(), true) } Err(e) => { panic!("{e:?}") diff --git a/src/webauthn/authenticator/responses.rs b/src/webauthn/authenticator/responses.rs index 425309e..242ff12 100644 --- a/src/webauthn/authenticator/responses.rs +++ b/src/webauthn/authenticator/responses.rs @@ -1,6 +1,14 @@ -use crate::webauthn::proto::web_message::PublicKeyCredential; +use crate::webauthn::proto::{raw_message::CoseAlgorithmIdentifier, web_message::PublicKeyCredentialRaw}; +use serde_derive::{Deserialize, Serialize}; +#[derive(Serialize)] pub struct AuthenticatorCredentialCreationResponse { - pub credential_response: PublicKeyCredential, - pub private_key: String, + pub credential_response: PublicKeyCredentialRaw, + pub private_key_response: String, +} + +#[derive(Serialize, Deserialize)] +pub struct PrivateKeyResponse { + pub private_key: Vec, + pub key_alg: CoseAlgorithmIdentifier, } diff --git a/src/webauthn/error.rs b/src/webauthn/error.rs index 07d7c60..e49618d 100644 --- a/src/webauthn/error.rs +++ b/src/webauthn/error.rs @@ -1,4 +1,5 @@ use base64::DecodeError; +#[cfg(feature = "webauthn-server")] use ring::error::Unspecified; use serde_cbor::Error as CborError; use serde_json::Error as JsonError; @@ -7,6 +8,7 @@ use std::{ fmt::{Display, Formatter}, io::Error as IoError, }; +#[cfg(feature = "webauthn-server")] use webpki::Error as WebPkiError; #[derive(Debug)] @@ -57,7 +59,9 @@ pub enum Error { Base64Error(DecodeError), CborError(CborError), JsonError(JsonError), + #[cfg(feature = "webauthn-server")] WebPkiError(WebPkiError), + #[cfg(feature = "webauthn-server")] RingError(Unspecified), Version, CredentialError(CredentialError), @@ -83,12 +87,14 @@ impl From for Error { } } +#[cfg(feature = "webauthn-server")] impl From for Error { fn from(e: WebPkiError) -> Self { Error::WebPkiError(e) } } +#[cfg(feature = "webauthn-server")] impl From for Error { fn from(e: Unspecified) -> Self { Error::RingError(e) @@ -129,7 +135,9 @@ impl Display for Error { Base64Error(e) => e.fmt(f), CborError(cb_e) => cb_e.fmt(f), JsonError(js_e) => js_e.fmt(f), + #[cfg(feature = "webauthn-server")] WebPkiError(wp_e) => wp_e.fmt(f), + #[cfg(feature = "webauthn-server")] RingError(r_e) => r_e.fmt(f), TpmError(tpm_e) => tpm_e.fmt(f), } diff --git a/src/webauthn/mod.rs b/src/webauthn/mod.rs index 9ae7f07..a726e53 100644 --- a/src/webauthn/mod.rs +++ b/src/webauthn/mod.rs @@ -1,4 +1,9 @@ +#[cfg(feature = "webauthn")] pub mod authenticator; +#[cfg(feature = "webauthn")] pub mod error; +#[cfg(feature = "webauthn")] pub mod proto; + +#[cfg(feature = "webauthn-server")] pub mod server; diff --git a/src/webauthn/proto/raw_message.rs b/src/webauthn/proto/raw_message.rs index 95809b5..b669494 100644 --- a/src/webauthn/proto/raw_message.rs +++ b/src/webauthn/proto/raw_message.rs @@ -115,7 +115,7 @@ impl AuthenticatorData { let flags = cursor.read_u8()?; - let sign_count = dbg!(cursor.read_u32::())?; + let sign_count = cursor.read_u32::()?; let attested_credential_data = if cursor.remaining() > 16 { let mut aaguid = [0u8; 16]; @@ -330,7 +330,7 @@ impl CredentialPublicKey { Ok(CredentialPublicKey { key_type, alg, - key_info: CoseKeyInfo::EC2(EC2 { curve, coords }), + key_info: CoseKeyInfo::OKP(OKP { curve, coords }), }) } (WEBAUTH_PUBLIC_KEY_TYPE_RSA, CoseAlgorithmIdentifier::RSA) => { @@ -418,6 +418,8 @@ impl CredentialPublicKey { } CoseKeyInfo::RSA(value) => { + map.insert(Value::Integer(1), Value::Integer(WEBAUTH_PUBLIC_KEY_TYPE_RSA as i128)); + map.insert(Value::Integer(3), Value::Integer(CoseAlgorithmIdentifier::RSA as i128)); map.insert(Value::Integer(-1), Value::Bytes(value.n)); map.insert(Value::Integer(-2), Value::Bytes(value.e)); } @@ -628,7 +630,7 @@ impl FromStr for Coordinates { } } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub enum CoseAlgorithmIdentifier { Ed25519 = -8, EC2 = -7, diff --git a/src/webauthn/proto/web_message.rs b/src/webauthn/proto/web_message.rs index d8e2590..ff1f242 100644 --- a/src/webauthn/proto/web_message.rs +++ b/src/webauthn/proto/web_message.rs @@ -1,3 +1,4 @@ +use http::Uri; use serde_derive::*; use serde_json::Value; @@ -139,6 +140,30 @@ pub struct PublicKeyCredential { pub response: Option, } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyCredentialRaw { + pub id: String, + pub raw_id: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub response: Option, +} + +impl From for PublicKeyCredential { + fn from(raw: PublicKeyCredentialRaw) -> Self { + PublicKeyCredential { + id: raw.id, + response: raw.response.map(|response| AuthenticatorAttestationResponse { + attestation_object: response.attestation_object.map(|a| base64::encode(&a)), + client_data_json: base64::encode(&response.client_data_json), + authenticator_data: response.authenticator_data.map(|a| base64::encode(&a)), + signature: response.signature.map(|s| base64::encode(&s)), + user_handle: response.user_handle.map(|u| base64::encode(&u)), + }), + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct AuthenticatorAttestationResponse { @@ -154,6 +179,21 @@ pub struct AuthenticatorAttestationResponse { pub user_handle: Option, } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticatorAttestationResponseRaw { + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation_object: Option>, + #[serde(rename = "clientDataJSON")] + pub client_data_json: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_data: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub signature: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_handle: Option>, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct CollectedClientData { @@ -179,3 +219,21 @@ pub enum TokenBindingStatus { Present, Supported, } + +pub fn get_default_rp_id(origin: &str) -> String { + origin + .parse::() + .ok() + .and_then(|u| u.authority().map(|a| a.host().to_string())) + .unwrap_or(origin.to_string()) +} + +#[test] +fn test_default_rp_id() { + assert_eq!(get_default_rp_id("https://login.example.com:1337"), "login.example.com"); + assert_eq!(get_default_rp_id("https://login.example.com"), "login.example.com"); + assert_eq!(get_default_rp_id("http://login.example.com:1337"), "login.example.com"); + assert_eq!(get_default_rp_id("http://login.example.com"), "login.example.com"); + assert_eq!(get_default_rp_id("login.example.com:1337"), "login.example.com"); + assert_eq!(get_default_rp_id("login.example.com"), "login.example.com"); +} diff --git a/src/webauthn/server/mod.rs b/src/webauthn/server/mod.rs index 4424e64..dc2ad38 100644 --- a/src/webauthn/server/mod.rs +++ b/src/webauthn/server/mod.rs @@ -1,4 +1,3 @@ -use http::Uri; use ring::{ signature, signature::{UnparsedPublicKey, VerificationAlgorithm}, @@ -21,7 +20,7 @@ use crate::webauthn::{ }, tpm::TpmAlgId, web_message::{ - AttestationConveyancePreference, AuthenticatorSelectionCriteria, CollectedClientData, PublicKeyCredential, + get_default_rp_id, AttestationConveyancePreference, AuthenticatorSelectionCriteria, CollectedClientData, PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, PublicKeyCredentialRequestOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity, UserVerificationRequirement, @@ -644,21 +643,3 @@ fn guid_bytes_to_string(guid: &[u8; 16]) -> Option { let uuid = uuid::Uuid::from_slice(guid).ok()?; Some(uuid.hyphenated().to_string()) } - -fn get_default_rp_id(origin: &str) -> String { - origin - .parse::() - .ok() - .and_then(|u| u.authority().map(|a| a.host().to_string())) - .unwrap_or(origin.to_string()) -} - -#[test] -fn test_default_rp_id() { - assert_eq!(get_default_rp_id("https://login.example.com:1337"), "login.example.com"); - assert_eq!(get_default_rp_id("https://login.example.com"), "login.example.com"); - assert_eq!(get_default_rp_id("http://login.example.com:1337"), "login.example.com"); - assert_eq!(get_default_rp_id("http://login.example.com"), "login.example.com"); - assert_eq!(get_default_rp_id("login.example.com:1337"), "login.example.com"); - assert_eq!(get_default_rp_id("login.example.com"), "login.example.com"); -} diff --git a/wasm/build.sh b/wasm/build.sh index 3ab1998..53c9f0e 100755 --- a/wasm/build.sh +++ b/wasm/build.sh @@ -1,2 +1,2 @@ #!/bin/bash -wasm-pack build --scope devolutions --out-dir ./dist/bundler --target bundler -- --no-default-features +wasm-pack build --scope devolutions --out-dir ./dist/bundler --target bundler -- --no-default-features --features "webauthn" From 9246cb5afbfecef621582d74c36c26e1c6d6e216 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Tue, 13 Feb 2024 11:12:55 -0500 Subject: [PATCH 03/10] Added credential request response to authenticator --- Cargo.toml | 2 +- src/webauthn/authenticator/mod.rs | 98 ++++++++++++++++++++----- src/webauthn/authenticator/responses.rs | 3 +- src/webauthn/error.rs | 47 +++++++++--- src/webauthn/proto/raw_message.rs | 6 ++ src/webauthn/server/mod.rs | 21 ++++-- 6 files changed, 138 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8001c1..e034c2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ base32 = "0.4" hex = "0.4" rsa = "0.9.2" x509-parser = "0.15.0" -ed25519-dalek = { version = "2.1.0", features = ["rand_core"] } +ed25519-dalek = { version = "2.1.0", features = ["rand_core", "pkcs8"] } p256 = "0.13.2" rand = "0.8.5" diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index 5e61fc1..6836ef4 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -17,10 +17,15 @@ use crate::webauthn::{ use base64::URL_SAFE_NO_PAD; use ed25519_dalek::{SignatureError, Signer}; use hmac::digest::Digest; -use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::ecdsa::VerifyingKey; use rand::rngs::OsRng; -use rsa::{pkcs1::EncodeRsaPrivateKey, traits::PublicKeyParts}; +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, + signature::SignatureEncoding, + traits::PublicKeyParts, +}; use serde_cbor::Value; +use sha1::Sha1; use sha2::Sha256; use uuid::Uuid; @@ -33,8 +38,10 @@ use crate::webauthn::{ }; #[cfg(test)] use crate::webauthn::{ - proto::web_message::{PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity}, - server::CredentialCreationVerifier, + proto::web_message::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity, + }, + server::{CredentialCreationVerifier, CredentialRequestVerifier}, }; #[derive(Debug)] @@ -141,8 +148,8 @@ impl WebauthnAuthenticator { } CoseAlgorithmIdentifier::EC2 => { - let secret_key = p256::SecretKey::random(&mut OsRng); - let keypair = secret_key.public_key().to_encoded_point(false); + let secret_key = p256::ecdsa::SigningKey::random(&mut OsRng); + let keypair = VerifyingKey::from(&secret_key).to_encoded_point(false); let y = keypair.y().ok_or(WebauthnCredentialRequestError::CouldNotGenerateKey)?; let x = keypair.x().ok_or(WebauthnCredentialRequestError::CouldNotGenerateKey)?; let private_key = PrivateKeyResponse { @@ -243,6 +250,7 @@ impl WebauthnAuthenticator { attestation_flags: u8, credential_request_options: PublicKeyCredentialRequestOptions, origin: Option, + user_handle: Option>, private_key: String, ) -> Result { if credential_request_options @@ -286,22 +294,36 @@ impl WebauthnAuthenticator { token_binding: None, }; let client_data_bytes = serde_json::to_string(&collected_client_data)?.into_bytes(); + let mut hasher = Sha256::new(); + hasher.update(client_data_bytes.as_slice()); + let hash = hasher.finalize_reset().to_vec(); let private_key_response: PrivateKeyResponse = serde_cbor::from_slice(&base64::decode(private_key)?)?; let signature = match private_key_response.key_alg { CoseAlgorithmIdentifier::Ed25519 => { let key = ed25519_dalek::SigningKey::try_from(private_key_response.private_key.as_slice())?; - key.sign( - [auth_data_bytes.as_slice(), "||".as_bytes(), client_data_bytes.as_slice()] - .concat() - .as_slice(), - ) - .to_vec() + key.sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()).to_vec() + } + CoseAlgorithmIdentifier::EC2 => { + let key = p256::ecdsa::SigningKey::try_from(private_key_response.private_key.as_slice())?; + let (sig, _) = key.sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()); + sig.to_der().to_vec() + } + CoseAlgorithmIdentifier::RSA => { + let key = rsa::RsaPrivateKey::from_pkcs1_der(&private_key_response.private_key)?; + let signing_key = rsa::pkcs1v15::SigningKey::::new(key); + signing_key + .sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()) + .to_vec() + } + CoseAlgorithmIdentifier::RS1 => { + let key = rsa::RsaPrivateKey::from_pkcs1_der(&private_key_response.private_key)?; + let signing_key = rsa::pkcs1v15::SigningKey::::new(key); + signing_key + .sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()) + .to_vec() } - CoseAlgorithmIdentifier::EC2 => Vec::new(), - CoseAlgorithmIdentifier::RSA => Vec::new(), - CoseAlgorithmIdentifier::RS1 => Vec::new(), CoseAlgorithmIdentifier::NotSupported => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), }; @@ -313,7 +335,7 @@ impl WebauthnAuthenticator { client_data_json: client_data_bytes, authenticator_data: Some(auth_data_bytes), signature: Some(signature), - user_handle: None, + user_handle, }), }) } @@ -398,6 +420,7 @@ fn test_best_alg() { #[test] fn test_credential_generation() { + let user_uuid = Uuid::new_v4(); let option = PublicKeyCredentialCreationOptions { challenge: "test".to_owned(), rp: PublicKeyCredentialRpEntity { @@ -406,14 +429,14 @@ fn test_credential_generation() { icon: None, }, user: PublicKeyCredentialUserEntity { - id: Uuid::new_v4().to_string(), + id: user_uuid.to_string(), name: "test".to_owned(), display_name: "test".to_owned(), icon: None, }, pub_key_cred_params: vec![PublicKeyCredentialParameters { auth_type: PublicKeyCredentialType::PublicKey, - alg: CoseAlgorithmIdentifier::Ed25519.into(), + alg: CoseAlgorithmIdentifier::RS1.into(), }], timeout: None, exclude_credentials: vec![], @@ -422,10 +445,11 @@ fn test_credential_generation() { extensions: None, }; + let cred_uuid = Uuid::new_v4().into_bytes().to_vec(); let credential = WebauthnAuthenticator::generate_credential_creation_response( option.clone(), Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89), - Uuid::new_v4().into_bytes().to_vec(), + cred_uuid.clone(), Some("http://localhost".to_owned()), AttestationFlags::AttestedCredentialDataIncluded as u8 + AttestationFlags::UserPresent as u8, ); @@ -433,7 +457,41 @@ fn test_credential_generation() { match credential { Ok(cred) => { let mut verifier = CredentialCreationVerifier::new(cred.credential_response.into(), option, "http://localhost"); - assert_eq!(verifier.verify().is_ok(), true) + let verif_res = verifier.verify(); + assert_eq!(verif_res.is_ok(), true); + + let req_option = PublicKeyCredentialRequestOptions { + challenge: "test".to_owned(), + timeout: None, + rp_id: Some("localhost".to_owned()), + allow_credentials: vec![PublicKeyCredentialDescriptor { + cred_type: PublicKeyCredentialType::PublicKey, + id: base64::encode_config(&cred_uuid, URL_SAFE_NO_PAD), + transports: None, + }], + extensions: None, + user_verification: None, + }; + + let req_credential = WebauthnAuthenticator::generate_credential_request_response( + cred_uuid, + AttestationFlags::UserVerified as u8 + AttestationFlags::UserPresent as u8, + req_option.clone(), + Some("http://localhost".to_owned()), + Some(user_uuid.into_bytes().to_vec()), + cred.private_key_response, + ) + .unwrap(); + + let mut req_verifier = CredentialRequestVerifier::new( + req_credential.into(), + verif_res.unwrap().public_key, + req_option, + "http://localhost", + user_uuid.as_bytes().as_slice(), + 0, + ); + assert_eq!(dbg!(req_verifier.verify()).is_ok(), true) } Err(e) => { panic!("{e:?}") diff --git a/src/webauthn/authenticator/responses.rs b/src/webauthn/authenticator/responses.rs index 242ff12..d414af1 100644 --- a/src/webauthn/authenticator/responses.rs +++ b/src/webauthn/authenticator/responses.rs @@ -1,7 +1,7 @@ use crate::webauthn::proto::{raw_message::CoseAlgorithmIdentifier, web_message::PublicKeyCredentialRaw}; use serde_derive::{Deserialize, Serialize}; -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct AuthenticatorCredentialCreationResponse { pub credential_response: PublicKeyCredentialRaw, pub private_key_response: String, @@ -10,5 +10,6 @@ pub struct AuthenticatorCredentialCreationResponse { #[derive(Serialize, Deserialize)] pub struct PrivateKeyResponse { pub private_key: Vec, + #[serde(default)] pub key_alg: CoseAlgorithmIdentifier, } diff --git a/src/webauthn/error.rs b/src/webauthn/error.rs index e49618d..0c114d6 100644 --- a/src/webauthn/error.rs +++ b/src/webauthn/error.rs @@ -5,7 +5,7 @@ use serde_cbor::Error as CborError; use serde_json::Error as JsonError; use std::{ error::Error as StdError, - fmt::{Display, Formatter}, + fmt::{Debug, Display, Formatter}, io::Error as IoError, }; #[cfg(feature = "webauthn-server")] @@ -65,7 +65,10 @@ pub enum Error { RingError(Unspecified), Version, CredentialError(CredentialError), + ED25519Error(ed25519_dalek::ed25519::Error), + ED25519ErrorSPKI(ed25519_dalek::pkcs8::spki::Error), TpmError(TpmError), + Pkcs1Error(rsa::pkcs1::Error), Other(String), } @@ -94,6 +97,20 @@ impl From for Error { } } +#[cfg(feature = "webauthn-server")] +impl From for Error { + fn from(e: ed25519_dalek::ed25519::Error) -> Self { + Error::ED25519Error(e) + } +} + +#[cfg(feature = "webauthn-server")] +impl From for Error { + fn from(e: ed25519_dalek::pkcs8::spki::Error) -> Self { + Error::ED25519ErrorSPKI(e) + } +} + #[cfg(feature = "webauthn-server")] impl From for Error { fn from(e: Unspecified) -> Self { @@ -101,6 +118,13 @@ impl From for Error { } } +#[cfg(feature = "webauthn-server")] +impl From for Error { + fn from(e: rsa::pkcs1::Error) -> Self { + Error::Pkcs1Error(e) + } +} + impl StdError for Error {} impl Display for CredentialError { @@ -128,18 +152,23 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { use Error::*; match self { - IoError(io_e) => io_e.fmt(f), + IoError(io_e) => std::fmt::Display::fmt(io_e, f), Version => write!(f, "Unsupported version"), - CredentialError(ce) => ce.fmt(f), + CredentialError(ce) => std::fmt::Display::fmt(ce, f), Other(s) => write!(f, "{}", s), - Base64Error(e) => e.fmt(f), - CborError(cb_e) => cb_e.fmt(f), - JsonError(js_e) => js_e.fmt(f), + Base64Error(e) => std::fmt::Display::fmt(e, f), + CborError(cb_e) => std::fmt::Display::fmt(cb_e, f), + JsonError(js_e) => std::fmt::Display::fmt(js_e, f), + #[cfg(feature = "webauthn-server")] + WebPkiError(wp_e) => std::fmt::Display::fmt(wp_e, f), + #[cfg(feature = "webauthn-server")] + RingError(r_e) => std::fmt::Display::fmt(r_e, f), + TpmError(tpm_e) => std::fmt::Display::fmt(tpm_e, f), #[cfg(feature = "webauthn-server")] - WebPkiError(wp_e) => wp_e.fmt(f), + ED25519Error(ed) => std::fmt::Display::fmt(ed, f), #[cfg(feature = "webauthn-server")] - RingError(r_e) => r_e.fmt(f), - TpmError(tpm_e) => tpm_e.fmt(f), + ED25519ErrorSPKI(eds) => std::fmt::Display::fmt(eds, f), + Pkcs1Error(pkc) => std::fmt::Display::fmt(pkc, f), } } } diff --git a/src/webauthn/proto/raw_message.rs b/src/webauthn/proto/raw_message.rs index b669494..4b900a1 100644 --- a/src/webauthn/proto/raw_message.rs +++ b/src/webauthn/proto/raw_message.rs @@ -639,6 +639,12 @@ pub enum CoseAlgorithmIdentifier { NotSupported, } +impl Default for CoseAlgorithmIdentifier { + fn default() -> Self { + CoseAlgorithmIdentifier::NotSupported + } +} + impl From for CoseAlgorithmIdentifier { fn from(value: i64) -> Self { match value { diff --git a/src/webauthn/server/mod.rs b/src/webauthn/server/mod.rs index dc2ad38..5294f89 100644 --- a/src/webauthn/server/mod.rs +++ b/src/webauthn/server/mod.rs @@ -10,9 +10,9 @@ use crate::webauthn::{ error::{CredentialError, Error}, proto::{ constants::{ - ECDSA_CURVE_P256, ECDSA_CURVE_P384, ECDSA_Y_PREFIX_UNCOMPRESSED, WEBAUTHN_REQUEST_TYPE_CREATE, WEBAUTHN_REQUEST_TYPE_GET, - WEBAUTHN_USER_PRESENT_FLAG, WEBAUTHN_USER_VERIFIED_FLAG, WEBAUTH_PUBLIC_KEY_TYPE_EC2, WEBAUTH_PUBLIC_KEY_TYPE_OKP, - WEBAUTH_PUBLIC_KEY_TYPE_RSA, + ECDAA_CURVE_ED25519, ECDSA_CURVE_P256, ECDSA_CURVE_P384, ECDSA_Y_PREFIX_UNCOMPRESSED, WEBAUTHN_REQUEST_TYPE_CREATE, + WEBAUTHN_REQUEST_TYPE_GET, WEBAUTHN_USER_PRESENT_FLAG, WEBAUTHN_USER_VERIFIED_FLAG, WEBAUTH_PUBLIC_KEY_TYPE_EC2, + WEBAUTH_PUBLIC_KEY_TYPE_OKP, WEBAUTH_PUBLIC_KEY_TYPE_RSA, }, raw_message::{ AttestationObject, AttestationStatement, AuthenticatorData, Coordinates, CoseAlgorithmIdentifier, CoseKeyInfo, @@ -363,6 +363,7 @@ impl CredentialCreationVerifier { } } +#[derive(Debug)] pub struct CredentialRequestResult { pub sign_count: u32, pub has_user_verification: bool, @@ -559,15 +560,12 @@ impl CredentialRequestVerifier { match &self.credential_pub.key_info { CoseKeyInfo::OKP(ed25519) => match ed25519.coords { - Coordinates::Compressed { x, y } => { - key.push(y); + Coordinates::Compressed { x, y: _ } => { key.append(&mut x.to_vec()); } - Coordinates::Uncompressed { x, y } => { - key.push(ECDSA_Y_PREFIX_UNCOMPRESSED); + Coordinates::Uncompressed { x, y: _ } => { key.append(&mut x.to_vec()); - key.append(&mut y.to_vec()); } _ => return Err(Error::Other("Expected coordinates found nothing".to_owned())), @@ -626,6 +624,12 @@ impl CredentialRequestVerifier { fn get_ring_alg_from_cose(id: i64, key_info: &CoseKeyInfo) -> Result<&'static dyn VerificationAlgorithm, Error> { match (CoseAlgorithmIdentifier::from(id), key_info) { + (CoseAlgorithmIdentifier::Ed25519, CoseKeyInfo::OKP(okp)) => match okp.curve { + ECDAA_CURVE_ED25519 => Ok(&signature::ED25519), + _ => Err(Error::CredentialError(CredentialError::Other(String::from( + "Unsupported algorithm", + )))), + }, (CoseAlgorithmIdentifier::EC2, CoseKeyInfo::EC2(ec2)) => match ec2.curve { ECDSA_CURVE_P256 => Ok(&signature::ECDSA_P256_SHA256_ASN1), ECDSA_CURVE_P384 => Ok(&signature::ECDSA_P384_SHA384_ASN1), @@ -634,6 +638,7 @@ fn get_ring_alg_from_cose(id: i64, key_info: &CoseKeyInfo) -> Result<&'static dy )))), }, (CoseAlgorithmIdentifier::RSA, CoseKeyInfo::RSA(_)) => Ok(&signature::RSA_PKCS1_2048_8192_SHA256), + (CoseAlgorithmIdentifier::RS1, CoseKeyInfo::RSA(_)) => Ok(&signature::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY), _ => Err(Error::CredentialError(CredentialError::Other(String::from( "Unsupported algorithm", )))), From ed684badc41dc741bf2174cc0eab9ee56fa05e75 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Mon, 19 Feb 2024 19:29:03 -0500 Subject: [PATCH 04/10] Finalized authenticator implementation --- Cargo.toml | 2 +- src/wasm.rs | 32 +++++- src/webauthn/authenticator/mod.rs | 170 ++++++++++++++++-------------- src/webauthn/error.rs | 29 ----- src/webauthn/server/mod.rs | 1 - 5 files changed, 118 insertions(+), 116 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e034c2b..fcbe4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slauth" -version = "0.7.5-beta.0" +version = "0.7.6-beta.0" authors = ["richer ", "LucFauvel "] edition = "2021" description = "oath HOTP and TOTP complient implementation" diff --git a/src/wasm.rs b/src/wasm.rs index 6998b3f..d07e1f0 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -7,7 +7,10 @@ use crate::{ totp::{TOTPBuilder, TOTPContext}, HashesAlgorithm, OtpAuth, }, - webauthn::{authenticator::WebauthnAuthenticator, proto::web_message::PublicKeyCredentialCreationOptions}, + webauthn::{ + authenticator::WebauthnAuthenticator, + proto::web_message::{PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions}, + }, }; #[wasm_bindgen] @@ -99,14 +102,37 @@ impl PasskeyAuthenticator { pub fn generate_credential_creation_response( &self, options: JsValue, - connection_id: Vec, + credential_id: Vec, attestation_flags: u8, origin: Option, ) -> Result { let options: PublicKeyCredentialCreationOptions = serde_wasm_bindgen::from_value(options).map_err(|e| format!("{e:?}"))?; let cred = - WebauthnAuthenticator::generate_credential_creation_response(options, self.aaguid, connection_id, origin, attestation_flags) + WebauthnAuthenticator::generate_credential_creation_response(options, self.aaguid, credential_id, origin, attestation_flags) .map_err(|e| format!("{e:?}"))?; serde_wasm_bindgen::to_value(&cred).map_err(|e| format!("{e:?}")) } + + #[wasm_bindgen(js_name = "generateCredentialRequestResponse")] + pub fn generate_credential_request_response( + &self, + options: JsValue, + credential_id: Vec, + attestation_flags: u8, + origin: Option, + user_handle: Option>, + private_key: String, + ) -> Result { + let options: PublicKeyCredentialRequestOptions = serde_wasm_bindgen::from_value(options).map_err(|e| format!("{e:?}"))?; + let cred = WebauthnAuthenticator::generate_credential_request_response( + credential_id, + attestation_flags, + options, + origin, + user_handle, + private_key, + ) + .map_err(|e| format!("{e:?}"))?; + serde_wasm_bindgen::to_value(&cred).map_err(|e| format!("{e:?}")) + } } diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index d289cc7..253b40d 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -101,7 +101,7 @@ impl WebauthnAuthenticator { pub fn generate_credential_creation_response( credential_creation_options: PublicKeyCredentialCreationOptions, aaguid: Uuid, - connection_id: Vec, + credential_id: Vec, origin: Option, attestation_flags: u8, ) -> Result { @@ -189,7 +189,7 @@ impl WebauthnAuthenticator { let attested_credential_data = if attestation_flags & AttestationFlags::AttestedCredentialDataIncluded as u8 != 0 { Some(AttestedCredentialData { aaguid: aaguid.into_bytes(), - credential_id: connection_id.clone(), + credential_id: credential_id.clone(), credential_public_key: CredentialPublicKey { key_type: key_info.key_type(), alg: alg.into(), @@ -228,8 +228,8 @@ impl WebauthnAuthenticator { }; let credential = PublicKeyCredentialRaw { - id: base64::encode_config(connection_id.clone(), URL_SAFE_NO_PAD), - raw_id: connection_id, + id: base64::encode_config(credential_id.clone(), URL_SAFE_NO_PAD), + raw_id: credential_id, response: Some(AuthenticatorAttestationResponseRaw { attestation_object: Some(attestation_object), client_data_json: serde_json::to_string(&collected_client_data)?.into_bytes(), @@ -246,7 +246,7 @@ impl WebauthnAuthenticator { } pub fn generate_credential_request_response( - connection_id: Vec, + credential_id: Vec, attestation_flags: u8, credential_request_options: PublicKeyCredentialRequestOptions, origin: Option, @@ -328,8 +328,8 @@ impl WebauthnAuthenticator { }; Ok(PublicKeyCredentialRaw { - id: base64::encode_config(connection_id.clone(), URL_SAFE_NO_PAD), - raw_id: connection_id, + id: base64::encode_config(credential_id.clone(), URL_SAFE_NO_PAD), + raw_id: credential_id, response: Some(AuthenticatorAttestationResponseRaw { attestation_object: None, client_data_json: client_data_bytes, @@ -420,81 +420,87 @@ fn test_best_alg() { #[test] fn test_credential_generation() { - let user_uuid = Uuid::new_v4(); - let option = PublicKeyCredentialCreationOptions { - challenge: "test".to_owned(), - rp: PublicKeyCredentialRpEntity { - id: Some("localhost".to_owned()), - name: "localhost".to_owned(), - icon: None, - }, - user: PublicKeyCredentialUserEntity { - id: user_uuid.to_string(), - name: "test".to_owned(), - display_name: "test".to_owned(), - icon: None, - }, - pub_key_cred_params: vec![PublicKeyCredentialParameters { - auth_type: PublicKeyCredentialType::PublicKey, - alg: CoseAlgorithmIdentifier::RS1.into(), - }], - timeout: None, - exclude_credentials: vec![], - authenticator_selection: None, - attestation: None, - extensions: Extensions::default(), - }; - - let cred_uuid = Uuid::new_v4().into_bytes().to_vec(); - let credential = WebauthnAuthenticator::generate_credential_creation_response( - option.clone(), - Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89), - cred_uuid.clone(), - Some("http://localhost".to_owned()), - AttestationFlags::AttestedCredentialDataIncluded as u8 + AttestationFlags::UserPresent as u8, - ); - - match credential { - Ok(cred) => { - let mut verifier = CredentialCreationVerifier::new(cred.credential_response.into(), option, "http://localhost"); - let verif_res = verifier.verify(); - assert_eq!(verif_res.is_ok(), true); - - let req_option = PublicKeyCredentialRequestOptions { - challenge: "test".to_owned(), - timeout: None, - rp_id: Some("localhost".to_owned()), - allow_credentials: vec![PublicKeyCredentialDescriptor { - cred_type: PublicKeyCredentialType::PublicKey, - id: base64::encode_config(&cred_uuid, URL_SAFE_NO_PAD), - transports: None, - }], - extensions: Extensions::default(), - user_verification: None, - }; - - let req_credential = WebauthnAuthenticator::generate_credential_request_response( - cred_uuid, - AttestationFlags::UserVerified as u8 + AttestationFlags::UserPresent as u8, - req_option.clone(), - Some("http://localhost".to_owned()), - Some(user_uuid.into_bytes().to_vec()), - cred.private_key_response, - ) - .unwrap(); - - let mut req_verifier = CredentialRequestVerifier::new( - req_credential.into(), - verif_res.unwrap().public_key, - req_option, - "http://localhost", - user_uuid.as_bytes().as_slice(), - 0, - ); - assert_eq!(dbg!(req_verifier.verify()).is_ok(), true) - } - Err(e) => { - panic!("{e:?}") + for alg in [ + CoseAlgorithmIdentifier::Ed25519, + CoseAlgorithmIdentifier::EC2, + CoseAlgorithmIdentifier::RSA, + ] { + let user_uuid = Uuid::new_v4(); + let option = PublicKeyCredentialCreationOptions { + challenge: "test".to_owned(), + rp: PublicKeyCredentialRpEntity { + id: Some("localhost".to_owned()), + name: "localhost".to_owned(), + icon: None, + }, + user: PublicKeyCredentialUserEntity { + id: user_uuid.to_string(), + name: "test".to_owned(), + display_name: "test".to_owned(), + icon: None, + }, + pub_key_cred_params: vec![PublicKeyCredentialParameters { + auth_type: PublicKeyCredentialType::PublicKey, + alg: alg.into(), + }], + timeout: None, + exclude_credentials: vec![], + authenticator_selection: None, + attestation: None, + extensions: Extensions::default(), + }; + + let cred_uuid = Uuid::new_v4().into_bytes().to_vec(); + let credential = WebauthnAuthenticator::generate_credential_creation_response( + option.clone(), + Uuid::from_u128(0xDE503f9c_21a4_4f76_b4b7_558eb55c6f89), + cred_uuid.clone(), + Some("http://localhost".to_owned()), + AttestationFlags::AttestedCredentialDataIncluded as u8 + AttestationFlags::UserPresent as u8, + ); + + match credential { + Ok(cred) => { + let mut verifier = CredentialCreationVerifier::new(cred.credential_response.into(), option, "http://localhost"); + let verif_res = verifier.verify(); + assert_eq!(verif_res.is_ok(), true); + + let req_option = PublicKeyCredentialRequestOptions { + challenge: "test".to_owned(), + timeout: None, + rp_id: Some("localhost".to_owned()), + allow_credentials: vec![PublicKeyCredentialDescriptor { + cred_type: PublicKeyCredentialType::PublicKey, + id: base64::encode_config(&cred_uuid, URL_SAFE_NO_PAD), + transports: None, + }], + extensions: Extensions::default(), + user_verification: None, + }; + + let req_credential = WebauthnAuthenticator::generate_credential_request_response( + cred_uuid, + AttestationFlags::UserVerified as u8 + AttestationFlags::UserPresent as u8, + req_option.clone(), + Some("http://localhost".to_owned()), + Some(user_uuid.into_bytes().to_vec()), + cred.private_key_response, + ) + .unwrap(); + + let mut req_verifier = CredentialRequestVerifier::new( + req_credential.into(), + verif_res.unwrap().public_key, + req_option, + "http://localhost", + user_uuid.as_bytes().as_slice(), + 0, + ); + assert_eq!(req_verifier.verify().is_ok(), true) + } + Err(e) => { + panic!("{e:?}") + } } } } diff --git a/src/webauthn/error.rs b/src/webauthn/error.rs index 0c114d6..c939fa2 100644 --- a/src/webauthn/error.rs +++ b/src/webauthn/error.rs @@ -65,10 +65,7 @@ pub enum Error { RingError(Unspecified), Version, CredentialError(CredentialError), - ED25519Error(ed25519_dalek::ed25519::Error), - ED25519ErrorSPKI(ed25519_dalek::pkcs8::spki::Error), TpmError(TpmError), - Pkcs1Error(rsa::pkcs1::Error), Other(String), } @@ -97,20 +94,6 @@ impl From for Error { } } -#[cfg(feature = "webauthn-server")] -impl From for Error { - fn from(e: ed25519_dalek::ed25519::Error) -> Self { - Error::ED25519Error(e) - } -} - -#[cfg(feature = "webauthn-server")] -impl From for Error { - fn from(e: ed25519_dalek::pkcs8::spki::Error) -> Self { - Error::ED25519ErrorSPKI(e) - } -} - #[cfg(feature = "webauthn-server")] impl From for Error { fn from(e: Unspecified) -> Self { @@ -118,13 +101,6 @@ impl From for Error { } } -#[cfg(feature = "webauthn-server")] -impl From for Error { - fn from(e: rsa::pkcs1::Error) -> Self { - Error::Pkcs1Error(e) - } -} - impl StdError for Error {} impl Display for CredentialError { @@ -164,11 +140,6 @@ impl Display for Error { #[cfg(feature = "webauthn-server")] RingError(r_e) => std::fmt::Display::fmt(r_e, f), TpmError(tpm_e) => std::fmt::Display::fmt(tpm_e, f), - #[cfg(feature = "webauthn-server")] - ED25519Error(ed) => std::fmt::Display::fmt(ed, f), - #[cfg(feature = "webauthn-server")] - ED25519ErrorSPKI(eds) => std::fmt::Display::fmt(eds, f), - Pkcs1Error(pkc) => std::fmt::Display::fmt(pkc, f), } } } diff --git a/src/webauthn/server/mod.rs b/src/webauthn/server/mod.rs index f871d22..f11b505 100644 --- a/src/webauthn/server/mod.rs +++ b/src/webauthn/server/mod.rs @@ -706,7 +706,6 @@ fn get_ring_alg_from_cose(id: i64, key_info: &CoseKeyInfo) -> Result<&'static dy )))), }, (CoseAlgorithmIdentifier::RSA, CoseKeyInfo::RSA(_)) => Ok(&signature::RSA_PKCS1_2048_8192_SHA256), - (CoseAlgorithmIdentifier::RS1, CoseKeyInfo::RSA(_)) => Ok(&signature::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY), _ => Err(Error::CredentialError(CredentialError::Other(String::from( "Unsupported algorithm", )))), From 71181afee1970810a2f83f62fafb571b6c81b22a Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Tue, 20 Feb 2024 10:17:34 -0500 Subject: [PATCH 05/10] Made new webauthn dependencies optional --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fcbe4f5..f26ccfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ native-bindings = [] u2f-server = ["u2f", "webpki"] u2f = ["auth-base", "untrusted", "serde_repr", ] webauthn-server = [ "webauthn", "webpki" ] -webauthn = [ "auth-base", "bytes", "serde_cbor", "uuid", "http" ] +webauthn = [ "auth-base", "bytes", "serde_cbor", "uuid", "http", "ed25519-dalek", "p256" ] auth-base = ["base64", "byteorder", "ring", "serde", "serde_derive", "serde_json", "serde_bytes"] android = ["jni"] @@ -33,10 +33,8 @@ time = "0.3" base32 = "0.4" hex = "0.4" rsa = "0.9.2" -x509-parser = "0.15.0" -ed25519-dalek = { version = "2.1.0", features = ["rand_core", "pkcs8"] } -p256 = "0.13.2" rand = "0.8.5" +x509-parser = "0.15.0" base64 = { version = "0.13", optional = true } byteorder = { version = "1.4", optional = true } @@ -52,6 +50,8 @@ webpki = { version = "0.22", optional = true, features = ["alloc"] } bytes = { version = "1.2", optional = true } http = { version = "1.0", optional = true } uuid = { version = "1.6", optional = true } +ed25519-dalek = { version = "2.1.0", features = ["rand_core", "pkcs8"], optional = true } +p256 = { version = "0.13.2", optional = true } [target.'cfg(target_os="android")'.dependencies] jni = { version = "0.20", default-features = false, optional = true } From abe437980aa3eef55b19af425c5f5cf6efc358ce Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 21 Feb 2024 09:24:30 -0500 Subject: [PATCH 06/10] chore: Fixed PR comments --- src/wasm.rs | 6 +++--- src/webauthn/authenticator/mod.rs | 4 ++-- src/webauthn/proto/raw_message.rs | 12 ++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/wasm.rs b/src/wasm.rs index d07e1f0..0335cef 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -93,9 +93,9 @@ pub struct PasskeyAuthenticator { #[wasm_bindgen] impl PasskeyAuthenticator { #[wasm_bindgen(constructor)] - pub fn new(aaguid: String) -> PasskeyAuthenticator { - let aaguid = Uuid::parse_str(aaguid.as_str()).expect("Failed to parse aaguid from string"); - PasskeyAuthenticator { aaguid } + pub fn new(aaguid: String) -> Result { + let aaguid = Uuid::parse_str(aaguid.as_str()).map_err(|_| "Failed to parse aaguid from string")?; + Ok(PasskeyAuthenticator { aaguid }) } #[wasm_bindgen(js_name = "generateCredentialCreationResponse")] diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index 253b40d..aea9e42 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -109,7 +109,7 @@ impl WebauthnAuthenticator { .authenticator_selection .as_ref() .and_then(|auth_selection| auth_selection.user_verification.as_ref()) - .filter(|user_verif| *user_verif == &UserVerificationRequirement::Required) + .filter(|user_verif| **user_verif == UserVerificationRequirement::Required) .is_some() && (attestation_flags & AttestationFlags::UserVerified as u8 == 0) { @@ -256,7 +256,7 @@ impl WebauthnAuthenticator { if credential_request_options .user_verification .as_ref() - .filter(|user_verif| *user_verif == &UserVerificationRequirement::Required) + .filter(|user_verif| **user_verif == UserVerificationRequirement::Required) .is_some() && (attestation_flags & AttestationFlags::UserVerified as u8 == 0) { diff --git a/src/webauthn/proto/raw_message.rs b/src/webauthn/proto/raw_message.rs index 5712f6d..e459b44 100644 --- a/src/webauthn/proto/raw_message.rs +++ b/src/webauthn/proto/raw_message.rs @@ -285,6 +285,9 @@ impl CredentialPublicKey { .get(&Value::Integer(-2)) .and_then(|val| match val { Value::Bytes(i) => { + if i.len() < 32 { + return None; + } let mut array = [0u8; 32]; array.copy_from_slice(&i[0..32]); Some(array) @@ -297,6 +300,9 @@ impl CredentialPublicKey { .get(&Value::Integer(-3)) .and_then(|val| match val { Value::Bytes(i) => { + if i.len() < 32 { + return None; + } let mut array = [0u8; 32]; array.copy_from_slice(&i[0..32]); Some(Coordinates::Uncompressed { x, y: array }) @@ -329,6 +335,9 @@ impl CredentialPublicKey { .get(&Value::Integer(-2)) .and_then(|val| match val { Value::Bytes(i) => { + if i.len() < 32 { + return None; + } let mut array = [0u8; 32]; array.copy_from_slice(&i[0..32]); Some(array) @@ -341,6 +350,9 @@ impl CredentialPublicKey { .get(&Value::Integer(-3)) .and_then(|val| match val { Value::Bytes(i) => { + if i.len() < 32 { + return None; + } let mut array = [0u8; 32]; array.copy_from_slice(&i[0..32]); Some(Coordinates::Uncompressed { x, y: array }) From 388a5d77fde4f405c48767a9361a758a0e047ddc Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 21 Feb 2024 10:39:58 -0500 Subject: [PATCH 07/10] chore(readme): Updated supported algorithm list --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4f2384..c03baf3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,11 @@ Status is describe by : ✔ as implemented, ❌ as not implemented and ⚠️ as | Raw Message | ✔ | [Spec](https://www.w3.org/TR/webauthn/) | | COSE | ⚠️ | [Spec](https://tools.ietf.org/html/rfc8152) | -For the server side validation, only ECDSA P256 and P384 key validation is supported at this time. Eventually RSA and ECDAA Key validation will be added. +For the server side validation, the following algorithm are implemented: +- `ES256` +- `ES384` +- `ED25519` +- `RS256` #### Universal Authentication Framework (UAF) From 25da4ffa736809dadcb2c575f27f1178b06c35f5 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 21 Feb 2024 11:40:40 -0500 Subject: [PATCH 08/10] chore: Removed RS1 from authenticator support --- src/webauthn/authenticator/mod.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index aea9e42..d1ef42f 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -168,7 +168,7 @@ impl WebauthnAuthenticator { ) } - CoseAlgorithmIdentifier::RSA | CoseAlgorithmIdentifier::RS1 => { + CoseAlgorithmIdentifier::RSA => { let key = rsa::RsaPrivateKey::new(&mut OsRng, 2048).map_err(|_| WebauthnCredentialRequestError::CouldNotGenerateKey)?; let private_key = PrivateKeyResponse { private_key: key.to_pkcs1_der()?.to_bytes().to_vec(), @@ -317,14 +317,7 @@ impl WebauthnAuthenticator { .sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()) .to_vec() } - CoseAlgorithmIdentifier::RS1 => { - let key = rsa::RsaPrivateKey::from_pkcs1_der(&private_key_response.private_key)?; - let signing_key = rsa::pkcs1v15::SigningKey::::new(key); - signing_key - .sign([auth_data_bytes.as_slice(), hash.as_slice()].concat().as_slice()) - .to_vec() - } - CoseAlgorithmIdentifier::NotSupported => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), + _ => return Err(WebauthnCredentialRequestError::AlgorithmNotSupported), }; Ok(PublicKeyCredentialRaw { @@ -345,7 +338,6 @@ impl WebauthnAuthenticator { ) -> Result { //Order of preference for credential type is: Ed25519 > EC2 > RSA > RS1 let mut possible_credential_types = vec![ - CoseAlgorithmIdentifier::RS1, CoseAlgorithmIdentifier::RSA, CoseAlgorithmIdentifier::EC2, CoseAlgorithmIdentifier::Ed25519, From d6807400a74a65a3cdabfb7b6ae62f4ff3581ec6 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 21 Feb 2024 11:51:05 -0500 Subject: [PATCH 09/10] clippy fix --- src/oath/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/oath/mod.rs b/src/oath/mod.rs index a521d3c..dabc485 100644 --- a/src/oath/mod.rs +++ b/src/oath/mod.rs @@ -2,7 +2,6 @@ use hmac::{ digest::{generic_array::GenericArray, FixedOutputReset, InvalidLength, OutputSizeUser}, Mac, SimpleHmac, }; -use sha1::Sha1; use sha2::{Sha256, Sha512}; pub mod hotp; From 87cdcb67ca524f38e6461df87d7a0a728360e70a Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 21 Feb 2024 11:52:02 -0500 Subject: [PATCH 10/10] other fix --- src/oath/mod.rs | 1 + src/webauthn/authenticator/mod.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oath/mod.rs b/src/oath/mod.rs index dabc485..a521d3c 100644 --- a/src/oath/mod.rs +++ b/src/oath/mod.rs @@ -2,6 +2,7 @@ use hmac::{ digest::{generic_array::GenericArray, FixedOutputReset, InvalidLength, OutputSizeUser}, Mac, SimpleHmac, }; +use sha1::Sha1; use sha2::{Sha256, Sha512}; pub mod hotp; diff --git a/src/webauthn/authenticator/mod.rs b/src/webauthn/authenticator/mod.rs index d1ef42f..086bceb 100644 --- a/src/webauthn/authenticator/mod.rs +++ b/src/webauthn/authenticator/mod.rs @@ -25,7 +25,6 @@ use rsa::{ traits::PublicKeyParts, }; use serde_cbor::Value; -use sha1::Sha1; use sha2::Sha256; use uuid::Uuid;