diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index 1d5f2cf..bbb5c3c 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -100,6 +100,11 @@ pub struct AuthenticatorData { pub signature_count: u32, pub attested_credential: Option, pub extensions: Option, + /// Raw authData bytes as received from the device, preserved verbatim so + /// the RP's signature over authData stays valid. `None` for authData the + /// platform synthesizes (e.g. the U2F upgrade path), which is rebuilt from + /// the fields above. + pub raw: Option>, } impl AuthenticatorData @@ -107,6 +112,12 @@ where T: Clone + Serialize, { pub fn to_response_bytes(&self) -> Result, Error> { + // Return the device's authData verbatim. Re-encoding from the parsed + // fields would reorder or drop unmodeled extensions, invalidating the + // authenticator's signature over these exact bytes. + if let Some(raw) = &self.raw { + return Ok(raw.clone()); + } // Name | Length // ----------------------------------- // rpIdHash | 32 @@ -284,6 +295,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData { signature_count, attested_credential, extensions, + raw: Some(data.to_vec()), }) } } @@ -349,6 +361,7 @@ mod tests { signature_count, attested_credential: Some(attested_credential.clone()), extensions: Some(extensions.clone()), + raw: None, }; let webauthn_auth_data = auth_data.to_response_bytes().unwrap(); assert_eq!(rp_id_hash, &webauthn_auth_data[..32]); @@ -432,6 +445,7 @@ mod tests { signature_count: 1, attested_credential: Some(attested_credential), extensions: None, + raw: None, }; let bytes = auth_data.to_response_bytes().unwrap(); @@ -445,4 +459,37 @@ mod tests { crate::proto::ctap2::Ctap2COSEAlgorithmIdentifier::RS256 ); } + + #[test] + fn to_response_bytes_preserves_extensions_verbatim() { + // The authenticator signs over the exact authData bytes, including the + // extensions block. Re-encoding from the typed struct drops keys it does + // not model (and may reorder the rest), which would break relying-party + // signature verification. The bytes must round-trip unchanged. + use crate::proto::ctap2::Ctap2MakeCredentialsResponseExtensions; + + let flags = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::EXTENSION_DATA; + let mut input = [0x11u8; 32].to_vec(); + input.push(flags.bits()); + input.extend_from_slice(&[0x00, 0x00, 0x00, 0x07]); // signCount + + // CBOR: { "credBlob": true, "thirdPartyPayment": true }. The second key + // is unmodeled, so the typed struct drops it on re-encode. + input.extend_from_slice(&[ + 0xA2, // map(2) + 0x68, b'c', b'r', b'e', b'd', b'B', b'l', b'o', b'b', 0xF5, // "credBlob": true + 0x71, b't', b'h', b'i', b'r', b'd', b'P', b'a', b'r', b't', b'y', b'P', b'a', b'y', + b'm', b'e', b'n', b't', 0xF5, // "thirdPartyPayment": true + ]); + + let wrapped = cbor::to_vec(&ByteBuf::from(input.clone())).unwrap(); + let parsed: AuthenticatorData = + cbor::from_slice(&wrapped).unwrap(); + + assert_eq!( + parsed.to_response_bytes().unwrap(), + input, + "authenticatorData must be preserved byte-for-byte" + ); + } } diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index cb45c78..62e51ca 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -143,6 +143,7 @@ impl UpgradableResponse for Regis signature_count, attested_credential: Some(attested_cred_data), extensions: None, + raw: None, }; // Let attestationStatement be a CBOR map (see "attStmtTemplate" in Generating an Attestation Object [WebAuthn]) @@ -201,6 +202,7 @@ impl UpgradableResponse for SignResponse { signature_count, attested_credential: None, extensions: None, + raw: None, }; // Let authenticatorGetAssertionResponse be a CBOR map with the following keys whose values are as follows: [..] diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index 4992376..2deaf92 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -1213,6 +1213,7 @@ mod tests { signature_count: 1, attested_credential: None, extensions: None, + raw: None, }; Assertion { diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index 43cf057..f368a81 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -1388,6 +1388,7 @@ mod tests { signature_count: 0, attested_credential: Some(attested_credential), extensions: None, + raw: None, }; MakeCredentialResponse { diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index f7039bc..cba6e6d 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -563,6 +563,7 @@ mod tests { signature_count: 0, attested_credential: None, extensions: None, + raw: None, }, signature: ByteBuf::from(vec![0u8; 32]), user: None,