@@ -80,6 +80,35 @@ fn registration_transports(transport: Option<Transport>) -> Vec<String> {
8080 tokens
8181}
8282
83+ fn scrub_aaguid ( authenticator_data : & mut [ u8 ] ) -> Result < ( ) , ResponseSerializationError > {
84+ const AAGUID_OFFSET : usize = 37 ;
85+ const AAGUID_LEN : usize = 16 ;
86+ authenticator_data
87+ . get_mut ( AAGUID_OFFSET ..AAGUID_OFFSET + AAGUID_LEN )
88+ . ok_or_else ( || {
89+ ResponseSerializationError :: AuthenticatorDataError (
90+ "authenticator data too short to scrub AAGUID" . into ( ) ,
91+ )
92+ } ) ?
93+ . fill ( 0 ) ;
94+ Ok ( ( ) )
95+ }
96+
97+ fn build_attestation_object (
98+ format : & str ,
99+ attestation_statement : & Ctap2AttestationStatement ,
100+ authenticator_data_bytes : & [ u8 ] ,
101+ ) -> Result < Vec < u8 > , ResponseSerializationError > {
102+ let attestation_object = AttestationObject {
103+ format,
104+ auth_data : authenticator_data_bytes,
105+ attestation_statement,
106+ } ;
107+
108+ cbor:: to_vec ( & attestation_object)
109+ . map_err ( |e| ResponseSerializationError :: AttestationObjectError ( e. to_string ( ) ) )
110+ }
111+
83112impl WebAuthnIDLResponse for MakeCredentialResponse {
84113 type IdlModel = RegistrationResponseJSON ;
85114 type Context = MakeCredentialRequest ;
@@ -102,11 +131,16 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
102131 let id = base64_url:: encode ( & attested. credential_id ) ;
103132 let raw_id = Base64UrlString :: from ( attested. credential_id . clone ( ) ) ;
104133
105- let authenticator_data_bytes = self
134+ let mut authenticator_data_bytes = self
106135 . authenticator_data
107136 . to_response_bytes ( )
108137 . map_err ( |e| ResponseSerializationError :: AuthenticatorDataError ( e. to_string ( ) ) ) ?;
109138
139+ let scrub_attestation = request. attestation . as_deref ( ) == Some ( "none" ) ;
140+ if scrub_attestation {
141+ scrub_aaguid ( & mut authenticator_data_bytes) ?;
142+ }
143+
110144 let public_key_algorithm = i64:: from (
111145 cose:: read_alg ( & attested. credential_public_key )
112146 . map_err ( |e| ResponseSerializationError :: PublicKeyError ( e. to_string ( ) ) ) ?,
@@ -120,7 +154,14 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
120154 . map ( Base64UrlString :: from) ;
121155
122156 // Build attestation object (CBOR map with authData, fmt, attStmt)
123- let attestation_object_bytes = self . build_attestation_object ( & authenticator_data_bytes) ?;
157+ let none_statement = Ctap2AttestationStatement :: None ( BTreeMap :: new ( ) ) ;
158+ let ( format, attestation_statement) = if scrub_attestation {
159+ ( "none" , & none_statement)
160+ } else {
161+ ( self . format . as_str ( ) , & self . attestation_statement )
162+ } ;
163+ let attestation_object_bytes =
164+ build_attestation_object ( format, attestation_statement, & authenticator_data_bytes) ?;
124165
125166 // WebAuthn getTransports(): the authenticator's getInfo 0x09 transports
126167 // folded with the ceremony transport, unique tokens lexicographically sorted.
@@ -151,20 +192,6 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
151192}
152193
153194impl MakeCredentialResponse {
154- fn build_attestation_object (
155- & self ,
156- authenticator_data_bytes : & [ u8 ] ,
157- ) -> Result < Vec < u8 > , ResponseSerializationError > {
158- let attestation_object = AttestationObject {
159- format : & self . format ,
160- auth_data : authenticator_data_bytes,
161- attestation_statement : & self . attestation_statement ,
162- } ;
163-
164- cbor:: to_vec ( & attestation_object)
165- . map_err ( |e| ResponseSerializationError :: AttestationObjectError ( e. to_string ( ) ) )
166- }
167-
168195 fn build_client_extension_results ( & self ) -> AuthenticationExtensionsClientOutputsJSON {
169196 let mut results = AuthenticationExtensionsClientOutputsJSON :: default ( ) ;
170197 let unsigned_ext = & self . unsigned_extensions_output ;
@@ -374,6 +401,8 @@ pub struct MakeCredentialRequest {
374401 pub exclude : Option < Vec < Ctap2PublicKeyCredentialDescriptor > > ,
375402 /// extensions
376403 pub extensions : Option < MakeCredentialsRequestExtensions > ,
404+ /// Attestation conveyance preference. `Some("none")` scrubs attestation.
405+ pub attestation : Option < String > ,
377406 pub timeout : Duration ,
378407}
379408
@@ -466,6 +495,8 @@ impl FromIdlModel<PublicKeyCredentialCreationOptionsJSON> for MakeCredentialRequ
466495 )
467496 } ,
468497 extensions : inner. extensions ,
498+ // WebAuthn IDL defaults attestation conveyance to "none".
499+ attestation : inner. attestation . or_else ( || Some ( "none" . to_string ( ) ) ) ,
469500 timeout,
470501 } )
471502 }
@@ -629,6 +660,7 @@ impl MakeCredentialRequest {
629660 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
630661 exclude : None ,
631662 extensions : None ,
663+ attestation : None ,
632664 resident_key : None ,
633665 user_verification : UserVerificationRequirement :: Discouraged ,
634666 timeout : Duration :: from_secs ( 10 ) ,
@@ -845,6 +877,7 @@ mod tests {
845877 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
846878 exclude : None ,
847879 extensions : None ,
880+ attestation : Some ( "none" . to_string ( ) ) ,
848881 timeout : Duration :: from_secs ( 30 ) ,
849882 }
850883 }
@@ -1450,6 +1483,7 @@ mod tests {
14501483 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
14511484 exclude : None ,
14521485 extensions : None ,
1486+ attestation : None ,
14531487 timeout : Duration :: from_secs ( 30 ) ,
14541488 }
14551489 }
@@ -1677,6 +1711,114 @@ mod tests {
16771711 }
16781712 }
16791713
1714+ fn create_attested_response ( aaguid : [ u8 ; 16 ] ) -> MakeCredentialResponse {
1715+ use crate :: fido:: { AttestedCredentialData , AuthenticatorData , AuthenticatorDataFlags } ;
1716+ use crate :: proto:: ctap2:: FidoU2fAttestationStmt ;
1717+ use cosey:: Bytes ;
1718+ use serde_bytes:: ByteBuf ;
1719+
1720+ let cose_public_key = cosey:: PublicKey :: P256Key ( cosey:: P256PublicKey {
1721+ x : Bytes :: from_slice ( & [ 0u8 ; 32 ] ) . unwrap ( ) ,
1722+ y : Bytes :: from_slice ( & [ 0u8 ; 32 ] ) . unwrap ( ) ,
1723+ } ) ;
1724+ let credential_public_key = cbor:: to_vec ( & cose_public_key) . unwrap ( ) ;
1725+
1726+ let authenticator_data = AuthenticatorData {
1727+ rp_id_hash : [ 0u8 ; 32 ] ,
1728+ flags : AuthenticatorDataFlags :: USER_PRESENT
1729+ | AuthenticatorDataFlags :: ATTESTED_CREDENTIALS ,
1730+ signature_count : 0 ,
1731+ attested_credential : Some ( AttestedCredentialData {
1732+ aaguid,
1733+ credential_id : vec ! [ 0x01 , 0x02 , 0x03 , 0x04 ] ,
1734+ credential_public_key,
1735+ } ) ,
1736+ extensions : None ,
1737+ raw : None ,
1738+ } ;
1739+
1740+ MakeCredentialResponse {
1741+ format : "fido-u2f" . to_string ( ) ,
1742+ authenticator_data,
1743+ attestation_statement : Ctap2AttestationStatement :: FidoU2F ( FidoU2fAttestationStmt {
1744+ signature : ByteBuf :: from ( vec ! [ 0xAA ; 16 ] ) ,
1745+ certificates : vec ! [ ByteBuf :: from( vec![ 0xBB ; 8 ] ) ] ,
1746+ } ) ,
1747+ enterprise_attestation : None ,
1748+ large_blob_key : None ,
1749+ unsigned_extensions_output : MakeCredentialsResponseUnsignedExtensions :: default ( ) ,
1750+ transport : None ,
1751+ authenticator_transports : None ,
1752+ }
1753+ }
1754+
1755+ #[ test]
1756+ fn attestation_none_conveyance_scrubs_fmt_attstmt_and_aaguid ( ) {
1757+ let response = create_attested_response ( [ 0x11u8 ; 16 ] ) ;
1758+ let mut request = create_test_request ( ) ;
1759+ request. attestation = Some ( "none" . to_string ( ) ) ;
1760+
1761+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1762+
1763+ let auth_data = & model. response . authenticator_data . 0 ;
1764+ assert_eq ! ( & auth_data[ 37 ..53 ] , & [ 0u8 ; 16 ] , "top-level authData AAGUID" ) ;
1765+
1766+ let attestation: cbor:: Value =
1767+ cbor:: from_slice ( & model. response . attestation_object . 0 ) . unwrap ( ) ;
1768+ let cbor:: Value :: Map ( map) = attestation else {
1769+ panic ! ( "attestation object should be a CBOR map" ) ;
1770+ } ;
1771+ let value_for = |key : & str | {
1772+ map. iter ( )
1773+ . find ( |( k, _) | matches ! ( k, cbor:: Value :: Text ( s) if s == key) )
1774+ . map ( |( _, v) | v)
1775+ } ;
1776+ assert ! (
1777+ matches!( value_for( "fmt" ) , Some ( cbor:: Value :: Text ( s) ) if s == "none" ) ,
1778+ "fmt must be scrubbed to none"
1779+ ) ;
1780+ match value_for ( "attStmt" ) {
1781+ Some ( cbor:: Value :: Map ( stmt) ) => assert ! ( stmt. is_empty( ) , "attStmt must be empty" ) ,
1782+ other => panic ! ( "attStmt must be an empty map, got {other:?}" ) ,
1783+ }
1784+ match value_for ( "authData" ) {
1785+ Some ( cbor:: Value :: Bytes ( embedded) ) => {
1786+ assert_eq ! ( & embedded[ 37 ..53 ] , & [ 0u8 ; 16 ] , "embedded authData AAGUID" ) ;
1787+ }
1788+ other => panic ! ( "authData must be CBOR bytes, got {other:?}" ) ,
1789+ }
1790+ }
1791+
1792+ #[ test]
1793+ fn attestation_direct_preserves_attestation ( ) {
1794+ let response = create_attested_response ( [ 0x11u8 ; 16 ] ) ;
1795+ let mut request = create_test_request ( ) ;
1796+ request. attestation = Some ( "direct" . to_string ( ) ) ;
1797+
1798+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1799+
1800+ let auth_data = & model. response . authenticator_data . 0 ;
1801+ assert_eq ! (
1802+ & auth_data[ 37 ..53 ] ,
1803+ & [ 0x11u8 ; 16 ] ,
1804+ "AAGUID must be preserved"
1805+ ) ;
1806+
1807+ let attestation: cbor:: Value =
1808+ cbor:: from_slice ( & model. response . attestation_object . 0 ) . unwrap ( ) ;
1809+ let cbor:: Value :: Map ( map) = attestation else {
1810+ panic ! ( "attestation object should be a CBOR map" ) ;
1811+ } ;
1812+ let fmt = map
1813+ . iter ( )
1814+ . find ( |( k, _) | matches ! ( k, cbor:: Value :: Text ( s) if s == "fmt" ) )
1815+ . map ( |( _, v) | v) ;
1816+ assert ! (
1817+ matches!( fmt, Some ( cbor:: Value :: Text ( s) ) if s == "fido-u2f" ) ,
1818+ "fmt must be preserved"
1819+ ) ;
1820+ }
1821+
16801822 #[ test]
16811823 fn test_response_with_extensions ( ) {
16821824 let mut response = create_test_response ( ) ;
0 commit comments