Skip to content

Commit a0768a4

Browse files
committed
Catch UvBlocked and have way to fall back to PIN internally
1 parent 417d707 commit a0768a4

File tree

4 files changed

+137
-101
lines changed

4 files changed

+137
-101
lines changed

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -644,26 +644,32 @@ impl Ctap2GetInfoResponse {
644644
(self.option_enabled("pinUvAuthToken") && self.option_enabled("uv"))
645645
}
646646

647-
pub fn uv_operation(&self) -> Ctap2UserVerificationOperation {
648-
if self.option_enabled("uv") {
647+
pub fn uv_operation(&self, uv_blocked: bool) -> Option<Ctap2UserVerificationOperation> {
648+
if self.option_enabled("uv") && !uv_blocked {
649649
if self.option_enabled("pinUvAuthToken") {
650650
debug!("getPinUvAuthTokenUsingUvWithPermissions");
651-
return Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions;
651+
return Some(
652+
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions,
653+
);
652654
} else {
653655
debug!("Deprecated FIDO 2.0 behaviour: populating 'uv' flag");
654-
return Ctap2UserVerificationOperation::None;
656+
return Some(Ctap2UserVerificationOperation::None);
655657
}
656658
} else {
657659
// !uv
658660
if self.option_enabled("pinUvAuthToken") {
659661
assert!(self.option_enabled("clientPin"));
660662
debug!("getPinUvAuthTokenUsingPinWithPermissions");
661-
return Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions;
662-
} else {
663+
return Some(
664+
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions,
665+
);
666+
} else if self.option_enabled("clientPin") {
663667
// !pinUvAuthToken
664-
assert!(self.option_enabled("clientPin"));
665668
debug!("getPinToken");
666-
return Ctap2UserVerificationOperation::GetPinToken;
669+
return Some(Ctap2UserVerificationOperation::GetPinToken);
670+
} else {
671+
debug!("No UV and no PIN (e.g. maybe UV was blocked and no PIN available)");
672+
return None;
667673
}
668674
}
669675
}

libwebauthn/src/proto/error.rs

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,53 @@ use crate::proto::ctap1::apdu::ApduResponseStatus;
77
#[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq)]
88
#[repr(u8)]
99
pub enum CtapError {
10-
Ok = 0x00, // CTAP1_ERR_SUCCESS, CTAP2_OK
11-
InvalidCommand = 0x01, // CTAP1_ERR_INVALID_COMMAND
12-
InvalidParameter = 0x02, // CTAP1_ERR_INVALID_PARAMETER
13-
InvalidLength = 0x03, // CTAP1_ERR_INVALID_LENGTH
14-
InvalidSeq = 0x04, // CTAP1_ERR_INVALID_SEQ
15-
Timeout = 0x05, // CTAP1_ERR_TIMEOUT
16-
ChannelBusy = 0x06, // CTAP1_ERR_CHANNEL_BUSY
17-
LockRequired = 0x0A, // CTAP1_ERR_LOCK_REQUIRED
18-
InvalidChannel = 0x0B, // CTAP1_ERR_INVALID_CHANNEL
19-
InvalidCborType = 0x11, // CTAP2_ERR_CBOR_UNEXPECTED_TYPE
20-
InvalidCbor = 0x12, // CTAP2_ERR_INVALID_CBOR
21-
MissingParameter = 0x14, // CTAP2_ERR_MISSING_PARAMETER
22-
LimitExceeded = 0x15, // CTAP2_ERR_LIMIT_EXCEEDED,
23-
UnsupportedExtension = 0x16, // CTAP2_ERR_UNSUPPORTED_EXTENSION
24-
CredentialExcluded = 0x19, // CTAP2_ERR_CREDENTIAL_EXCLUDED
25-
Processing = 0x21, // CTAP2_ERR_PROCESSING
26-
InvalidCredential = 0x22, // CTAP2_ERR_INVALID_CREDENTIAL
27-
UserActionPending = 0x23, // CTAP2_ERR_USER_ACTION_PENDING
28-
OperationPending = 0x24, // CTAP2_ERR_OPERATION_PENDING
29-
NoOperations = 0x25, // CTAP2_ERR_NO_OPERATIONS
30-
UnsupportedAlgorithm = 0x26, // CTAP2_ERR_UNSUPPORTED_ALGORITHM
31-
OperationDenied = 0x27, // CTAP2_ERR_OPERATION_DENIED
32-
KeyStoreFull = 0x28, // CTAP2_ERR_KEY_STORE_FULL
33-
NoOperationPending = 0x2A, // CTAP2_ERR_NO_OPERATION_PENDING
34-
UnsupportedOption = 0x2B, // CTAP2_ERR_UNSUPPORTED_OPTION
35-
InvalidOption = 0x2C, // CTAP2_ERR_INVALID_OPTION
36-
KeepAliveCancel = 0x2D, // CTAP2_ERR_KEEPALIVE_CANCEL
37-
NoCredentials = 0x2E, // CTAP2_ERR_NO_CREDENTIALS
38-
UserActionTimeout = 0x2F, // CTAP2_ERR_USER_ACTION_TIMEOUT
39-
NotAllowed = 0x30, // CTAP2_ERR_NOT_ALLOWED
40-
PINInvalid = 0x31, // CTAP2_ERR_PIN_INVALID
41-
PINBlocked = 0x32, // CTAP2_ERR_PIN_BLOCKED
42-
PINAuthInvalid = 0x33, // CTAP2_ERR_PIN_AUTH_INVALID
43-
PINAuthBlocked = 0x34, // CTAP2_ERR_PIN_AUTH_BLOCKED
44-
PINNotSet = 0x35, // CTAP2_ERR_PIN_NOT_SET
45-
PINRequired = 0x36, // CTAP2_ERR_PIN_REQUIRED
46-
PINPolicyViolation = 0x37, // CTAP2_ERR_PIN_POLICY_VIOLATION
47-
PINTokenExpired = 0x38, // CTAP2_ERR_PIN_TOKEN_EXPIRED
48-
RequestTooLarge = 0x39, // CTAP2_ERR_REQUEST_TOO_LARGE
49-
ActionTimeout = 0x3A, // CTAP2_ERR_ACTION_TIMEOUT
50-
UserPresenceRequired = 0x3B, // CTAP2_ERR_UP_REQUIRED
51-
UVInvalid = 0x3F, // CTAP2_ERR_UV_INVALID
52-
Other = 0x7F, // CTAP1_ERR_OTHER
10+
Ok = 0x00, // CTAP1_ERR_SUCCESS, CTAP2_OK
11+
InvalidCommand = 0x01, // CTAP1_ERR_INVALID_COMMAND
12+
InvalidParameter = 0x02, // CTAP1_ERR_INVALID_PARAMETER
13+
InvalidLength = 0x03, // CTAP1_ERR_INVALID_LENGTH
14+
InvalidSeq = 0x04, // CTAP1_ERR_INVALID_SEQ
15+
Timeout = 0x05, // CTAP1_ERR_TIMEOUT
16+
ChannelBusy = 0x06, // CTAP1_ERR_CHANNEL_BUSY
17+
LockRequired = 0x0A, // CTAP1_ERR_LOCK_REQUIRED
18+
InvalidChannel = 0x0B, // CTAP1_ERR_INVALID_CHANNEL
19+
InvalidCborType = 0x11, // CTAP2_ERR_CBOR_UNEXPECTED_TYPE
20+
InvalidCbor = 0x12, // CTAP2_ERR_INVALID_CBOR
21+
MissingParameter = 0x14, // CTAP2_ERR_MISSING_PARAMETER
22+
LimitExceeded = 0x15, // CTAP2_ERR_LIMIT_EXCEEDED,
23+
UnsupportedExtension = 0x16, // CTAP2_ERR_UNSUPPORTED_EXTENSION
24+
CredentialExcluded = 0x19, // CTAP2_ERR_CREDENTIAL_EXCLUDED
25+
Processing = 0x21, // CTAP2_ERR_PROCESSING
26+
InvalidCredential = 0x22, // CTAP2_ERR_INVALID_CREDENTIAL
27+
UserActionPending = 0x23, // CTAP2_ERR_USER_ACTION_PENDING
28+
OperationPending = 0x24, // CTAP2_ERR_OPERATION_PENDING
29+
NoOperations = 0x25, // CTAP2_ERR_NO_OPERATIONS
30+
UnsupportedAlgorithm = 0x26, // CTAP2_ERR_UNSUPPORTED_ALGORITHM
31+
OperationDenied = 0x27, // CTAP2_ERR_OPERATION_DENIED
32+
KeyStoreFull = 0x28, // CTAP2_ERR_KEY_STORE_FULL
33+
NoOperationPending = 0x2A, // CTAP2_ERR_NO_OPERATION_PENDING
34+
UnsupportedOption = 0x2B, // CTAP2_ERR_UNSUPPORTED_OPTION
35+
InvalidOption = 0x2C, // CTAP2_ERR_INVALID_OPTION
36+
KeepAliveCancel = 0x2D, // CTAP2_ERR_KEEPALIVE_CANCEL
37+
NoCredentials = 0x2E, // CTAP2_ERR_NO_CREDENTIALS
38+
UserActionTimeout = 0x2F, // CTAP2_ERR_USER_ACTION_TIMEOUT
39+
NotAllowed = 0x30, // CTAP2_ERR_NOT_ALLOWED
40+
PINInvalid = 0x31, // CTAP2_ERR_PIN_INVALID
41+
PINBlocked = 0x32, // CTAP2_ERR_PIN_BLOCKED
42+
PINAuthInvalid = 0x33, // CTAP2_ERR_PIN_AUTH_INVALID
43+
PINAuthBlocked = 0x34, // CTAP2_ERR_PIN_AUTH_BLOCKED
44+
PINNotSet = 0x35, // CTAP2_ERR_PIN_NOT_SET
45+
PINRequired = 0x36, // CTAP2_ERR_PIN_REQUIRED
46+
PINPolicyViolation = 0x37, // CTAP2_ERR_PIN_POLICY_VIOLATION
47+
PINTokenExpired = 0x38, // CTAP2_ERR_PIN_TOKEN_EXPIRED
48+
RequestTooLarge = 0x39, // CTAP2_ERR_REQUEST_TOO_LARGE
49+
ActionTimeout = 0x3A, // CTAP2_ERR_ACTION_TIMEOUT
50+
UserPresenceRequired = 0x3B, // CTAP2_ERR_UP_REQUIRED
51+
UvBlocked = 0x3C, // CTAP2_ERR_UV_BLOCKED
52+
IntegrityFailure = 0x3D, // CTAP2_ERR_INTEGRITY_FAILURE
53+
InvalidSubcommand = 0x3E, // CTAP2_ERR_INVALID_SUBCOMMAND
54+
UVInvalid = 0x3F, // CTAP2_ERR_UV_INVALID
55+
UnauthorizedPermission = 0x40, // CTAP2_ERR_UNAUTHORIZED_PERMISSION
56+
Other = 0x7F, // CTAP1_ERR_OTHER
5357
}
5458

5559
impl CtapError {

libwebauthn/src/transport/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub enum PlatformError {
55
PinTooShort,
66
PinTooLong,
77
PinNotSupported,
8+
NoUvAvailable,
89
}
910

1011
impl std::error::Error for PlatformError {}

libwebauthn/src/webauthn.rs

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::proto::ctap2::{
1919
Ctap2, Ctap2ClientPinRequest, Ctap2GetAssertionRequest, Ctap2GetInfoResponse,
2020
Ctap2MakeCredentialRequest, Ctap2UserVerifiableRequest, Ctap2UserVerificationOperation,
2121
};
22-
pub use crate::transport::error::{CtapError, Error, TransportError};
22+
pub use crate::transport::error::{CtapError, Error, PlatformError, TransportError};
2323
use crate::transport::Channel;
2424

2525
#[async_trait]
@@ -248,60 +248,85 @@ where
248248
return Ok(());
249249
}
250250

251-
let uv_operation = get_info_response.uv_operation();
252-
if let Ctap2UserVerificationOperation::None = uv_operation {
253-
debug!("No client operation. Setting deprecated request options.uv flag to true.");
254-
ctap2_request.ensure_uv_set();
255-
return Ok(());
256-
}
257-
258-
// For operations that include a PIN, we want to fetch one before obtaining a shared secret.
259-
// This prevents the shared secret from expiring whilst we wait for the user to enter a PIN.
260-
let pin = match uv_operation {
261-
Ctap2UserVerificationOperation::None => unreachable!(),
262-
Ctap2UserVerificationOperation::GetPinToken
263-
| Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions => {
264-
Some(obtain_pin(channel, pin_provider, timeout).await?)
265-
}
266-
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions => {
267-
None // TODO probably?
251+
let mut uv_blocked = false;
252+
let (uv_proto, token_response, shared_secret) = loop {
253+
let uv_operation = get_info_response.uv_operation(uv_blocked).ok_or_else(|| {
254+
if uv_blocked {
255+
Error::Ctap(CtapError::UvBlocked)
256+
} else {
257+
Error::Platform(PlatformError::NoUvAvailable)
258+
}
259+
})?;
260+
if let Ctap2UserVerificationOperation::None = uv_operation {
261+
debug!("No client operation. Setting deprecated request options.uv flag to true.");
262+
ctap2_request.ensure_uv_set();
263+
return Ok(());
268264
}
269-
};
270265

271-
// In preparation for obtaining pinUvAuthToken, the platform:
272-
// * Obtains a shared secret.
273-
let uv_proto = select_uv_proto(&get_info_response).await?;
274-
let (public_key, shared_secret) = obtain_shared_secret(channel, &uv_proto, timeout).await?;
275-
276-
// Then the platform obtains a pinUvAuthToken from the authenticator, with the mc (and likely also with the ga)
277-
// permission (see "pre-flight", mentioned above), using the selected operation.
278-
let token_request = match uv_operation {
279-
Ctap2UserVerificationOperation::None => unreachable!(),
280-
Ctap2UserVerificationOperation::GetPinToken => Ctap2ClientPinRequest::new_get_pin_token(
281-
uv_proto.version(),
282-
public_key,
283-
&uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?,
284-
),
285-
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions => {
286-
Ctap2ClientPinRequest::new_get_pin_token_with_perm(
287-
uv_proto.version(),
288-
public_key,
289-
&uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?,
290-
ctap2_request.permissions(),
291-
ctap2_request.permissions_rpid(),
292-
)
293-
}
294-
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions => {
295-
Ctap2ClientPinRequest::new_get_uv_token_with_perm(
296-
uv_proto.version(),
297-
public_key,
298-
ctap2_request.permissions(),
299-
ctap2_request.permissions_rpid(),
300-
)
266+
// For operations that include a PIN, we want to fetch one before obtaining a shared secret.
267+
// This prevents the shared secret from expiring whilst we wait for the user to enter a PIN.
268+
let pin = match uv_operation {
269+
Ctap2UserVerificationOperation::None => unreachable!(),
270+
Ctap2UserVerificationOperation::GetPinToken
271+
| Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions => {
272+
Some(obtain_pin(channel, pin_provider, timeout).await?)
273+
}
274+
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions => {
275+
None // TODO probably?
276+
}
277+
};
278+
279+
// In preparation for obtaining pinUvAuthToken, the platform:
280+
// * Obtains a shared secret.
281+
let uv_proto = select_uv_proto(&get_info_response).await?;
282+
let (public_key, shared_secret) = obtain_shared_secret(channel, &uv_proto, timeout).await?;
283+
284+
// Then the platform obtains a pinUvAuthToken from the authenticator, with the mc (and likely also with the ga)
285+
// permission (see "pre-flight", mentioned above), using the selected operation.
286+
let token_request = match uv_operation {
287+
Ctap2UserVerificationOperation::None => unreachable!(),
288+
Ctap2UserVerificationOperation::GetPinToken => {
289+
Ctap2ClientPinRequest::new_get_pin_token(
290+
uv_proto.version(),
291+
public_key,
292+
&uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?,
293+
)
294+
}
295+
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions => {
296+
Ctap2ClientPinRequest::new_get_pin_token_with_perm(
297+
uv_proto.version(),
298+
public_key,
299+
&uv_proto.encrypt(&shared_secret, &pin_hash(&pin.unwrap()))?,
300+
ctap2_request.permissions(),
301+
ctap2_request.permissions_rpid(),
302+
)
303+
}
304+
Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions => {
305+
Ctap2ClientPinRequest::new_get_uv_token_with_perm(
306+
uv_proto.version(),
307+
public_key,
308+
ctap2_request.permissions(),
309+
ctap2_request.permissions_rpid(),
310+
)
311+
}
312+
};
313+
314+
match channel.ctap2_client_pin(&token_request, timeout).await {
315+
Ok(t) => {
316+
break (uv_proto, t, shared_secret);
317+
}
318+
// Internal retry, because we otherwise can't fall back to PIN, if the UV is blocked
319+
Err(Error::Ctap(CtapError::UvBlocked)) => {
320+
warn!("UV failed too many times and is now blocked. Trying to fall back to PIN.");
321+
uv_blocked = true;
322+
continue;
323+
}
324+
Err(x) => {
325+
return Err(x);
326+
}
301327
}
302328
};
303329

304-
let token_response = channel.ctap2_client_pin(&token_request, timeout).await?;
305330
let Some(encrypted_pin_uv_auth_token) = token_response.pin_uv_auth_token else {
306331
error!("Client PIN response did not include a PIN UV auth token");
307332
return Err(Error::Ctap(CtapError::Other));

0 commit comments

Comments
 (0)