Skip to content

Commit 2677e25

Browse files
authored
Merge pull request #76 from bitwarden/bitwarden/add-ui-hints
Add more context to check_user()
2 parents f465818 + ddde57e commit 2677e25

File tree

12 files changed

+196
-59
lines changed

12 files changed

+196
-59
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
- ⚠ BREAKING: The `UserValidationMethod` trait has been updated to use `UiHint`
6+
to give the implementation more information about the request, which can be used
7+
to decide whether additional validations are needed. To reflect this, the
8+
`UserValidationMethod` trait now also returns which validations were performed.
9+
510
## Passkey v0.5.0
611

712
- Migrate project to Rust 2024 edition

passkey-authenticator/src/authenticator.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use passkey_types::{
44
webauthn,
55
};
66

7-
use crate::{CredentialStore, UserValidationMethod};
7+
use crate::{CredentialStore, UserValidationMethod, user_validation::UiHint};
88

99
pub mod extensions;
1010
mod get_assertion;
@@ -201,16 +201,16 @@ where
201201
/// CTAP2_ERR_OPERATION_DENIED error.
202202
async fn check_user(
203203
&self,
204+
hint: UiHint<'_, <U as UserValidationMethod>::PasskeyItem>,
204205
options: &passkey_types::ctap2::make_credential::Options,
205-
credential: Option<&<U as UserValidationMethod>::PasskeyItem>,
206206
) -> Result<Flags, Ctap2Error> {
207207
if options.uv && self.user_validation.is_verification_enabled() != Some(true) {
208208
return Err(Ctap2Error::UnsupportedOption);
209209
};
210210

211211
let check_result = self
212212
.user_validation
213-
.check_user(credential, options.up, options.uv)
213+
.check_user(hint, options.up, options.uv)
214214
.await?;
215215

216216
if options.up && !check_result.presence {

passkey-authenticator/src/authenticator/get_assertion.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use passkey_types::{
77
webauthn::PublicKeyCredentialUserEntity,
88
};
99

10-
use crate::{Authenticator, CredentialStore, UserValidationMethod, private_key_from_cose_key};
10+
use crate::{
11+
Authenticator, CredentialStore, UserValidationMethod, private_key_from_cose_key,
12+
user_validation::UiHint,
13+
};
1114

1215
impl<S, U> Authenticator<S, U>
1316
where
@@ -70,9 +73,11 @@ where
7073
// 7. Collect user consent if required. This step MUST happen before the following steps due
7174
// to privacy reasons (i.e., authenticator cannot disclose existence of a credential
7275
// until the user interacted with the device):
73-
let flags = self
74-
.check_user(&input.options, maybe_credential.as_ref().ok())
75-
.await?;
76+
let hint = match &maybe_credential {
77+
Ok(credential) => UiHint::RequestExistingCredential(credential),
78+
Err(_) => UiHint::InformNoCredentialsFound,
79+
};
80+
let flags = self.check_user(hint, &input.options).await?;
7681

7782
// 8. If no credentials were located in step 1, return CTAP2_ERR_NO_CREDENTIALS.
7883
let mut credential = maybe_credential?

passkey-authenticator/src/authenticator/get_assertion/tests.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use passkey_types::{
22
Passkey, StoredHmacSecret,
33
ctap2::{
4-
Aaguid,
4+
Aaguid, Ctap2Error,
55
get_assertion::{ExtensionInputs, Options, Request},
66
},
77
rand::random_vec,
@@ -10,6 +10,7 @@ use passkey_types::{
1010
use crate::{
1111
Authenticator, MockUserValidationMethod,
1212
extensions::{self, prf_eval_request},
13+
user_validation::MockUiHint,
1314
};
1415

1516
fn create_passkey(hmac_secret: Option<Vec<u8>>) -> Passkey {
@@ -42,18 +43,40 @@ fn good_request() -> Request {
4243
}
4344
}
4445

46+
#[tokio::test]
47+
async fn get_assertion_returns_no_credentials_found() {
48+
// Arrange
49+
let request = good_request();
50+
let store = None;
51+
let mut authenticator = Authenticator::new(
52+
Aaguid::new_empty(),
53+
store,
54+
MockUserValidationMethod::verified_user_with_hint(1, MockUiHint::InformNoCredentialsFound),
55+
);
56+
57+
// Act
58+
let response = authenticator.get_assertion(request).await;
59+
60+
// Assert
61+
assert_eq!(response.unwrap_err(), Ctap2Error::NoCredentials.into(),);
62+
}
63+
4564
#[tokio::test]
4665
async fn get_assertion_increments_signature_counter_when_counter_is_some() {
4766
// Arrange
4867
let request = good_request();
49-
let store = Some(Passkey {
68+
let passkey = Passkey {
5069
counter: Some(9000),
5170
..create_passkey(None)
52-
});
71+
};
72+
let store = Some(passkey.clone());
5373
let mut authenticator = Authenticator::new(
5474
Aaguid::new_empty(),
5575
store,
56-
MockUserValidationMethod::verified_user(1),
76+
MockUserValidationMethod::verified_user_with_hint(
77+
1,
78+
MockUiHint::RequestExistingCredential(passkey),
79+
),
5780
);
5881

5982
// Act

passkey-authenticator/src/authenticator/make_credential.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::ops::Not;
2-
31
use p256::SecretKey;
42
use passkey_types::{
53
Passkey,
@@ -9,18 +7,16 @@ use passkey_types::{
97
},
108
};
119

12-
use crate::{Authenticator, CoseKeyPair, CredentialStore, UserValidationMethod};
10+
use crate::{Authenticator, CoseKeyPair, CredentialStore, UiHint, UserValidationMethod};
1311

1412
impl<S, U> Authenticator<S, U>
1513
where
1614
S: CredentialStore + Sync,
17-
U: UserValidationMethod + Sync,
15+
U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
1816
{
1917
/// This method is invoked by the host to request generation of a new credential in the authenticator.
2018
pub async fn make_credential(&mut self, input: Request) -> Result<Response, StatusCode> {
21-
let flags = if input.options.up {
22-
self.check_user(&input.options, None).await?
23-
} else {
19+
if !input.options.up {
2420
return Err(Ctap2Error::InvalidOption.into());
2521
};
2622

@@ -35,22 +31,23 @@ where
3531
.as_ref()
3632
.filter(|list| !list.is_empty())
3733
.is_some()
38-
&& self
34+
{
35+
if let Some(excluded_credential) = self
3936
.store()
4037
.find_credentials(
4138
input.exclude_list.as_deref(),
4239
&input.rp.id,
4340
Some(&input.user.id),
4441
)
4542
.await?
46-
.is_empty()
47-
.not()
48-
{
49-
// TODO: We should instead remove the excluded credentials from a possible future
50-
// "update list".
51-
// This would require prompting from within the context of this function using
52-
// the [UserValidationMethod]
53-
log::warn!("An excluded credential was found, may be overwritten.")
43+
.first()
44+
{
45+
self.check_user(
46+
UiHint::InformExcludedCredentialFound(excluded_credential),
47+
&input.options,
48+
)
49+
.await?;
50+
}
5451
}
5552

5653
// 2. If the pubKeyCredParams parameter does not contain a valid COSEAlgorithmIdentifier
@@ -98,6 +95,12 @@ where
9895
// authenticator-specific way (e.g., flash the LED light). Request permission to create
9996
// a credential. If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED
10097
// error.
98+
let flags = self
99+
.check_user(
100+
UiHint::RequestNewCredential(&input.user.clone().into(), &input.rp),
101+
&input.options,
102+
)
103+
.await?;
101104

102105
// 9. Generate a new credential key pair for the algorithm specified.
103106
let credential_id = passkey_types::rand::random_vec(self.credential_id_length.into());
@@ -124,7 +127,7 @@ where
124127
key: private,
125128
rp_id: input.rp.id.clone(),
126129
credential_id: credential_id.into(),
127-
user_handle: is_passkey_rk.then(|| input.user.id.clone()),
130+
user_handle: is_passkey_rk.then_some(input.user.id.clone()),
128131
counter: self.make_credentials_with_signature_counter.then_some(0),
129132
extensions: extensions.credential,
130133
};

passkey-authenticator/src/authenticator/make_credential/tests.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
MemoryStore,
2222
credential_store::{DiscoverabilitySupport, StoreInfo},
2323
extensions,
24-
user_validation::MockUserValidationMethod,
24+
user_validation::{MockUiHint, MockUserValidationMethod},
2525
};
2626

2727
fn good_request() -> Request {
@@ -54,14 +54,17 @@ fn good_request() -> Request {
5454

5555
#[tokio::test]
5656
async fn assert_storage_on_success() {
57+
let request = good_request();
58+
5759
let shared_store = Arc::new(Mutex::new(MemoryStore::new()));
58-
let user_mock = MockUserValidationMethod::verified_user(1);
60+
let user_mock = MockUserValidationMethod::verified_user_with_hint(
61+
1,
62+
MockUiHint::RequestNewCredential(request.user.clone().into(), request.rp.clone()),
63+
);
5964

6065
let mut authenticator =
6166
Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock);
6267

63-
let request = good_request();
64-
6568
authenticator
6669
.make_credential(request)
6770
.await
@@ -93,7 +96,27 @@ async fn assert_excluded_credentials() {
9396
extensions: Default::default(),
9497
};
9598
let shared_store = Arc::new(Mutex::new(MemoryStore::new()));
96-
let user_mock = MockUserValidationMethod::verified_user(1);
99+
let mut user_mock = MockUserValidationMethod::verified_user_with_hint(
100+
1,
101+
MockUiHint::InformExcludedCredentialFound(passkey.clone()),
102+
);
103+
let expected_user = response.user.clone().into();
104+
let expected_rp = response.rp.clone();
105+
user_mock
106+
.expect_check_user()
107+
.withf(move |hint, _up, _uv| {
108+
if let UiHint::RequestNewCredential(user, rp) = *hint {
109+
*user == expected_user && *rp == expected_rp
110+
} else {
111+
false
112+
}
113+
})
114+
.returning(|_hint, up, uv| {
115+
Ok(crate::UserCheck {
116+
presence: up,
117+
verification: uv,
118+
})
119+
});
97120

98121
shared_store.lock().await.insert(cred_id.into(), passkey);
99122

@@ -112,7 +135,7 @@ async fn assert_excluded_credentials() {
112135

113136
#[tokio::test]
114137
async fn assert_unsupported_algorithm() {
115-
let user_mock = MockUserValidationMethod::verified_user(1);
138+
let user_mock = MockUserValidationMethod::verified_user(0);
116139
let mut authenticator = Authenticator::new(Aaguid::new_empty(), MemoryStore::new(), user_mock);
117140

118141
let request = Request {
@@ -401,7 +424,7 @@ async fn make_credential_returns_err_when_rk_is_requested_but_not_supported() {
401424

402425
// Arrange
403426
let store = StoreWithoutDiscoverableSupport;
404-
let user_mock = MockUserValidationMethod::verified_user(1);
427+
let user_mock = MockUserValidationMethod::verified_user(0);
405428
let request = good_request();
406429
let mut authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
407430
authenticator.set_make_credentials_with_signature_counter(true);

passkey-authenticator/src/authenticator/tests.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use passkey_types::ctap2::{Aaguid, Flags};
22

3-
use crate::{Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck};
3+
use crate::{
4+
Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck, user_validation::UiHint,
5+
};
46

57
#[tokio::test]
68
async fn check_user_does_not_check_up_or_uv_when_not_requested() {
@@ -31,7 +33,10 @@ async fn check_user_does_not_check_up_or_uv_when_not_requested() {
3133
};
3234

3335
// Act
34-
let result = authenticator.check_user(&options, None).await.unwrap();
36+
let result = authenticator
37+
.check_user(UiHint::InformNoCredentialsFound, &options)
38+
.await
39+
.unwrap();
3540

3641
// Assert
3742
assert_eq!(result, Flags::empty());
@@ -66,7 +71,10 @@ async fn check_user_checks_up_when_requested() {
6671
};
6772

6873
// Act
69-
let result = authenticator.check_user(&options, None).await.unwrap();
74+
let result = authenticator
75+
.check_user(UiHint::InformNoCredentialsFound, &options)
76+
.await
77+
.unwrap();
7078

7179
// Assert
7280
assert_eq!(result, Flags::UP);
@@ -104,7 +112,10 @@ async fn check_user_checks_uv_when_requested() {
104112
};
105113

106114
// Act
107-
let result = authenticator.check_user(&options, None).await.unwrap();
115+
let result = authenticator
116+
.check_user(UiHint::InformNoCredentialsFound, &options)
117+
.await
118+
.unwrap();
108119

109120
// Assert
110121
assert_eq!(result, Flags::UP | Flags::UV);
@@ -139,7 +150,9 @@ async fn check_user_returns_operation_denied_when_up_was_requested_but_not_retur
139150
};
140151

141152
// Act
142-
let result = authenticator.check_user(&options, None).await;
153+
let result = authenticator
154+
.check_user(UiHint::InformNoCredentialsFound, &options)
155+
.await;
143156

144157
// Assert
145158
assert_eq!(
@@ -180,7 +193,9 @@ async fn check_user_returns_operation_denied_when_uv_was_requested_but_not_retur
180193
};
181194

182195
// Act
183-
let result = authenticator.check_user(&options, None).await;
196+
let result = authenticator
197+
.check_user(UiHint::InformNoCredentialsFound, &options)
198+
.await;
184199

185200
// Assert
186201
assert_eq!(
@@ -207,7 +222,9 @@ async fn check_user_returns_unsupported_option_when_uv_was_requested_but_is_not_
207222
};
208223

209224
// Act
210-
let result = authenticator.check_user(&options, None).await;
225+
let result = authenticator
226+
.check_user(UiHint::InformNoCredentialsFound, &options)
227+
.await;
211228

212229
// Assert
213230
assert_eq!(
@@ -249,7 +266,10 @@ async fn check_user_returns_up_and_uv_flags_when_neither_up_or_uv_was_requested_
249266
};
250267

251268
// Act
252-
let result = authenticator.check_user(&options, None).await.unwrap();
269+
let result = authenticator
270+
.check_user(UiHint::InformNoCredentialsFound, &options)
271+
.await
272+
.unwrap();
253273

254274
// Assert
255275
assert_eq!(result, Flags::UP | Flags::UV);

passkey-authenticator/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub use self::{
4646
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
4747
ctap2::Ctap2Api,
4848
u2f::U2fApi,
49-
user_validation::{UserCheck, UserValidationMethod},
49+
user_validation::{UiHint, UserCheck, UserValidationMethod},
5050
};
5151

5252
#[cfg(any(test, feature = "testable"))]

0 commit comments

Comments
 (0)