@@ -100,13 +100,24 @@ pub struct AuthenticatorData<T> {
100100 pub signature_count : u32 ,
101101 pub attested_credential : Option < AttestedCredentialData > ,
102102 pub extensions : Option < T > ,
103+ /// Raw authData bytes as received from the device, preserved verbatim so
104+ /// the RP's signature over authData stays valid. `None` for authData the
105+ /// platform synthesizes (e.g. the U2F upgrade path), which is rebuilt from
106+ /// the fields above.
107+ pub raw : Option < Vec < u8 > > ,
103108}
104109
105110impl < T > AuthenticatorData < T >
106111where
107112 T : Clone + Serialize ,
108113{
109114 pub fn to_response_bytes ( & self ) -> Result < Vec < u8 > , Error > {
115+ // Return the device's authData verbatim. Re-encoding from the parsed
116+ // fields would reorder or drop unmodeled extensions, invalidating the
117+ // authenticator's signature over these exact bytes.
118+ if let Some ( raw) = & self . raw {
119+ return Ok ( raw. clone ( ) ) ;
120+ }
110121 // Name | Length
111122 // -----------------------------------
112123 // rpIdHash | 32
@@ -284,6 +295,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData<T> {
284295 signature_count,
285296 attested_credential,
286297 extensions,
298+ raw : Some ( data. to_vec ( ) ) ,
287299 } )
288300 }
289301 }
@@ -349,6 +361,7 @@ mod tests {
349361 signature_count,
350362 attested_credential : Some ( attested_credential. clone ( ) ) ,
351363 extensions : Some ( extensions. clone ( ) ) ,
364+ raw : None ,
352365 } ;
353366 let webauthn_auth_data = auth_data. to_response_bytes ( ) . unwrap ( ) ;
354367 assert_eq ! ( rp_id_hash, & webauthn_auth_data[ ..32 ] ) ;
@@ -432,6 +445,7 @@ mod tests {
432445 signature_count : 1 ,
433446 attested_credential : Some ( attested_credential) ,
434447 extensions : None ,
448+ raw : None ,
435449 } ;
436450
437451 let bytes = auth_data. to_response_bytes ( ) . unwrap ( ) ;
@@ -445,4 +459,37 @@ mod tests {
445459 crate :: proto:: ctap2:: Ctap2COSEAlgorithmIdentifier :: RS256
446460 ) ;
447461 }
462+
463+ #[ test]
464+ fn to_response_bytes_preserves_extensions_verbatim ( ) {
465+ // The authenticator signs over the exact authData bytes, including the
466+ // extensions block. Re-encoding from the typed struct drops keys it does
467+ // not model (and may reorder the rest), which would break relying-party
468+ // signature verification. The bytes must round-trip unchanged.
469+ use crate :: proto:: ctap2:: Ctap2MakeCredentialsResponseExtensions ;
470+
471+ let flags = AuthenticatorDataFlags :: USER_PRESENT | AuthenticatorDataFlags :: EXTENSION_DATA ;
472+ let mut input = [ 0x11u8 ; 32 ] . to_vec ( ) ;
473+ input. push ( flags. bits ( ) ) ;
474+ input. extend_from_slice ( & [ 0x00 , 0x00 , 0x00 , 0x07 ] ) ; // signCount
475+
476+ // CBOR: { "credBlob": true, "thirdPartyPayment": true }. The second key
477+ // is unmodeled, so the typed struct drops it on re-encode.
478+ input. extend_from_slice ( & [
479+ 0xA2 , // map(2)
480+ 0x68 , b'c' , b'r' , b'e' , b'd' , b'B' , b'l' , b'o' , b'b' , 0xF5 , // "credBlob": true
481+ 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' ,
482+ b'm' , b'e' , b'n' , b't' , 0xF5 , // "thirdPartyPayment": true
483+ ] ) ;
484+
485+ let wrapped = cbor:: to_vec ( & ByteBuf :: from ( input. clone ( ) ) ) . unwrap ( ) ;
486+ let parsed: AuthenticatorData < Ctap2MakeCredentialsResponseExtensions > =
487+ cbor:: from_slice ( & wrapped) . unwrap ( ) ;
488+
489+ assert_eq ! (
490+ parsed. to_response_bytes( ) . unwrap( ) ,
491+ input,
492+ "authenticatorData must be preserved byte-for-byte"
493+ ) ;
494+ }
448495}
0 commit comments