Skip to content

Commit 01ca5d5

Browse files
feat(ctap2): implement authenticatorReset command
1 parent dca2054 commit 01ca5d5

5 files changed

Lines changed: 164 additions & 4 deletions

File tree

libwebauthn/src/management.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//!
77
//! Use [`CredentialManagement`] to enumerate and delete resident credentials,
88
//! [`AuthenticatorConfig`] to adjust device settings such as PIN policy and
9-
//! enterprise attestation, and [`BioEnrollment`] to manage biometric templates.
9+
//! enterprise attestation, [`BioEnrollment`] to manage biometric templates, and
10+
//! [`AuthenticatorReset`] to restore an authenticator to factory defaults.
1011
//! Each trait is blanket-implemented for any
1112
//! [`Channel`](crate::transport::Channel), so the same API works across every
1213
//! transport.
@@ -17,5 +18,8 @@ pub use bio_enrollment::BioEnrollment;
1718
mod authenticator_config;
1819
pub use authenticator_config::AuthenticatorConfig;
1920

21+
mod authenticator_reset;
22+
pub use authenticator_reset::AuthenticatorReset;
23+
2024
mod credential_management;
2125
pub use credential_management::CredentialManagement;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::time::Duration;
2+
3+
use async_trait::async_trait;
4+
use tracing::warn;
5+
6+
use crate::pin::persistent_token::recognize_authenticator;
7+
use crate::proto::ctap2::Ctap2;
8+
use crate::transport::Channel;
9+
use crate::webauthn::error::Error;
10+
11+
#[async_trait]
12+
pub trait AuthenticatorReset {
13+
/// Reset the authenticator to factory defaults, evicting any stored persistent token.
14+
async fn reset(&mut self, timeout: Duration) -> Result<(), Error>;
15+
}
16+
17+
#[async_trait]
18+
impl<C> AuthenticatorReset for C
19+
where
20+
C: Channel,
21+
{
22+
async fn reset(&mut self, timeout: Duration) -> Result<(), Error> {
23+
// Recognize before reset, while the device identifier is still derivable.
24+
let record_id = match self.persistent_token_store() {
25+
Some(store) => match self.ctap2_get_info().await {
26+
Ok(info) => recognize_authenticator(store.as_ref(), &info)
27+
.await
28+
.map(|(id, _)| id),
29+
Err(error) => {
30+
warn!(
31+
?error,
32+
"getInfo before reset failed; cannot evict persistent token"
33+
);
34+
None
35+
}
36+
},
37+
None => None,
38+
};
39+
40+
self.ctap2_authenticator_reset(timeout).await?;
41+
42+
if let (Some(store), Some(id)) = (self.persistent_token_store(), record_id) {
43+
store.delete(&id).await;
44+
}
45+
Ok(())
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use std::sync::Arc;
52+
use std::time::Duration;
53+
54+
use serde_bytes::ByteBuf;
55+
56+
use super::AuthenticatorReset;
57+
use crate::pin::persistent_token::{
58+
build_enc_identifier, MemoryPersistentTokenStore, PersistentTokenRecord,
59+
PersistentTokenStore,
60+
};
61+
use crate::proto::ctap2::cbor::{CborRequest, CborResponse};
62+
use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol};
63+
use crate::transport::mock::channel::MockChannel;
64+
use crate::webauthn::error::{CtapError, Error};
65+
66+
const TIMEOUT: Duration = Duration::from_secs(1);
67+
68+
fn ok_response(data: Option<Vec<u8>>) -> CborResponse {
69+
CborResponse {
70+
status_code: CtapError::Ok,
71+
data,
72+
}
73+
}
74+
75+
#[tokio::test]
76+
async fn reset_evicts_recognized_persistent_token() {
77+
let token = vec![0x07; 32];
78+
let device_identifier = [0x42; 16];
79+
80+
let store = MemoryPersistentTokenStore::new();
81+
store
82+
.put(
83+
&"id-1".to_string(),
84+
&PersistentTokenRecord {
85+
persistent_token: token.clone(),
86+
pin_uv_auth_protocol: Ctap2PinUvAuthProtocol::Two,
87+
device_identifier,
88+
aaguid: [0x22; 16],
89+
},
90+
)
91+
.await;
92+
93+
let info = Ctap2GetInfoResponse {
94+
enc_identifier: Some(ByteBuf::from(build_enc_identifier(
95+
&token,
96+
&device_identifier,
97+
&[0x33; 16],
98+
))),
99+
..Default::default()
100+
};
101+
let info_bytes = crate::proto::ctap2::cbor::to_vec(&info).unwrap();
102+
103+
let mut channel = MockChannel::new();
104+
channel.set_persistent_token_store(Arc::new(store.clone()));
105+
channel.push_command_pair(
106+
CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo),
107+
ok_response(Some(info_bytes)),
108+
);
109+
channel.push_command_pair(
110+
CborRequest::new(Ctap2CommandCode::AuthenticatorReset),
111+
ok_response(None),
112+
);
113+
114+
channel.reset(TIMEOUT).await.unwrap();
115+
116+
assert!(
117+
store.list().await.is_empty(),
118+
"reset must evict the recognized persistent token record"
119+
);
120+
}
121+
122+
#[tokio::test]
123+
async fn reset_propagates_non_ok_status() {
124+
let mut channel = MockChannel::new();
125+
channel.push_command_pair(
126+
CborRequest::new(Ctap2CommandCode::AuthenticatorReset),
127+
CborResponse {
128+
status_code: CtapError::OperationDenied,
129+
data: None,
130+
},
131+
);
132+
133+
let result = channel.reset(TIMEOUT).await;
134+
assert_eq!(result.err(), Some(Error::Ctap(CtapError::OperationDenied)));
135+
}
136+
}

