diff --git a/libwebauthn/src/lib.rs b/libwebauthn/src/lib.rs index f6ac9bad..0d2c6c49 100644 --- a/libwebauthn/src/lib.rs +++ b/libwebauthn/src/lib.rs @@ -144,6 +144,18 @@ pub enum Transport { Hybrid, } +impl Transport { + /// The WebAuthn authenticator attachment modality this transport implies. + /// libwebauthn only drives roaming authenticators, so all map to `"cross-platform"`. + pub fn authenticator_attachment(self) -> &'static str { + match self { + Transport::Usb | Transport::Ble | Transport::Nfc | Transport::Hybrid => { + "cross-platform" + } + } + } +} + #[derive(Debug, Clone)] #[cfg_attr(any(test, feature = "virt"), derive(PartialEq))] pub enum UvUpdate { diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index 9f62d545..1a3fbc90 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -30,6 +30,7 @@ use crate::{ Ctap2PublicKeyCredentialUserEntity, }, webauthn::CtapError, + Transport, }; use super::timeout::DEFAULT_TIMEOUT; @@ -478,6 +479,8 @@ pub struct Assertion { pub credentials_count: Option, pub user_selected: Option, pub unsigned_extensions_output: Option, + /// Transport the assertion was produced over, stamped by the channel. + pub transport: Option, } impl WebAuthnIDLResponse for Assertion { @@ -522,7 +525,9 @@ impl WebAuthnIDLResponse for Assertion { signature: Base64UrlString::from(self.signature.clone()), user_handle, }, - authenticator_attachment: None, + authenticator_attachment: self + .transport + .map(|t| t.authenticator_attachment().to_string()), client_extension_results, r#type: "public-key".to_string(), }) @@ -1422,6 +1427,7 @@ mod tests { credentials_count: None, user_selected: None, unsigned_extensions_output: None, + transport: None, } } @@ -1480,6 +1486,33 @@ mod tests { assert_eq!(model.response.signature.0, vec![0xDE, 0xAD, 0xC0, 0xDE]); } + #[test] + fn test_assertion_to_idl_model_populates_attachment() { + // libwebauthn drives roaming authenticators, so every known transport + // yields authenticatorAttachment "cross-platform" (WebAuthn L3 §5.1). + let mut assertion = create_test_assertion(); + let request = create_test_request(); + + for transport in [ + Transport::Usb, + Transport::Ble, + Transport::Nfc, + Transport::Hybrid, + ] { + assertion.transport = Some(transport); + let model = assertion.to_idl_model(&request).unwrap(); + assert_eq!( + model.authenticator_attachment.as_deref(), + Some("cross-platform") + ); + } + + // An unknown transport omits the field. + assertion.transport = None; + let model = assertion.to_idl_model(&request).unwrap(); + assert_eq!(model.authenticator_attachment, None); + } + #[test] fn test_assertion_with_user_handle() { use crate::proto::ctap2::Ctap2PublicKeyCredentialUserEntity; diff --git a/libwebauthn/src/ops/webauthn/large_blob.rs b/libwebauthn/src/ops/webauthn/large_blob.rs index e85bc389..20552004 100644 --- a/libwebauthn/src/ops/webauthn/large_blob.rs +++ b/libwebauthn/src/ops/webauthn/large_blob.rs @@ -1063,6 +1063,10 @@ mod tests { .await .expect("webauthn_get_assertion should succeed"); assert_eq!(response.assertions.len(), 1); + assert_eq!( + response.assertions[0].transport, + Some(crate::Transport::Usb) + ); let large_blob = response.assertions[0] .unsigned_extensions_output .as_ref() @@ -1184,6 +1188,9 @@ mod tests { .await .expect("get_assertion should succeed"); assert_eq!(response.assertions.len(), 2); + for a in &response.assertions { + assert_eq!(a.transport, Some(crate::Transport::Usb)); + } let blob = |i: usize| { response.assertions[i] .unsigned_extensions_output diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index 177a6dd7..d54ffd46 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -185,7 +185,9 @@ impl WebAuthnIDLResponse for MakeCredentialResponse { public_key_algorithm, attestation_object: Base64UrlString::from(attestation_object_bytes), }, - authenticator_attachment: None, + authenticator_attachment: self + .transport + .map(|t| t.authenticator_attachment().to_string()), client_extension_results, r#type: "public-key".to_string(), }) @@ -1814,6 +1816,33 @@ mod tests { assert!(model.response.transports.is_empty()); } + #[test] + fn test_response_to_idl_model_populates_attachment() { + // libwebauthn drives roaming authenticators, so every known transport + // yields authenticatorAttachment "cross-platform" (WebAuthn L3 §5.1). + let mut response = create_test_response(); + let request = create_test_request(); + + for transport in [ + Transport::Usb, + Transport::Ble, + Transport::Nfc, + Transport::Hybrid, + ] { + response.transport = Some(transport); + let model = response.to_idl_model(&request).unwrap(); + assert_eq!( + model.authenticator_attachment.as_deref(), + Some("cross-platform") + ); + } + + // An unknown transport omits the field. + response.transport = None; + let model = response.to_idl_model(&request).unwrap(); + assert_eq!(model.authenticator_attachment, None); + } + #[test] fn test_response_to_idl_model_transports_from_get_info() { // The authenticator's getInfo (0x09) transports are folded with the diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index de0f746f..707b0292 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -683,6 +683,7 @@ impl Ctap2GetAssertionResponse { credentials_count: self.credentials_count, user_selected: self.user_selected, unsigned_extensions_output, + transport: None, } } } diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index 29601be6..461db570 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -73,8 +73,9 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { async fn status(&self) -> ChannelStatus; async fn close(&mut self); - /// The transport this channel speaks over, used to populate the registration - /// response `transports` member. + /// The transport this channel speaks over. Drives the registration response + /// `transports` member and the `authenticatorAttachment` of both registration + /// and assertion responses. fn transport(&self) -> Transport { Transport::Usb } diff --git a/libwebauthn/src/webauthn.rs b/libwebauthn/src/webauthn.rs index 386bd4c4..e3285229 100644 --- a/libwebauthn/src/webauthn.rs +++ b/libwebauthn/src/webauthn.rs @@ -218,10 +218,15 @@ where }; trace!(?op, "WebAuthn GetAssertion request"); let protocol = negotiate_protocol(self, op.is_downgradable()).await?; - match protocol { + let mut response = match protocol { FidoProtocol::FIDO2 => get_assertion_fido2(self, op).await, FidoProtocol::U2F => get_assertion_u2f(self, op).await, + }?; + let transport = self.transport(); + for assertion in &mut response.assertions { + assertion.transport = Some(transport); } + Ok(response) } }