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..51af187d --- /dev/null +++ b/libwebauthn/src/management/authenticator_reset.rs @@ -0,0 +1,143 @@ +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::WebAuthnError; + +#[async_trait] +pub trait AuthenticatorReset: Channel { + /// Reset the authenticator to factory defaults, evicting any stored persistent token. + async fn reset(&mut self, timeout: Duration) + -> Result<(), WebAuthnError>; +} + +#[async_trait] +impl AuthenticatorReset for C +where + C: Channel, +{ + async fn reset( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { + // 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, WebAuthnError}; + + 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!(matches!( + result.unwrap_err(), + WebAuthnError::Ctap(CtapError::OperationDenied) + )); + } +} diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index f09a9b3d..7b7217ea 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -78,6 +78,7 @@ pub enum Ctap2CommandCode { AuthenticatorGetAssertion = 0x02, AuthenticatorGetInfo = 0x04, AuthenticatorClientPin = 0x06, + AuthenticatorReset = 0x07, AuthenticatorGetNextAssertion = 0x08, AuthenticatorBioEnrollment = 0x09, AuthenticatorBioEnrollmentPreview = 0x40, @@ -86,9 +87,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 8acde140..8cf44368 100644 --- a/libwebauthn/src/proto/ctap2/protocol.rs +++ b/libwebauthn/src/proto/ctap2/protocol.rs @@ -65,6 +65,10 @@ pub trait Ctap2: Channel { &mut self, timeout: Duration, ) -> Result<(), WebAuthnError>; + async fn ctap2_authenticator_reset( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError>; async fn ctap2_authenticator_config( &mut self, request: &Ctap2AuthenticatorConfigRequest, @@ -218,6 +222,32 @@ where } } + #[instrument(skip_all)] + async fn ctap2_authenticator_reset( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { + debug!("CTAP2 Authenticator Reset request"); + let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorReset); + self.cbor_send(&cbor_request, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; + match cbor_response.status_code { + CtapError::Ok => Ok(()), + error => { + warn!( + ?error, + "Authenticator reset request failed with status code" + ); + Err(WebAuthnError::Ctap(error)) + } + } + } + #[instrument(skip_all)] async fn ctap2_client_pin( &mut self,