libwebauthn/src/ops/webauthn/large_blob.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,6 +1840,9 @@ mod tests {
18401840
async fn ctap2_selection(&mut self, _t: Duration) -> Result<(), Error> {
18411841
unimplemented!()
18421842
}
1843+
async fn ctap2_authenticator_reset(&mut self, _t: Duration) -> Result<(), Error> {
1844+
unimplemented!()
1845+
}
18431846
async fn ctap2_authenticator_config(
18441847
&mut self,
18451848
_r: &Ctap2AuthenticatorConfigRequest,

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub enum Ctap2CommandCode {
5757
AuthenticatorGetAssertion = 0x02,
5858
AuthenticatorGetInfo = 0x04,
5959
AuthenticatorClientPin = 0x06,
60+
AuthenticatorReset = 0x07,
6061
AuthenticatorGetNextAssertion = 0x08,
6162
AuthenticatorBioEnrollment = 0x09,
6263
AuthenticatorBioEnrollmentPreview = 0x40,
@@ -65,9 +66,6 @@ pub enum Ctap2CommandCode {
6566
AuthenticatorSelection = 0x0B,
6667
AuthenticatorLargeBlobs = 0x0C,
6768
AuthenticatorConfig = 0x0D,
68-
// TODO: authenticatorReset (0x07) is not implemented. When it is added, a successful
69-
// reset must evict this device's persistent pcmr record from the persistent token
70-
// store, since reset regenerates the device identifier and invalidates the token.
7169
}
7270

7371
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

libwebauthn/src/proto/ctap2/protocol.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub trait Ctap2 {
5858
timeout: Duration,
5959
) -> Result<Ctap2GetAssertionResponse, Error>;
6060
async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error>;
61+
async fn ctap2_authenticator_reset(&mut self, timeout: Duration) -> Result<(), Error>;
6162
async fn ctap2_authenticator_config(
6263
&mut self,
6364
request: &Ctap2AuthenticatorConfigRequest,
@@ -181,6 +182,24 @@ where
181182
}
182183
}
183184

185+
#[instrument(skip_all)]
186+
async fn ctap2_authenticator_reset(&mut self, timeout: Duration) -> Result<(), Error> {
187+
debug!("CTAP2 Authenticator Reset request");
188+
let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorReset);
189+
self.cbor_send(&cbor_request, timeout).await?;
190+
let cbor_response = self.cbor_recv(timeout).await?;
191+
match cbor_response.status_code {
192+
CtapError::Ok => Ok(()),
193+
error => {
194+
warn!(
195+
?error,
196+
"Authenticator reset request failed with status code"
197+
);
198+
Err(Error::Ctap(error))
199+
}
200+
}
201+
}
202+
184203
#[instrument(skip_all)]
185204
async fn ctap2_client_pin(
186205
&mut self,

0 commit comments

Comments
 (0)