Skip to content

Commit 5376aac

Browse files
fix(authnr-config): gate config subcommands on getInfo capabilities
Each AuthenticatorConfig method now reads getInfo at entry and gates on capabilities before any UV round-trip, returning a clean platform error instead of relying on device rejection. toggle_always_uv is gated on the alwaysUv option only, not setMinPINLength. The two are independent subcommand options per CTAP 2.1 6.2.5 and the existing HID example treats them separately.
1 parent d6b9dcb commit 5376aac

1 file changed

Lines changed: 124 additions & 1 deletion

File tree

libwebauthn/src/management/authenticator_config.rs

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::proto::ctap2::cbor;
22
use crate::proto::ctap2::Ctap2ClientPinRequest;
33
use crate::transport::Channel;
4-
pub use crate::webauthn::error::{CtapError, Error};
4+
pub use crate::webauthn::error::{CtapError, Error, PlatformError};
55
use crate::webauthn::handle_errors;
66
use crate::webauthn::pin_uv_auth_token::{user_verification, UsedPinUvAuthToken};
77
use crate::{
@@ -45,6 +45,12 @@ where
4545
C: Channel,
4646
{
4747
async fn toggle_always_uv(&mut self, timeout: Duration) -> Result<(), Error> {
48+
let info = self.ctap2_get_info().await?;
49+
// CTAP 2.1 6.2.5: toggleAlwaysUv is gated on the alwaysUv option only.
50+
if !info.option_enabled("authnrCfg") || !info.option_exists("alwaysUv") {
51+
return Err(Error::Platform(PlatformError::NotSupported));
52+
}
53+
4854
let mut req = Ctap2AuthenticatorConfigRequest::new_toggle_always_uv();
4955

5056
loop {
@@ -66,6 +72,11 @@ where
6672
}
6773

6874
async fn enable_enterprise_attestation(&mut self, timeout: Duration) -> Result<(), Error> {
75+
let info = self.ctap2_get_info().await?;
76+
if !info.option_enabled("authnrCfg") || !info.option_exists("ep") {
77+
return Err(Error::Platform(PlatformError::NotSupported));
78+
}
79+
6980
let mut req = Ctap2AuthenticatorConfigRequest::new_enable_enterprise_attestation();
7081

7182
loop {
@@ -91,6 +102,11 @@ where
91102
new_pin_length: u64,
92103
timeout: Duration,
93104
) -> Result<(), Error> {
105+
let info = self.ctap2_get_info().await?;
106+
if !info.option_enabled("authnrCfg") || !info.option_exists("setMinPINLength") {
107+
return Err(Error::Platform(PlatformError::NotSupported));
108+
}
109+
94110
let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length(new_pin_length);
95111

96112
loop {
@@ -112,6 +128,11 @@ where
112128
}
113129

114130
async fn force_change_pin(&mut self, force: bool, timeout: Duration) -> Result<(), Error> {
131+
let info = self.ctap2_get_info().await?;
132+
if !info.option_enabled("authnrCfg") || !info.option_exists("setMinPINLength") {
133+
return Err(Error::Platform(PlatformError::NotSupported));
134+
}
135+
115136
let mut req = Ctap2AuthenticatorConfigRequest::new_force_change_pin(force);
116137

117138
loop {
@@ -137,6 +158,18 @@ where
137158
rpids: Vec<String>,
138159
timeout: Duration,
139160
) -> Result<(), Error> {
161+
let info = self.ctap2_get_info().await?;
162+
if !info.option_enabled("authnrCfg")
163+
|| !info.option_exists("setMinPINLength")
164+
|| !info.supports_extension("minPinLength")
165+
{
166+
return Err(Error::Platform(PlatformError::NotSupported));
167+
}
168+
let max_rpids = u64::from(info.max_rpids_for_setminpinlength.unwrap_or(u32::MAX));
169+
if rpids.len() as u64 > max_rpids {
170+
return Err(Error::Platform(PlatformError::RequestTooLarge));
171+
}
172+
140173
let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length_rpids(rpids);
141174
loop {
142175
let uv_auth_used = user_verification(
@@ -201,3 +234,93 @@ impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest {
201234
false
202235
}
203236
}
237+
238+
#[cfg(test)]
239+
mod test {
240+
use std::collections::HashMap;
241+
use std::time::Duration;
242+
243+
use super::{AuthenticatorConfig, Error, PlatformError};
244+
use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse};
245+
use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse};
246+
use crate::transport::mock::channel::MockChannel;
247+
248+
const TIMEOUT: Duration = Duration::from_secs(1);
249+
250+
fn push_get_info(channel: &mut MockChannel, info: &Ctap2GetInfoResponse) {
251+
let req = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
252+
let resp = CborResponse::new_success_from_slice(&cbor::to_vec(info).unwrap());
253+
channel.push_command_pair(req, resp);
254+
}
255+
256+
#[tokio::test]
257+
async fn toggle_always_uv_rejected_when_authnr_cfg_absent() {
258+
let mut channel = MockChannel::new();
259+
push_get_info(
260+
&mut channel,
261+
&Ctap2GetInfoResponse {
262+
options: Some(HashMap::from([("alwaysUv".to_string(), false)])),
263+
..Default::default()
264+
},
265+
);
266+
267+
let result = channel.toggle_always_uv(TIMEOUT).await;
268+
assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported)));
269+
}
270+
271+
#[tokio::test]
272+
async fn toggle_always_uv_rejected_when_always_uv_option_absent() {
273+
let mut channel = MockChannel::new();
274+
push_get_info(
275+
&mut channel,
276+
&Ctap2GetInfoResponse {
277+
options: Some(HashMap::from([("authnrCfg".to_string(), true)])),
278+
..Default::default()
279+
},
280+
);
281+
282+
let result = channel.toggle_always_uv(TIMEOUT).await;
283+
assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported)));
284+
}
285+
286+
#[tokio::test]
287+
async fn set_min_pin_length_rpids_rejected_when_extension_absent() {
288+
let mut channel = MockChannel::new();
289+
push_get_info(
290+
&mut channel,
291+
&Ctap2GetInfoResponse {
292+
options: Some(HashMap::from([
293+
("authnrCfg".to_string(), true),
294+
("setMinPINLength".to_string(), true),
295+
])),
296+
..Default::default()
297+
},
298+
);
299+
300+
let result = channel
301+
.set_min_pin_length_rpids(vec!["example.com".to_string()], TIMEOUT)
302+
.await;
303+
assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported)));
304+
}
305+
306+
#[tokio::test]
307+
async fn set_min_pin_length_rpids_rejected_when_too_many_rpids() {
308+
let mut channel = MockChannel::new();
309+
push_get_info(
310+
&mut channel,
311+
&Ctap2GetInfoResponse {
312+
options: Some(HashMap::from([
313+
("authnrCfg".to_string(), true),
314+
("setMinPINLength".to_string(), true),
315+
])),
316+
extensions: Some(vec!["minPinLength".to_string()]),
317+
max_rpids_for_setminpinlength: Some(1),
318+
..Default::default()
319+
},
320+
);
321+
322+
let rpids = vec!["example.com".to_string(), "example.org".to_string()];
323+
let result = channel.set_min_pin_length_rpids(rpids, TIMEOUT).await;
324+
assert_eq!(result, Err(Error::Platform(PlatformError::RequestTooLarge)));
325+
}
326+
}

0 commit comments

Comments
 (0)