Skip to content

Commit 3a440ad

Browse files
fix(webauthn): enforce userVerification=required on the shared-secret path (#248)
When a ceremony also needed a shared secret, for example any request using the PRF extension, the check that enforces required user verification was skipped. On a device that can verify the user but is not yet enrolled, a registration requested with required user verification could complete with no user verification at all, and the PRF output came from the non-verified variant. This enforces required user verification regardless of whether a shared secret is needed. When the device cannot verify the user, the request now fails clearly instead of proceeding. Requests that prefer or discourage user verification are unaffected. Includes a regression test covering the required, not-yet-enrolled case.
1 parent 09623ac commit 3a440ad

1 file changed

Lines changed: 78 additions & 24 deletions

File tree

libwebauthn/src/webauthn/pin_uv_auth_token.rs

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -137,43 +137,45 @@ where
137137
{
138138
let rp_uv_preferred = user_verification.is_preferred();
139139
let rp_uv_discouraged = user_verification.is_discouraged();
140-
let dev_uv_protected = get_info_response.is_uv_protected();
140+
let mut dev_uv_protected = get_info_response.is_uv_protected();
141141
let can_establish_shared_secret = get_info_response.can_establish_shared_secret();
142142
let needs_shared_secret = ctap2_request.needs_shared_secret(get_info_response);
143143
// If it is not discouraged and either RP or device requires it.
144144
let uv = !rp_uv_discouraged && (rp_uv_preferred || dev_uv_protected);
145145

146146
debug!(%rp_uv_preferred, %rp_uv_discouraged, %dev_uv_protected, %uv, %needs_shared_secret, %can_establish_shared_secret, "Checking if user verification is required");
147+
// Enforce required UV even on the shared-secret path. A shared-secret-only result performs no UV.
148+
if user_verification.is_required() && !dev_uv_protected {
149+
error!(
150+
"Request requires user verification, but device user verification is not available. Try letting the user set a PIN."
151+
);
152+
// Lets try setting a PIN. Either we succeed in some fashion, or we return the resulting error here instead.
153+
try_to_set_pin(
154+
channel,
155+
get_info_response,
156+
PinNotSetReason::PinNotSet,
157+
timeout,
158+
)
159+
.await?;
160+
// Update get_info_response, because now maybe "clientPin" is set to `Some(true)`
161+
*get_info_response = channel.ctap2_get_info().await?;
162+
dev_uv_protected = get_info_response.is_uv_protected();
163+
// Still no usable UV: do not silently fall back to a shared-secret-only result.
164+
if !dev_uv_protected {
165+
return Err(Error::Platform(PlatformError::NoUvAvailable));
166+
}
167+
}
168+
147169
// If we do not need to create a shared secret, we can error out here early
148170
if !needs_shared_secret {
149171
if !uv {
150172
debug!("User verification not requested by either RP nor authenticator. Ignoring.");
151173
return Ok(UsedPinUvAuthToken::None);
152174
}
153175

154-
if !dev_uv_protected {
155-
if user_verification.is_required() {
156-
error!(
157-
"Request requires user verification, but device user verification is not available. Try letting the user set a PIN."
158-
);
159-
// Lets try setting a PIN. Either we succeed in some fashion, or we return the resulting error here instead.
160-
try_to_set_pin(
161-
channel,
162-
get_info_response,
163-
PinNotSetReason::PinNotSet,
164-
timeout,
165-
)
166-
.await?;
167-
// Update get_info_response, because now maybe "clientPin" is set to `Some(true)`
168-
*get_info_response = channel.ctap2_get_info().await?;
169-
// Then simply continue with the normal flow. Either we have a PIN set now,
170-
// then the user has to enter it (again), as in the normal flow, or the device
171-
// itself has some kind of internal protocol to handle this situation.
172-
// If not, it will (should) return an error like PinNotSet.
173-
} else if user_verification.is_preferred() {
174-
warn!("User verification is preferred, but device user verification is not available. Ignoring.");
175-
return Ok(UsedPinUvAuthToken::None);
176-
}
176+
if !dev_uv_protected && user_verification.is_preferred() {
177+
warn!("User verification is preferred, but device user verification is not available. Ignoring.");
178+
return Ok(UsedPinUvAuthToken::None);
177179
}
178180
} else if !can_establish_shared_secret && !uv {
179181
// We need a shared secret, but the device does not support any form of query-able UV, so we can't establish a
@@ -1191,6 +1193,58 @@ mod test {
11911193
}
11921194
}
11931195

1196+
#[tokio::test]
1197+
async fn required_uv_unenrolled_with_shared_secret_errors_instead_of_shared_secret_only() {
1198+
// Device is UV-capable (built-in `uv` present) but not enrolled, yet can establish a
1199+
// shared secret via `pinUvAuthToken`. With PRF requested the request needs a shared
1200+
// secret, but `userVerification=required` must not resolve to a shared-secret-only
1201+
// result that performs no user verification.
1202+
let mut channel = MockChannel::new();
1203+
let status_recv = channel.get_ux_update_receiver();
1204+
1205+
let mut info = create_info(
1206+
&[("uv", false), ("pinUvAuthToken", true)],
1207+
Some(&["hmac-secret"]),
1208+
);
1209+
info.pin_auth_protos = Some(vec![1]);
1210+
let info_req = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
1211+
let info_resp = CborResponse::new_success_from_slice(to_vec(&info).unwrap().as_slice());
1212+
// Initial GetInfo plus the refresh after the (no-op) PIN-set attempt.
1213+
channel.push_command_pair(info_req.clone(), info_resp.clone());
1214+
channel.push_command_pair(info_req, info_resp);
1215+
1216+
let extensions = Some(GetAssertionRequestExtensions {
1217+
prf: Some(PrfInput {
1218+
eval: Some(PrfInputValue {
1219+
first: vec![0; 32],
1220+
second: None,
1221+
}),
1222+
eval_by_credential: HashMap::new(),
1223+
}),
1224+
..Default::default()
1225+
});
1226+
let mut getassertion = create_get_assertion(&info, extensions);
1227+
1228+
let resp = user_verification(
1229+
&mut channel,
1230+
UserVerificationRequirement::Required,
1231+
&mut getassertion,
1232+
TIMEOUT,
1233+
)
1234+
.await;
1235+
1236+
assert_eq!(
1237+
resp,
1238+
Err(Error::Platform(
1239+
crate::webauthn::PlatformError::NoUvAvailable
1240+
))
1241+
);
1242+
// No shared-secret-only fallback ended up in the auth store.
1243+
assert!(channel.get_auth_data().is_none());
1244+
// No UX updates: the device has no clientPin to set.
1245+
assert!(status_recv.is_empty());
1246+
}
1247+
11941248
#[tokio::test]
11951249
async fn full_ceremony_using_uv() {
11961250
let testcases = vec![

0 commit comments

Comments
 (0)