webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

commit ac55944f66d2533d5ab9542f2b54c1dc9d72ea9d
parent b0e379d3648ac7b6d49f0b70da361620bbe442a6
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri, 21 Mar 2025 17:25:02 -0600

fixed bug in u128::decode_from_buffer

Diffstat:
MCargo.toml | 4++--
Msrc/bin.rs | 2+-
Msrc/request/auth.rs | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/request/register.rs | 421+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 656 insertions(+), 3 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -10,7 +10,7 @@ name = "webauthn_rp" readme = "README.md" repository = "https://git.philomathiclife.com/repos/webauthn_rp/" rust-version = "1.85.0" -version = "0.2.6" +version = "0.2.7" [package.metadata.docs.rs] all-features = true @@ -23,7 +23,7 @@ p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } p384 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } precis-profiles = { version = "0.1.11", default-features = false } rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] } -rsa = { version = "0.9.7", default-features = false, features = ["sha2"] } +rsa = { version = "0.9.8", default-features = false, features = ["sha2"] } serde = { version = "1.0.219", default-features = false, features = ["alloc"], optional = true } serde_json = { version = "1.0.140", default-features = false, features = ["alloc"], optional = true } url = { version = "2.5.4", default-features = false } diff --git a/src/bin.rs b/src/bin.rs @@ -258,7 +258,7 @@ impl<'a> DecodeBuffer<'a> for u128 { reason = "we must standardize the endianness to remove ambiguity" )] fn decode_from_buffer(data: &mut &'a [u8]) -> Result<Self, Self::Err> { - data.split_at_checked(8) + data.split_at_checked(16) .ok_or(EncDecErr) .map(|(le_bytes, rem)| { *data = rem; diff --git a/src/request/auth.rs b/src/request/auth.rs @@ -1171,3 +1171,235 @@ impl Ord for AuthenticationServerState { self.challenge.cmp(&other.challenge) } } +#[cfg(test)] +mod tests { + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + use super::{ + super::{ + super::{ + AggErr, + bin::{Decode as _, Encode as _}, + }, + AsciiDomain, AuthTransports, + }, + AllowedCredential, AllowedCredentials, AuthenticationServerState, Challenge, CredentialId, + CredentialSpecificExtension, Credentials as _, Extension, ExtensionReq, PrfInputOwned, + PublicKeyCredentialDescriptor, PublicKeyCredentialRequestOptions, RpId, ServerPrfInfo, + UserVerificationRequirement, + }; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + use rsa::sha2::{Digest as _, Sha256}; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_BYTES: u8 = 0b010_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_TEXT: u8 = 0b011_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_MAP: u8 = 0b101_00000; + #[test] + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + fn ed25519_auth_ser() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); + let mut creds = AllowedCredentials::with_capacity(1); + creds.push(AllowedCredential { + credential: PublicKeyCredentialDescriptor { + id: CredentialId::try_from(vec![0; 16])?, + transports: AuthTransports::NONE, + }, + extension: CredentialSpecificExtension { + prf: Some(PrfInputOwned { + first: Vec::new(), + second: Some(Vec::new()), + ext_info: ExtensionReq::Require, + }), + }, + }); + let mut opts = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?; + opts.user_verification = UserVerificationRequirement::Required; + opts.challenge = Challenge(0); + opts.extensions = Extension { prf: None }; + let client_data_json = br#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); + // We over-allocate by 32 bytes. See [`AuthenticatorAssertion::new`] for more information. + let mut authenticator_data = Vec::with_capacity(164); + authenticator_data.extend_from_slice( + [ + // rpIdHash. + // This will be overwritten later. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // flags. + // UP, UV, and ED (right-to-left). + 0b1000_0101, + // signCount. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + CBOR_MAP | 1, + CBOR_TEXT | 11, + b'h', + b'm', + b'a', + b'c', + b'-', + b's', + b'e', + b'c', + b'r', + b'e', + b't', + CBOR_BYTES | 24, + // Length is 80. + 80, + // Two HMAC outputs concatenated and encrypted. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + .as_slice(), + ); + authenticator_data[..32] + .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice()); + authenticator_data + .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); + authenticator_data.truncate(132); + let server = opts.start_ceremony()?.0; + let server_2 = AuthenticationServerState::decode(server.encode()?.as_slice())?; + assert_eq!(server.challenge.0, server_2.challenge.0); + assert_eq!(server.allow_credentials.len(), 1); + assert_eq!( + server.allow_credentials.len(), + server_2.allow_credentials.len() + ); + assert_eq!( + server.allow_credentials[0].id, + server_2.allow_credentials[0].id + ); + assert!(matches!( + server.allow_credentials[0].ext.prf.unwrap(), + ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require) + )); + assert!(server_2.allow_credentials[0].ext.prf.map_or(false, |prf| { + matches!(prf, ServerPrfInfo::Two(req) if matches!(req, ExtensionReq::Require)) + })); + assert!( + matches!( + server.user_verification, + UserVerificationRequirement::Required + ) && matches!( + server_2.user_verification, + UserVerificationRequirement::Required + ) + ); + assert!(server.extensions.prf.is_none() && server_2.extensions.prf.is_none()); + assert_eq!(server.expiration, server_2.expiration); + Ok(()) + } +} diff --git a/src/request/register.rs b/src/request/register.rs @@ -1744,3 +1744,424 @@ impl Ord for RegistrationServerState { self.challenge.cmp(&other.challenge) } } +#[cfg(test)] +mod tests { + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + use super::{ + super::{ + super::{ + AggErr, + bin::{Decode as _, Encode as _}, + }, + AsciiDomain, + }, + AuthenticatorAttachmentReq, Challenge, CredProtect, CredentialMediationRequirement, + Extension, ExtensionInfo, Hint, PublicKeyCredentialCreationOptions, + PublicKeyCredentialUserEntity, RegistrationServerState, ResidentKeyRequirement, RpId, + UserHandle, UserVerificationRequirement, + }; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + use ed25519_dalek::{Signer as _, SigningKey}; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + use rsa::sha2::{Digest as _, Sha256}; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_UINT: u8 = 0b000_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_NEG: u8 = 0b001_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_BYTES: u8 = 0b010_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_TEXT: u8 = 0b011_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_MAP: u8 = 0b101_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_SIMPLE: u8 = 0b111_00000; + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + const CBOR_TRUE: u8 = CBOR_SIMPLE | 21; + #[test] + #[cfg(all(feature = "custom", feature = "serializable_server_state"))] + fn ed25519_reg_ser() -> Result<(), AggErr> { + let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); + let id = UserHandle::try_from([0; 1].as_slice())?; + let mut opts = PublicKeyCredentialCreationOptions::passkey( + &rp_id, + PublicKeyCredentialUserEntity { + name: "foo".try_into()?, + id, + display_name: None, + }, + Vec::new(), + ); + opts.challenge = Challenge(0); + opts.extensions = Extension { + cred_props: None, + cred_protect: CredProtect::UserVerificationRequired(ExtensionInfo::RequireEnforceValue), + min_pin_length: Some((10, ExtensionInfo::RequireEnforceValue)), + prf: Some(ExtensionInfo::RequireEnforceValue), + }; + let client_data_json = br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAA","origin":"https://example.com","crossOrigin":false}"#.to_vec(); + // We over-allocate by 32 bytes. See [`AuthenticatorAttestation::new`] for more information. + let mut attestation_object = Vec::new(); + attestation_object.extend_from_slice( + [ + CBOR_MAP | 3, + CBOR_TEXT | 3, + b'f', + b'm', + b't', + CBOR_TEXT | 6, + b'p', + b'a', + b'c', + b'k', + b'e', + b'd', + CBOR_TEXT | 7, + b'a', + b't', + b't', + b'S', + b't', + b'm', + b't', + CBOR_MAP | 2, + CBOR_TEXT | 3, + b'a', + b'l', + b'g', + // COSE EdDSA. + CBOR_NEG | 7, + CBOR_TEXT | 3, + b's', + b'i', + b'g', + CBOR_BYTES | 24, + 64, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_TEXT | 8, + b'a', + b'u', + b't', + b'h', + b'D', + b'a', + b't', + b'a', + CBOR_BYTES | 24, + // Length is 154. + 154, + // RP ID HASH. + // This will be overwritten later. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // FLAGS. + // UP, UV, AT, and ED (right-to-left). + 0b1100_0101, + // COUNTER. + // 0 as 32-bit big endian. + 0, + 0, + 0, + 0, + // AAGUID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + // L. + // CREDENTIAL ID length is 16 as 16-bit big endian. + 0, + 16, + // CREDENTIAL ID. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 4, + // COSE kty. + CBOR_UINT | 1, + // COSE OKP. + CBOR_UINT | 1, + // COSE alg. + CBOR_UINT | 3, + // COSE EdDSA. + CBOR_NEG | 7, + // COSE OKP crv. + CBOR_NEG, + // COSE Ed25519. + CBOR_UINT | 6, + // COSE OKP x. + CBOR_NEG | 1, + CBOR_BYTES | 24, + // Length is 32. + 32, + // Compressed-y coordinate. + // This will be overwritten later. + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CBOR_MAP | 3, + CBOR_TEXT | 11, + b'c', + b'r', + b'e', + b'd', + b'P', + b'r', + b'o', + b't', + b'e', + b'c', + b't', + // userVerificationRequired. + CBOR_UINT | 3, + // CBOR text of length 11. + CBOR_TEXT | 11, + b'h', + b'm', + b'a', + b'c', + b'-', + b's', + b'e', + b'c', + b'r', + b'e', + b't', + CBOR_TRUE, + CBOR_TEXT | 12, + b'm', + b'i', + b'n', + b'P', + b'i', + b'n', + b'L', + b'e', + b'n', + b'g', + b't', + b'h', + CBOR_UINT | 16, + ] + .as_slice(), + ); + attestation_object + .extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice()); + let sig_key = SigningKey::from_bytes(&[0; 32]); + let ver_key = sig_key.verifying_key(); + let pub_key = ver_key.as_bytes(); + attestation_object[107..139] + .copy_from_slice(Sha256::digest(rp_id.as_ref().as_bytes()).as_slice()); + attestation_object[188..220].copy_from_slice(pub_key); + let sig = sig_key.sign(&attestation_object[107..]); + attestation_object[32..96].copy_from_slice(sig.to_bytes().as_slice()); + attestation_object.truncate(261); + let server = opts.start_ceremony()?.0; + let server_2 = RegistrationServerState::decode(server.encode()?.as_slice())?; + assert!( + matches!(server.mediation, CredentialMediationRequirement::Optional) + && matches!(server_2.mediation, CredentialMediationRequirement::Optional) + ); + assert_eq!(server.challenge.0, server_2.challenge.0); + assert_eq!(server.pub_key_cred_params.0, server_2.pub_key_cred_params.0); + assert!( + matches!(server.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + && matches!(server_2.authenticator_selection.authenticator_attachment, AuthenticatorAttachmentReq::None(hint) if matches!(hint, Hint::None)) + ); + assert!( + matches!( + server.authenticator_selection.resident_key, + ResidentKeyRequirement::Required + ) && matches!( + server_2.authenticator_selection.resident_key, + ResidentKeyRequirement::Required + ) + ); + assert!( + matches!( + server.authenticator_selection.user_verification, + UserVerificationRequirement::Required + ) && matches!( + server_2.authenticator_selection.user_verification, + UserVerificationRequirement::Required + ) + ); + assert!(server.extensions.cred_props.is_none() && server_2.extensions.cred_props.is_none()); + assert!( + matches!( + server.extensions.cred_protect, + CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue) + ) && matches!( + server_2.extensions.cred_protect, + CredProtect::UserVerificationRequired(info) if matches!(info, ExtensionInfo::RequireEnforceValue) + ) + ); + let (pin, info) = server.extensions.min_pin_length.unwrap(); + assert!( + server_2 + .extensions + .min_pin_length + .map_or(false, |(pin2, info2)| pin == pin2 + && matches!(info, ExtensionInfo::RequireEnforceValue) + && matches!(info2, ExtensionInfo::RequireEnforceValue)) + ); + assert!( + matches!( + server.extensions.prf.unwrap(), + ExtensionInfo::RequireEnforceValue + ) && server_2.extensions.prf.map_or(false, |info| matches!( + info, + ExtensionInfo::RequireEnforceValue + )) + ); + assert_eq!(server.expiration, server_2.expiration); + Ok(()) + } +}