Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions libwebauthn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,12 @@ macro_rules! unwrap_field {
use pin::{PinNotSetReason, PinRequestReason};
pub(crate) use unwrap_field;

#[derive(Debug)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transport {
Usb,
Ble,
Nfc,
Hybrid,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -213,6 +215,27 @@ impl PartialEq for PinNotSetUpdate {
}
}

pub fn available_transports() -> Vec<Transport> {
vec![Transport::Usb, Transport::Ble]
/// The transports usable right now. USB is always available. BLE and Hybrid
/// (caBLE) need a Bluetooth adapter. NFC needs a reader and a compiled backend.
pub async fn available_transports() -> Vec<Transport> {
let mut transports = vec![Transport::Usb];
// BLE and Hybrid (caBLE) both need a Bluetooth adapter.
if transport::ble::is_available().await {
transports.push(Transport::Ble);
transports.push(Transport::Hybrid);
}
if transport::nfc::is_nfc_available() {
transports.push(Transport::Nfc);
}
transports
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn available_transports_always_includes_usb() {
assert!(available_transports().await.contains(&Transport::Usb));
}
}
118 changes: 115 additions & 3 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ use crate::{
cbor, cbor::Value, cose, parse_unsigned_prf, Ctap2AttestationStatement,
Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2GetInfoResponse,
Ctap2MakeCredentialsResponseExtensions, Ctap2PublicKeyCredentialDescriptor,
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity,
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, Ctap2Transport,
UnsignedPrfOutput,
},
},
transport::AuthTokenData,
Transport,
};

use super::timeout::DEFAULT_TIMEOUT;
Expand All @@ -47,6 +48,10 @@ pub struct MakeCredentialResponse {
pub enterprise_attestation: Option<bool>,
pub large_blob_key: Option<Vec<u8>>,
pub unsigned_extensions_output: MakeCredentialsResponseUnsignedExtensions,
/// Transport the credential was created over, stamped by the channel.
pub transport: Option<Transport>,
/// Transports the authenticator advertised in getInfo (0x09), if any.
pub authenticator_transports: Option<Vec<String>>,
}

/// Serializable attestation object for CBOR encoding.
Expand All @@ -60,6 +65,21 @@ struct AttestationObject<'a> {
attestation_statement: &'a Ctap2AttestationStatement,
}

/// Maps the active transport to AuthenticatorTransport tokens for the registration
/// `transports` member. The list is deduplicated and lexicographically sorted per
/// WebAuthn L3 §5.2.1.1, and is empty when the transport is unknown.
fn registration_transports(transport: Option<Transport>) -> Vec<String> {
let mut tokens: Vec<String> = transport
.into_iter()
.map(Ctap2Transport::from)
.filter_map(|t| serde_json::to_value(t).ok())
.filter_map(|v| v.as_str().map(str::to_owned))
.collect();
tokens.sort();
tokens.dedup();
tokens
}

impl WebAuthnIDLResponse for MakeCredentialResponse {
type IdlModel = RegistrationResponseJSON;
type Context = MakeCredentialRequest;
Expand Down Expand Up @@ -102,8 +122,12 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
// Build attestation object (CBOR map with authData, fmt, attStmt)
let attestation_object_bytes = self.build_attestation_object(&authenticator_data_bytes)?;

// Get transports (we don't have direct access, so return empty for now)
let transports = Vec::new();
// WebAuthn getTransports(): the authenticator's getInfo 0x09 transports
// folded with the ceremony transport, unique tokens lexicographically sorted.
let mut transports = self.authenticator_transports.clone().unwrap_or_default();
transports.extend(registration_transports(self.transport));
transports.sort();
transports.dedup();

// Build client extension results
let client_extension_results = self.build_client_extension_results();
Expand Down Expand Up @@ -1409,6 +1433,8 @@ mod tests {
enterprise_attestation: None,
large_blob_key: None,
unsigned_extensions_output: MakeCredentialsResponseUnsignedExtensions::default(),
transport: None,
authenticator_transports: None,
}
}

Expand Down Expand Up @@ -1503,6 +1529,92 @@ mod tests {
assert!(model.response.transports.is_empty());
}

