@@ -29,7 +29,7 @@ use crate::{
2929 cbor, cbor:: Value , cose, parse_unsigned_prf, Ctap2AttestationStatement ,
3030 Ctap2COSEAlgorithmIdentifier , Ctap2CredentialType , Ctap2GetInfoResponse ,
3131 Ctap2MakeCredentialsResponseExtensions , Ctap2PublicKeyCredentialDescriptor ,
32- Ctap2PublicKeyCredentialRpEntity , Ctap2PublicKeyCredentialUserEntity ,
32+ Ctap2PublicKeyCredentialRpEntity , Ctap2PublicKeyCredentialUserEntity , Ctap2Transport ,
3333 UnsignedPrfOutput ,
3434 } ,
3535 } ,
@@ -60,13 +60,29 @@ struct AttestationObject<'a> {
6060 attestation_statement : & ' a Ctap2AttestationStatement ,
6161}
6262
63+ /// Maps the active transport to AuthenticatorTransport tokens for the registration
64+ /// `transports` member. The list is deduplicated and lexicographically sorted per
65+ /// WebAuthn L3 §5.2.1.1, and is empty when the transport is unknown.
66+ fn registration_transports ( transport : Option < crate :: Transport > ) -> Vec < String > {
67+ let mut tokens: Vec < String > = transport
68+ . into_iter ( )
69+ . map ( Ctap2Transport :: from)
70+ . filter_map ( |t| serde_json:: to_value ( t) . ok ( ) )
71+ . filter_map ( |v| v. as_str ( ) . map ( str:: to_owned) )
72+ . collect ( ) ;
73+ tokens. sort ( ) ;
74+ tokens. dedup ( ) ;
75+ tokens
76+ }
77+
6378impl WebAuthnIDLResponse for MakeCredentialResponse {
6479 type IdlModel = RegistrationResponseJSON ;
6580 type Context = MakeCredentialRequest ;
6681
6782 fn to_idl_model (
6883 & self ,
6984 request : & Self :: Context ,
85+ transport : Option < crate :: Transport > ,
7086 ) -> Result < Self :: IdlModel , ResponseSerializationError > {
7187 // The AT flag MUST be set on makeCredential responses per CTAP 2.2 §6.1.
7288 let attested = self
@@ -102,8 +118,7 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
102118 // Build attestation object (CBOR map with authData, fmt, attStmt)
103119 let attestation_object_bytes = self . build_attestation_object ( & authenticator_data_bytes) ?;
104120
105- // Get transports (we don't have direct access, so return empty for now)
106- let transports = Vec :: new ( ) ;
121+ let transports = registration_transports ( transport) ;
107122
108123 // Build client extension results
109124 let client_extension_results = self . build_client_extension_results ( ) ;
@@ -1434,7 +1449,7 @@ mod tests {
14341449
14351450 let response = create_test_response ( ) ;
14361451 let request = create_test_request ( ) ;
1437- let json = response. to_json_string ( & request, JsonFormat :: default ( ) ) ;
1452+ let json = response. to_json_string ( & request, None , JsonFormat :: default ( ) ) ;
14381453 assert ! ( json. is_ok( ) ) ;
14391454
14401455 let json_str = json. unwrap ( ) ;
@@ -1489,7 +1504,7 @@ mod tests {
14891504 fn test_response_to_idl_model ( ) {
14901505 let response = create_test_response ( ) ;
14911506 let request = create_test_request ( ) ;
1492- let model = response. to_idl_model ( & request) . unwrap ( ) ;
1507+ let model = response. to_idl_model ( & request, None ) . unwrap ( ) ;
14931508
14941509 // Verify the credential ID
14951510 assert_eq ! ( model. raw_id. 0 , vec![ 0x01 , 0x02 , 0x03 , 0x04 ] ) ;
@@ -1503,6 +1518,41 @@ mod tests {
15031518 assert ! ( model. response. transports. is_empty( ) ) ;
15041519 }
15051520
1521+ #[ test]
1522+ fn test_response_to_idl_model_populates_transports ( ) {
1523+ // WebAuthn L3 §5.2.1.1: the registration `transports` member reports the
1524+ // transport the credential was created over, as AuthenticatorTransport tokens.
1525+ // Both the FIDO2 and U2F-downgrade paths converge on this serialization.
1526+ let response = create_test_response ( ) ;
1527+ let request = create_test_request ( ) ;
1528+
1529+ for ( transport, token) in [
1530+ ( crate :: Transport :: Usb , "usb" ) ,
1531+ ( crate :: Transport :: Ble , "ble" ) ,
1532+ ( crate :: Transport :: Nfc , "nfc" ) ,
1533+ ( crate :: Transport :: Hybrid , "hybrid" ) ,
1534+ ] {
1535+ let model = response. to_idl_model ( & request, Some ( transport) ) . unwrap ( ) ;
1536+ assert_eq ! ( model. response. transports, vec![ token. to_string( ) ] ) ;
1537+ }
1538+
1539+ // The token reaches the JSON wire format too.
1540+ let json = response
1541+ . to_json_string (
1542+ & request,
1543+ Some ( crate :: Transport :: Nfc ) ,
1544+ crate :: ops:: webauthn:: idl:: response:: JsonFormat :: default ( ) ,
1545+ )
1546+ . unwrap ( ) ;
1547+ let parsed: serde_json:: Value = serde_json:: from_str ( & json) . unwrap ( ) ;
1548+ let transports = parsed[ "response" ] [ "transports" ] . as_array ( ) . unwrap ( ) ;
1549+ assert_eq ! ( transports, & vec![ serde_json:: Value :: from( "nfc" ) ] ) ;
1550+
1551+ // An unknown transport leaves the list empty.
1552+ let model = response. to_idl_model ( & request, None ) . unwrap ( ) ;
1553+ assert ! ( model. response. transports. is_empty( ) ) ;
1554+ }
1555+
15061556 #[ test]
15071557 fn test_response_emits_spki_for_es256 ( ) {
15081558 // The test fixture builds an ES256 P-256 credential, so getPublicKey()
@@ -1512,7 +1562,7 @@ mod tests {
15121562 // by the secp256r1 OID and the uncompressed point.
15131563 let response = create_test_response ( ) ;
15141564 let request = create_test_request ( ) ;
1515- let model = response. to_idl_model ( & request) . unwrap ( ) ;
1565+ let model = response. to_idl_model ( & request, None ) . unwrap ( ) ;
15161566
15171567 let public_key_bytes = model
15181568 . response
@@ -1536,7 +1586,7 @@ mod tests {
15361586 fn test_response_attestation_object_format ( ) {
15371587 let response = create_test_response ( ) ;
15381588 let request = create_test_request ( ) ;
1539- let model = response. to_idl_model ( & request) . unwrap ( ) ;
1589+ let model = response. to_idl_model ( & request, None ) . unwrap ( ) ;
15401590
15411591 // Decode the attestation object
15421592 let attestation_bytes = model. response . attestation_object . 0 ;
@@ -1581,7 +1631,7 @@ mod tests {
15811631 } ;
15821632
15831633 let request = create_test_request ( ) ;
1584- let model = response. to_idl_model ( & request) . unwrap ( ) ;
1634+ let model = response. to_idl_model ( & request, None ) . unwrap ( ) ;
15851635
15861636 // Verify cred_props extension
15871637 let cred_props = model. client_extension_results . cred_props . as_ref ( ) . unwrap ( ) ;
0 commit comments