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:
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(())
+ }
+}