Skip to content

Commit ac426c2

Browse files
fix(authnr-config): gate config subcommands on getInfo capabilities (#291)
Authenticator configuration subcommands were sent without first confirming the device advertises support, and the minimum PIN length RP list was sent without a bound. Each subcommand now checks the relevant getInfo capability before being sent, and the RP list is bounded to the device limit. This avoids predictable rejections and respects the authenticator declared limits.
1 parent d6b9dcb commit ac426c2

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)