Skip to content

Commit d834534

Browse files
feat(transport): advertise nfc and hybrid and populate registration transports
Add NFC and hybrid/caBLE to the public Transport enum and report compiled-in transports from available_transports. NFC is gated behind the nfc-backend features and hybrid is always present. Thread the active transport from the channel into registration response serialization so the response transports member carries the AuthenticatorTransport token. The list is deduplicated, sorted, and stays empty when the transport is unknown. Both the FIDO2 and U2F-downgrade paths are covered.
1 parent d593d6b commit d834534

18 files changed

Lines changed: 174 additions & 33 deletions

libwebauthn/examples/ceremony/webauthn_ble.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7575
.unwrap();
7676
println!("WebAuthn MakeCredential response: {:?}", response);
7777

78-
match response.to_json_string(&make_credentials_request, JsonFormat::Prettified) {
78+
match response.to_json_string(
79+
&make_credentials_request,
80+
channel.transport(),
81+
JsonFormat::Prettified,
82+
) {
7983
Ok(response_json) => {
8084
println!(
8185
"WebAuthn MakeCredential response (JSON):\n{}",
@@ -113,7 +117,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
113117
println!("WebAuthn GetAssertion response: {:?}", response);
114118

115119
for assertion in &response.assertions {
116-
match assertion.to_json_string(&get_assertion, JsonFormat::Prettified) {
120+
match assertion.to_json_string(
121+
&get_assertion,
122+
channel.transport(),
123+
JsonFormat::Prettified,
124+
) {
117125
Ok(assertion_json) => {
118126
println!("WebAuthn GetAssertion response (JSON):\n{}", assertion_json);
119127
}

libwebauthn/examples/ceremony/webauthn_cable.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
9292

9393
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
9494
let response_json = response
95-
.to_json_string(&request, JsonFormat::Prettified)
95+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
9696
.expect("Failed to serialize MakeCredential response");
9797
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
9898

libwebauthn/examples/ceremony/webauthn_cable_wss.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
110110

111111
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
112112
let response_json = response
113-
.to_json_string(&request, JsonFormat::Prettified)
113+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
114114
.expect("Failed to serialize MakeCredential response");
115115
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
116116
}
@@ -182,7 +182,7 @@ async fn run_get_assertion(
182182
let response = retry_user_errors!(channel.webauthn_get_assertion(&request)).unwrap();
183183
for assertion in &response.assertions {
184184
let assertion_json = assertion
185-
.to_json_string(&request, JsonFormat::Prettified)
185+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
186186
.expect("Failed to serialize GetAssertion response");
187187
println!("WebAuthn GetAssertion response (JSON):\n{assertion_json}");
188188
}

libwebauthn/examples/ceremony/webauthn_hid.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7979
.unwrap();
8080
println!("WebAuthn MakeCredential response: {:?}", response);
8181

82-
match response.to_json_string(&make_credentials_request, JsonFormat::Prettified) {
82+
match response.to_json_string(
83+
&make_credentials_request,
84+
channel.transport(),
85+
JsonFormat::Prettified,
86+
) {
8387
Ok(response_json) => {
8488
println!(
8589
"WebAuthn MakeCredential response (JSON):\n{}",
@@ -117,7 +121,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
117121
println!("WebAuthn GetAssertion response: {:?}", response);
118122

119123
for assertion in &response.assertions {
120-
match assertion.to_json_string(&get_assertion, JsonFormat::Prettified) {
124+
match assertion.to_json_string(
125+
&get_assertion,
126+
channel.transport(),
127+
JsonFormat::Prettified,
128+
) {
121129
Ok(assertion_json) => {
122130
println!("WebAuthn GetAssertion response (JSON):\n{}", assertion_json);
123131
}

libwebauthn/examples/ceremony/webauthn_nfc.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7777
let response =
7878
retry_user_errors!(channel.webauthn_make_credential(&make_credentials_request)).unwrap();
7979
let response_json = response
80-
.to_json_string(&make_credentials_request, JsonFormat::Prettified)
80+
.to_json_string(
81+
&make_credentials_request,
82+
channel.transport(),
83+
JsonFormat::Prettified,
84+
)
8185
.expect("Failed to serialize MakeCredential response");
8286
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
8387

@@ -100,7 +104,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
100104
let response = retry_user_errors!(channel.webauthn_get_assertion(&get_assertion)).unwrap();
101105
for assertion in &response.assertions {
102106
let assertion_json = assertion
103-
.to_json_string(&get_assertion, JsonFormat::Prettified)
107+
.to_json_string(&get_assertion, channel.transport(), JsonFormat::Prettified)
104108
.expect("Failed to serialize GetAssertion response");
105109
println!("WebAuthn GetAssertion response (JSON):\n{assertion_json}");
106110
}

libwebauthn/examples/features/webauthn_prf_cable.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ async fn create() -> Result<(), Box<dyn Error>> {
124124
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
125125

126126
let response_json = response
127-
.to_json_string(&request, JsonFormat::Prettified)
127+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
128128
.expect("Failed to serialize MakeCredential response");
129129
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
130130

@@ -191,7 +191,7 @@ async fn get(credential_id: Option<&str>) -> Result<(), Box<dyn Error>> {
191191

192192
for (num, assertion) in response.assertions.iter().enumerate() {
193193
let assertion_json = assertion
194-
.to_json_string(&request, JsonFormat::Prettified)
194+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
195195
.expect("Failed to serialize GetAssertion response");
196196
println!("Assertion {num} (JSON):\n{assertion_json}");
197197
}

libwebauthn/examples/features/webauthn_related_origins_hid.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7676

7777
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
7878
let response_json = response
79-
.to_json_string(&request, JsonFormat::Prettified)
79+
.to_json_string(&request, channel.transport(), JsonFormat::Prettified)
8080
.expect("Failed to serialize MakeCredential response");
8181
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
8282
}

libwebauthn/src/lib.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ macro_rules! unwrap_field {
120120
use pin::{PinNotSetReason, PinRequestReason};
121121
pub(crate) use unwrap_field;
122122

123-
#[derive(Debug)]
123+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124124
pub enum Transport {
125125
Usb,
126126
Ble,
127+
Nfc,
128+
Hybrid,
127129
}
128130

129131
#[derive(Debug, Clone)]
@@ -213,6 +215,36 @@ impl PartialEq for PinNotSetUpdate {
213215
}
214216
}
215217

218+
/// Transports compiled into this build. Hybrid/caBLE is always included. Using it
219+
/// at runtime still needs a BLE adapter (see `transport::cable::is_available`).
220+
/// NFC appears only when an `nfc-backend-*` feature is enabled.
216221
pub fn available_transports() -> Vec<Transport> {
217-
vec![Transport::Usb, Transport::Ble]
222+
[
223+
Transport::Usb,
224+
Transport::Ble,
225+
Transport::Hybrid,
226+
#[cfg(any(feature = "nfc-backend-pcsc", feature = "nfc-backend-libnfc"))]
227+
Transport::Nfc,
228+
]
229+
.into_iter()
230+
.collect()
231+
}
232+
233+
#[cfg(test)]
234+
mod tests {
235+
use super::*;
236+
237+
#[test]
238+
fn available_transports_reports_compiled_in() {
239+
let transports = available_transports();
240+
assert!(transports.contains(&Transport::Usb));
241+
assert!(transports.contains(&Transport::Ble));
242+
assert!(transports.contains(&Transport::Hybrid));
243+
244+
let nfc_compiled = cfg!(any(
245+
feature = "nfc-backend-pcsc",
246+
feature = "nfc-backend-libnfc"
247+
));
248+
assert_eq!(transports.contains(&Transport::Nfc), nfc_compiled);
249+
}
218250
}

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ impl WebAuthnIDLResponse for Assertion {
489489
fn to_idl_model(
490490
&self,
491491
request: &Self::Context,
492+
_transport: Option<crate::Transport>,
492493
) -> Result<Self::IdlModel, ResponseSerializationError> {
493494
// Get credential ID - either from credential_id field or from authenticator_data
494495
let credential_id_bytes = self
@@ -1306,7 +1307,7 @@ mod tests {
13061307

13071308
let assertion = create_test_assertion();
13081309
let request = create_test_request();
1309-
let json = assertion.to_json_string(&request, JsonFormat::default());
1310+
let json = assertion.to_json_string(&request, None, JsonFormat::default());
13101311
assert!(json.is_ok());
13111312

13121313
let json_str = json.unwrap();
@@ -1332,7 +1333,7 @@ mod tests {
13321333
fn test_assertion_to_idl_model() {
13331334
let assertion = create_test_assertion();
13341335
let request = create_test_request();
1335-
let model = assertion.to_idl_model(&request).unwrap();
1336+
let model = assertion.to_idl_model(&request, None).unwrap();
13361337

13371338
// Verify the credential ID
13381339
assert_eq!(model.raw_id.0, vec![0x01, 0x02, 0x03, 0x04]);
@@ -1354,7 +1355,7 @@ mod tests {
13541355
));
13551356

13561357
let request = create_test_request();
1357-
let model = assertion.to_idl_model(&request).unwrap();
1358+
let model = assertion.to_idl_model(&request, None).unwrap();
13581359

13591360
// Verify user handle is present
13601361
assert!(model.response.user_handle.is_some());
@@ -1381,7 +1382,7 @@ mod tests {
13811382
});
13821383

13831384
let request = create_test_request();
1384-
let model = assertion.to_idl_model(&request).unwrap();
1385+
let model = assertion.to_idl_model(&request, None).unwrap();
13851386

13861387
// Verify extension outputs - PRF should be set with correct values
13871388
let prf = model.client_extension_results.prf.as_ref().unwrap();
@@ -1403,7 +1404,7 @@ mod tests {
14031404
});
14041405

14051406
let request = create_test_request();
1406-
let model = assertion.to_idl_model(&request).unwrap();
1407+
let model = assertion.to_idl_model(&request, None).unwrap();
14071408
assert_eq!(model.client_extension_results.appid, Some(true));
14081409

14091410
// The output should also round-trip through the JSON wire format.
@@ -1427,7 +1428,7 @@ mod tests {
14271428
});
14281429

14291430
let request = create_test_request();
1430-
let model = assertion.to_idl_model(&request).unwrap();
1431+
let model = assertion.to_idl_model(&request, None).unwrap();
14311432
assert_eq!(model.client_extension_results.appid, None);
14321433

14331434
let json = serde_json::to_value(&model.client_extension_results).unwrap();

libwebauthn/src/ops/webauthn/idl/response.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,33 @@ pub trait WebAuthnIDLResponse: Sized {
5555
/// Context required for serialization (e.g., client data JSON).
5656
type Context;
5757

58-
/// Converts this response to a JSON-serializable IDL model.
58+
/// Converts this response to a JSON-serializable IDL model. `transport` is the
59+
/// transport the ceremony ran over, used to populate the registration
60+
/// `transports` member. Pass `None` when it is unknown.
5961
fn to_idl_model(
6062
&self,
6163
ctx: &Self::Context,
64+
transport: Option<crate::Transport>,
6265
) -> Result<Self::IdlModel, ResponseSerializationError>;
6366

6467
/// Serializes this response to a `serde_json::Value`.
6568
fn to_json_value(
6669
&self,
6770
ctx: &Self::Context,
71+
transport: Option<crate::Transport>,
6872
) -> Result<serde_json::Value, ResponseSerializationError> {
69-
let model = self.to_idl_model(ctx)?;
73+
let model = self.to_idl_model(ctx, transport)?;
7074
Ok(serde_json::to_value(&model)?)
7175
}
7276

7377
/// Serializes this response to a JSON string.
7478
fn to_json_string(
7579
&self,
7680
ctx: &Self::Context,
81+
transport: Option<crate::Transport>,
7782
format: JsonFormat,
7883
) -> Result<String, ResponseSerializationError> {
79-
let value = self.to_json_value(ctx)?;
84+
let value = self.to_json_value(ctx, transport)?;
8085
match format {
8186
JsonFormat::Minified => Ok(serde_json::to_string(&value)?),
8287
JsonFormat::Prettified => Ok(serde_json::to_string_pretty(&value)?),

0 commit comments

Comments
 (0)