#[test]
fn test_response_to_idl_model_populates_transports() {
// WebAuthn L3 §5.2.1.1: the registration `transports` member reports the
// transport the credential was created over, as AuthenticatorTransport tokens.
// Both the FIDO2 and U2F-downgrade paths converge on this serialization.
let mut response = create_test_response();
let request = create_test_request();

for (transport, token) in [
(Transport::Usb, "usb"),
(Transport::Ble, "ble"),
(Transport::Nfc, "nfc"),
(Transport::Hybrid, "hybrid"),
] {
response.transport = Some(transport);
let model = response.to_idl_model(&request).unwrap();
assert_eq!(model.response.transports, vec![token.to_string()]);
}

// The token reaches the JSON wire format too.
response.transport = Some(Transport::Nfc);
let json = response
.to_json_string(
&request,
crate::ops::webauthn::idl::response::JsonFormat::default(),
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let transports = parsed["response"]["transports"].as_array().unwrap();
assert_eq!(transports, &vec![serde_json::Value::from("nfc")]);

// An unknown transport leaves the list empty.
response.transport = None;
let model = response.to_idl_model(&request).unwrap();
assert!(model.response.transports.is_empty());
}

#[test]
fn test_response_to_idl_model_transports_from_get_info() {
// The authenticator's getInfo (0x09) transports are folded with the
// ceremony transport, as unique tokens in lexicographical order.
let mut response = create_test_response();
let request = create_test_request();

// Reported out of order with a duplicate; the ceremony transport (ble) folds in.
response.transport = Some(Transport::Ble);
response.authenticator_transports = Some(vec![
"usb".to_string(),
"nfc".to_string(),
"usb".to_string(),
]);
let model = response.to_idl_model(&request).unwrap();
assert_eq!(
model.response.transports,
vec!["ble".to_string(), "nfc".to_string(), "usb".to_string()]
);

// A ceremony transport already in the reported list is not duplicated.
response.transport = Some(Transport::Usb);
response.authenticator_transports = Some(vec!["usb".to_string(), "nfc".to_string()]);
let model = response.to_idl_model(&request).unwrap();
assert_eq!(
model.response.transports,
vec!["nfc".to_string(), "usb".to_string()]
);

// No reported transports leaves just the ceremony transport.
response.authenticator_transports = None;
let model = response.to_idl_model(&request).unwrap();
assert_eq!(model.response.transports, vec!["usb".to_string()]);

// Unknown tokens pass through, folded with the ceremony transport.
response.transport = Some(Transport::Ble);
response.authenticator_transports =
Some(vec!["smart-card".to_string(), "custom".to_string()]);
let model = response.to_idl_model(&request).unwrap();
assert_eq!(
model.response.transports,
vec![
"ble".to_string(),
"custom".to_string(),
"smart-card".to_string()
]
);
}

#[test]
fn test_response_emits_spki_for_es256() {
// The test fixture builds an ES256 P-256 credential, so getPublicKey()
Expand Down
12 changes: 12 additions & 0 deletions libwebauthn/src/proto/ctap2/model.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::proto::ctap1::Ctap1Transport;
use crate::Transport;
use crate::{
ops::webauthn::idl::create::PublicKeyCredentialUserEntity, pin::PinUvAuthProtocol,
webauthn::Error,
Expand Down Expand Up @@ -167,6 +168,17 @@ impl From<&Ctap1Transport> for Ctap2Transport {
}
}

impl From<Transport> for Ctap2Transport {
fn from(transport: Transport) -> Ctap2Transport {
match transport {
Transport::Usb => Ctap2Transport::Usb,
Transport::Ble => Ctap2Transport::Ble,
Transport::Nfc => Ctap2Transport::Nfc,
Transport::Hybrid => Ctap2Transport::Hybrid,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Ctap2PublicKeyCredentialDescriptor {
pub id: ByteBuf,
Expand Down
2 changes: 2 additions & 0 deletions libwebauthn/src/proto/ctap2/model/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ impl Ctap2MakeCredentialResponse {
enterprise_attestation: self.enterprise_attestation,
large_blob_key: self.large_blob_key.map(|x| x.into_vec()),
unsigned_extensions_output,
transport: None,
authenticator_transports: info.and_then(|i| i.transports.clone()),
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions libwebauthn/src/transport/ble/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::transport::channel::{
use crate::transport::device::SupportedProtocols;
use crate::transport::error::TransportError;
use crate::webauthn::error::Error;
use crate::Transport;
use crate::UvUpdate;

use super::btleplug::manager::SupportedRevisions;
Expand Down Expand Up @@ -81,6 +82,10 @@ impl Display for BleChannel<'_> {
impl<'a> Channel for BleChannel<'a> {
type UxUpdate = UvUpdate;

fn transport(&self) -> Transport {
Transport::Ble
}

async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
Ok(self.revision.into())
}
Expand Down
5 changes: 5 additions & 0 deletions libwebauthn/src/transport/cable/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::transport::{
channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenStore,
};
use crate::webauthn::error::Error;
use crate::Transport;
use crate::UvUpdate;

use super::known_devices::CableKnownDevice;
Expand Down Expand Up @@ -125,6 +126,10 @@ impl From<UvUpdate> for CableUxUpdate {
impl Channel for CableChannel {
type UxUpdate = CableUxUpdate;

fn transport(&self) -> Transport {
Transport::Hybrid
}

async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
Ok(SupportedProtocols::fido2_only())
}
Expand Down
7 changes: 7 additions & 0 deletions libwebauthn/src/transport/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::proto::{
ctap2::cbor::{CborRequest, CborResponse},
};
use crate::webauthn::error::Error;
use crate::Transport;
use crate::UvUpdate;

use async_trait::async_trait;
Expand Down Expand Up @@ -64,6 +65,12 @@ 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.
fn transport(&self) -> Transport {
Transport::Usb
}

async fn apdu_send(&mut self, request: &ApduRequest, timeout: Duration) -> Result<(), Error>;
async fn apdu_recv(&mut self, timeout: Duration) -> Result<ApduResponse, Error>;

Expand Down
5 changes: 5 additions & 0 deletions libwebauthn/src/transport/hid/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::transport::hid::framing::{
HidCommand, HidMessage, HidMessageParser, HidMessageParserState,
};
use crate::webauthn::error::{Error, PlatformError};
use crate::Transport;
use crate::UvUpdate;

use super::device::get_hidapi;
Expand Down Expand Up @@ -505,6 +506,10 @@ impl Display for HidChannel<'_> {
impl Channel for HidChannel<'_> {
type UxUpdate = UvUpdate;

fn transport(&self) -> Transport {
Transport::Usb
}

async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
let cbor_supported = self.init.caps.contains(Caps::CBOR);
let apdu_supported = !self.init.caps.contains(Caps::NO_MSG);
Expand Down
7 changes: 7 additions & 0 deletions libwebauthn/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ pub mod hid;
pub mod mock;
#[cfg(any(feature = "nfc-backend-pcsc", feature = "nfc-backend-libnfc"))]
pub mod nfc;
// No NFC backend compiled: a stub so callers need not gate on the feature.
#[cfg(not(any(feature = "nfc-backend-pcsc", feature = "nfc-backend-libnfc")))]
pub mod nfc {
pub fn is_nfc_available() -> bool {
false
}
}

mod channel;
#[allow(clippy::module_inception)]
Expand Down
5 changes: 5 additions & 0 deletions libwebauthn/src/transport/nfc/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::transport::channel::{
use crate::transport::device::SupportedProtocols;
use crate::transport::error::TransportError;
use crate::webauthn::Error;
use crate::Transport;
use crate::UvUpdate;

use super::commands::{command_ctap_msg, command_get_response};
Expand Down Expand Up @@ -259,6 +260,10 @@ where
{
type UxUpdate = UvUpdate;

fn transport(&self) -> Transport {
Transport::Nfc
}

async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
Ok(self.supported)
}
Expand Down
6 changes: 4 additions & 2 deletions libwebauthn/src/webauthn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@ where
};
trace!(?op, "WebAuthn MakeCredential request");
let protocol = negotiate_protocol(self, op.is_downgradable()).await?;
match protocol {
let mut response = match protocol {
FidoProtocol::FIDO2 => make_credential_fido2(self, op).await,
FidoProtocol::U2F => make_credential_u2f(self, op).await,
}
}?;
response.transport = Some(self.transport());
Ok(response)
}

#[instrument(skip_all, fields(dev = % self))]
Expand Down
Loading