Skip to content
Merged
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
177 changes: 140 additions & 37 deletions libwebauthn/src/management/credential_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ where
req
)
}?;
self.set_cred_mgmt_preview(req.use_legacy_preview);
Ok((
Ctap2RPData::new(
unwrap_field!(resp.rp),
Expand All @@ -122,24 +123,8 @@ where

async fn enumerate_rps_next_rp(&mut self, timeout: Duration) -> Result<Ctap2RPData, Error> {
let mut req = Ctap2CredentialManagementRequest::new_enumerate_rps_next_rp();
let resp = loop {
let uv_auth_used = user_verification(
self,
UserVerificationRequirement::Preferred,
&mut req,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2AuthenticatorConfigResponse
handle_errors!(
self,
self.ctap2_credential_management(&req, timeout).await,
uv_auth_used,
timeout,
req
)
}?;
req.use_legacy_preview = self.cred_mgmt_preview();
let resp = self.ctap2_credential_management(&req, timeout).await?;
Ok(Ctap2RPData::new(
unwrap_field!(resp.rp),
unwrap_field!(resp.rp_id_hash).into_vec(),
Expand Down Expand Up @@ -170,6 +155,7 @@ where
req
)
}?;
self.set_cred_mgmt_preview(req.use_legacy_preview);
let cred = Ctap2CredentialData::new(
unwrap_field!(resp.user),
unwrap_field!(resp.credential_id),
Expand All @@ -186,24 +172,8 @@ where
timeout: Duration,
) -> Result<Ctap2CredentialData, Error> {
let mut req = Ctap2CredentialManagementRequest::new_enumerate_credentials_next();
let resp = loop {
let uv_auth_used = user_verification(
self,
UserVerificationRequirement::Preferred,
&mut req,
timeout,
)
.await?;

// On success, this is an all-empty Ctap2AuthenticatorConfigResponse
handle_errors!(
self,
self.ctap2_credential_management(&req, timeout).await,
uv_auth_used,
timeout,
req
)
}?;
req.use_legacy_preview = self.cred_mgmt_preview();
let resp = self.ctap2_credential_management(&req, timeout).await?;
let cred = Ctap2CredentialData::new(
unwrap_field!(resp.user),
unwrap_field!(resp.credential_id),
Expand Down Expand Up @@ -367,9 +337,11 @@ mod test {
use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse};
use crate::proto::ctap2::{
Ctap2CommandCode, Ctap2CredentialManagementRequest, Ctap2GetInfoResponse,
Ctap2PublicKeyCredentialRpEntity,
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity,
};
use crate::transport::mock::channel::MockChannel;
use std::collections::{BTreeMap, HashMap};

const TIMEOUT: Duration = Duration::from_secs(1);

Expand Down Expand Up @@ -413,4 +385,135 @@ mod test {
assert_eq!(rp_data.rp_id_hash.len(), 32);
assert_eq!(rp_data.rp_id_hash, hash.to_vec());
}

// GetNextRP returns only rp (0x03) and rpIDHash (0x04).
#[derive(SerializeIndexed)]
struct EnumerateRpsNextResponse {
#[serde(index = 0x03)]
rp: Ctap2PublicKeyCredentialRpEntity,
#[serde(index = 0x04)]
rp_id_hash: ByteBuf,
}

// GetNextCredential returns user (0x06), credentialID (0x07), publicKey (0x08), credProtect (0x0A).
#[derive(SerializeIndexed)]
struct EnumerateCredsNextResponse {
#[serde(index = 0x06)]
user: Ctap2PublicKeyCredentialUserEntity,
#[serde(index = 0x07)]
credential_id: Ctap2PublicKeyCredentialDescriptor,
#[serde(index = 0x08)]
public_key: BTreeMap<i8, i8>,
#[serde(index = 0x0A)]
cred_protect: u64,
}

#[tokio::test]
async fn enumerate_rps_next_rp_sends_only_the_subcommand() {
let req = Ctap2CredentialManagementRequest::new_enumerate_rps_next_rp();
let cbor_req: CborRequest = (&req).try_into().unwrap();
assert_eq!(
cbor_req.command,
Ctap2CommandCode::AuthenticatorCredentialManagement
);
// CTAP 2.1 §6.8.3: {subCommand: enumerateRPsGetNextRP}, no pinUvAuth keys.
assert_eq!(cbor_req.encoded_data, vec![0xA1, 0x01, 0x03]);

let hash = [0xCD_u8; 32];
let fixture = EnumerateRpsNextResponse {
rp: Ctap2PublicKeyCredentialRpEntity::new("example.org", "Example"),
rp_id_hash: ByteBuf::from(hash.to_vec()),
};
let resp = CborResponse::new_success_from_slice(&cbor::to_vec(&fixture).unwrap());
// Queue only the GetNext exchange: any interleaved command panics the mock.
let mut channel = MockChannel::new();
channel.push_command_pair(cbor_req, resp);

let rp_data = channel.enumerate_rps_next_rp(TIMEOUT).await.unwrap();
assert_eq!(rp_data.rp_id_hash, hash.to_vec());
}

#[tokio::test]
async fn enumerate_credentials_next_sends_only_the_subcommand() {
let req = Ctap2CredentialManagementRequest::new_enumerate_credentials_next();
let cbor_req: CborRequest = (&req).try_into().unwrap();
assert_eq!(
cbor_req.command,
Ctap2CommandCode::AuthenticatorCredentialManagement
);
// CTAP 2.1 §6.8.4: {subCommand: enumerateCredentialsGetNextCredential}, no pinUvAuth keys.
assert_eq!(cbor_req.encoded_data, vec![0xA1, 0x01, 0x05]);

let fixture = EnumerateCredsNextResponse {
user: Ctap2PublicKeyCredentialUserEntity::new(&[0x0B; 16], "bob", "bob"),
credential_id: Ctap2PublicKeyCredentialDescriptor {
id: ByteBuf::from(vec![0x1D; 32]),
r#type: Ctap2PublicKeyCredentialType::PublicKey,
transports: None,
},
public_key: BTreeMap::from([(1, 2), (3, -7)]),
cred_protect: 1,
};
let resp = CborResponse::new_success_from_slice(&cbor::to_vec(&fixture).unwrap());
let mut channel = MockChannel::new();
channel.push_command_pair(cbor_req, resp);

let cred = channel.enumerate_credentials_next(TIMEOUT).await.unwrap();
assert_eq!(cred.user.id, ByteBuf::from(vec![0x0B; 16]));
assert_eq!(cred.cred_protect, 1);
assert!(cred.large_blob_key.is_none());
}

#[tokio::test]
async fn get_next_reuses_preview_command_resolved_by_begin() {
let mut channel = MockChannel::new();

// Device advertises credentialMgmtPreview only: Begin must resolve 0x41.
let info = Ctap2GetInfoResponse {
options: Some(HashMap::from([("credentialMgmtPreview".to_string(), true)])),
..Default::default()
};
let info_req = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
let info_resp = CborResponse::new_success_from_slice(&cbor::to_vec(&info).unwrap());
channel.push_command_pair(info_req, info_resp);

let mut begin_req = Ctap2CredentialManagementRequest::new_enumerate_rps_begin();
begin_req.use_legacy_preview = true;
let begin_cbor: CborRequest = (&begin_req).try_into().unwrap();
assert_eq!(
begin_cbor.command,
Ctap2CommandCode::AuthenticatorCredentialManagementPreview
);
let begin_fixture = EnumerateRpsResponse {
rp: Ctap2PublicKeyCredentialRpEntity::new("example.com", "Example"),
rp_id_hash: ByteBuf::from(vec![0xEF; 32]),
total_rps: 2,
};
channel.push_command_pair(
begin_cbor,
CborResponse::new_success_from_slice(&cbor::to_vec(&begin_fixture).unwrap()),
);

let mut next_req = Ctap2CredentialManagementRequest::new_enumerate_rps_next_rp();
next_req.use_legacy_preview = true;
let next_cbor: CborRequest = (&next_req).try_into().unwrap();
assert_eq!(
next_cbor.command,
Ctap2CommandCode::AuthenticatorCredentialManagementPreview
);
assert_eq!(next_cbor.encoded_data, vec![0xA1, 0x01, 0x03]);
let next_fixture = EnumerateRpsNextResponse {
rp: Ctap2PublicKeyCredentialRpEntity::new("example.org", "Example Two"),
rp_id_hash: ByteBuf::from(vec![0x11; 32]),
};
channel.push_command_pair(
next_cbor,
CborResponse::new_success_from_slice(&cbor::to_vec(&next_fixture).unwrap()),
);

let (_, total) = channel.enumerate_rps_begin(TIMEOUT).await.unwrap();
assert_eq!(total, 2);
let rp_data = channel.enumerate_rps_next_rp(TIMEOUT).await.unwrap();
assert_eq!(rp_data.rp_id_hash, vec![0x11; 32]);
}
}
10 changes: 10 additions & 0 deletions libwebauthn/src/transport/ble/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct BleChannel<'a> {
connection: Connection,
revision: FidoRevision,
auth_token_data: Option<AuthTokenData>,
cred_mgmt_preview: bool,
persistent_token_store: Option<Arc<dyn PersistentTokenStore>>,
ux_update_sender: broadcast::Sender<UvUpdate>,
}
Expand All @@ -57,6 +58,7 @@ impl<'a> BleChannel<'a> {
connection,
revision,
auth_token_data: None,
cred_mgmt_preview: false,
persistent_token_store: settings.persistent_token_store,
ux_update_sender,
};
Expand Down Expand Up @@ -197,6 +199,14 @@ impl Ctap2AuthTokenStore for BleChannel<'_> {
self.auth_token_data = None;
}

fn set_cred_mgmt_preview(&mut self, uses_preview: bool) {
self.cred_mgmt_preview = uses_preview;
}

fn cred_mgmt_preview(&self) -> bool {
self.cred_mgmt_preview
}

fn persistent_token_store(&self) -> Option<Arc<dyn PersistentTokenStore>> {
self.persistent_token_store.clone()
}
Expand Down
6 changes: 6 additions & 0 deletions libwebauthn/src/transport/cable/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ impl Ctap2AuthTokenStore for CableChannel {

fn clear_uv_auth_token_store(&mut self) {}

fn set_cred_mgmt_preview(&mut self, _uses_preview: bool) {}

fn cred_mgmt_preview(&self) -> bool {
false
}

fn persistent_token_store(&self) -> Option<Arc<dyn PersistentTokenStore>> {
self.persistent_token_store.clone()
}
Expand Down
4 changes: 4 additions & 0 deletions libwebauthn/src/transport/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ pub trait Ctap2AuthTokenStore {
fn store_auth_data(&mut self, auth_token_data: AuthTokenData);
fn get_auth_data(&self) -> Option<&AuthTokenData>;
fn clear_uv_auth_token_store(&mut self);
/// Command set resolved by the last credMgmt state-initializing request, so
/// stateful GetNext continuations reuse it without re-fetching getInfo.
fn set_cred_mgmt_preview(&mut self, uses_preview: bool);
fn cred_mgmt_preview(&self) -> bool;
fn get_uv_auth_token(&self, requested_permission: &Ctap2AuthTokenPermission) -> Option<&[u8]> {
if let Some(stored_data) = self.get_auth_data() {
if let Some(permission) = &stored_data.permission {
Expand Down
10 changes: 10 additions & 0 deletions libwebauthn/src/transport/hid/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub struct HidChannel<'d> {
open_device: OpenHidDevice,
init: InitResponse,
auth_token_data: Option<AuthTokenData>,
cred_mgmt_preview: bool,
persistent_token_store: Option<Arc<dyn PersistentTokenStore>>,
ux_update_sender: broadcast::Sender<UvUpdate>,
handle: HidChannelHandle,
Expand Down Expand Up @@ -113,6 +114,7 @@ impl<'d> HidChannel<'d> {
},
init: InitResponse::default(),
auth_token_data: None,
cred_mgmt_preview: false,
persistent_token_store: settings.persistent_token_store,
ux_update_sender,
handle,
Expand Down Expand Up @@ -615,6 +617,14 @@ impl Ctap2AuthTokenStore for HidChannel<'_> {
self.auth_token_data = None;
}

fn set_cred_mgmt_preview(&mut self, uses_preview: bool) {
self.cred_mgmt_preview = uses_preview;
}

fn cred_mgmt_preview(&self) -> bool {
self.cred_mgmt_preview
}

fn persistent_token_store(&self) -> Option<Arc<dyn PersistentTokenStore>> {
self.persistent_token_store.clone()
}
Expand Down
10 changes: 10 additions & 0 deletions libwebauthn/src/transport/mock/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct MockChannel {
expected_requests: VecDeque<CborRequest>,
responses: VecDeque<CborResponse>,
auth_token_data: Option<AuthTokenData>,
cred_mgmt_preview: bool,
persistent_token_store: Option<Arc<dyn PersistentTokenStore>>,
ux_update_sender: broadcast::Sender<UvUpdate>,
pre_send_delay: Option<Duration>,
Expand All @@ -39,6 +40,7 @@ impl MockChannel {
expected_requests: VecDeque::new(),
responses: VecDeque::new(),
auth_token_data: None,
cred_mgmt_preview: false,
persistent_token_store: None,
ux_update_sender,
pre_send_delay: None,
Expand Down Expand Up @@ -73,6 +75,14 @@ impl Ctap2AuthTokenStore for MockChannel {
self.auth_token_data = None;
}

fn set_cred_mgmt_preview(&mut self, uses_preview: bool) {
self.cred_mgmt_preview = uses_preview;
}

fn cred_mgmt_preview(&self) -> bool {
self.cred_mgmt_preview
}

fn persistent_token_store(&self) -> Option<Arc<dyn PersistentTokenStore>> {
self.persistent_token_store.clone()
}
Expand Down
10 changes: 10 additions & 0 deletions libwebauthn/src/transport/nfc/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ where
{
delegate: Box<dyn NfcBackend<Ctx> + Send + Sync>,
auth_token_data: Option<AuthTokenData>,
cred_mgmt_preview: bool,
persistent_token_store: Option<Arc<dyn PersistentTokenStore>>,
ux_update_sender: broadcast::Sender<UvUpdate>,
handle: NfcChannelHandle,
Expand Down Expand Up @@ -137,6 +138,7 @@ where
NfcChannel {
delegate,
auth_token_data: None,
cred_mgmt_preview: false,
persistent_token_store: settings.persistent_token_store,
ux_update_sender,
handle,
Expand Down Expand Up @@ -364,6 +366,14 @@ where
self.auth_token_data = None;
}

fn set_cred_mgmt_preview(&mut self, uses_preview: bool) {
self.cred_mgmt_preview = uses_preview;
}

fn cred_mgmt_preview(&self) -> bool {
self.cred_mgmt_preview
}

fn persistent_token_store(&self) -> Option<Arc<dyn PersistentTokenStore>> {
self.persistent_token_store.clone()
}
Expand Down
Loading