@@ -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
@@ -468,6 +497,8 @@ impl FromIdlModel<PublicKeyCredentialCreationOptionsJSON> for MakeCredentialRequ
468497 )
469498 } ,
470499 extensions : inner. extensions ,
500+ // WebAuthn IDL defaults attestation conveyance to "none".
501+ attestation : inner. attestation . or_else ( || Some ( "none" . to_string ( ) ) ) ,
471502 timeout,
472503 } )
473504 }
@@ -631,6 +662,7 @@ impl MakeCredentialRequest {
631662 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
632663 exclude : None ,
633664 extensions : None ,
665+ attestation : None ,
634666 resident_key : None ,
635667 user_verification : UserVerificationRequirement :: Discouraged ,
636668 timeout : Duration :: from_secs ( 10 ) ,
@@ -847,6 +879,7 @@ mod tests {
847879 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
848880 exclude : None ,
849881 extensions : None ,
882+ attestation : Some ( "none" . to_string ( ) ) ,
850883 timeout : Duration :: from_secs ( 30 ) ,
851884 }
852885 }
@@ -1504,6 +1537,7 @@ mod tests {
15041537 algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
15051538 exclude : None ,
15061539 extensions : None ,
1540+ attestation : None ,
15071541 timeout : Duration :: from_secs ( 30 ) ,
15081542 }
15091543 }
@@ -1731,6 +1765,114 @@ mod tests {
17311765 }
17321766 }
17331767
1768+ fn create_attested_response ( aaguid : [ u8 ; 16 ] ) -> MakeCredentialResponse {
1769+ use crate :: fido:: { AttestedCredentialData , AuthenticatorData , AuthenticatorDataFlags } ;
1770+ use crate :: proto:: ctap2:: FidoU2fAttestationStmt ;
1771+ use cosey:: Bytes ;
1772+ use serde_bytes:: ByteBuf ;
1773+
1774+ let cose_public_key = cosey:: PublicKey :: P256Key ( cosey:: P256PublicKey {
1775+ x : Bytes :: from_slice ( & [ 0u8 ; 32 ] ) . unwrap ( ) ,
1776+ y : Bytes :: from_slice ( & [ 0u8 ; 32 ] ) . unwrap ( ) ,
1777+ } ) ;
1778+ let credential_public_key = cbor:: to_vec ( & cose_public_key) . unwrap ( ) ;
1779+
1780+ let authenticator_data = AuthenticatorData {
1781+ rp_id_hash : [ 0u8 ; 32 ] ,
1782+ flags : AuthenticatorDataFlags :: USER_PRESENT
1783+ | AuthenticatorDataFlags :: ATTESTED_CREDENTIALS ,
1784+ signature_count : 0 ,
1785+ attested_credential : Some ( AttestedCredentialData {
1786+ aaguid,
1787+ credential_id : vec ! [ 0x01 , 0x02 , 0x03 , 0x04 ] ,
1788+ credential_public_key,
1789+ } ) ,
1790+ extensions : None ,
1791+ raw : None ,
1792+ } ;
1793+
1794+ MakeCredentialResponse {
1795+ format : "fido-u2f" . to_string ( ) ,
1796+ authenticator_data,
1797+ attestation_statement : Ctap2AttestationStatement :: FidoU2F ( FidoU2fAttestationStmt {
1798+ signature : ByteBuf :: from ( vec ! [ 0xAA ; 16 ] ) ,
1799+ certificates : vec ! [ ByteBuf :: from( vec![ 0xBB ; 8 ] ) ] ,
1800+ } ) ,
1801+ enterprise_attestation : None ,
1802+ large_blob_key : None ,
1803+ unsigned_extensions_output : MakeCredentialsResponseUnsignedExtensions :: default ( ) ,
1804+ transport : None ,
1805+ authenticator_transports : None ,
1806+ }
1807+ }
1808+
1809+ #[ test]
1810+ fn attestation_none_conveyance_scrubs_fmt_attstmt_and_aaguid ( ) {
1811+ let response = create_attested_response ( [ 0x11u8 ; 16 ] ) ;
1812+ let mut request = create_test_request ( ) ;
1813+ request. attestation = Some ( "none" . to_string ( ) ) ;
1814+
1815+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1816+
1817+ let auth_data = & model. response . authenticator_data . 0 ;
1818+ assert_eq ! ( & auth_data[ 37 ..53 ] , & [ 0u8 ; 16 ] , "top-level authData AAGUID" ) ;
1819+
1820+ let attestation: cbor:: Value =
1821+ cbor:: from_slice ( & model. response . attestation_object . 0 ) . unwrap ( ) ;
1822+ let cbor:: Value :: Map ( map) = attestation else {
1823+ panic ! ( "attestation object should be a CBOR map" ) ;
1824+ } ;
1825+ let value_for = |key : & str | {
1826+ map. iter ( )
1827+ . find ( |( k, _) | matches ! ( k, cbor:: Value :: Text ( s) if s == key) )
1828+ . map ( |( _, v) | v)
1829+ } ;
1830+ assert ! (
1831+ matches!( value_for( "fmt" ) , Some ( cbor:: Value :: Text ( s) ) if s == "none" ) ,
1832+ "fmt must be scrubbed to none"
1833+ ) ;
1834+ match value_for ( "attStmt" ) {
1835+ Some ( cbor:: Value :: Map ( stmt) ) => assert ! ( stmt. is_empty( ) , "attStmt must be empty" ) ,
1836+ other => panic ! ( "attStmt must be an empty map, got {other:?}" ) ,
1837+ }
1838+ match value_for ( "authData" ) {
1839+ Some ( cbor:: Value :: Bytes ( embedded) ) => {
1840+ assert_eq ! ( & embedded[ 37 ..53 ] , & [ 0u8 ; 16 ] , "embedded authData AAGUID" ) ;
1841+ }
1842+ other => panic ! ( "authData must be CBOR bytes, got {other:?}" ) ,
1843+ }
1844+ }
1845+
1846+ #[ test]
1847+ fn attestation_direct_preserves_attestation ( ) {
1848+ let response = create_attested_response ( [ 0x11u8 ; 16 ] ) ;
1849+ let mut request = create_test_request ( ) ;
1850+ request. attestation = Some ( "direct" . to_string ( ) ) ;
1851+
1852+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1853+
1854+ let auth_data = & model. response . authenticator_data . 0 ;
1855+ assert_eq ! (
1856+ & auth_data[ 37 ..53 ] ,
1857+ & [ 0x11u8 ; 16 ] ,
1858+ "AAGUID must be preserved"
1859+ ) ;
1860+
1861+ let attestation: cbor:: Value =
1862+ cbor:: from_slice ( & model. response . attestation_object . 0 ) . unwrap ( ) ;
1863+ let cbor:: Value :: Map ( map) = attestation else {
1864+ panic ! ( "attestation object should be a CBOR map" ) ;
1865+ } ;
1866+ let fmt = map
1867+ . iter ( )
1868+ . find ( |( k, _) | matches ! ( k, cbor:: Value :: Text ( s) if s == "fmt" ) )
1869+ . map ( |( _, v) | v) ;
1870+ assert ! (
1871+ matches!( fmt, Some ( cbor:: Value :: Text ( s) ) if s == "fido-u2f" ) ,
1872+ "fmt must be preserved"
1873+ ) ;
1874+ }
1875+
17341876 #[ test]
17351877 fn test_response_with_extensions ( ) {
17361878 let mut response = create_test_response ( ) ;
0 commit comments