@@ -49,6 +49,8 @@ pub struct MakeCredentialResponse {
4949 pub unsigned_extensions_output : MakeCredentialsResponseUnsignedExtensions ,
5050 /// Transport the credential was created over, stamped by the channel.
5151 pub transport : Option < crate :: Transport > ,
52+ /// Transports the authenticator advertised in getInfo (0x09), if any.
53+ pub authenticator_transports : Option < Vec < String > > ,
5254}
5355
5456/// Serializable attestation object for CBOR encoding.
@@ -119,7 +121,17 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
119121 // Build attestation object (CBOR map with authData, fmt, attStmt)
120122 let attestation_object_bytes = self . build_attestation_object ( & authenticator_data_bytes) ?;
121123
122- let transports = registration_transports ( self . transport ) ;
124+ // Prefer the authenticator's getInfo 0x09 transports; else the ceremony
125+ // transport. WebAuthn L3 §5.2.1.1: unique tokens, lexicographically sorted.
126+ let transports = match self . authenticator_transports . as_ref ( ) {
127+ Some ( reported) if !reported. is_empty ( ) => {
128+ let mut tokens = reported. clone ( ) ;
129+ tokens. sort ( ) ;
130+ tokens. dedup ( ) ;
131+ tokens
132+ }
133+ _ => registration_transports ( self . transport ) ,
134+ } ;
123135
124136 // Build client extension results
125137 let client_extension_results = self . build_client_extension_results ( ) ;
@@ -1426,6 +1438,7 @@ mod tests {
14261438 large_blob_key : None ,
14271439 unsigned_extensions_output : MakeCredentialsResponseUnsignedExtensions :: default ( ) ,
14281440 transport : None ,
1441+ authenticator_transports : None ,
14291442 }
14301443 }
14311444
@@ -1557,6 +1570,42 @@ mod tests {
15571570 assert ! ( model. response. transports. is_empty( ) ) ;
15581571 }
15591572
1573+ #[ test]
1574+ fn test_response_to_idl_model_transports_from_get_info ( ) {
1575+ // The authenticator's getInfo (0x09) transports source the registration
1576+ // `transports` member as unique tokens in lexicographical order, taking
1577+ // precedence over the single ceremony transport (no union).
1578+ let mut response = create_test_response ( ) ;
1579+ let request = create_test_request ( ) ;
1580+
1581+ // Reported out of order with a duplicate; ceremony transport differs.
1582+ response. transport = Some ( crate :: Transport :: Ble ) ;
1583+ response. authenticator_transports = Some ( vec ! [
1584+ "usb" . to_string( ) ,
1585+ "nfc" . to_string( ) ,
1586+ "usb" . to_string( ) ,
1587+ ] ) ;
1588+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1589+ assert_eq ! (
1590+ model. response. transports,
1591+ vec![ "nfc" . to_string( ) , "usb" . to_string( ) ]
1592+ ) ;
1593+
1594+ // An empty reported list falls back to the ceremony transport.
1595+ response. authenticator_transports = Some ( Vec :: new ( ) ) ;
1596+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1597+ assert_eq ! ( model. response. transports, vec![ "ble" . to_string( ) ] ) ;
1598+
1599+ // Unknown tokens pass through unchanged.
1600+ response. authenticator_transports =
1601+ Some ( vec ! [ "smart-card" . to_string( ) , "custom" . to_string( ) ] ) ;
1602+ let model = response. to_idl_model ( & request) . unwrap ( ) ;
1603+ assert_eq ! (
1604+ model. response. transports,
1605+ vec![ "custom" . to_string( ) , "smart-card" . to_string( ) ]
1606+ ) ;
1607+ }
1608+
15601609 #[ test]
15611610 fn test_response_emits_spki_for_es256 ( ) {
15621611 // The test fixture builds an ES256 P-256 credential, so getPublicKey()
0 commit comments