From 01ca5d5203a40255e677d802aa58ee5ea1f76b8f Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 20 Jun 2026 12:27:43 +0100 Subject: [PATCH] feat(ctap2): implement authenticatorReset command --- libwebauthn/src/management.rs | 6 +- .../src/management/authenticator_reset.rs | 136 ++++++++++++++++++ libwebauthn/src/ops/webauthn/large_blob.rs | 3 + libwebauthn/src/proto/ctap2/model.rs | 4 +- libwebauthn/src/proto/ctap2/protocol.rs | 19 +++ 5 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 libwebauthn/src/management/authenticator_reset.rs diff --git a/libwebauthn/src/management.rs b/libwebauthn/src/management.rs index 9534ef3f..19ea94f9 100644 --- a/libwebauthn/src/management.rs +++ b/libwebauthn/src/management.rs @@ -6,7 +6,8 @@ //! //! Use [`CredentialManagement`] to enumerate and delete resident credentials, //! [`AuthenticatorConfig`] to adjust device settings such as PIN policy and -//! enterprise attestation, and [`BioEnrollment`] to manage biometric templates. +//! enterprise attestation, [`BioEnrollment`] to manage biometric templates, and +//! [`AuthenticatorReset`] to restore an authenticator to factory defaults. //! Each trait is blanket-implemented for any //! [`Channel`](crate::transport::Channel), so the same API works across every //! transport. @@ -17,5 +18,8 @@ pub use bio_enrollment::BioEnrollment; mod authenticator_config; pub use authenticator_config::AuthenticatorConfig; +mod authenticator_reset; +pub use authenticator_reset::AuthenticatorReset; + mod credential_management; pub use credential_management::CredentialManagement; diff --git a/libwebauthn/src/management/authenticator_reset.rs b/libwebauthn/src/management/authenticator_reset.rs new file mode 100644 index 00000000..a8051ec7 --- /dev/null +++ b/libwebauthn/src/management/authenticator_reset.rs @@ -0,0 +1,136 @@ +use std::time::Duration; + +use async_trait::async_trait; +use tracing::warn; + +use crate::pin::persistent_token::recognize_authenticator; +use crate::proto::ctap2::Ctap2; +use crate::transport::Channel; +use crate::webauthn::error::Error; + +#[async_trait] +pub trait AuthenticatorReset { + /// Reset the authenticator to factory defaults, evicting any stored persistent token. + async fn reset(&mut self, timeout: Duration) -> Result<(), Error>; +} + +#[async_trait] +impl AuthenticatorReset for C +where + C: Channel, +{ + async fn reset(&mut self, timeout: Duration) -> Result<(), Error> { + // Recognize before reset, while the device identifier is still derivable. + let record_id = match self.persistent_token_store() { + Some(store) => match self.ctap2_get_info().await { + Ok(info) => recognize_authenticator(store.as_ref(), &info) + .await + .map(|(id, _)| id), + Err(error) => { + warn!( + ?error, + "getInfo before reset failed; cannot evict persistent token" + ); + None + } + }, + None => None, + }; + + self.ctap2_authenticator_reset(timeout).await?; + + if let (Some(store), Some(id)) = (self.persistent_token_store(), record_id) { + store.delete(&id).await; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use serde_bytes::ByteBuf; + + use super::AuthenticatorReset; + use crate::pin::persistent_token::{ + build_enc_identifier, MemoryPersistentTokenStore, PersistentTokenRecord, + PersistentTokenStore, + }; + use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; + use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol}; + use crate::transport::mock::channel::MockChannel; + use crate::webauthn::error::{CtapError, Error}; + + const TIMEOUT: Duration = Duration::from_secs(1); + + fn ok_response(data: Option>) -> CborResponse { + CborResponse { + status_code: CtapError::Ok, + data, + } + } + + #[tokio::test] + async fn reset_evicts_recognized_persistent_token() { + let token = vec![0x07; 32]; + let device_identifier = [0x42; 16]; + + let store = MemoryPersistentTokenStore::new(); + store + .put( + &"id-1".to_string(), + &PersistentTokenRecord { + persistent_token: token.clone(), + pin_uv_auth_protocol: Ctap2PinUvAuthProtocol::Two, + device_identifier, + aaguid: [0x22; 16], + }, + ) + .await; + + let info = Ctap2GetInfoResponse { + enc_identifier: Some(ByteBuf::from(build_enc_identifier( + &token, + &device_identifier, + &[0x33; 16], + ))), + ..Default::default() + }; + let info_bytes = crate::proto::ctap2::cbor::to_vec(&info).unwrap(); + + let mut channel = MockChannel::new(); + channel.set_persistent_token_store(Arc::new(store.clone())); + channel.push_command_pair( + CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo), + ok_response(Some(info_bytes)), + ); + channel.push_command_pair( + CborRequest::new(Ctap2CommandCode::AuthenticatorReset), + ok_response(None), + ); + + channel.reset(TIMEOUT).await.unwrap(); + + assert!( + store.list().await.is_empty(), + "reset must evict the recognized persistent token record" + ); + } + + #[tokio::test] + async fn reset_propagates_non_ok_status() { + let mut channel = MockChannel::new(); + channel.push_command_pair( + CborRequest::new(Ctap2CommandCode::AuthenticatorReset), + CborResponse { + status_code: CtapError::OperationDenied, + data: None, + }, + ); + + let result = channel.reset(TIMEOUT).await; + assert_eq!(result.err(), Some(Error::Ctap(CtapError::OperationDenied))); + } +} diff --git a/libwebauthn/src/ops/webauthn/large_blob.rs b/libwebauthn/src/ops/webauthn/large_blob.rs index 14787bf5..cff4a16b 100644 --- a/libwebauthn/src/ops/webauthn/large_blob.rs +++ b/libwebauthn/src/ops/webauthn/large_blob.rs @@ -1840,6 +1840,9 @@ mod tests { async fn ctap2_selection(&mut self, _t: Duration) -> Result<(), Error> { unimplemented!() } + async fn ctap2_authenticator_reset(&mut self, _t: Duration) -> Result<(), Error> { + unimplemented!() + } async fn ctap2_authenticator_config( &mut self, _r: &Ctap2AuthenticatorConfigRequest, diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index a8e86e4f..a7482534 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -57,6 +57,7 @@ pub enum Ctap2CommandCode { AuthenticatorGetAssertion = 0x02, AuthenticatorGetInfo = 0x04, AuthenticatorClientPin = 0x06, + AuthenticatorReset = 0x07, AuthenticatorGetNextAssertion = 0x08, AuthenticatorBioEnrollment = 0x09, AuthenticatorBioEnrollmentPreview = 0x40, @@ -65,9 +66,6 @@ pub enum Ctap2CommandCode { AuthenticatorSelection = 0x0B, AuthenticatorLargeBlobs = 0x0C, AuthenticatorConfig = 0x0D, - // TODO: authenticatorReset (0x07) is not implemented. When it is added, a successful - // reset must evict this device's persistent pcmr record from the persistent token - // store, since reset regenerates the device identifier and invalidates the token. } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/libwebauthn/src/proto/ctap2/protocol.rs b/libwebauthn/src/proto/ctap2/protocol.rs index 73e00dfd..eb2583e5 100644 --- a/libwebauthn/src/proto/ctap2/protocol.rs +++ b/libwebauthn/src/proto/ctap2/protocol.rs @@ -58,6 +58,7 @@ pub trait Ctap2 { timeout: Duration, ) -> Result; async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error>; + async fn ctap2_authenticator_reset(&mut self, timeout: Duration) -> Result<(), Error>; async fn ctap2_authenticator_config( &mut self, request: &Ctap2AuthenticatorConfigRequest, @@ -181,6 +182,24 @@ where } } + #[instrument(skip_all)] + async fn ctap2_authenticator_reset(&mut self, timeout: Duration) -> Result<(), Error> { + debug!("CTAP2 Authenticator Reset request"); + let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorReset); + self.cbor_send(&cbor_request, timeout).await?; + let cbor_response = self.cbor_recv(timeout).await?; + match cbor_response.status_code { + CtapError::Ok => Ok(()), + error => { + warn!( + ?error, + "Authenticator reset request failed with status code" + ); + Err(Error::Ctap(error)) + } + } + } + #[instrument(skip_all)] async fn ctap2_client_pin( &mut self,