From b2121c2cb4e782af570c39ad36d5f47025d4730c Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Thu, 28 May 2026 22:15:11 +0100 Subject: [PATCH 1/2] feat(pin): add PersistentTokenStore trait and in-memory store --- Cargo.lock | 1 + libwebauthn/Cargo.toml | 1 + libwebauthn/src/{pin.rs => pin/mod.rs} | 2 + libwebauthn/src/pin/persistent_token.rs | 173 ++++++++++++++++++++++++ libwebauthn/src/transport/channel.rs | 8 ++ 5 files changed, 185 insertions(+) rename libwebauthn/src/{pin.rs => pin/mod.rs} (99%) create mode 100644 libwebauthn/src/pin/persistent_token.rs diff --git a/Cargo.lock b/Cargo.lock index 4a1b8386..30437f97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2004,6 +2004,7 @@ dependencies = [ "url", "uuid", "x509-parser", + "zeroize", ] [[package]] diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 09f32c96..8fd45f77 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -87,6 +87,7 @@ aes = "0.8.2" hmac = "0.12.1" cbc = { version = "0.1", features = ["alloc"] } hkdf = "0.12" +zeroize = { version = "1.8", features = ["derive"] } text_io = "0.1" tungstenite = { version = "0.26.2" } tokio-tungstenite = { version = "0.26", features = [ diff --git a/libwebauthn/src/pin.rs b/libwebauthn/src/pin/mod.rs similarity index 99% rename from libwebauthn/src/pin.rs rename to libwebauthn/src/pin/mod.rs index ffb5b86f..ba74b7eb 100644 --- a/libwebauthn/src/pin.rs +++ b/libwebauthn/src/pin/mod.rs @@ -30,6 +30,8 @@ use sha2::{Digest, Sha256}; use tracing::{error, instrument, warn}; use x509_parser::nom::AsBytes; +pub mod persistent_token; + use crate::{ proto::{ ctap2::{Ctap2, Ctap2ClientPinRequest, Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol}, diff --git a/libwebauthn/src/pin/persistent_token.rs b/libwebauthn/src/pin/persistent_token.rs new file mode 100644 index 00000000..74876458 --- /dev/null +++ b/libwebauthn/src/pin/persistent_token.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; +use std::fmt; +use std::sync::Arc; + +use async_trait::async_trait; +use tokio::sync::Mutex; +use tracing::{debug, trace}; +use zeroize::ZeroizeOnDrop; + +use crate::proto::ctap2::Ctap2PinUvAuthProtocol; + +/// Opaque identifier for a stored persistent-token record. Random per record. +pub type PersistentTokenRecordId = String; + +/// A persistent pinUvAuthToken (`pcmr`) together with the data needed to recognize +/// the authenticator it belongs to and to reuse the token on later connections. +#[derive(Clone, ZeroizeOnDrop)] +pub struct PersistentTokenRecord { + /// Decrypted pcmr token; the HMAC key used to authenticate reuse. + pub persistent_token: Vec, + /// PIN/UV auth protocol the token was minted under. + #[zeroize(skip)] + pub pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, + /// 128-bit device identifier recovered from `encIdentifier`; the recognition key. + #[zeroize(skip)] + pub device_identifier: [u8; 16], + /// Authenticator AAGUID; a non-secret label, used only for orphan reaping. + #[zeroize(skip)] + pub aaguid: [u8; 16], +} + +impl fmt::Debug for PersistentTokenRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PersistentTokenRecord") + .field("persistent_token", &"") + .field("pin_uv_auth_protocol", &self.pin_uv_auth_protocol) + .field("device_identifier", &self.device_identifier) + .field("aaguid", &self.aaguid) + .finish() + } +} + +/// Caller-supplied store for persistent pinUvAuthTokens (`pcmr`), surviving the +/// authenticator power cycle so a credential manager need not re-prompt for the PIN +/// on every launch or replug. +/// +/// # Security +/// +/// Each [`PersistentTokenRecord::persistent_token`] is a cleartext, long-lived bearer +/// secret. Implementations MUST persist it with confidentiality equivalent to other +/// credential secrets: an OS keyring, or encrypted-at-rest with OS access control. +/// Implementations MUST NOT write it to world- or group-readable files, to logs, or to +/// unprotected sync/backup. A leaked token lets an attacker, with no PIN and no user +/// presence, perform read-only credential management on that one authenticator +/// (enumerate RPs, usernames, display names, user handles, credential metadata). It +/// grants no assertion, creation, update, or deletion. +/// +/// libwebauthn ships only the in-memory [`MemoryPersistentTokenStore`] and leaves the +/// choice of any durable backend to the embedder. +/// +/// All methods are infallible by design: a failed read behaves as a cache miss and the +/// flow falls back to a normal PIN/UV ceremony, and a failed write is best-effort. +#[async_trait] +pub trait PersistentTokenStore: fmt::Debug + Send + Sync { + /// Returns every stored record. Recognition trial-decrypts `encIdentifier` against + /// each, so the whole set is enumerated on connect. + async fn list(&self) -> Vec<(PersistentTokenRecordId, PersistentTokenRecord)>; + /// Inserts or replaces the record under `id`. + async fn put(&self, id: &PersistentTokenRecordId, record: &PersistentTokenRecord); + /// Removes the record under `id`, if present. + async fn delete(&self, id: &PersistentTokenRecordId); +} + +/// In-memory [`PersistentTokenStore`], holding records for the lifetime of the process. +/// Suitable for tests and for long-lived processes such as a system daemon. +#[derive(Debug, Default, Clone)] +pub struct MemoryPersistentTokenStore { + records: Arc>>, +} + +impl MemoryPersistentTokenStore { + pub fn new() -> Self { + Self { + records: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +#[async_trait] +impl PersistentTokenStore for MemoryPersistentTokenStore { + async fn list(&self) -> Vec<(PersistentTokenRecordId, PersistentTokenRecord)> { + let records = self.records.lock().await; + debug!(count = records.len(), "Listing persistent token records"); + records + .iter() + .map(|(id, record)| (id.clone(), record.clone())) + .collect() + } + + async fn put(&self, id: &PersistentTokenRecordId, record: &PersistentTokenRecord) { + debug!(?id, "Storing persistent token record"); + trace!(?record); + self.records.lock().await.insert(id.clone(), record.clone()); + } + + async fn delete(&self, id: &PersistentTokenRecordId) { + debug!(?id, "Deleting persistent token record"); + self.records.lock().await.remove(id); + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn sample_record() -> PersistentTokenRecord { + PersistentTokenRecord { + persistent_token: vec![0xAB; 32], + pin_uv_auth_protocol: Ctap2PinUvAuthProtocol::Two, + device_identifier: [0x11; 16], + aaguid: [0x22; 16], + } + } + + #[tokio::test] + async fn put_list_delete_round_trip() { + let store = MemoryPersistentTokenStore::new(); + assert!(store.list().await.is_empty()); + + let id = "record-1".to_string(); + store.put(&id, &sample_record()).await; + + let listed = store.list().await; + assert_eq!(listed.len(), 1); + let (listed_id, listed_record) = &listed[0]; + assert_eq!(listed_id, &id); + assert_eq!(listed_record.persistent_token, vec![0xAB; 32]); + assert_eq!(listed_record.device_identifier, [0x11; 16]); + + store.delete(&id).await; + assert!(store.list().await.is_empty()); + } + + #[tokio::test] + async fn put_replaces_existing_id() { + let store = MemoryPersistentTokenStore::new(); + let id = "record-1".to_string(); + store.put(&id, &sample_record()).await; + + let mut replacement = sample_record(); + replacement.persistent_token = vec![0xCD; 32]; + store.put(&id, &replacement).await; + + let listed = store.list().await; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].1.persistent_token, vec![0xCD; 32]); + } + + #[test] + fn debug_redacts_token() { + let rendered = format!("{:?}", sample_record()); + assert!(rendered.contains("")); + // The token bytes (0xAB repeated) must never appear in any rendering. + assert!(!rendered.contains("171, 171")); + assert!(!rendered.contains("ab, ab")); + } + + #[test] + fn record_is_zeroize_on_drop() { + fn assert_zeroize_on_drop() {} + assert_zeroize_on_drop::(); + } +} diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index 7bea3487..da8bde91 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -1,6 +1,8 @@ use std::fmt::{Debug, Display}; +use std::sync::Arc; use std::time::Duration; +use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::ctap2::{ Ctap2AuthTokenPermissionRole, Ctap2PinUvAuthProtocol, Ctap2UserVerificationOperation, }; @@ -164,4 +166,10 @@ pub trait Ctap2AuthTokenStore { } false } + + /// Caller-supplied persistent pinUvAuthToken (pcmr) store, if one is configured. + /// Defaults to `None`; only channels wired with a store override this. + fn persistent_token_store(&self) -> Option> { + None + } } From 28cfe9f019fa96d4f03468670a96b442da0ec8f1 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 30 May 2026 18:22:43 +0100 Subject: [PATCH 2/2] feat(transport): thread persistent token store through ChannelSettings --- libwebauthn-tests/tests/basic_ctap1.rs | 4 ++-- libwebauthn-tests/tests/basic_ctap2.rs | 4 ++-- libwebauthn-tests/tests/pin_protocols.rs | 6 +++--- libwebauthn-tests/tests/preflight.rs | 16 +++++++-------- libwebauthn-tests/tests/prf.rs | 20 +++++++++---------- libwebauthn/examples/ceremony/u2f_ble.rs | 4 ++-- libwebauthn/examples/ceremony/u2f_hid.rs | 4 ++-- libwebauthn/examples/ceremony/u2f_nfc.rs | 4 ++-- libwebauthn/examples/ceremony/webauthn_ble.rs | 4 ++-- .../examples/ceremony/webauthn_cable.rs | 4 ++-- .../examples/ceremony/webauthn_cable_wss.rs | 11 ++++++---- libwebauthn/examples/ceremony/webauthn_hid.rs | 4 ++-- libwebauthn/examples/ceremony/webauthn_nfc.rs | 4 ++-- .../examples/features/device_selection_hid.rs | 4 ++-- libwebauthn/examples/features/prf_replay.rs | 4 ++-- .../features/webauthn_extensions_hid.rs | 4 ++-- .../features/webauthn_preflight_hid.rs | 4 ++-- .../examples/features/webauthn_prf_hid.rs | 4 ++-- .../features/webauthn_related_origins_hid.rs | 4 ++-- .../management/authenticator_config_hid.rs | 4 ++-- .../examples/management/bio_enrollment_hid.rs | 4 ++-- .../examples/management/change_pin_hid.rs | 4 ++-- .../management/cred_management_hid.rs | 4 ++-- libwebauthn/src/lib.rs | 4 ++-- libwebauthn/src/transport/ble/channel.rs | 13 +++++++++++- libwebauthn/src/transport/ble/device.rs | 5 +++-- libwebauthn/src/transport/cable/channel.rs | 7 +++++++ .../src/transport/cable/known_devices.rs | 4 +++- .../src/transport/cable/qr_code_device.rs | 4 +++- libwebauthn/src/transport/channel.rs | 11 ++++++++++ libwebauthn/src/transport/device.rs | 4 ++-- libwebauthn/src/transport/hid/channel.rs | 16 +++++++++++++-- libwebauthn/src/transport/hid/device.rs | 6 +++--- libwebauthn/src/transport/mod.rs | 2 +- libwebauthn/src/transport/nfc/channel.rs | 18 +++++++++++++++-- libwebauthn/src/transport/nfc/device.rs | 17 +++++++++------- libwebauthn/src/transport/nfc/libnfc/mod.rs | 5 +++-- libwebauthn/src/transport/nfc/pcsc/mod.rs | 5 +++-- 38 files changed, 159 insertions(+), 91 deletions(-) diff --git a/libwebauthn-tests/tests/basic_ctap1.rs b/libwebauthn-tests/tests/basic_ctap1.rs index 20d0d86d..b86e279f 100644 --- a/libwebauthn-tests/tests/basic_ctap1.rs +++ b/libwebauthn-tests/tests/basic_ctap1.rs @@ -1,7 +1,7 @@ use std::time::Duration; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; -use libwebauthn::transport::{Channel, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::u2f::U2F; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; @@ -21,7 +21,7 @@ async fn test_webauthn_basic_ctap1() { let mut device = get_virtual_device(); println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.wink(TIMEOUT).await.unwrap(); const APP_ID: &str = "https://foo.example.org"; diff --git a/libwebauthn-tests/tests/basic_ctap2.rs b/libwebauthn-tests/tests/basic_ctap2.rs index 96d7cc31..a0885ab1 100644 --- a/libwebauthn-tests/tests/basic_ctap2.rs +++ b/libwebauthn-tests/tests/basic_ctap2.rs @@ -2,7 +2,7 @@ use std::time::Duration; use libwebauthn::ops::webauthn::{GetAssertionRequest, GetAssertionRequestExtensions}; use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; -use libwebauthn::transport::{Channel, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; use libwebauthn::UvUpdate; use libwebauthn::{ @@ -33,7 +33,7 @@ async fn test_webauthn_basic_ctap2() { let challenge: [u8; 32] = thread_rng().gen(); println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.wink(TIMEOUT).await.unwrap(); // Make Credentials ceremony diff --git a/libwebauthn-tests/tests/pin_protocols.rs b/libwebauthn-tests/tests/pin_protocols.rs index afe7ecc1..b2624973 100644 --- a/libwebauthn-tests/tests/pin_protocols.rs +++ b/libwebauthn-tests/tests/pin_protocols.rs @@ -2,7 +2,7 @@ use std::time::Duration; use libwebauthn::pin::PinManagement; use libwebauthn::proto::ctap2::Ctap2PinUvAuthProtocol; -use libwebauthn::transport::{Channel, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; use test_log::test; @@ -23,7 +23,7 @@ async fn test_webauthn_change_pin_once() { let protos = [Ctap2PinUvAuthProtocol::One, Ctap2PinUvAuthProtocol::Two]; for proto in protos { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let mut state_recv = channel.get_ux_update_receiver(); @@ -43,7 +43,7 @@ async fn test_webauthn_change_pin_twice() { let protos = [Ctap2PinUvAuthProtocol::One, Ctap2PinUvAuthProtocol::Two]; for proto in protos { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let state_recv = channel.get_ux_update_receiver(); let update_handle = tokio::spawn(handle_updates(state_recv)); diff --git a/libwebauthn-tests/tests/preflight.rs b/libwebauthn-tests/tests/preflight.rs index da530e52..ee83d289 100644 --- a/libwebauthn-tests/tests/preflight.rs +++ b/libwebauthn-tests/tests/preflight.rs @@ -11,7 +11,7 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::proto::CtapError; use libwebauthn::transport::hid::channel::HidChannel; -use libwebauthn::transport::{Channel, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::webauthn::{Error, WebAuthn}; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; @@ -98,7 +98,7 @@ fn create_credential(id: &[u8]) -> Ctap2PublicKeyCredentialDescriptor { async fn preflight_no_exclude_list() { // Make credential with exclude_list: None. Should do nothing in preflight and return a credential let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); @@ -119,7 +119,7 @@ async fn preflight_nonsense_exclude_list() { // Make credential with nonsense exclude_list. Should remove everything in preflight and return a credential let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); @@ -147,7 +147,7 @@ async fn preflight_mixed_exclude_list() { // Make credential with a mixed exclude_list that contains 2 real ones. Should remove the two fake ones in preflight and return an error let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); @@ -194,7 +194,7 @@ async fn preflight_no_allow_list() { // Get assertion with allow_list: None. Should do nothing in preflight and return an error OR credentials, if a discoverable credential for example.org is present on the device let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); @@ -221,7 +221,7 @@ async fn preflight_nonsense_allow_list() { // Get assertion with nonsense allow_list. Should remove everything in preflight and return an error, AND run a dummy request to provoke a touch let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); @@ -255,7 +255,7 @@ async fn preflight_with_appid_exclude_finds_legacy_credential() { // while passing the legacy rpId as `appid_exclude`. The credential // should be detected, matching WebAuthn L3 ยง10.1.2 semantics. let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); let _state_recv = channel.get_ux_update_receiver(); @@ -305,7 +305,7 @@ async fn preflight_mixed_allow_list() { // Get assertion with a mixed allow_list that contains 2 real ones. Should remove the two fake ones in preflight let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); diff --git a/libwebauthn-tests/tests/prf.rs b/libwebauthn-tests/tests/prf.rs index 682cd8bb..2524bbfa 100644 --- a/libwebauthn-tests/tests/prf.rs +++ b/libwebauthn-tests/tests/prf.rs @@ -8,7 +8,7 @@ use libwebauthn::ops::webauthn::{ use libwebauthn::pin::PinManagement; use libwebauthn::proto::ctap2::{Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor}; use libwebauthn::transport::hid::channel::HidChannel; -use libwebauthn::transport::{Channel, Ctap2AuthTokenStore, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Ctap2AuthTokenStore, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, PlatformError, WebAuthn}; use libwebauthn::UvUpdate; use libwebauthn::{ @@ -28,14 +28,14 @@ const TIMEOUT: Duration = Duration::from_secs(10); #[test(tokio::test)] async fn test_webauthn_prf_no_pin_set() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); run_test_battery(&mut channel, false).await; } #[test(tokio::test)] async fn test_webauthn_prf_with_pin_set() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel .change_pin(String::from("1234"), TIMEOUT) .await @@ -46,7 +46,7 @@ async fn test_webauthn_prf_with_pin_set() { #[test(tokio::test)] async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_one() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.set_forced_pin_protocol(Ctap2PinUvAuthProtocol::One); channel .change_pin(String::from("1234"), TIMEOUT) @@ -58,7 +58,7 @@ async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_one() { #[test(tokio::test)] async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_two() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.set_forced_pin_protocol(Ctap2PinUvAuthProtocol::Two); channel .change_pin(String::from("1234"), TIMEOUT) @@ -74,7 +74,7 @@ async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_two() { #[test(tokio::test)] async fn test_webauthn_prf_eval_at_create_degrades_when_unsupported() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let state_recv = channel.get_ux_update_receiver(); // PRF forces UV=required (webauthn#2337); no-PIN device drives PIN setup. tokio::spawn(handle_updates( @@ -659,7 +659,7 @@ async fn run_failed_test( #[test(tokio::test)] async fn test_webauthn_prf_variable_length_input() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); let challenge: [u8; 32] = thread_rng().gen(); @@ -810,7 +810,7 @@ fn basic_make_credential_request( #[test(tokio::test)] async fn test_webauthn_prf_upgrades_uv_at_registration() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.change_pin("1234".into(), TIMEOUT).await.unwrap(); let state_recv = channel.get_ux_update_receiver(); @@ -852,7 +852,7 @@ async fn test_webauthn_prf_upgrades_uv_at_registration() { #[test(tokio::test)] async fn test_webauthn_no_prf_no_upgrade() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.change_pin("1234".into(), TIMEOUT).await.unwrap(); let state_recv = channel.get_ux_update_receiver(); @@ -886,7 +886,7 @@ async fn test_webauthn_no_prf_no_upgrade() { #[test(tokio::test)] async fn test_webauthn_prf_upgrades_uv_at_assertion() { let mut device = get_virtual_device(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); channel.change_pin("1234".into(), TIMEOUT).await.unwrap(); let user_id: [u8; 32] = thread_rng().gen(); diff --git a/libwebauthn/examples/ceremony/u2f_ble.rs b/libwebauthn/examples/ceremony/u2f_ble.rs index 0717fd53..01ad4146 100644 --- a/libwebauthn/examples/ceremony/u2f_ble.rs +++ b/libwebauthn/examples/ceremony/u2f_ble.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; use libwebauthn::transport::ble::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::u2f::U2F; #[path = "../common/mod.rs"] @@ -19,7 +19,7 @@ pub async fn main() -> Result<(), Box> { println!("Found {} devices.", devices.len()); for mut device in devices { - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; const APP_ID: &str = "https://foo.example.org"; let challenge: &[u8] = diff --git a/libwebauthn/examples/ceremony/u2f_hid.rs b/libwebauthn/examples/ceremony/u2f_hid.rs index 69dc8514..02b5fd0e 100644 --- a/libwebauthn/examples/ceremony/u2f_hid.rs +++ b/libwebauthn/examples/ceremony/u2f_hid.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::u2f::U2F; #[path = "../common/mod.rs"] @@ -20,7 +20,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Winking device: {}", device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; const APP_ID: &str = "https://foo.example.org"; diff --git a/libwebauthn/examples/ceremony/u2f_nfc.rs b/libwebauthn/examples/ceremony/u2f_nfc.rs index 1eb15097..122169c4 100644 --- a/libwebauthn/examples/ceremony/u2f_nfc.rs +++ b/libwebauthn/examples/ceremony/u2f_nfc.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available}; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::u2f::U2F; #[path = "../common/mod.rs"] @@ -24,7 +24,7 @@ pub async fn main() -> Result<(), Box> { if let Some(mut device) = device { println!("Selected NFC authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; const APP_ID: &str = "https://foo.example.org"; let challenge: &[u8] = diff --git a/libwebauthn/examples/ceremony/webauthn_ble.rs b/libwebauthn/examples/ceremony/webauthn_ble.rs index 0f9df727..981214ef 100644 --- a/libwebauthn/examples/ceremony/webauthn_ble.rs +++ b/libwebauthn/examples/ceremony/webauthn_ble.rs @@ -6,7 +6,7 @@ use libwebauthn::ops::webauthn::{ }; use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; use libwebauthn::transport::ble::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -21,7 +21,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected BLE authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; let request_origin: RequestOrigin = "https://example.org".try_into().expect("Invalid origin"); diff --git a/libwebauthn/examples/ceremony/webauthn_cable.rs b/libwebauthn/examples/ceremony/webauthn_cable.rs index f00a9630..975c3a67 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable.rs @@ -14,7 +14,7 @@ use libwebauthn::ops::webauthn::{ DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings, WebAuthnIDLResponse as _, }; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -79,7 +79,7 @@ pub async fn main() -> Result<(), Box> { .build(); println!("{}", image); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); println!("Channel established {:?}", channel); let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs index 9c5b3876..e9555583 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs @@ -18,7 +18,7 @@ use libwebauthn::ops::webauthn::{ RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _, }; use libwebauthn::transport::cable::channel::CableChannel; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -89,7 +89,7 @@ pub async fn main() -> Result<(), Box> { .build(); println!("{}", image); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); println!("Channel established {:?}", channel); let state_recv = channel.get_ux_update_receiver(); @@ -131,7 +131,10 @@ pub async fn main() -> Result<(), Box> { ) .await .unwrap(); - let mut channel = known_device.channel().await.unwrap(); + let mut channel = known_device + .channel(ChannelSettings::default()) + .await + .unwrap(); println!("Channel established {:?}", channel); run_get_assertion(&mut channel, &request_origin, &psl).await?; } else { @@ -148,7 +151,7 @@ pub async fn main() -> Result<(), Box> { .light_color(unicode::Dense1x2::Dark) .build(); println!("{}", image); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); println!("Channel established {:?}", channel); run_get_assertion(&mut channel, &request_origin, &psl).await?; } diff --git a/libwebauthn/examples/ceremony/webauthn_hid.rs b/libwebauthn/examples/ceremony/webauthn_hid.rs index 4e69df06..6e7ec823 100644 --- a/libwebauthn/examples/ceremony/webauthn_hid.rs +++ b/libwebauthn/examples/ceremony/webauthn_hid.rs @@ -7,7 +7,7 @@ use libwebauthn::ops::webauthn::{ }; use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -24,7 +24,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let request_origin: RequestOrigin = diff --git a/libwebauthn/examples/ceremony/webauthn_nfc.rs b/libwebauthn/examples/ceremony/webauthn_nfc.rs index dc57ad00..f3d27654 100644 --- a/libwebauthn/examples/ceremony/webauthn_nfc.rs +++ b/libwebauthn/examples/ceremony/webauthn_nfc.rs @@ -5,7 +5,7 @@ use libwebauthn::ops::webauthn::{ RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _, }; use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available}; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -24,7 +24,7 @@ pub async fn main() -> Result<(), Box> { return Ok(()); }; println!("Selected NFC authenticator: {}", device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; let request_origin: RequestOrigin = "https://example.org".try_into().expect("Invalid origin"); let psl = SystemPublicSuffixList::auto().expect( diff --git a/libwebauthn/examples/features/device_selection_hid.rs b/libwebauthn/examples/features/device_selection_hid.rs index fa3304eb..a710cfa3 100644 --- a/libwebauthn/examples/features/device_selection_hid.rs +++ b/libwebauthn/examples/features/device_selection_hid.rs @@ -4,7 +4,7 @@ use std::time::Duration; use libwebauthn::transport::hid::channel::HidChannelHandle; use libwebauthn::transport::hid::{list_devices, HidDevice}; -use libwebauthn::transport::Device; +use libwebauthn::transport::{ChannelSettings, Device}; #[path = "../common/mod.rs"] mod common; @@ -28,7 +28,7 @@ pub async fn main() -> Result<(), Box> { tokio::spawn(async move { let dev = device.clone(); - let mut channel = device.channel().await.unwrap(); + let mut channel = device.channel(ChannelSettings::default()).await.unwrap(); let handle = channel.get_handle(); stx.send((idx, dev, handle)).await.unwrap(); drop(stx); diff --git a/libwebauthn/examples/features/prf_replay.rs b/libwebauthn/examples/features/prf_replay.rs index 58ba267a..5a4ae944 100644 --- a/libwebauthn/examples/features/prf_replay.rs +++ b/libwebauthn/examples/features/prf_replay.rs @@ -12,7 +12,7 @@ use libwebauthn::ops::webauthn::{ }; use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -51,7 +51,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/features/webauthn_extensions_hid.rs b/libwebauthn/examples/features/webauthn_extensions_hid.rs index ae9a8fc5..73176a34 100644 --- a/libwebauthn/examples/features/webauthn_extensions_hid.rs +++ b/libwebauthn/examples/features/webauthn_extensions_hid.rs @@ -14,7 +14,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -48,7 +48,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/features/webauthn_preflight_hid.rs b/libwebauthn/examples/features/webauthn_preflight_hid.rs index 3c363669..bf3ceb83 100644 --- a/libwebauthn/examples/features/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/features/webauthn_preflight_hid.rs @@ -13,7 +13,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, }; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel, Device}; +use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::webauthn::{CtapError, Error as WebAuthnError, WebAuthn}; #[path = "../common/mod.rs"] @@ -35,7 +35,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/features/webauthn_prf_hid.rs b/libwebauthn/examples/features/webauthn_prf_hid.rs index b54fc993..fa45ee42 100644 --- a/libwebauthn/examples/features/webauthn_prf_hid.rs +++ b/libwebauthn/examples/features/webauthn_prf_hid.rs @@ -16,7 +16,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, PlatformError, WebAuthn}; #[path = "../common/mod.rs"] @@ -41,7 +41,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/features/webauthn_related_origins_hid.rs b/libwebauthn/examples/features/webauthn_related_origins_hid.rs index 3ecb126f..d83620f3 100644 --- a/libwebauthn/examples/features/webauthn_related_origins_hid.rs +++ b/libwebauthn/examples/features/webauthn_related_origins_hid.rs @@ -16,7 +16,7 @@ use libwebauthn::ops::webauthn::{ WebAuthnIDLResponse as _, }; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; #[path = "../common/mod.rs"] @@ -33,7 +33,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let request_origin: RequestOrigin = diff --git a/libwebauthn/examples/management/authenticator_config_hid.rs b/libwebauthn/examples/management/authenticator_config_hid.rs index c9028fe5..aef087a5 100644 --- a/libwebauthn/examples/management/authenticator_config_hid.rs +++ b/libwebauthn/examples/management/authenticator_config_hid.rs @@ -6,7 +6,7 @@ use std::time::Duration; use libwebauthn::management::AuthenticatorConfig; use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse}; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use text_io::read; #[path = "../common/mod.rs"] @@ -74,7 +74,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/management/bio_enrollment_hid.rs b/libwebauthn/examples/management/bio_enrollment_hid.rs index 511b250d..ebfbc765 100644 --- a/libwebauthn/examples/management/bio_enrollment_hid.rs +++ b/libwebauthn/examples/management/bio_enrollment_hid.rs @@ -7,7 +7,7 @@ use text_io::read; use libwebauthn::management::BioEnrollment; use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse, Ctap2LastEnrollmentSampleStatus}; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::Error as WebAuthnError; #[path = "../common/mod.rs"] @@ -85,7 +85,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/examples/management/change_pin_hid.rs b/libwebauthn/examples/management/change_pin_hid.rs index 8cc21abd..3814dee1 100644 --- a/libwebauthn/examples/management/change_pin_hid.rs +++ b/libwebauthn/examples/management/change_pin_hid.rs @@ -4,7 +4,7 @@ use std::time::Duration; use libwebauthn::pin::PinManagement; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use text_io::read; #[path = "../common/mod.rs"] @@ -21,7 +21,7 @@ pub async fn main() -> Result<(), Box> { for mut device in devices { println!("Selected HID authenticator: {}", device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; print!("PIN: Please enter the _new_ PIN: "); diff --git a/libwebauthn/examples/management/cred_management_hid.rs b/libwebauthn/examples/management/cred_management_hid.rs index 744f5324..30f56761 100644 --- a/libwebauthn/examples/management/cred_management_hid.rs +++ b/libwebauthn/examples/management/cred_management_hid.rs @@ -7,7 +7,7 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::proto::CtapError; use libwebauthn::transport::hid::list_devices; -use libwebauthn::transport::{Channel as _, Device}; +use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::Error as WebAuthnError; use std::io::{self, Write}; use text_io::read; @@ -85,7 +85,7 @@ pub async fn main() -> Result<(), WebAuthnError> { for mut device in devices { println!("Selected HID authenticator: {}", &device); - let mut channel = device.channel().await?; + let mut channel = device.channel(ChannelSettings::default()).await?; channel.wink(TIMEOUT).await?; let state_recv = channel.get_ux_update_receiver(); diff --git a/libwebauthn/src/lib.rs b/libwebauthn/src/lib.rs index 2aa0a380..6b64e279 100644 --- a/libwebauthn/src/lib.rs +++ b/libwebauthn/src/lib.rs @@ -36,7 +36,7 @@ //! SystemPublicSuffixList, //! }; //! use libwebauthn::transport::hid::list_devices; -//! use libwebauthn::transport::Device; +//! use libwebauthn::transport::{ChannelSettings, Device}; //! use libwebauthn::webauthn::WebAuthn; //! //! # async fn run() -> Result<(), Box> { @@ -45,7 +45,7 @@ //! //! for mut device in devices { //! // 2. Open a channel to the device. -//! let mut channel = device.channel().await?; +//! let mut channel = device.channel(ChannelSettings::default()).await?; //! //! // 3. Build a request from its WebAuthn IDL JSON. //! let origin: RequestOrigin = "https://example.org".try_into().expect("invalid origin"); diff --git a/libwebauthn/src/transport/ble/channel.rs b/libwebauthn/src/transport/ble/channel.rs index 8243d1be..2b577f60 100644 --- a/libwebauthn/src/transport/ble/channel.rs +++ b/libwebauthn/src/transport/ble/channel.rs @@ -1,13 +1,17 @@ use std::convert::TryInto; use std::fmt::{Display, Formatter}; +use std::sync::Arc; use std::time::Duration; use crate::fido::FidoRevision; +use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::proto::CtapError; use crate::transport::ble::btleplug; -use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::channel::{ + AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, +}; use crate::transport::device::SupportedProtocols; use crate::transport::error::TransportError; use crate::webauthn::error::Error; @@ -29,6 +33,7 @@ pub struct BleChannel<'a> { connection: Connection, revision: FidoRevision, auth_token_data: Option, + persistent_token_store: Option>, ux_update_sender: broadcast::Sender, } @@ -36,6 +41,7 @@ impl<'a> BleChannel<'a> { pub async fn new( device: &'a BleDevice, revisions: &SupportedRevisions, + settings: ChannelSettings, ) -> Result, Error> { let (ux_update_sender, _) = broadcast::channel(16); @@ -51,6 +57,7 @@ impl<'a> BleChannel<'a> { connection, revision, auth_token_data: None, + persistent_token_store: settings.persistent_token_store, ux_update_sender, }; channel @@ -189,4 +196,8 @@ impl Ctap2AuthTokenStore for BleChannel<'_> { fn clear_uv_auth_token_store(&mut self) { self.auth_token_data = None; } + + fn persistent_token_store(&self) -> Option> { + self.persistent_token_store.clone() + } } diff --git a/libwebauthn/src/transport/ble/device.rs b/libwebauthn/src/transport/ble/device.rs index c567d18a..cb677f4a 100644 --- a/libwebauthn/src/transport/ble/device.rs +++ b/libwebauthn/src/transport/ble/device.rs @@ -7,6 +7,7 @@ use tracing::{info, instrument}; use crate::transport::device::Device; use crate::transport::error::TransportError; +use crate::transport::ChannelSettings; use crate::webauthn::error::Error; use super::btleplug::manager::SupportedRevisions; @@ -78,9 +79,9 @@ impl fmt::Display for BleDevice { #[async_trait] impl<'d> Device<'d, Ble, BleChannel<'d>> for BleDevice { - async fn channel(&'d mut self) -> Result, Error> { + async fn channel(&'d mut self, settings: ChannelSettings) -> Result, Error> { let revisions = self.supported_revisions().await?; - let channel = BleChannel::new(self, &revisions).await?; + let channel = BleChannel::new(self, &revisions, settings).await?; Ok(channel) } diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/cable/channel.rs index e4a3cfed..0212138a 100644 --- a/libwebauthn/src/transport/cable/channel.rs +++ b/libwebauthn/src/transport/cable/channel.rs @@ -1,4 +1,5 @@ use std::fmt::{Display, Formatter}; +use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; @@ -6,6 +7,7 @@ use tokio::sync::{broadcast, mpsc, watch}; use tokio::{task, time}; use tracing::error; +use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::{ ctap1::apdu::{ApduRequest, ApduResponse}, ctap2::cbor::{CborRequest, CborResponse}, @@ -44,6 +46,7 @@ pub struct CableChannel { pub(crate) cbor_receiver: mpsc::Receiver, pub(crate) ux_update_sender: broadcast::Sender, pub(crate) connection_state_receiver: watch::Receiver, + pub(crate) persistent_token_store: Option>, } impl CableChannel { @@ -198,4 +201,8 @@ impl Ctap2AuthTokenStore for CableChannel { } fn clear_uv_auth_token_store(&mut self) {} + + fn persistent_token_store(&self) -> Option> { + self.persistent_token_store.clone() + } } diff --git a/libwebauthn/src/transport/cable/known_devices.rs b/libwebauthn/src/transport/cable/known_devices.rs index 45c5407c..3eadfb03 100644 --- a/libwebauthn/src/transport/cable/known_devices.rs +++ b/libwebauthn/src/transport/cable/known_devices.rs @@ -10,6 +10,7 @@ use crate::transport::cable::connection_stages::{ }; use crate::transport::error::TransportError; +use crate::transport::ChannelSettings; use crate::transport::Device; use crate::webauthn::error::Error; @@ -191,7 +192,7 @@ impl CableKnownDevice { #[async_trait] impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { - async fn channel(&'d mut self) -> Result { + async fn channel(&'d mut self, settings: ChannelSettings) -> Result { debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); let (ux_update_sender, _) = broadcast::channel(16); @@ -245,6 +246,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { cbor_receiver: cbor_rx_recv, ux_update_sender, connection_state_receiver, + persistent_token_store: settings.persistent_token_store, }) } } diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn/src/transport/cable/qr_code_device.rs index ee3bac8b..d3b065b9 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn/src/transport/cable/qr_code_device.rs @@ -25,6 +25,7 @@ use super::tunnel::KNOWN_TUNNEL_DOMAINS; use super::{channel::CableChannel, channel::ConnectionState, Cable}; use crate::proto::ctap2::cbor; use crate::transport::cable::digit_encode; +use crate::transport::ChannelSettings; use crate::transport::Device; use crate::webauthn::error::Error; use crate::webauthn::TransportError; @@ -239,7 +240,7 @@ impl Display for CableQrCodeDevice { #[async_trait] impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { - async fn channel(&'d mut self) -> Result { + async fn channel(&'d mut self, settings: ChannelSettings) -> Result { let (ux_update_sender, _) = broadcast::channel(16); let (cbor_tx_send, cbor_tx_recv) = mpsc::channel(16); let (cbor_rx_send, cbor_rx_recv) = mpsc::channel(16); @@ -290,6 +291,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { cbor_receiver: cbor_rx_recv, ux_update_sender, connection_state_receiver, + persistent_token_store: settings.persistent_token_store, }) } diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index da8bde91..2db66c32 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -27,6 +27,17 @@ pub enum ChannelStatus { Closed, } +/// Per-channel configuration supplied by the caller when opening a channel via +/// [`Device::channel`](crate::transport::Device::channel). Transport-agnostic, so the +/// same options apply to HID, BLE, NFC, and hybrid (caBLE). +#[derive(Debug, Default, Clone)] +pub struct ChannelSettings { + /// Caller-supplied store for persistent pinUvAuthTokens (pcmr). When set, read-only + /// credential management reuses a stored token across sessions instead of + /// re-prompting for the PIN. See [`PersistentTokenStore`]. + pub persistent_token_store: Option>, +} + #[async_trait] pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { /// UX updates for this channel, must include UV updates. diff --git a/libwebauthn/src/transport/device.rs b/libwebauthn/src/transport/device.rs index a29f0b37..e6fd21cd 100644 --- a/libwebauthn/src/transport/device.rs +++ b/libwebauthn/src/transport/device.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use crate::transport::ble::btleplug::manager::SupportedRevisions; use crate::webauthn::error::Error; -use super::{Channel, Transport}; +use super::{Channel, ChannelSettings, Transport}; #[async_trait] pub trait Device<'d, T, C>: Send + Display @@ -14,7 +14,7 @@ where T: Transport, C: Channel + 'd, { - async fn channel(&'d mut self) -> Result; + async fn channel(&'d mut self, settings: ChannelSettings) -> Result; // async fn supported_protocols(&mut self) -> Result; } diff --git a/libwebauthn/src/transport/hid/channel.rs b/libwebauthn/src/transport/hid/channel.rs index 1d9328d8..552a0948 100644 --- a/libwebauthn/src/transport/hid/channel.rs +++ b/libwebauthn/src/transport/hid/channel.rs @@ -15,6 +15,7 @@ use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::time::sleep; use tracing::{debug, info, instrument, trace, warn, Level}; +use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap1::{Ctap1, Ctap1RegisterRequest}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; @@ -22,7 +23,9 @@ use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::proto::ctap2::Ctap2PinUvAuthProtocol; use crate::proto::ctap2::{Ctap2, Ctap2MakeCredentialRequest}; use crate::proto::CtapError; -use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::channel::{ + AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, +}; use crate::transport::device::SupportedProtocols; use crate::transport::error::TransportError; #[cfg(feature = "virt")] @@ -79,6 +82,7 @@ pub struct HidChannel<'d> { open_device: OpenHidDevice, init: InitResponse, auth_token_data: Option, + persistent_token_store: Option>, ux_update_sender: broadcast::Sender, handle: HidChannelHandle, #[cfg(feature = "virt")] @@ -86,7 +90,10 @@ pub struct HidChannel<'d> { } impl<'d> HidChannel<'d> { - pub async fn new(device: &'d HidDevice) -> Result, Error> { + pub async fn new( + device: &'d HidDevice, + settings: ChannelSettings, + ) -> Result, Error> { let (ux_update_sender, _) = broadcast::channel(16); let (handle_tx, handle_rx) = mpsc::channel(1); let handle = HidChannelHandle { tx: handle_tx }; @@ -106,6 +113,7 @@ impl<'d> HidChannel<'d> { }, init: InitResponse::default(), auth_token_data: None, + persistent_token_store: settings.persistent_token_store, ux_update_sender, handle, #[cfg(feature = "virt")] @@ -606,4 +614,8 @@ impl Ctap2AuthTokenStore for HidChannel<'_> { fn clear_uv_auth_token_store(&mut self) { self.auth_token_data = None; } + + fn persistent_token_store(&self) -> Option> { + self.persistent_token_store.clone() + } } diff --git a/libwebauthn/src/transport/hid/device.rs b/libwebauthn/src/transport/hid/device.rs index 4652608c..b860ecb9 100644 --- a/libwebauthn/src/transport/hid/device.rs +++ b/libwebauthn/src/transport/hid/device.rs @@ -12,7 +12,7 @@ use tracing::{debug, info, instrument}; #[cfg(feature = "virt")] use super::framing::HidMessage; use crate::transport::error::TransportError; -use crate::transport::Device; +use crate::transport::{ChannelSettings, Device}; use crate::webauthn::error::Error; #[cfg(feature = "virt")] @@ -82,8 +82,8 @@ pub fn virtual_device(backend: B) -> HidDevice { #[async_trait] impl<'d> Device<'d, Hid, HidChannel<'d>> for HidDevice { - async fn channel(&'d mut self) -> Result, Error> { - let channel = HidChannel::new(self).await?; + async fn channel(&'d mut self, settings: ChannelSettings) -> Result, Error> { + let channel = HidChannel::new(self, settings).await?; Ok(channel) } diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index 8f6c0306..cdd410de 100644 --- a/libwebauthn/src/transport/mod.rs +++ b/libwebauthn/src/transport/mod.rs @@ -29,7 +29,7 @@ mod channel; mod transport; pub(crate) use channel::{AuthTokenData, Ctap2AuthTokenPermission}; -pub use channel::{Channel, Ctap2AuthTokenStore}; +pub use channel::{Channel, ChannelSettings, Ctap2AuthTokenStore}; #[cfg(test)] pub use channel::ChannelStatus; diff --git a/libwebauthn/src/transport/nfc/channel.rs b/libwebauthn/src/transport/nfc/channel.rs index 3e66649e..4323a136 100644 --- a/libwebauthn/src/transport/nfc/channel.rs +++ b/libwebauthn/src/transport/nfc/channel.rs @@ -4,16 +4,20 @@ use apdu_core; use async_trait::async_trait; use std::fmt; use std::fmt::{Debug, Display, Formatter}; +use std::sync::Arc; use std::time::Duration; use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Sender}; #[allow(unused_imports)] use tracing::{debug, instrument, trace, warn, Level}; +use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::proto::ctap2::Ctap2; -use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::channel::{ + AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, +}; use crate::transport::device::SupportedProtocols; use crate::transport::error::TransportError; use crate::webauthn::Error; @@ -99,6 +103,7 @@ where { delegate: Box + Send + Sync>, auth_token_data: Option, + persistent_token_store: Option>, ux_update_sender: broadcast::Sender, handle: NfcChannelHandle, ctx: Ctx, @@ -121,13 +126,18 @@ impl NfcChannel where Ctx: fmt::Debug + Display + Copy + Send + Sync, { - pub fn new(delegate: Box + Send + Sync>, ctx: Ctx) -> Self { + pub fn new( + delegate: Box + Send + Sync>, + ctx: Ctx, + settings: ChannelSettings, + ) -> Self { let (ux_update_sender, _) = broadcast::channel(16); let (handle_tx, _handle_rx) = mpsc::channel(1); let handle = NfcChannelHandle { tx: handle_tx }; NfcChannel { delegate, auth_token_data: None, + persistent_token_store: settings.persistent_token_store, ux_update_sender, handle, ctx, @@ -342,6 +352,10 @@ where fn clear_uv_auth_token_store(&mut self) { self.auth_token_data = None; } + + fn persistent_token_store(&self) -> Option> { + self.persistent_token_store.clone() + } } #[cfg(test)] diff --git a/libwebauthn/src/transport/nfc/device.rs b/libwebauthn/src/transport/nfc/device.rs index a59d7b10..a7c8596d 100644 --- a/libwebauthn/src/transport/nfc/device.rs +++ b/libwebauthn/src/transport/nfc/device.rs @@ -4,7 +4,7 @@ use std::fmt; use tracing::{debug, info, instrument, trace}; use crate::{ - transport::{device::Device, Channel}, + transport::{device::Device, Channel, ChannelSettings}, webauthn::Error, }; @@ -60,13 +60,13 @@ impl NfcDevice { } } - async fn channel_sync(&self) -> Result, Error> { + async fn channel_sync(&self, settings: ChannelSettings) -> Result, Error> { trace!("nfc channel {:?}", self); let mut channel: NfcChannel = match &self.info { #[cfg(feature = "nfc-backend-libnfc")] - DeviceInfo::LibNfc(info) => info.channel(), + DeviceInfo::LibNfc(info) => info.channel(settings), #[cfg(feature = "nfc-backend-pcsc")] - DeviceInfo::Pcsc(info) => info.channel(), + DeviceInfo::Pcsc(info) => info.channel(settings), }?; channel.select_fido2().await?; @@ -76,14 +76,17 @@ impl NfcDevice { #[async_trait] impl<'d> Device<'d, Nfc, NfcChannel> for NfcDevice { - async fn channel(&'d mut self) -> Result, Error> { - self.channel_sync().await + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result, Error> { + self.channel_sync(settings).await } } async fn is_fido(device: &NfcDevice) -> bool { async fn inner(device: &NfcDevice) -> Result { - let chan = device.channel_sync().await?; + let chan = device.channel_sync(ChannelSettings::default()).await?; let protocols = chan.supported_protocols().await?; Ok(protocols.fido2 || protocols.u2f) } diff --git a/libwebauthn/src/transport/nfc/libnfc/mod.rs b/libwebauthn/src/transport/nfc/libnfc/mod.rs index d47d0ea8..e5a25301 100644 --- a/libwebauthn/src/transport/nfc/libnfc/mod.rs +++ b/libwebauthn/src/transport/nfc/libnfc/mod.rs @@ -2,6 +2,7 @@ use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; use super::device::NfcDevice; use super::Context; use crate::transport::error::TransportError; +use crate::transport::ChannelSettings; use crate::webauthn::Error; use apdu::core::HandleError; use apdu_core; @@ -69,7 +70,7 @@ impl Info { } } - pub fn channel(&self) -> Result, Error> { + pub fn channel(&self, settings: ChannelSettings) -> Result, Error> { let context = nfc1::Context::new().map_err(map_error)?; let mut chan = Channel::new(self, context)?; @@ -90,7 +91,7 @@ impl Info { debug!("Selected: {:?}", target); let ctx = Context {}; - let channel = NfcChannel::new(Box::new(chan), ctx); + let channel = NfcChannel::new(Box::new(chan), ctx, settings); Ok(channel) } } diff --git a/libwebauthn/src/transport/nfc/pcsc/mod.rs b/libwebauthn/src/transport/nfc/pcsc/mod.rs index f6018999..3b5be6b9 100644 --- a/libwebauthn/src/transport/nfc/pcsc/mod.rs +++ b/libwebauthn/src/transport/nfc/pcsc/mod.rs @@ -2,6 +2,7 @@ use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; use super::device::NfcDevice; use super::Context; use crate::transport::error::TransportError; +use crate::transport::ChannelSettings; use crate::webauthn::Error; use apdu::core::HandleError; use pcsc; @@ -84,12 +85,12 @@ impl Info { } } - pub fn channel(&self) -> Result, Error> { + pub fn channel(&self, settings: ChannelSettings) -> Result, Error> { let context = pcsc::Context::establish(pcsc::Scope::User)?; let chan = Channel::new(self, context)?; let ctx = Context {}; - let channel = NfcChannel::new(Box::new(chan), ctx); + let channel = NfcChannel::new(Box::new(chan), ctx, settings); Ok(channel) } }