1use super::{Error, Result};
16use chisel_kernel::UserId;
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20use webauthn_kit::attestation::AttestationPolicy;
21use webauthn_kit::config::WebauthnConfig;
22use webauthn_kit::credential::{
23 AllowCredential, AuthenticationOptions, AuthenticationResult, AuthenticatorSelection,
24 ExcludeCredential, PubKeyCredParam, RegistrationOptions, RegistrationResult, RelyingParty,
25 WebauthnUser,
26};
27use webauthn_kit::crypto::generate_challenge_bytes;
28use webauthn_kit::policy::CredentialPolicy;
29use webauthn_kit::protocol::AuthenticationParams;
30
31#[derive(Debug, Clone)]
33pub struct StoredPasskey {
34 pub user: UserId,
36 pub credential_id: String,
38 pub public_key_cose: Vec<u8>,
40 pub sign_count: u32,
42 pub label: String,
44}
45
46pub trait CredentialStore: Send + Sync {
48 fn put(&self, credential: StoredPasskey);
50 fn get(&self, credential_id: &str) -> Option<StoredPasskey>;
52 fn for_user(&self, user: &UserId) -> Vec<StoredPasskey>;
54 fn remove(&self, credential_id: &str);
56}
57
58pub trait CeremonyVerifier: Send + Sync {
63 fn verify_registration(
65 &self,
66 challenge_bytes: &[u8],
67 client_data_json_b64: &str,
68 attestation_object_b64: &str,
69 rp_id: &str,
70 rp_origins: &[String],
71 ) -> std::result::Result<RegistrationResult, webauthn_kit::WebauthnError>;
72
73 fn verify_authentication(
75 &self,
76 params: &AuthenticationParams,
77 ) -> std::result::Result<AuthenticationResult, webauthn_kit::WebauthnError>;
78}
79
80#[derive(Debug, Clone)]
83pub struct KitVerifier {
84 pub rp_id: String,
86 pub rp_origins: Vec<String>,
88 pub attestation: AttestationPolicy,
90 pub policy: CredentialPolicy,
92}
93
94impl CeremonyVerifier for KitVerifier {
95 fn verify_registration(
96 &self,
97 challenge_bytes: &[u8],
98 client_data_json_b64: &str,
99 attestation_object_b64: &str,
100 rp_id: &str,
101 rp_origins: &[String],
102 ) -> std::result::Result<RegistrationResult, webauthn_kit::WebauthnError> {
103 webauthn_kit::protocol::verify_registration(
104 challenge_bytes,
105 client_data_json_b64,
106 attestation_object_b64,
107 "",
108 rp_id,
109 rp_origins,
110 &self.attestation,
111 &self.policy,
112 )
113 }
114
115 fn verify_authentication(
116 &self,
117 params: &AuthenticationParams,
118 ) -> std::result::Result<AuthenticationResult, webauthn_kit::WebauthnError> {
119 webauthn_kit::protocol::verify_authentication(params)
120 }
121}
122
123struct PendingChallenge {
125 bytes: Vec<u8>,
126 user: UserId,
127 expires: Instant,
128}
129
130struct ChallengeMap {
132 map: Mutex<HashMap<String, PendingChallenge>>,
133 ttl: Duration,
134}
135
136impl ChallengeMap {
137 fn new(ttl: Duration) -> Self {
138 Self {
139 map: Mutex::new(HashMap::new()),
140 ttl,
141 }
142 }
143 fn put(&self, user: &UserId, bytes: Vec<u8>) {
144 self.map
145 .lock()
146 .unwrap_or_else(std::sync::PoisonError::into_inner)
147 .insert(
148 user.as_str().to_owned(),
149 PendingChallenge {
150 bytes,
151 user: user.clone(),
152 expires: Instant::now() + self.ttl,
153 },
154 );
155 }
156 fn take(&self, user: &str) -> Option<PendingChallenge> {
157 let mut map = self
158 .map
159 .lock()
160 .unwrap_or_else(std::sync::PoisonError::into_inner);
161 match map.remove(user) {
162 Some(c) if c.expires > Instant::now() => Some(c),
163 other => {
164 map.retain(|_, c| c.expires > Instant::now());
165 other
166 }
167 }
168 }
169}
170
171#[derive(Clone)]
174pub struct PasskeyService {
175 config: WebauthnConfig,
176 verifier: Arc<dyn CeremonyVerifier>,
177 store: Arc<dyn CredentialStore>,
178 challenges: Arc<ChallengeMap>,
179 timeout_ms: u64,
180}
181
182impl std::fmt::Debug for PasskeyService {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.debug_struct("PasskeyService")
185 .field("rp_id", &self.config.rp_id)
186 .field("rp_origins", &self.config.rp_origins)
187 .finish_non_exhaustive()
188 }
189}
190
191impl PasskeyService {
192 #[must_use]
195 pub fn new(
196 rp_id: impl Into<String>,
197 rp_name: impl Into<String>,
198 rp_origins: Vec<String>,
199 store: Arc<dyn CredentialStore>,
200 ) -> Self {
201 let rp_id = rp_id.into();
202 let verifier = KitVerifier {
203 rp_id: rp_id.clone(),
204 rp_origins: rp_origins.clone(),
205 attestation: AttestationPolicy::strict(),
206 policy: CredentialPolicy::default(),
207 };
208 Self::with_verifier(rp_id, rp_name, rp_origins, store, Arc::new(verifier))
209 }
210
211 #[must_use]
215 pub fn with_verifier(
216 rp_id: impl Into<String>,
217 rp_name: impl Into<String>,
218 rp_origins: Vec<String>,
219 store: Arc<dyn CredentialStore>,
220 verifier: Arc<dyn CeremonyVerifier>,
221 ) -> Self {
222 Self {
223 config: WebauthnConfig {
224 rp_id: rp_id.into(),
225 rp_name: rp_name.into(),
226 rp_origins,
227 ..WebauthnConfig::default()
228 },
229 verifier,
230 store,
231 challenges: Arc::new(ChallengeMap::new(Duration::from_secs(300))),
232 timeout_ms: 60_000,
233 }
234 }
235
236 #[must_use]
240 pub fn registration_options(&self, user: &UserId, label: &str) -> RegistrationOptions {
241 let bytes = generate_challenge_bytes();
242 let exclude: Vec<ExcludeCredential> = self
243 .store
244 .for_user(user)
245 .into_iter()
246 .map(|c| ExcludeCredential {
247 id: c.credential_id,
248 type_: "public-key".into(),
249 transports: None,
250 })
251 .collect();
252 self.challenges.put(user, bytes.clone());
253 RegistrationOptions {
254 challenge: webauthn_kit::crypto::base64_encode_urlsafe(&bytes),
255 rp: RelyingParty {
256 id: self.config.rp_id.clone(),
257 name: self.config.rp_name.clone(),
258 },
259 user: WebauthnUser {
260 id: user.as_str().to_owned(),
261 name: user.as_str().to_owned(),
262 display_name: label.to_owned(),
263 },
264 pub_key_cred_params: self
265 .config
266 .allowed_algorithms
267 .iter()
268 .map(|&alg| PubKeyCredParam {
269 alg,
270 type_: "public-key".into(),
271 })
272 .collect(),
273 timeout: self.timeout_ms,
274 exclude_credentials: exclude,
275 attestation: "none".into(),
276 authenticator_selection: AuthenticatorSelection {
277 resident_key: "required".into(),
278 user_verification: "preferred".into(),
279 require_resident_key: false,
280 },
281 }
282 }
283
284 pub fn finish_registration(
291 &self,
292 user: &UserId,
293 client_data_json_b64: &str,
294 attestation_object_b64: &str,
295 ) -> Result<RegistrationResult> {
296 let pending = self
297 .challenges
298 .take(user.as_str())
299 .ok_or_else(|| Error::Token("no in-flight passkey challenge for this user".into()))?;
300 if &pending.user != user {
301 return Err(Error::Token("challenge belongs to another user".into()));
302 }
303 let result = self
304 .verifier
305 .verify_registration(
306 &pending.bytes,
307 client_data_json_b64,
308 attestation_object_b64,
309 &self.config.rp_id,
310 &self.config.rp_origins,
311 )
312 .map_err(|e| Error::Kit(e.to_string()))?;
313 self.store.put(StoredPasskey {
314 user: user.clone(),
315 credential_id: result.credential_id.clone(),
316 public_key_cose: result.public_key_cose.clone(),
317 sign_count: 0,
318 label: String::new(),
319 });
320 Ok(result)
321 }
322
323 #[must_use]
326 pub fn authentication_options(&self, user: &UserId) -> AuthenticationOptions {
327 let bytes = generate_challenge_bytes();
328 let allow: Vec<AllowCredential> = self
329 .store
330 .for_user(user)
331 .into_iter()
332 .map(|c| AllowCredential {
333 id: c.credential_id.clone(),
334 type_: "public-key".into(),
335 transports: None,
336 })
337 .collect();
338 self.challenges.put(user, bytes.clone());
339 AuthenticationOptions {
340 challenge: webauthn_kit::crypto::base64_encode_urlsafe(&bytes),
341 rp_id: self.config.rp_id.clone(),
342 allow_credentials: allow,
343 timeout: self.timeout_ms,
344 user_verification: "preferred".into(),
345 }
346 }
347
348 pub fn finish_authentication(
357 &self,
358 user: &UserId,
359 credential_id: &str,
360 client_data_json_b64: &str,
361 authenticator_data_b64: &str,
362 signature_b64: &str,
363 ) -> Result<()> {
364 let stored = self
365 .store
366 .get(credential_id)
367 .ok_or_else(|| Error::Token("unknown passkey credential".into()))?;
368 if &stored.user != user {
369 return Err(Error::Token("credential belongs to another user".into()));
370 }
371 let pending = self
372 .challenges
373 .take(user.as_str())
374 .ok_or_else(|| Error::Token("no in-flight passkey challenge for this user".into()))?;
375 let params = AuthenticationParams {
376 challenge_bytes: pending.bytes,
377 client_data_json_b64: client_data_json_b64.to_owned(),
378 authenticator_data_b64: authenticator_data_b64.to_owned(),
379 signature_b64: signature_b64.to_owned(),
380 credential_id_b64: credential_id.to_owned(),
381 public_key_cose: stored.public_key_cose.clone(),
382 current_sign_count: stored.sign_count,
383 allowed_credential_ids: vec![credential_id.to_owned()],
384 rp_id: self.config.rp_id.clone(),
385 rp_origins: self.config.rp_origins.clone(),
386 policy: self.verifier_policy(),
387 };
388 let _result = self
389 .verifier
390 .verify_authentication(¶ms)
391 .map_err(|e| Error::Kit(e.to_string()))?;
392 Ok(())
393 }
394
395 fn verifier_policy(&self) -> CredentialPolicy {
396 CredentialPolicy::default()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 #![allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)]
403 use super::*;
404 use sha2::Digest;
405 use webauthn_kit::attestation::{AttestationFormat, AttestationResult, TrustLevel};
406
407 #[derive(Default)]
409 struct MemStore(Mutex<Vec<StoredPasskey>>);
410
411 impl CredentialStore for MemStore {
412 fn put(&self, credential: StoredPasskey) {
413 let mut all = self
414 .0
415 .lock()
416 .unwrap_or_else(std::sync::PoisonError::into_inner);
417 all.retain(|c| c.credential_id != credential.credential_id);
418 all.push(credential);
419 }
420 fn get(&self, credential_id: &str) -> Option<StoredPasskey> {
421 self.0
422 .lock()
423 .unwrap_or_else(std::sync::PoisonError::into_inner)
424 .iter()
425 .find(|c| c.credential_id == credential_id)
426 .cloned()
427 }
428 fn for_user(&self, user: &UserId) -> Vec<StoredPasskey> {
429 self.0
430 .lock()
431 .unwrap_or_else(std::sync::PoisonError::into_inner)
432 .iter()
433 .filter(|c| &c.user == user)
434 .cloned()
435 .collect()
436 }
437 fn remove(&self, credential_id: &str) {
438 self.0
439 .lock()
440 .unwrap_or_else(std::sync::PoisonError::into_inner)
441 .retain(|c| c.credential_id != credential_id);
442 }
443 }
444
445 #[derive(Default)]
448 struct Stub {
449 refuse_registration: bool,
450 refuse_authentication: bool,
451 seen: Mutex<Option<AuthenticationParams>>,
452 }
453
454 impl CeremonyVerifier for Stub {
455 fn verify_registration(
456 &self,
457 _: &[u8],
458 _: &str,
459 _: &str,
460 _: &str,
461 _: &[String],
462 ) -> std::result::Result<RegistrationResult, webauthn_kit::WebauthnError> {
463 if self.refuse_registration {
464 return Err(webauthn_kit::WebauthnError::Config(
465 "stub refused the registration".into(),
466 ));
467 }
468 Ok(RegistrationResult {
469 credential_id: "cred-1".into(),
470 public_key_cose: vec![1, 2, 3],
471 device_name: "test device".into(),
472 attestation_format: "none".into(),
473 attestation: AttestationResult {
474 format: AttestationFormat::None,
475 aaguid: None,
476 trust_level: TrustLevel::None,
477 warnings: vec![],
478 },
479 user_verified: true,
480 backup_eligible: false,
481 backup_state: false,
482 })
483 }
484
485 fn verify_authentication(
486 &self,
487 params: &AuthenticationParams,
488 ) -> std::result::Result<AuthenticationResult, webauthn_kit::WebauthnError> {
489 *self
490 .seen
491 .lock()
492 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(params.clone());
493 if self.refuse_authentication {
494 return Err(webauthn_kit::WebauthnError::Config(
495 "stub refused the authentication".into(),
496 ));
497 }
498 Ok(AuthenticationResult {
499 credential_id: params.credential_id_b64.clone(),
500 new_sign_count: 7,
501 user_verified: true,
502 backup_state: false,
503 backup_eligible: false,
504 })
505 }
506 }
507
508 fn service(store: Arc<MemStore>, stub: Stub) -> PasskeyService {
509 PasskeyService::with_verifier(
510 "chisel.example",
511 "Chisel Docs",
512 vec!["https://chisel.example".into()],
513 store,
514 Arc::new(stub),
515 )
516 }
517
518 fn alice() -> UserId {
519 UserId::try_from("alice").unwrap()
520 }
521
522 fn bob() -> UserId {
523 UserId::try_from("bob").unwrap()
524 }
525
526 fn passkey(user: &UserId, id: &str, cose: &[u8], sign_count: u32) -> StoredPasskey {
527 StoredPasskey {
528 user: user.clone(),
529 credential_id: id.into(),
530 public_key_cose: cose.to_vec(),
531 sign_count,
532 label: String::new(),
533 }
534 }
535
536 #[test]
537 fn registration_options_exclude_existing_keys_and_carry_the_relying_party() {
538 let store = Arc::new(MemStore::default());
539 store.put(passkey(&alice(), "old-key", &[9], 3));
540 let svc = service(store, Stub::default());
541 let options = svc.registration_options(&alice(), "Alice's laptop");
542 assert!(!options.challenge.is_empty());
543 assert_eq!(options.rp.id, "chisel.example");
544 assert_eq!(options.rp.name, "Chisel Docs");
545 assert_eq!(options.user.id, "alice");
546 assert_eq!(options.user.display_name, "Alice's laptop");
547 assert_eq!(options.exclude_credentials.len(), 1);
548 assert_eq!(options.exclude_credentials[0].id, "old-key");
549 assert_eq!(options.authenticator_selection.resident_key, "required");
550 }
551
552 #[test]
553 fn finish_registration_stores_the_verified_credential() {
554 let store = Arc::new(MemStore::default());
555 let svc = service(Arc::clone(&store), Stub::default());
556 let _ = svc.registration_options(&alice(), "Alice's laptop");
557 let result = svc
558 .finish_registration(&alice(), "client-data-b64", "attestation-b64")
559 .expect("a stubbed registration finishes");
560 assert_eq!(result.credential_id, "cred-1");
561 let stored = store.get("cred-1").expect("the credential is persisted");
562 assert_eq!(stored.user, alice());
563 assert_eq!(stored.public_key_cose, vec![1, 2, 3]);
564 assert_eq!(stored.sign_count, 0);
565 }
566
567 #[test]
568 fn finish_registration_needs_an_in_flight_challenge() {
569 let svc = service(Arc::new(MemStore::default()), Stub::default());
570 let err = svc.finish_registration(&alice(), "c", "a").unwrap_err();
571 assert!(
572 err.to_string().contains("no in-flight passkey challenge"),
573 "{err}"
574 );
575 }
576
577 #[test]
578 fn finish_registration_maps_verifier_failures_to_kit_errors() {
579 let svc = service(
580 Arc::new(MemStore::default()),
581 Stub {
582 refuse_registration: true,
583 ..Stub::default()
584 },
585 );
586 let _ = svc.registration_options(&alice(), "Alice's laptop");
587 let err = svc.finish_registration(&alice(), "c", "a").unwrap_err();
588 assert!(
589 err.to_string().contains("stub refused the registration"),
590 "{err}"
591 );
592 }
593
594 #[test]
595 fn authentication_options_allow_only_the_users_own_keys() {
596 let store = Arc::new(MemStore::default());
597 store.put(passkey(&alice(), "a-key", &[1], 0));
598 store.put(passkey(&bob(), "b-key", &[2], 0));
599 let svc = service(store, Stub::default());
600 let options = svc.authentication_options(&alice());
601 assert_eq!(options.allow_credentials.len(), 1);
602 assert_eq!(options.allow_credentials[0].id, "a-key");
603 assert_eq!(options.rp_id, "chisel.example");
604 assert!(!options.challenge.is_empty());
605 }
606
607 #[test]
608 fn finish_authentication_verifies_against_the_stored_key() {
609 let store = Arc::new(MemStore::default());
610 store.put(passkey(&alice(), "a-key", &[7, 7], 41));
611 let stub = Arc::new(Stub::default());
612 let svc = PasskeyService::with_verifier(
613 "chisel.example",
614 "Chisel Docs",
615 vec!["https://chisel.example".into()],
616 store,
617 Arc::clone(&stub) as Arc<dyn CeremonyVerifier>,
618 );
619 let _ = svc.authentication_options(&alice());
620 svc.finish_authentication(&alice(), "a-key", "cdj-b64", "auth-data-b64", "sig-b64")
621 .expect("a stubbed authentication finishes");
622 let seen = stub
623 .seen
624 .lock()
625 .unwrap_or_else(std::sync::PoisonError::into_inner);
626 let ceremony = seen.as_ref().expect("the verifier saw the ceremony");
627 assert_eq!(ceremony.public_key_cose, vec![7, 7]);
628 assert_eq!(ceremony.current_sign_count, 41);
629 assert_eq!(ceremony.rp_id, "chisel.example");
630 assert_eq!(ceremony.allowed_credential_ids, ["a-key"]);
631 assert_eq!(ceremony.credential_id_b64, "a-key");
632 }
633
634 #[test]
635 fn finish_authentication_refuses_unknown_and_foreign_credentials() {
636 let store = Arc::new(MemStore::default());
637 store.put(passkey(&bob(), "b-key", &[2], 0));
638 let svc = service(store, Stub::default());
639 let err = svc
640 .finish_authentication(&alice(), "nope", "c", "a", "s")
641 .unwrap_err();
642 assert!(
643 err.to_string().contains("unknown passkey credential"),
644 "{err}"
645 );
646 let err = svc
647 .finish_authentication(&alice(), "b-key", "c", "a", "s")
648 .unwrap_err();
649 assert!(
650 err.to_string()
651 .contains("credential belongs to another user"),
652 "{err}"
653 );
654 }
655
656 #[test]
657 fn finish_authentication_needs_an_in_flight_challenge() {
658 let store = Arc::new(MemStore::default());
659 store.put(passkey(&alice(), "a-key", &[1], 0));
660 let svc = service(store, Stub::default());
661 let err = svc
662 .finish_authentication(&alice(), "a-key", "c", "a", "s")
663 .unwrap_err();
664 assert!(
665 err.to_string().contains("no in-flight passkey challenge"),
666 "{err}"
667 );
668 }
669
670 #[test]
671 fn finish_authentication_maps_verifier_failures_to_kit_errors() {
672 let store = Arc::new(MemStore::default());
673 store.put(passkey(&alice(), "a-key", &[1], 0));
674 let svc = service(
675 store,
676 Stub {
677 refuse_authentication: true,
678 ..Stub::default()
679 },
680 );
681 let _ = svc.authentication_options(&alice());
682 let err = svc
683 .finish_authentication(&alice(), "a-key", "c", "a", "s")
684 .unwrap_err();
685 assert!(
686 err.to_string().contains("stub refused the authentication"),
687 "{err}"
688 );
689 }
690
691 #[test]
692 fn both_constructors_report_the_relying_party_in_debug() {
693 let store = Arc::new(MemStore::default());
694 let injected = service(Arc::clone(&store), Stub::default());
695 assert!(format!("{injected:?}").contains("chisel.example"));
696 let kit = PasskeyService::new(
697 "kit.example",
698 "Chisel Docs",
699 vec!["https://kit.example".into()],
700 store,
701 );
702 assert!(format!("{kit:?}").contains("kit.example"));
703 }
704
705 const RP_ID: &str = "localhost";
713 const ORIGIN: &str = "http://localhost:8080";
714
715 fn kit_service(store: Arc<MemStore>) -> PasskeyService {
716 PasskeyService::new(RP_ID, "Chisel Docs", vec![ORIGIN.to_string()], store)
717 }
718
719 fn build_attestation_object(auth_data: &[u8]) -> Vec<u8> {
721 let map = vec![
722 (
723 ciborium::Value::Text("fmt".into()),
724 ciborium::Value::Text("none".into()),
725 ),
726 (
727 ciborium::Value::Text("attStmt".into()),
728 ciborium::Value::Map(vec![]),
729 ),
730 (
731 ciborium::Value::Text("authData".into()),
732 ciborium::Value::Bytes(auth_data.to_vec()),
733 ),
734 ];
735 let mut buf = Vec::new();
736 ciborium::ser::into_writer(&ciborium::Value::Map(map), &mut buf).expect("CBOR encode");
737 buf
738 }
739
740 fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
742 let map = vec![
743 (
744 ciborium::Value::Integer(1.into()),
745 ciborium::Value::Integer(2.into()),
746 ),
747 (
748 ciborium::Value::Integer(3.into()),
749 ciborium::Value::Integer((-7).into()),
750 ),
751 (
752 ciborium::Value::Integer((-1).into()),
753 ciborium::Value::Integer(1.into()),
754 ),
755 (
756 ciborium::Value::Integer((-2).into()),
757 ciborium::Value::Bytes(x.to_vec()),
758 ),
759 (
760 ciborium::Value::Integer((-3).into()),
761 ciborium::Value::Bytes(y.to_vec()),
762 ),
763 ];
764 let mut buf = Vec::new();
765 ciborium::ser::into_writer(&ciborium::Value::Map(map), &mut buf).expect("CBOR encode");
766 buf
767 }
768
769 fn build_auth_data(
772 rp_id: &str,
773 flags: u8,
774 sign_count: u32,
775 credential_id: &[u8],
776 cose_key: &[u8],
777 ) -> Vec<u8> {
778 let mut auth_data = sha2::Sha256::digest(rp_id.as_bytes()).to_vec();
779 auth_data.push(flags);
780 auth_data.extend_from_slice(&sign_count.to_be_bytes());
781 if flags & 0x40 != 0 {
782 auth_data.extend_from_slice(&[0u8; 16]); auth_data.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
784 auth_data.extend_from_slice(credential_id);
785 auth_data.extend_from_slice(cose_key);
786 }
787 auth_data
788 }
789
790 fn es256_key() -> (ring::signature::EcdsaKeyPair, Vec<u8>) {
792 use ring::signature::{EcdsaKeyPair, KeyPair as _};
793 let rng = ring::rand::SystemRandom::new();
794 let pkcs8 =
795 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
796 .expect("key generation");
797 let key_pair = EcdsaKeyPair::from_pkcs8(
798 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
799 pkcs8.as_ref(),
800 &rng,
801 )
802 .expect("key parsing");
803 let pub_bytes = key_pair.public_key().as_ref();
804 let cose = build_cose_ec2_key(&pub_bytes[1..33], &pub_bytes[33..65]);
805 (key_pair, cose)
806 }
807
808 fn b64(bytes: &[u8]) -> String {
809 webauthn_kit::base64_encode_urlsafe(bytes)
810 }
811
812 fn register_real(
815 svc: &PasskeyService,
816 user: &UserId,
817 key_pair: &ring::signature::EcdsaKeyPair,
818 cose_key: &[u8],
819 credential_id: &[u8],
820 origin: &str,
821 ) -> crate::Result<webauthn_kit::credential::RegistrationResult> {
822 let options = svc.registration_options(user, "laptop");
823 let client_data = serde_json::json!({
824 "type": "webauthn.create",
825 "challenge": options.challenge,
826 "origin": origin,
827 });
828 let client_data_b64 = b64(&serde_json::to_vec(&client_data).expect("json"));
829 let auth_data = build_auth_data(RP_ID, 0x45, 0, credential_id, cose_key); let _ = key_pair; svc.finish_registration(
832 user,
833 &client_data_b64,
834 &b64(&build_attestation_object(&auth_data)),
835 )
836 }
837
838 #[test]
839 fn kit_verifier_roundtrips_a_real_es256_ceremony() {
840 let store = Arc::new(MemStore::default());
841 let svc = kit_service(Arc::clone(&store));
842 let (key_pair, cose_key) = es256_key();
843 let credential_id = vec![0xDE, 0xAD, 0xBE, 0xEF];
844
845 let result = register_real(&svc, &alice(), &key_pair, &cose_key, &credential_id, ORIGIN)
847 .expect("a fmt=none ES256 registration verifies");
848 assert_eq!(result.attestation_format, "none");
849 assert!(result.user_verified);
850 let stored = store
851 .get(&result.credential_id)
852 .expect("credential persisted");
853 assert_eq!(stored.public_key_cose, cose_key);
854 assert_eq!(stored.user, alice());
855
856 let options = svc.authentication_options(&alice());
858 let client_data = serde_json::json!({
859 "type": "webauthn.get",
860 "challenge": options.challenge,
861 "origin": ORIGIN,
862 });
863 let client_data_bytes = serde_json::to_vec(&client_data).expect("json");
864 let assertion_auth_data = build_auth_data(RP_ID, 0x01, 1, &[], &[]); let mut signed = assertion_auth_data.clone();
866 signed.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
867 let rng = ring::rand::SystemRandom::new();
868 let signature = key_pair.sign(&rng, &signed).expect("ES256 signing");
869
870 svc.finish_authentication(
871 &alice(),
872 &result.credential_id,
873 &b64(&client_data_bytes),
874 &b64(&assertion_auth_data),
875 &b64(signature.as_ref()),
876 )
877 .expect("a correctly signed assertion verifies");
878 }
879
880 #[test]
881 fn kit_verifier_rejects_a_registration_from_a_foreign_origin() {
882 let svc = kit_service(Arc::new(MemStore::default()));
883 let (key_pair, cose_key) = es256_key();
884 let err = register_real(
885 &svc,
886 &alice(),
887 &key_pair,
888 &cose_key,
889 &[1, 2, 3],
890 "https://evil.example",
891 )
892 .expect_err("a foreign origin must not verify");
893 assert!(err.to_string().contains("origin"), "{err}");
894 }
895
896 #[test]
897 fn kit_verifier_rejects_an_assertion_signed_by_another_key() {
898 let store = Arc::new(MemStore::default());
899 let svc = kit_service(Arc::clone(&store));
900 let (key_pair, cose_key) = es256_key();
901 let result = register_real(&svc, &alice(), &key_pair, &cose_key, &[4, 5, 6], ORIGIN)
902 .expect("registration verifies");
903
904 let (other_key, _) = es256_key();
906 let options = svc.authentication_options(&alice());
907 let client_data = serde_json::json!({
908 "type": "webauthn.get",
909 "challenge": options.challenge,
910 "origin": ORIGIN,
911 });
912 let client_data_bytes = serde_json::to_vec(&client_data).expect("json");
913 let assertion_auth_data = build_auth_data(RP_ID, 0x01, 1, &[], &[]);
914 let mut signed = assertion_auth_data.clone();
915 signed.extend_from_slice(&sha2::Sha256::digest(&client_data_bytes));
916 let rng = ring::rand::SystemRandom::new();
917 let signature = other_key.sign(&rng, &signed).expect("ES256 signing");
918
919 let err = svc
920 .finish_authentication(
921 &alice(),
922 &result.credential_id,
923 &b64(&client_data_bytes),
924 &b64(&assertion_auth_data),
925 &b64(signature.as_ref()),
926 )
927 .expect_err("an assertion from the wrong key must not verify");
928 assert!(err.to_string().contains("kit error"), "{err}");
929 }
930}