1use coset::iana;
2use passkey_types::{
3 ctap2::{Aaguid, Ctap2Error, Flags},
4 webauthn,
5};
6
7use crate::{CredentialStore, UserValidationMethod};
8
9pub mod extensions;
10mod get_assertion;
11mod get_info;
12mod make_credential;
13
14use extensions::Extensions;
15
16#[derive(Debug, Clone, Copy)]
24#[repr(transparent)]
25pub struct CredentialIdLength(u8);
26
27impl CredentialIdLength {
28 pub const DEFAULT: Self = Self(Self::MIN);
33
34 const MIN: u8 = 16;
35
36 const MAX: u8 = 64;
39
40 pub fn randomized(rng: &mut impl rand::Rng) -> Self {
42 let length = rng.gen_range(Self::MIN..=Self::MAX);
43 Self(length)
44 }
45}
46
47impl Default for CredentialIdLength {
48 fn default() -> Self {
49 Self::DEFAULT
50 }
51}
52
53impl From<u8> for CredentialIdLength {
54 fn from(value: u8) -> Self {
55 let value = core::cmp::min(Self::MAX, value);
57 let value = core::cmp::max(Self::MIN, value);
59 Self(value)
60 }
61}
62
63impl From<CredentialIdLength> for usize {
64 fn from(value: CredentialIdLength) -> Self {
65 usize::from(value.0)
66 }
67}
68
69pub struct Authenticator<S, U> {
71 aaguid: Aaguid,
73 store: S,
75 algs: Vec<iana::Algorithm>,
77 transports: Vec<webauthn::AuthenticatorTransport>,
81 user_validation: U,
83
84 make_credentials_with_signature_counter: bool,
90
91 credential_id_length: CredentialIdLength,
93
94 extensions: Extensions,
96}
97
98impl<S, U> Authenticator<S, U>
99where
100 S: CredentialStore,
101 U: UserValidationMethod,
102{
103 pub fn new(aaguid: Aaguid, store: S, user: U) -> Self {
105 Self {
106 aaguid,
107 store,
108 algs: vec![iana::Algorithm::ES256],
110 transports: vec![
111 webauthn::AuthenticatorTransport::Internal,
112 webauthn::AuthenticatorTransport::Hybrid,
113 ],
114 user_validation: user,
115 make_credentials_with_signature_counter: false,
116 credential_id_length: CredentialIdLength::default(),
117 extensions: Extensions::default(),
118 }
119 }
120
121 pub fn set_make_credentials_with_signature_counter(&mut self, value: bool) {
126 self.make_credentials_with_signature_counter = value;
127 }
128
129 pub fn make_credentials_with_signature_counter(&self) -> bool {
131 self.make_credentials_with_signature_counter
132 }
133
134 pub fn set_make_credential_id_length(&mut self, length: CredentialIdLength) {
136 self.credential_id_length = length;
137 }
138
139 pub fn make_credential_id_length(&self) -> CredentialIdLength {
141 self.credential_id_length
142 }
143
144 pub fn store(&self) -> &S {
146 &self.store
147 }
148
149 pub fn store_mut(&mut self) -> &mut S {
151 &mut self.store
152 }
153
154 pub fn aaguid(&self) -> &Aaguid {
156 &self.aaguid
157 }
158
159 pub fn attachment_type(&self) -> webauthn::AuthenticatorAttachment {
161 webauthn::AuthenticatorAttachment::Platform
163 }
164
165 pub fn choose_algorithm(
175 &self,
176 params: &[webauthn::PublicKeyCredentialParameters],
177 ) -> Result<iana::Algorithm, Ctap2Error> {
178 params
179 .iter()
180 .find(|param| self.algs.contains(¶m.alg))
181 .map(|param| param.alg)
182 .ok_or(Ctap2Error::UnsupportedAlgorithm)
183 }
184
185 pub fn transports(self, transports: Vec<webauthn::AuthenticatorTransport>) -> Self {
187 Self { transports, ..self }
188 }
189
190 async fn check_user(
202 &self,
203 options: &passkey_types::ctap2::make_credential::Options,
204 credential: Option<&<U as UserValidationMethod>::PasskeyItem>,
205 ) -> Result<Flags, Ctap2Error> {
206 if options.uv && self.user_validation.is_verification_enabled() != Some(true) {
207 return Err(Ctap2Error::UnsupportedOption);
208 };
209
210 let check_result = self
211 .user_validation
212 .check_user(credential, options.up, options.uv)
213 .await?;
214
215 if options.up && !check_result.presence {
216 return Err(Ctap2Error::OperationDenied);
217 }
218
219 if options.uv && !check_result.verification {
220 return Err(Ctap2Error::OperationDenied);
221 }
222
223 let mut flags = Flags::empty();
224 if check_result.presence {
225 flags |= Flags::UP;
226 }
227
228 if check_result.verification {
229 flags |= Flags::UV;
230 }
231
232 Ok(flags)
233 }
234
235 pub fn hmac_secret(mut self, ext: extensions::HmacSecretConfig) -> Self {
237 self.extensions.hmac_secret = Some(ext);
238 self
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use passkey_types::ctap2::{Aaguid, Flags};
245
246 use crate::{Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck};
247
248 #[tokio::test]
249 async fn check_user_does_not_check_up_or_uv_when_not_requested() {
250 let mut user_mock = MockUserValidationMethod::new();
252 user_mock
253 .expect_check_user()
254 .with(
255 mockall::predicate::always(),
256 mockall::predicate::eq(false),
257 mockall::predicate::eq(false),
258 )
259 .returning(|_, _, _| {
260 Ok(UserCheck {
261 presence: false,
262 verification: false,
263 })
264 })
265 .once();
266
267 let store = None;
269 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
270 let options = passkey_types::ctap2::make_credential::Options {
271 up: false,
272 uv: false,
273 ..Default::default()
274 };
275
276 let result = authenticator.check_user(&options, None).await.unwrap();
278
279 assert_eq!(result, Flags::empty());
281 }
282
283 #[tokio::test]
284 async fn check_user_checks_up_when_requested() {
285 let mut user_mock = MockUserValidationMethod::new();
287 user_mock
288 .expect_check_user()
289 .with(
290 mockall::predicate::always(),
291 mockall::predicate::eq(true),
292 mockall::predicate::eq(false),
293 )
294 .returning(|_, _, _| {
295 Ok(UserCheck {
296 presence: true,
297 verification: false,
298 })
299 })
300 .once();
301
302 let store = None;
304 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
305 let options = passkey_types::ctap2::make_credential::Options {
306 up: true,
307 uv: false,
308 ..Default::default()
309 };
310
311 let result = authenticator.check_user(&options, None).await.unwrap();
313
314 assert_eq!(result, Flags::UP);
316 }
317
318 #[tokio::test]
319 async fn check_user_checks_uv_when_requested() {
320 let mut user_mock = MockUserValidationMethod::new();
322 user_mock
323 .expect_is_verification_enabled()
324 .returning(|| Some(true));
325 user_mock
326 .expect_check_user()
327 .with(
328 mockall::predicate::always(),
329 mockall::predicate::eq(true),
330 mockall::predicate::eq(true),
331 )
332 .returning(|_, _, _| {
333 Ok(UserCheck {
334 presence: true,
335 verification: true,
336 })
337 })
338 .once();
339
340 let store = None;
342 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
343 let options = passkey_types::ctap2::make_credential::Options {
344 up: true,
345 uv: true,
346 ..Default::default()
347 };
348
349 let result = authenticator.check_user(&options, None).await.unwrap();
351
352 assert_eq!(result, Flags::UP | Flags::UV);
354 }
355
356 #[tokio::test]
357 async fn check_user_returns_operation_denied_when_up_was_requested_but_not_returned() {
358 let mut user_mock = MockUserValidationMethod::new();
360 user_mock
361 .expect_check_user()
362 .with(
363 mockall::predicate::always(),
364 mockall::predicate::eq(true),
365 mockall::predicate::eq(false),
366 )
367 .returning(|_, _, _| {
368 Ok(UserCheck {
369 presence: false,
370 verification: false,
371 })
372 })
373 .once();
374
375 let store = None;
377 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
378 let options = passkey_types::ctap2::make_credential::Options {
379 up: true,
380 uv: false,
381 ..Default::default()
382 };
383
384 let result = authenticator.check_user(&options, None).await;
386
387 assert_eq!(
389 result,
390 Err(passkey_types::ctap2::Ctap2Error::OperationDenied)
391 );
392 }
393
394 #[tokio::test]
395 async fn check_user_returns_operation_denied_when_uv_was_requested_but_not_returned() {
396 let mut user_mock = MockUserValidationMethod::new();
398 user_mock
399 .expect_is_verification_enabled()
400 .returning(|| Some(true));
401 user_mock
402 .expect_check_user()
403 .with(
404 mockall::predicate::always(),
405 mockall::predicate::eq(true),
406 mockall::predicate::eq(true),
407 )
408 .returning(|_, _, _| {
409 Ok(UserCheck {
410 presence: true,
411 verification: false,
412 })
413 })
414 .once();
415
416 let store = None;
418 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
419 let options = passkey_types::ctap2::make_credential::Options {
420 up: true,
421 uv: true,
422 ..Default::default()
423 };
424
425 let result = authenticator.check_user(&options, None).await;
427
428 assert_eq!(
430 result,
431 Err(passkey_types::ctap2::Ctap2Error::OperationDenied)
432 );
433 }
434
435 #[tokio::test]
436 async fn check_user_returns_unsupported_option_when_uv_was_requested_but_is_not_supported() {
437 let mut user_mock = MockUserValidationMethod::new();
439 user_mock
440 .expect_is_verification_enabled()
441 .returning(|| None);
442
443 let store = None;
445 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
446 let options = passkey_types::ctap2::make_credential::Options {
447 up: true,
448 uv: true,
449 ..Default::default()
450 };
451
452 let result = authenticator.check_user(&options, None).await;
454
455 assert_eq!(
457 result,
458 Err(passkey_types::ctap2::Ctap2Error::UnsupportedOption)
459 );
460 }
461
462 #[tokio::test]
463 async fn check_user_returns_up_and_uv_flags_when_neither_up_or_uv_was_requested_but_performed_anyways(
464 ) {
465 let mut user_mock = MockUserValidationMethod::new();
467 user_mock
468 .expect_is_verification_enabled()
469 .returning(|| Some(true));
470 user_mock
471 .expect_check_user()
472 .with(
473 mockall::predicate::always(),
474 mockall::predicate::eq(false),
475 mockall::predicate::eq(false),
476 )
477 .returning(|_, _, _| {
478 Ok(UserCheck {
479 presence: true,
480 verification: true,
481 })
482 })
483 .once();
484
485 let store = None;
487 let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock);
488 let options = passkey_types::ctap2::make_credential::Options {
489 up: false,
490 uv: false,
491 ..Default::default()
492 };
493
494 let result = authenticator.check_user(&options, None).await.unwrap();
496
497 assert_eq!(result, Flags::UP | Flags::UV);
499 }
500
501 #[test]
502 fn credential_id_lengths_validate() {
503 for num in 0..u8::MAX {
504 let length = CredentialIdLength::from(num);
505 if !(16..=64).contains(&num) {
506 if num < 16 {
507 assert_eq!(length.0, CredentialIdLength::DEFAULT.0);
509 } else {
510 assert_eq!(length.0, 64);
512 }
513 }
514 }
515
516 assert_eq!(
517 CredentialIdLength::DEFAULT.0,
518 CredentialIdLength::default().0
519 );
520 }
521
522 #[test]
523 fn credential_id_generation() {
524 let mut rng = rand::thread_rng();
525 let valid_range = 0..=64;
526 for _ in 0..=100 {
527 let length = CredentialIdLength::randomized(&mut rng).0;
528 assert!(valid_range.contains(&length));
529 }
530 }
531}