1use crate::ceremony::{
14 AuthenticatorData, AuthenticatorFlags, ClientData, RelyingParty, UserVerification,
15};
16use crate::challenge::{Ceremony, Challenge, ChallengeStore};
17use crate::credential::{Credential, CredentialStore};
18use crate::{Error, Json, Result};
19use rustlavel_auth::{base64, unix_now};
20
21#[derive(Debug, Clone)]
23pub struct PublicKeyCredentialRequestOptions {
24 rp_id: String,
25 challenge: Vec<u8>,
26 timeout_ms: u64,
27 user_verification: UserVerification,
28 allow: Vec<Vec<u8>>,
29}
30
31impl PublicKeyCredentialRequestOptions {
32 pub fn allowing<I: IntoIterator<Item = Vec<u8>>>(
39 mut self,
40 credential_ids: I,
41 ) -> PublicKeyCredentialRequestOptions {
42 self.allow = credential_ids.into_iter().collect();
43 self
44 }
45
46 pub fn challenge(&self) -> &[u8] {
47 &self.challenge
48 }
49
50 pub fn allow_credentials(&self) -> &[Vec<u8>] {
51 &self.allow
52 }
53
54 pub fn json(&self) -> Json {
56 let allow: Vec<Json> = self
57 .allow
58 .iter()
59 .map(|id| {
60 Json::object([
61 ("type", Json::String("public-key".into())),
62 ("id", Json::String(base64::encode_url(id))),
63 ])
64 })
65 .collect();
66
67 Json::object([
68 ("rpId", Json::String(self.rp_id.clone())),
69 ("challenge", Json::String(base64::encode_url(&self.challenge))),
70 ("timeout", Json::Number(self.timeout_ms as f64)),
71 ("userVerification", Json::String(self.user_verification.as_str().into())),
72 ("allowCredentials", Json::Array(allow)),
73 ])
74 }
75}
76
77impl std::fmt::Display for PublicKeyCredentialRequestOptions {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{}", self.json())
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct AuthenticationResponse {
87 id: Vec<u8>,
88 client_data_json: Vec<u8>,
89 authenticator_data: Vec<u8>,
90 signature: Vec<u8>,
91 user_handle: Option<Vec<u8>>,
92}
93
94impl AuthenticationResponse {
95 pub fn new(
96 id: impl Into<Vec<u8>>,
97 client_data_json: impl Into<Vec<u8>>,
98 authenticator_data: impl Into<Vec<u8>>,
99 signature: impl Into<Vec<u8>>,
100 ) -> AuthenticationResponse {
101 AuthenticationResponse {
102 id: id.into(),
103 client_data_json: client_data_json.into(),
104 authenticator_data: authenticator_data.into(),
105 signature: signature.into(),
106 user_handle: None,
107 }
108 }
109
110 pub fn with_user_handle(mut self, user_handle: impl Into<Vec<u8>>) -> AuthenticationResponse {
112 self.user_handle = Some(user_handle.into());
113 self
114 }
115
116 pub fn from_json(value: &Json) -> Result<AuthenticationResponse> {
118 let response = AuthenticationResponse {
119 id: crate::ceremony::base64_field(value, "rawId")?,
120 client_data_json: crate::ceremony::base64_field(value, "response.clientDataJSON")?,
121 authenticator_data: crate::ceremony::base64_field(
122 value,
123 "response.authenticatorData",
124 )?,
125 signature: crate::ceremony::base64_field(value, "response.signature")?,
126 user_handle: None,
127 };
128
129 match value.get("response.userHandle") {
131 Some(handle) if !handle.is_null() => {
132 Ok(response.with_user_handle(crate::ceremony::base64_field(
133 value,
134 "response.userHandle",
135 )?))
136 }
137 _ => Ok(response),
138 }
139 }
140
141 pub fn parse(body: &str) -> Result<AuthenticationResponse> {
143 AuthenticationResponse::from_json(&Json::parse(body)?)
144 }
145
146 pub fn id(&self) -> &[u8] {
147 &self.id
148 }
149
150 pub fn client_data_json(&self) -> &[u8] {
151 &self.client_data_json
152 }
153
154 pub fn authenticator_data(&self) -> &[u8] {
155 &self.authenticator_data
156 }
157
158 pub fn signature(&self) -> &[u8] {
159 &self.signature
160 }
161
162 pub fn user_handle(&self) -> Option<&[u8]> {
163 self.user_handle.as_deref()
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct Authentication {
170 credential_id: Vec<u8>,
171 user_handle: Vec<u8>,
172 sign_count: u32,
173 flags: AuthenticatorFlags,
174}
175
176impl Authentication {
177 pub fn credential_id(&self) -> &[u8] {
178 &self.credential_id
179 }
180
181 pub fn user_handle(&self) -> &[u8] {
184 &self.user_handle
185 }
186
187 pub fn sign_count(&self) -> u32 {
188 self.sign_count
189 }
190
191 pub fn flags(&self) -> AuthenticatorFlags {
192 self.flags
193 }
194
195 pub fn user_verified(&self) -> bool {
196 self.flags.user_verified()
197 }
198}
199
200impl RelyingParty {
201 pub fn request_options(&self, challenge: &Challenge) -> PublicKeyCredentialRequestOptions {
203 PublicKeyCredentialRequestOptions {
204 rp_id: self.id().to_string(),
205 challenge: challenge.bytes().to_vec(),
206 timeout_ms: self.timeout_ms(),
207 user_verification: self.user_verification(),
208 allow: Vec::new(),
209 }
210 }
211
212 pub async fn start_authentication(
219 &self,
220 challenges: &dyn ChallengeStore,
221 ) -> Result<PublicKeyCredentialRequestOptions> {
222 let challenge = Challenge::issue(Ceremony::Authentication);
223 let options = self.request_options(&challenge);
224 challenges.store(challenge).await?;
225
226 Ok(options)
227 }
228
229 pub async fn start_authentication_for(
236 &self,
237 user_handle: &[u8],
238 challenges: &dyn ChallengeStore,
239 credentials: &dyn CredentialStore,
240 ) -> Result<PublicKeyCredentialRequestOptions> {
241 let challenge =
242 Challenge::issue(Ceremony::Authentication).bound_to(user_handle.to_vec());
243 let options = self.request_options(&challenge);
244 challenges.store(challenge).await?;
245
246 let theirs = credentials.find_for_user(user_handle).await?;
247 Ok(options.allowing(theirs.into_iter().map(|credential| credential.id().to_vec())))
248 }
249
250 pub async fn finish_authentication(
256 &self,
257 response: &AuthenticationResponse,
258 challenges: &dyn ChallengeStore,
259 credentials: &dyn CredentialStore,
260 ) -> Result<Authentication> {
261 let credential = credentials.find(response.id()).await?.ok_or_else(|| {
262 Error::msg(
263 "no credential is registered under that id, so there is no key to check this \
264 assertion against",
265 )
266 })?;
267
268 let client_data = ClientData::parse(response.client_data_json())?;
269
270 client_data.expect_kind(Ceremony::Authentication.client_data_type())?;
272
273 let key = base64::encode_url(client_data.challenge());
276 let challenge = challenges.take(&key).await?.ok_or_else(|| {
277 Error::msg(
278 "that challenge is not one this server issued, or it has already been answered. \
279 A challenge that can be answered twice is a password.",
280 )
281 })?;
282 challenge.accept(Ceremony::Authentication, unix_now())?;
283
284 if let Some(bound) = challenge.user_handle()
285 && bound != credential.user_handle()
286 {
287 return Err(Error::msg(
288 "that challenge was issued for a different user. An assertion from one account \
289 must not finish another account's login.",
290 ));
291 }
292
293 client_data.expect_origin(self)?;
295
296 let auth_data = AuthenticatorData::parse(response.authenticator_data())?;
297 auth_data.expect_rp_id(self)?;
298 auth_data.expect_user_present()?;
299 auth_data.expect_user_verification(self.user_verification())?;
300
301 if let Some(claimed) = response.user_handle()
305 && claimed != credential.user_handle()
306 {
307 return Err(Error::msg(
308 "the assertion claims a user handle that does not belong to that credential",
309 ));
310 }
311
312 let mut message = response.authenticator_data().to_vec();
314 message.extend_from_slice(&crate::ceremony::sha256(response.client_data_json()));
315
316 if !credential.key().verify(&message, response.signature()) {
317 return Err(Error::msg(
318 "the signature does not verify under the registered public key. Either the \
319 signer was not this credential, or the authenticator data or client data was \
320 changed after it was signed.",
321 ));
322 }
323
324 check_sign_count(&credential, auth_data.sign_count())?;
325
326 let now = unix_now();
327 credentials.record_use(credential.id(), auth_data.sign_count(), now).await?;
328
329 Ok(Authentication {
330 credential_id: credential.id().to_vec(),
331 user_handle: credential.user_handle().to_vec(),
332 sign_count: auth_data.sign_count(),
333 flags: auth_data.flags(),
334 })
335 }
336}
337
338fn check_sign_count(credential: &Credential, reported: u32) -> Result<()> {
355 let stored = credential.sign_count();
356
357 if stored != 0 && reported <= stored {
358 return Err(Error::msg(format!(
359 "the sign counter went backwards: this credential last reported {stored} and now \
360 reports {reported}. Two authenticators are signing with the same private key, \
361 which means it has been copied off the device that was supposed to hold it."
362 )));
363 }
364
365 Ok(())
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::ceremony::fake::{ASSERTION_FLAGS, Authenticator, REGISTRATION_FLAGS};
372 use crate::ceremony::UserEntity;
373 use crate::challenge::MemoryChallengeStore;
374 use crate::credential::MemoryCredentialStore;
375 use crate::registration::RegistrationResponse;
376
377 const ORIGIN: &str = "https://example.com";
378
379 fn relying_party() -> RelyingParty {
380 RelyingParty::new("example.com", "Example")
381 }
382
383 fn user() -> UserEntity {
384 UserEntity::new(b"user-41".to_vec(), "ada", "Ada Lovelace")
385 }
386
387 struct Session {
390 rp: RelyingParty,
391 device: Authenticator,
392 challenges: MemoryChallengeStore,
393 credentials: MemoryCredentialStore,
394 challenge: Vec<u8>,
395 }
396
397 impl Session {
398 async fn fresh() -> Session {
399 Session::with(relying_party(), Authenticator::new(1)).await
400 }
401
402 async fn with(rp: RelyingParty, device: Authenticator) -> Session {
403 let challenges = MemoryChallengeStore::new();
404 let credentials = MemoryCredentialStore::new();
405
406 let options =
409 rp.start_registration(&user(), &challenges, &credentials).await.unwrap();
410 let auth_data = device.authenticator_data(rp.id(), REGISTRATION_FLAGS, 0);
411 let registration = RegistrationResponse::new(
412 device.credential_id.clone(),
413 device.client_data("webauthn.create", options.challenge(), ORIGIN),
414 device.attestation_object("none", 0, &auth_data),
415 );
416 rp.finish_registration(&user(), ®istration, &challenges, &credentials)
417 .await
418 .unwrap();
419
420 let request = rp.start_authentication(&challenges).await.unwrap();
421 let challenge = request.challenge().to_vec();
422
423 Session { rp, device, challenges, credentials, challenge }
424 }
425
426 fn assert_with(
427 &self,
428 flags: u8,
429 rp_id: &str,
430 origin: &str,
431 sign_count: u32,
432 challenge: &[u8],
433 ) -> AuthenticationResponse {
434 let auth_data = self.device.authenticator_data(rp_id, flags, sign_count);
435 let client_data = self.device.client_data("webauthn.get", challenge, origin);
436 let signature = self.device.sign(&auth_data, &client_data);
437
438 AuthenticationResponse::new(
439 self.device.credential_id.clone(),
440 client_data,
441 auth_data,
442 signature,
443 )
444 }
445
446 fn healthy(&self) -> AuthenticationResponse {
447 self.assert_with(ASSERTION_FLAGS, self.rp.id(), ORIGIN, 0, &self.challenge)
448 }
449
450 async fn finish(&self, response: &AuthenticationResponse) -> Result<Authentication> {
451 self.rp.finish_authentication(response, &self.challenges, &self.credentials).await
452 }
453 }
454
455 #[tokio::test]
456 async fn an_assertion_round_trips_and_records_the_login() {
457 let session = Session::fresh().await;
458
459 let authentication = session.finish(&session.healthy()).await.unwrap();
460
461 assert_eq!(authentication.credential_id(), session.device.credential_id.as_slice());
462 assert_eq!(authentication.user_handle(), b"user-41");
463 assert!(authentication.user_verified());
464
465 let stored =
466 session.credentials.find(&session.device.credential_id).await.unwrap().unwrap();
467 assert!(stored.last_used_at().is_some(), "the login was not recorded");
468 }
469
470 #[tokio::test]
471 async fn a_signature_from_a_different_credential_is_refused() {
472 let session = Session::fresh().await;
474 let intruder =
475 Authenticator::new(2).with_credential_id(session.device.credential_id.clone());
476
477 let auth_data = intruder.authenticator_data("example.com", ASSERTION_FLAGS, 0);
478 let client_data = intruder.client_data("webauthn.get", &session.challenge, ORIGIN);
479 let response = AuthenticationResponse::new(
480 session.device.credential_id.clone(),
481 client_data.clone(),
482 auth_data.clone(),
483 intruder.sign(&auth_data, &client_data),
484 );
485
486 let error = session.finish(&response).await.unwrap_err().to_string();
487 assert!(error.contains("does not verify"), "got {error}");
488 }
489
490 #[tokio::test]
491 async fn a_tampered_authenticator_data_is_refused() {
492 let session = Session::fresh().await;
494 let mut response = session.healthy();
495 response.authenticator_data[36] = 1;
496
497 let error = session.finish(&response).await.unwrap_err().to_string();
498 assert!(error.contains("does not verify"), "got {error}");
499 }
500
501 #[tokio::test]
502 async fn a_tampered_client_data_json_is_refused() {
503 let session = Session::fresh().await;
507 let auth_data = session.device.authenticator_data("example.com", ASSERTION_FLAGS, 0);
508 let signed =
509 session.device.client_data("webauthn.get", &session.challenge, "https://evil.test");
510 let signature = session.device.sign(&auth_data, &signed);
511 let presented =
512 session.device.client_data("webauthn.get", &session.challenge, ORIGIN);
513
514 let response = AuthenticationResponse::new(
515 session.device.credential_id.clone(),
516 presented,
517 auth_data,
518 signature,
519 );
520
521 let error = session.finish(&response).await.unwrap_err().to_string();
522 assert!(error.contains("does not verify"), "got {error}");
523 }
524
525 #[tokio::test]
526 async fn a_regressed_sign_counter_is_refused_as_a_cloned_authenticator() {
527 let session = Session::fresh().await;
528
529 let response =
531 session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 5, &session.challenge);
532 session.finish(&response).await.unwrap();
533
534 let stored =
535 session.credentials.find(&session.device.credential_id).await.unwrap().unwrap();
536 assert_eq!(stored.sign_count(), 5);
537
538 let request = session.rp.start_authentication(&session.challenges).await.unwrap();
540 let cloned =
541 session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 3, request.challenge());
542
543 let error = session.finish(&cloned).await.unwrap_err().to_string();
544 assert!(error.contains("went backwards"), "got {error}");
545 assert!(error.contains("copied off the device"), "got {error}");
546 }
547
548 #[tokio::test]
549 async fn a_sign_counter_of_zero_on_both_sides_is_normal_and_not_a_clone() {
550 let session = Session::fresh().await;
553
554 session.finish(&session.healthy()).await.unwrap();
555
556 let request = session.rp.start_authentication(&session.challenges).await.unwrap();
557 let again =
558 session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, request.challenge());
559
560 assert!(session.finish(&again).await.is_ok(), "a passkey was mistaken for a clone");
561 }
562
563 #[tokio::test]
564 async fn a_replayed_assertion_is_refused_because_answering_burned_the_challenge() {
565 let session = Session::fresh().await;
566 let response = session.healthy();
567
568 session.finish(&response).await.unwrap();
569
570 let error = session.finish(&response).await.unwrap_err().to_string();
572 assert!(error.contains("already been answered"), "got {error}");
573 }
574
575 #[tokio::test]
576 async fn an_expired_challenge_is_refused() {
577 let session = Session::fresh().await;
578 let stale = Challenge::issue_lasting(Ceremony::Authentication, 0);
579 let bytes = stale.bytes().to_vec();
580 session.challenges.store(stale).await.unwrap();
581
582 let response = session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, &bytes);
583
584 let error = session.finish(&response).await.unwrap_err().to_string();
585 assert!(error.contains("expired"), "got {error}");
586 }
587
588 #[tokio::test]
589 async fn a_registration_challenge_cannot_be_spent_on_a_login() {
590 let session = Session::fresh().await;
591 let other = Challenge::issue(Ceremony::Registration);
592 let bytes = other.bytes().to_vec();
593 session.challenges.store(other).await.unwrap();
594
595 let response = session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, &bytes);
596
597 let error = session.finish(&response).await.unwrap_err().to_string();
598 assert!(error.contains("issued for registration"), "got {error}");
599 }
600
601 #[tokio::test]
602 async fn a_wrong_origin_is_refused() {
603 let session = Session::fresh().await;
604 let response = session.assert_with(
605 ASSERTION_FLAGS,
606 "example.com",
607 "https://evil.test",
608 0,
609 &session.challenge,
610 );
611
612 let error = session.finish(&response).await.unwrap_err().to_string();
613 assert!(error.contains("evil.test"), "got {error}");
614 }
615
616 #[tokio::test]
617 async fn an_origin_that_is_only_a_prefix_of_the_right_one_is_refused() {
618 let session = Session::fresh().await;
622 let response = session.assert_with(
623 ASSERTION_FLAGS,
624 "example.com",
625 "https://example.com.evil.test",
626 0,
627 &session.challenge,
628 );
629
630 let error = session.finish(&response).await.unwrap_err().to_string();
631 assert!(error.contains("exact"), "got {error}");
632 }
633
634 #[tokio::test]
635 async fn a_wrong_relying_party_id_hash_is_refused() {
636 let session = Session::fresh().await;
637 let response =
638 session.assert_with(ASSERTION_FLAGS, "evil.test", ORIGIN, 0, &session.challenge);
639
640 let error = session.finish(&response).await.unwrap_err().to_string();
641 assert!(error.contains("different relying party"), "got {error}");
642 }
643
644 #[tokio::test]
645 async fn user_present_not_set_is_refused() {
646 let session = Session::fresh().await;
647 let response = session.assert_with(0x00, "example.com", ORIGIN, 0, &session.challenge);
648
649 let error = session.finish(&response).await.unwrap_err().to_string();
650 assert!(error.contains("User Present"), "got {error}");
651 }
652
653 #[tokio::test]
654 async fn user_verification_is_refused_when_required_and_not_performed() {
655 let rp = relying_party().with_user_verification(UserVerification::Required);
656 let session = Session::with(rp, Authenticator::new(1)).await;
657 let response = session.assert_with(0x01, "example.com", ORIGIN, 0, &session.challenge);
658
659 let error = session.finish(&response).await.unwrap_err().to_string();
660 assert!(error.contains("user verification was required"), "got {error}");
661 }
662
663 #[tokio::test]
664 async fn a_user_handle_that_belongs_to_somebody_else_is_refused() {
665 let session = Session::fresh().await;
666 let response = session.healthy().with_user_handle(b"user-99".to_vec());
667
668 let error = session.finish(&response).await.unwrap_err().to_string();
669 assert!(error.contains("does not belong"), "got {error}");
670 }
671
672 #[tokio::test]
673 async fn an_unknown_credential_id_is_refused() {
674 let session = Session::fresh().await;
675 let mut response = session.healthy();
676 response.id = b"never registered".to_vec();
677
678 let error = session.finish(&response).await.unwrap_err().to_string();
679 assert!(error.contains("no credential is registered"), "got {error}");
680 }
681
682 #[tokio::test]
683 async fn a_login_bound_to_one_user_cannot_be_finished_by_another() {
684 let session = Session::fresh().await;
685 let bound = session
686 .rp
687 .start_authentication_for(b"user-99", &session.challenges, &session.credentials)
688 .await
689 .unwrap();
690
691 let response =
692 session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, bound.challenge());
693
694 let error = session.finish(&response).await.unwrap_err().to_string();
695 assert!(error.contains("issued for a different user"), "got {error}");
696 }
697
698 #[tokio::test]
699 async fn a_login_started_for_a_user_names_the_credentials_they_have() {
700 let session = Session::fresh().await;
701 let options = session
702 .rp
703 .start_authentication_for(b"user-41", &session.challenges, &session.credentials)
704 .await
705 .unwrap();
706
707 assert_eq!(
708 options.allow_credentials(),
709 std::slice::from_ref(&session.device.credential_id)
710 );
711
712 let json = options.json();
713 assert_eq!(json.get("rpId").and_then(Json::as_str), Some("example.com"));
714 assert_eq!(
715 json.get("allowCredentials.0.id").and_then(Json::as_str),
716 Some(base64::encode_url(&session.device.credential_id).as_str())
717 );
718
719 let anonymous = session.rp.start_authentication(&session.challenges).await.unwrap();
722 assert!(anonymous.allow_credentials().is_empty());
723 let json = anonymous.json();
724 assert!(json.get("allowCredentials").and_then(Json::as_array).unwrap().is_empty());
725 }
726
727 #[test]
728 fn a_response_can_be_read_from_the_json_a_browser_posts() {
729 let body = format!(
730 "{{\"id\":\"{id}\",\"rawId\":\"{id}\",\"type\":\"public-key\",\"response\":\
731 {{\"clientDataJSON\":\"{client}\",\"authenticatorData\":\"{auth}\",\
732 \"signature\":\"{signature}\",\"userHandle\":\"{handle}\"}}}}",
733 id = base64::encode_url(b"credential"),
734 client = base64::encode_url(b"{}"),
735 auth = base64::encode_url(b"authdata"),
736 signature = base64::encode_url(b"sig"),
737 handle = base64::encode_url(b"user-41"),
738 );
739
740 let response = AuthenticationResponse::parse(&body).unwrap();
741
742 assert_eq!(response.id(), b"credential");
743 assert_eq!(response.authenticator_data(), b"authdata");
744 assert_eq!(response.signature(), b"sig");
745 assert_eq!(response.user_handle(), Some(b"user-41".as_slice()));
746
747 let without = body.replace(
750 &format!("\"userHandle\":\"{}\"", base64::encode_url(b"user-41")),
751 "\"userHandle\":null",
752 );
753 assert_eq!(AuthenticationResponse::parse(&without).unwrap().user_handle(), None);
754
755 assert!(AuthenticationResponse::parse("{}").is_err());
756 }
757}