1use crate::authentication::credentials::{Credential, CredentialMetadata};
99use crate::errors::{AuthError, Result};
100use crate::methods::{AuthMethod, MethodResult};
101use crate::tokens::{AuthToken, TokenManager};
102use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
103use serde::{Deserialize, Serialize};
104use std::time::SystemTime;
105use std::{collections::HashMap, sync::RwLock};
106
107#[cfg(feature = "passkeys")]
108use std::time::Duration;
109
110#[cfg(feature = "passkeys")]
112use coset::iana::Algorithm;
113#[cfg(feature = "passkeys")]
114use passkey::{
115 authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod},
116 client::Client,
117 types::{
118 Bytes, Passkey,
119 ctap2::{Aaguid, Ctap2Error},
120 rand::random_vec,
121 webauthn::{
122 AttestationConveyancePreference, AuthenticatedPublicKeyCredential,
123 CreatedPublicKeyCredential, CredentialCreationOptions, CredentialRequestOptions,
124 PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
125 PublicKeyCredentialParameters, PublicKeyCredentialRequestOptions,
126 PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
127 UserVerificationRequirement,
128 },
129 },
130};
131#[cfg(feature = "passkeys")]
132use passkey_client::DefaultClientData;
133#[cfg(feature = "passkeys")]
134use url::Url;
135
136#[cfg(feature = "passkeys")]
138struct PasskeyUserValidation;
139
140#[cfg(feature = "passkeys")]
141#[async_trait::async_trait]
142impl UserValidationMethod for PasskeyUserValidation {
143 type PasskeyItem = Passkey;
144
145 async fn check_user<'a>(
146 &self,
147 _hint: UiHint<'a, Passkey>,
148 presence: bool,
149 verification: bool,
150 ) -> std::result::Result<UserCheck, Ctap2Error> {
151 Ok(UserCheck {
152 presence,
153 verification,
154 })
155 }
156
157 fn is_verification_enabled(&self) -> Option<bool> {
158 Some(true)
159 }
160
161 fn is_presence_enabled(&self) -> bool {
162 true
163 }
164}
165
166pub struct PasskeyAuthMethod {
248 pub config: PasskeyConfig,
249 pub token_manager: TokenManager,
250 pub registered_passkeys: RwLock<HashMap<String, PasskeyRegistration>>,
252 #[cfg(feature = "passkeys")]
255 pending_challenges: RwLock<HashMap<String, Vec<u8>>>,
256}
257
258impl std::fmt::Debug for PasskeyAuthMethod {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 let mut s = f.debug_struct("PasskeyAuthMethod");
261 s.field("config", &self.config);
262 s.field("token_manager", &"<TokenManager>");
263 s.field("registered_passkeys", &"<RwLock<HashMap>>");
264 #[cfg(feature = "passkeys")]
265 s.field("pending_challenges", &"<RwLock<HashMap>>");
266 s.finish()
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct PasskeyConfig {
273 pub rp_id: String,
275 pub rp_name: String,
277 pub origin: String,
279 pub timeout_ms: u32,
281 pub user_verification: String, pub authenticator_attachment: Option<String>, pub require_resident_key: bool,
287 #[serde(default = "default_attestation_conveyance")]
290 pub attestation_conveyance: String,
291}
292
293fn default_attestation_conveyance() -> String {
294 "direct".to_string()
295}
296
297impl Default for PasskeyConfig {
298 fn default() -> Self {
299 Self {
300 rp_id: "localhost".to_string(),
301 rp_name: "Auth Framework Demo".to_string(),
302 origin: "http://localhost:3000".to_string(),
303 timeout_ms: 60000, user_verification: "preferred".to_string(),
305 authenticator_attachment: None,
306 require_resident_key: false,
307 attestation_conveyance: default_attestation_conveyance(),
308 }
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PasskeyRegistration {
315 pub user_id: String,
316 pub user_name: String,
317 pub user_display_name: String,
318 pub credential_id: Vec<u8>,
319 pub passkey_data: String, pub created_at: SystemTime,
321 pub last_used: Option<SystemTime>,
322}
323
324impl PasskeyAuthMethod {
325 pub fn new(config: PasskeyConfig, token_manager: TokenManager) -> Result<Self> {
327 #[cfg(feature = "passkeys")]
328 {
329 Ok(Self {
330 config,
331 token_manager,
332 registered_passkeys: RwLock::new(HashMap::new()),
333 pending_challenges: RwLock::new(HashMap::new()),
334 })
335 }
336
337 #[cfg(not(feature = "passkeys"))]
338 {
339 let _ = (config, token_manager); Err(AuthError::config(
341 "Passkey support not compiled in. Enable 'passkeys' feature.",
342 ))
343 }
344 }
345
346 #[cfg(feature = "passkeys")]
348 pub async fn register_passkey(
349 &mut self,
350 user_id: &str,
351 user_name: &str,
352 user_display_name: &str,
353 ) -> Result<CreatedPublicKeyCredential> {
354 let origin = Url::parse(&self.config.origin)
355 .map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
356
357 let aaguid = Aaguid::new_empty();
359 let user_validation = PasskeyUserValidation;
360 let store: Option<Passkey> = None;
361 let authenticator = Authenticator::new(aaguid, store, user_validation);
362
363 let mut client = Client::new(authenticator);
365
366 let challenge: Bytes = random_vec(32).into();
368
369 let user_entity = PublicKeyCredentialUserEntity {
371 id: Bytes::from(user_id.as_bytes().to_vec()),
372 display_name: user_display_name.to_string(),
373 name: user_name.to_string(),
374 };
375
376 let request = CredentialCreationOptions {
378 public_key: PublicKeyCredentialCreationOptions {
379 rp: PublicKeyCredentialRpEntity {
380 id: None, name: self.config.rp_name.clone(),
382 },
383 user: user_entity,
384 challenge,
385 pub_key_cred_params: vec![
386 PublicKeyCredentialParameters {
387 ty: PublicKeyCredentialType::PublicKey,
388 alg: Algorithm::ES256,
389 },
390 PublicKeyCredentialParameters {
391 ty: PublicKeyCredentialType::PublicKey,
392 alg: Algorithm::RS256,
393 },
394 ],
395 timeout: Some(self.config.timeout_ms),
396 exclude_credentials: None,
397 authenticator_selection: None,
398 hints: None,
399 attestation: match self.config.attestation_conveyance.as_str() {
400 "none" => AttestationConveyancePreference::None,
401 "indirect" => AttestationConveyancePreference::Indirect,
402 "enterprise" => AttestationConveyancePreference::Enterprise,
403 _ => AttestationConveyancePreference::Direct,
405 },
406 attestation_formats: None,
407 extensions: None,
408 },
409 };
410
411 let credential = client
413 .register(&origin, request, DefaultClientData)
414 .await
415 .map_err(|e| AuthError::validation(format!("Passkey registration failed: {:?}", e)))?;
416
417 let credential_id = &credential.raw_id;
419 let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
420
421 let credential_value = serde_json::to_value(&credential).unwrap_or(serde_json::Value::Null);
424 let public_key_value = credential_value
425 .pointer("/response/authenticatorData/attestedCredentialData/credentialPublicKey")
426 .cloned()
427 .unwrap_or(serde_json::Value::Null);
428 let sign_count = credential_value
429 .pointer("/response/authenticatorData/signCount")
430 .and_then(|v| v.as_u64())
431 .unwrap_or(0);
432 let passkey_data_json = serde_json::json!({
433 "credential_id": credential_id_b64,
434 "public_key": public_key_value,
435 "signature_counter": sign_count,
436 "raw_attestation": credential_value,
437 });
438 let passkey_data_str = serde_json::to_string(&passkey_data_json).map_err(|e| {
439 AuthError::validation(format!("Failed to serialize passkey data: {}", e))
440 })?;
441
442 let registration = PasskeyRegistration {
443 user_id: user_id.to_string(),
444 user_name: user_name.to_string(),
445 user_display_name: user_display_name.to_string(),
446 credential_id: credential_id.as_slice().to_vec(),
447 passkey_data: passkey_data_str,
448 created_at: SystemTime::now(),
449 last_used: None,
450 };
451
452 {
453 let mut passkeys = self.registered_passkeys.write().map_err(|_| {
454 AuthError::internal(
455 "Lock poisoned — a prior thread panicked while holding this lock",
456 )
457 })?;
458 passkeys.insert(credential_id_b64.clone(), registration);
459 }
460
461 tracing::info!("Successfully registered passkey for user: {}", user_id);
462 Ok(credential)
463 }
464
465 #[cfg(feature = "passkeys")]
467 pub async fn initiate_authentication(
468 &self,
469 user_id: Option<&str>,
470 ) -> Result<CredentialRequestOptions> {
471 let challenge_bytes = random_vec(32);
472 let challenge: Bytes = challenge_bytes.clone().into();
473
474 let allow_credential_ids: Vec<(Vec<u8>, String)> = {
475 let passkeys = self.registered_passkeys.read().map_err(|_| {
476 AuthError::internal(
477 "Lock poisoned — a prior thread panicked while holding this lock",
478 )
479 })?;
480 let iter: Box<dyn Iterator<Item = &PasskeyRegistration>> = if let Some(uid) = user_id {
481 Box::new(passkeys.values().filter(move |reg| reg.user_id == uid))
482 } else {
483 Box::new(passkeys.values())
484 };
485 iter.map(|reg| {
486 (
487 reg.credential_id.clone(),
488 URL_SAFE_NO_PAD.encode(®.credential_id),
489 )
490 })
491 .collect()
492 };
493
494 {
497 let mut challenges = self.pending_challenges.write().map_err(|_| {
498 AuthError::internal(
499 "Lock poisoned — a prior thread panicked while holding this lock",
500 )
501 })?;
502 for (_, cred_id_b64) in &allow_credential_ids {
503 challenges.insert(cred_id_b64.clone(), challenge_bytes.clone());
504 }
505 challenges.insert("latest".to_string(), challenge_bytes.clone());
507 }
508
509 let allow_credentials: Vec<PublicKeyCredentialDescriptor> = allow_credential_ids
510 .into_iter()
511 .map(|(raw_id, _)| PublicKeyCredentialDescriptor {
512 ty: PublicKeyCredentialType::PublicKey,
513 id: raw_id.into(),
514 transports: None,
515 })
516 .collect();
517
518 let request_options = CredentialRequestOptions {
519 public_key: PublicKeyCredentialRequestOptions {
520 challenge,
521 timeout: Some(self.config.timeout_ms),
522 rp_id: Some(self.config.rp_id.clone()),
523 allow_credentials: Some(allow_credentials),
524 user_verification: match self.config.user_verification.as_str() {
525 "required" => UserVerificationRequirement::Required,
526 "discouraged" => UserVerificationRequirement::Discouraged,
527 _ => UserVerificationRequirement::Preferred,
528 },
529 hints: None,
530 attestation: match self.config.attestation_conveyance.as_str() {
531 "none" => AttestationConveyancePreference::None,
532 "indirect" => AttestationConveyancePreference::Indirect,
533 "enterprise" => AttestationConveyancePreference::Enterprise,
534 _ => AttestationConveyancePreference::Direct,
535 },
536 attestation_formats: None,
537 extensions: None,
538 },
539 };
540
541 tracing::info!("Generated passkey authentication options");
542 Ok(request_options)
543 }
544
545 #[cfg(feature = "passkeys")]
547 pub async fn complete_authentication(
548 &mut self,
549 credential_response: &AuthenticatedPublicKeyCredential,
550 ) -> Result<AuthToken> {
551 let credential_id = &credential_response.raw_id;
552 let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
553
554 let mut registration = {
556 let passkeys = self.registered_passkeys.read().map_err(|_| {
557 AuthError::internal(
558 "Lock poisoned — a prior thread panicked while holding this lock",
559 )
560 })?;
561 passkeys
562 .get(&credential_id_b64)
563 .ok_or_else(|| AuthError::validation("Unknown credential ID"))?
564 .clone()
565 };
566
567 let assertion_response = serde_json::to_string(credential_response).map_err(|e| {
570 AuthError::InvalidCredential {
571 credential_type: "passkey".to_string(),
572 message: format!("Failed to serialize credential response: {}", e),
573 }
574 })?;
575
576 let passkey_data: serde_json::Value = serde_json::from_str(®istration.passkey_data)
578 .map_err(|e| AuthError::InvalidCredential {
579 credential_type: "passkey".to_string(),
580 message: format!("Failed to parse stored passkey data: {}", e),
581 })?;
582
583 let public_key_jwk = passkey_data
584 .get("public_key")
585 .cloned()
586 .unwrap_or(serde_json::Value::Null);
587 let stored_counter = passkey_data
588 .get("signature_counter")
589 .and_then(|v| v.as_u64())
590 .unwrap_or(0) as u32;
591
592 let expected_challenge_bytes: Vec<u8> = {
594 let challenges = self.pending_challenges.read().map_err(|_| {
595 AuthError::internal(
596 "Lock poisoned — a prior thread panicked while holding this lock",
597 )
598 })?;
599 challenges
600 .get(&credential_id_b64)
601 .or_else(|| challenges.get("latest"))
602 .cloned()
603 .ok_or_else(|| {
604 AuthError::validation(
605 "No pending challenge found; call initiate_authentication first",
606 )
607 })?
608 };
609
610 let verification_result = self
612 .advanced_verification_flow(
613 &assertion_response,
614 expected_challenge_bytes.as_slice(),
615 stored_counter,
616 &public_key_jwk,
617 )
618 .await?;
619
620 if !verification_result.signature_valid {
621 return Err(AuthError::validation(
622 "Passkey signature verification failed",
623 ));
624 }
625
626 let mut passkey_data: serde_json::Value = serde_json::from_str(®istration.passkey_data)
628 .map_err(|e| AuthError::InvalidCredential {
629 credential_type: "passkey".to_string(),
630 message: format!(
631 "Failed to parse stored passkey data during counter update: {}",
632 e
633 ),
634 })?;
635 passkey_data["signature_counter"] =
636 serde_json::Value::Number(serde_json::Number::from(verification_result.new_counter));
637 registration.passkey_data =
638 serde_json::to_string(&passkey_data).map_err(|e| AuthError::InvalidCredential {
639 credential_type: "passkey".to_string(),
640 message: format!("Failed to serialize updated passkey data: {}", e),
641 })?;
642 registration.last_used = Some(SystemTime::now());
643
644 {
645 let mut passkeys = self.registered_passkeys.write().map_err(|_| {
646 AuthError::internal(
647 "Lock poisoned — a prior thread panicked while holding this lock",
648 )
649 })?;
650 passkeys.insert(credential_id_b64.clone(), registration.clone());
651 }
652
653 let token = self.token_manager.create_jwt_token(
655 ®istration.user_id,
656 vec![],
657 Some(Duration::from_secs(3600)),
658 )?;
659
660 tracing::info!(
661 "Passkey authentication successful for user: {} (counter: {} -> {})",
662 registration.user_id,
663 stored_counter,
664 verification_result.new_counter
665 );
666 Ok(AuthToken::new(
667 ®istration.user_id,
668 token,
669 Duration::from_secs(3600),
670 "passkey",
671 ))
672 }
673
674 #[cfg(not(feature = "passkeys"))]
676 pub async fn register_passkey(
677 &mut self,
678 _user_id: &str,
679 _user_name: &str,
680 _user_display_name: &str,
681 ) -> Result<()> {
682 Err(AuthError::config(
683 "Passkey support not compiled in. Enable 'passkeys' feature.",
684 ))
685 }
686}
687
688impl AuthMethod for PasskeyAuthMethod {
689 type MethodResult = MethodResult;
690 type AuthToken = AuthToken;
691
692 fn name(&self) -> &str {
693 "passkey"
694 }
695
696 async fn authenticate(
697 &self,
698 credential: Credential,
699 _metadata: CredentialMetadata,
700 ) -> Result<Self::MethodResult> {
701 #[cfg(feature = "passkeys")]
702 {
703 match credential {
704 Credential::Passkey {
705 credential_id,
706 assertion_response,
707 } => {
708 let credential_id_b64 = URL_SAFE_NO_PAD.encode(&credential_id);
710 let registration = {
711 let passkeys = self.registered_passkeys.read().map_err(|_| {
712 AuthError::internal(
713 "Lock poisoned — a prior thread panicked while holding this lock",
714 )
715 })?;
716 passkeys
717 .get(&credential_id_b64)
718 .cloned()
719 .ok_or_else(|| AuthError::validation("Unknown credential ID"))?
720 };
721
722 tracing::debug!(
723 "Processing passkey assertion for credential: {}",
724 credential_id_b64
725 );
726
727 let passkey_data: serde_json::Value =
730 serde_json::from_str(®istration.passkey_data).map_err(|e| {
731 AuthError::InvalidCredential {
732 credential_type: "passkey".to_string(),
733 message: format!("Failed to parse stored passkey data: {}", e),
734 }
735 })?;
736
737 let public_key_jwk = passkey_data
738 .get("public_key")
739 .cloned()
740 .unwrap_or(serde_json::Value::Null);
741 let stored_counter = passkey_data
742 .get("signature_counter")
743 .and_then(|v| v.as_u64())
744 .unwrap_or(0) as u32;
745
746 let expected_challenge_bytes: Vec<u8> = {
749 let challenges = self.pending_challenges.read().map_err(|_| {
750 AuthError::internal(
751 "Lock poisoned — a prior thread panicked while holding this lock",
752 )
753 })?;
754 challenges
755 .get(&credential_id_b64)
756 .or_else(|| challenges.get("latest"))
757 .cloned()
758 .ok_or_else(|| {
759 AuthError::validation(
760 "No pending challenge found; call initiate_authentication first",
761 )
762 })?
763 };
764
765 match self
767 .advanced_verification_flow(
768 &assertion_response,
769 expected_challenge_bytes.as_slice(),
770 stored_counter,
771 &public_key_jwk,
772 )
773 .await
774 {
775 Ok(verification_result) => {
776 if !verification_result.signature_valid {
777 return Err(AuthError::validation(
778 "Passkey signature verification failed",
779 ));
780 }
781
782 let mut updated_registration = registration.clone();
784
785 let mut passkey_data: serde_json::Value = serde_json::from_str(
787 &updated_registration.passkey_data,
788 )
789 .map_err(|e| AuthError::InvalidCredential {
790 credential_type: "passkey".to_string(),
791 message: format!("Failed to parse stored passkey data: {}", e),
792 })?;
793
794 passkey_data["signature_counter"] = serde_json::Value::Number(
795 serde_json::Number::from(verification_result.new_counter),
796 );
797
798 updated_registration.passkey_data =
799 serde_json::to_string(&passkey_data).map_err(|e| {
800 AuthError::InvalidCredential {
801 credential_type: "passkey".to_string(),
802 message: format!(
803 "Failed to serialize updated passkey data: {}",
804 e
805 ),
806 }
807 })?;
808
809 updated_registration.last_used = Some(SystemTime::now());
810
811 {
812 let mut passkeys = self.registered_passkeys.write()
813 .map_err(|_| AuthError::internal("Lock poisoned — a prior thread panicked while holding this lock"))?;
814 passkeys.insert(credential_id_b64.clone(), updated_registration);
815 }
816
817 tracing::info!(
818 "Advanced passkey verification successful for user: {} (counter: {} -> {})",
819 registration.user_id,
820 stored_counter,
821 verification_result.new_counter
822 );
823 }
824 Err(e) => {
825 tracing::error!("Advanced passkey verification failed: {}", e);
826 return Err(e);
827 }
828 }
829
830 tracing::debug!("Assertion response length: {}", assertion_response.len());
832
833 tracing::info!(
836 "Passkey assertion verified successfully for user: {}",
837 registration.user_id
838 );
839
840 let token = self.token_manager.create_jwt_token(
841 ®istration.user_id,
842 vec![], Some(Duration::from_secs(3600)), )?;
845
846 let auth_token = AuthToken::new(
847 ®istration.user_id,
848 token,
849 Duration::from_secs(3600),
850 "passkey",
851 );
852
853 tracing::info!(
854 "Passkey authentication successful for user: {}",
855 registration.user_id
856 );
857 Ok(MethodResult::Success(Box::new(auth_token)))
858 }
859 _ => Ok(MethodResult::Failure {
860 reason: "Invalid credential type for passkey authentication".to_string(),
861 }),
862 }
863 }
864
865 #[cfg(not(feature = "passkeys"))]
866 {
867 let _ = credential; Ok(MethodResult::Failure {
869 reason: "Passkey support not compiled in. Enable 'passkeys' feature.".to_string(),
870 })
871 }
872 }
873
874 fn validate_config(&self) -> Result<()> {
875 if self.config.rp_id.is_empty() {
876 return Err(AuthError::config("Passkey RP ID cannot be empty"));
877 }
878 if self.config.origin.is_empty() {
879 return Err(AuthError::config("Passkey origin cannot be empty"));
880 }
881 if self.config.timeout_ms == 0 {
882 return Err(AuthError::config("Passkey timeout must be greater than 0"));
883 }
884
885 match self.config.user_verification.as_str() {
887 "required" | "preferred" | "discouraged" => {}
888 _ => return Err(AuthError::config("Invalid user verification requirement")),
889 }
890
891 #[cfg(feature = "passkeys")]
893 {
894 Url::parse(&self.config.origin)
895 .map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
896 }
897
898 Ok(())
899 }
900
901 fn supports_refresh(&self) -> bool {
902 false }
904
905 async fn refresh_token(&self, _refresh_token: String) -> Result<Self::AuthToken, AuthError> {
906 Err(AuthError::validation(
907 "Passkeys do not support token refresh",
908 ))
909 }
910}
911
912impl PasskeyAuthMethod {
913 pub async fn advanced_verification_flow(
916 &self,
917 assertion_response: &str,
918 expected_challenge: &[u8],
919 stored_counter: u32,
920 public_key_jwk: &serde_json::Value,
921 ) -> Result<AdvancedVerificationResult> {
922 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
923 use ring::digest;
924
925 tracing::info!("Starting advanced passkey verification flow");
926
927 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
929 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
930
931 let client_data_json = assertion
933 .get("response")
934 .and_then(|r| r.get("clientDataJSON"))
935 .and_then(|c| c.as_str())
936 .ok_or_else(|| AuthError::validation("Missing clientDataJSON"))?;
937
938 let decoded_client_data = URL_SAFE_NO_PAD
939 .decode(client_data_json)
940 .map_err(|_| AuthError::validation("Invalid base64 in clientDataJSON"))?;
941
942 let client_data_str = std::str::from_utf8(&decoded_client_data)
943 .map_err(|_| AuthError::validation("Invalid UTF-8 in clientDataJSON"))?;
944
945 let client_data: serde_json::Value = serde_json::from_str(client_data_str)
946 .map_err(|_| AuthError::validation("Invalid JSON in clientDataJSON"))?;
947
948 let response_challenge = client_data
950 .get("challenge")
951 .and_then(|c| c.as_str())
952 .ok_or_else(|| AuthError::validation("Missing challenge in clientDataJSON"))?;
953
954 let decoded_challenge = URL_SAFE_NO_PAD
955 .decode(response_challenge)
956 .map_err(|_| AuthError::validation("Invalid challenge base64"))?;
957
958 if decoded_challenge != expected_challenge {
959 return Err(AuthError::validation("Challenge mismatch"));
960 }
961
962 let origin = client_data
964 .get("origin")
965 .and_then(|o| o.as_str())
966 .ok_or_else(|| AuthError::validation("Missing origin"))?;
967
968 if origin != self.config.origin {
969 return Err(AuthError::validation("Origin mismatch"));
970 }
971
972 let operation_type = client_data
974 .get("type")
975 .and_then(|t| t.as_str())
976 .ok_or_else(|| AuthError::validation("Missing operation type"))?;
977
978 if operation_type != "webauthn.get" {
979 return Err(AuthError::validation("Invalid operation type"));
980 }
981
982 let authenticator_data = assertion
984 .get("response")
985 .and_then(|r| r.get("authenticatorData"))
986 .and_then(|a| a.as_str())
987 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
988
989 let auth_data_bytes = URL_SAFE_NO_PAD
990 .decode(authenticator_data)
991 .map_err(|_| AuthError::validation("Invalid authenticatorData base64"))?;
992
993 if auth_data_bytes.len() < 37 {
994 return Err(AuthError::validation("AuthenticatorData too short"));
995 }
996
997 let rp_id_hash = &auth_data_bytes[0..32];
999 let expected_rp_id_hash = {
1000 let mut context = digest::Context::new(&digest::SHA256);
1001 context.update(self.config.rp_id.as_bytes());
1002 context.finish()
1003 };
1004
1005 if rp_id_hash != expected_rp_id_hash.as_ref() {
1006 return Err(AuthError::validation("RP ID hash mismatch"));
1007 }
1008
1009 let flags = auth_data_bytes[32];
1011 let user_present = (flags & 0x01) != 0;
1012 let user_verified = (flags & 0x04) != 0;
1013
1014 if !user_present {
1015 return Err(AuthError::validation("User not present"));
1016 }
1017
1018 let new_counter = self.extract_counter_from_assertion(assertion_response)?;
1020
1021 if new_counter != 0 && new_counter <= stored_counter {
1026 return Err(AuthError::validation(
1027 "Counter did not increase - possible replay attack or cloned authenticator",
1028 ));
1029 }
1030
1031 self.verify_assertion_signature(
1033 assertion_response,
1034 &auth_data_bytes,
1035 &decoded_client_data,
1036 public_key_jwk,
1037 )?;
1038
1039 tracing::info!("Advanced passkey verification completed successfully");
1040
1041 Ok(AdvancedVerificationResult {
1042 user_present,
1043 user_verified,
1044 new_counter,
1045 signature_valid: true,
1046 attestation_valid: true,
1047 })
1048 }
1049
1050 fn verify_webauthn_signature(
1052 &self,
1053 signed_data: &[u8],
1054 signature_bytes: &[u8],
1055 public_key_jwk: &serde_json::Value,
1056 ) -> Result<()> {
1057 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1058 use ring::signature;
1059
1060 let key_type = public_key_jwk
1061 .get("kty")
1062 .and_then(|v| v.as_str())
1063 .ok_or_else(|| AuthError::validation("Missing key type in JWK"))?;
1064
1065 let algorithm = public_key_jwk
1066 .get("alg")
1067 .and_then(|v| v.as_str())
1068 .ok_or_else(|| AuthError::validation("Missing algorithm in JWK"))?;
1069
1070 match key_type {
1071 "RSA" => {
1072 let n = public_key_jwk
1074 .get("n")
1075 .and_then(|v| v.as_str())
1076 .ok_or_else(|| AuthError::validation("Missing 'n' in RSA JWK"))?;
1077 let e = public_key_jwk
1078 .get("e")
1079 .and_then(|v| v.as_str())
1080 .ok_or_else(|| AuthError::validation("Missing 'e' in RSA JWK"))?;
1081
1082 let n_bytes = URL_SAFE_NO_PAD
1083 .decode(n.as_bytes())
1084 .map_err(|_| AuthError::validation("Invalid 'n' base64"))?;
1085 let e_bytes = URL_SAFE_NO_PAD
1086 .decode(e.as_bytes())
1087 .map_err(|_| AuthError::validation("Invalid 'e' base64"))?;
1088
1089 let n_ref: &[u8] = &n_bytes;
1093 let e_ref: &[u8] = &e_bytes;
1094 let rsa_key = signature::RsaPublicKeyComponents { n: n_ref, e: e_ref };
1095
1096 match algorithm {
1097 "RS256" => rsa_key.verify(
1098 &signature::RSA_PKCS1_2048_8192_SHA256,
1099 signed_data,
1100 signature_bytes,
1101 ),
1102 "RS384" => rsa_key.verify(
1103 &signature::RSA_PKCS1_2048_8192_SHA384,
1104 signed_data,
1105 signature_bytes,
1106 ),
1107 "RS512" => rsa_key.verify(
1108 &signature::RSA_PKCS1_2048_8192_SHA512,
1109 signed_data,
1110 signature_bytes,
1111 ),
1112 _ => return Err(AuthError::validation("Unsupported RSA algorithm")),
1113 }
1114 .map_err(|_| AuthError::validation("RSA signature verification failed"))?;
1115 }
1116 "EC" => {
1117 let curve = public_key_jwk
1119 .get("crv")
1120 .and_then(|v| v.as_str())
1121 .ok_or_else(|| AuthError::validation("Missing curve in EC JWK"))?;
1122 let x = public_key_jwk
1123 .get("x")
1124 .and_then(|v| v.as_str())
1125 .ok_or_else(|| AuthError::validation("Missing 'x' in EC JWK"))?;
1126 let y = public_key_jwk
1127 .get("y")
1128 .and_then(|v| v.as_str())
1129 .ok_or_else(|| AuthError::validation("Missing 'y' in EC JWK"))?;
1130
1131 let x_bytes = URL_SAFE_NO_PAD
1132 .decode(x.as_bytes())
1133 .map_err(|_| AuthError::validation("Invalid 'x' base64"))?;
1134 let y_bytes = URL_SAFE_NO_PAD
1135 .decode(y.as_bytes())
1136 .map_err(|_| AuthError::validation("Invalid 'y' base64"))?;
1137
1138 let (verification_algorithm, expected_coord_len) = match (curve, algorithm) {
1139 ("P-256", "ES256") => (&signature::ECDSA_P256_SHA256_ASN1, 32),
1140 ("P-384", "ES384") => (&signature::ECDSA_P384_SHA384_ASN1, 48),
1141 _ => return Err(AuthError::validation("Unsupported EC curve/algorithm")),
1142 };
1143
1144 if x_bytes.len() != expected_coord_len || y_bytes.len() != expected_coord_len {
1145 return Err(AuthError::validation("Invalid coordinate length"));
1146 }
1147
1148 let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
1150 public_key_bytes.push(0x04); public_key_bytes.extend_from_slice(&x_bytes);
1152 public_key_bytes.extend_from_slice(&y_bytes);
1153
1154 let public_key =
1155 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
1156
1157 public_key
1158 .verify(signed_data, signature_bytes)
1159 .map_err(|_| AuthError::validation("ECDSA signature verification failed"))?;
1160 }
1161 _ => return Err(AuthError::validation("Unsupported key type for WebAuthn")),
1162 }
1163
1164 Ok(())
1165 }
1166
1167 pub async fn cross_platform_verification(
1169 &self,
1170 assertion_response: &str,
1171 authenticator_types: &[AuthenticatorType],
1172 ) -> Result<CrossPlatformVerificationResult> {
1173 tracing::info!("Starting cross-platform passkey verification");
1174
1175 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1177 .map_err(|_| AuthError::validation("Invalid assertion response"))?;
1178
1179 let authenticator_data = assertion
1181 .get("response")
1182 .and_then(|r| r.get("authenticatorData"))
1183 .and_then(|a| a.as_str())
1184 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
1185
1186 let auth_data_bytes = URL_SAFE_NO_PAD
1187 .decode(authenticator_data)
1188 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1189
1190 let aaguid = if auth_data_bytes.len() >= 53 && (auth_data_bytes[32] & 0x40) != 0 {
1192 Some(&auth_data_bytes[37..53])
1193 } else {
1194 None
1195 };
1196
1197 let detected_type = self.detect_authenticator_type(aaguid)?;
1199
1200 if !authenticator_types.contains(&detected_type) {
1202 return Err(AuthError::validation("Authenticator type not allowed"));
1203 }
1204
1205 let type_specific_result = match detected_type {
1207 AuthenticatorType::Platform => {
1208 tracing::debug!("Performing platform authenticator validation");
1209 self.validate_platform_authenticator(&assertion).await?
1210 }
1211 AuthenticatorType::CrossPlatform => {
1212 tracing::debug!("Performing cross-platform authenticator validation");
1213 self.validate_cross_platform_authenticator(&assertion)
1214 .await?
1215 }
1216 AuthenticatorType::SecurityKey => {
1217 tracing::debug!("Performing security key validation");
1218 self.validate_security_key(&assertion).await?
1219 }
1220 };
1221
1222 tracing::info!("Cross-platform verification completed successfully");
1223
1224 Ok(CrossPlatformVerificationResult {
1225 authenticator_type: detected_type,
1226 validation_result: type_specific_result,
1227 aaguid: aaguid.map(|a| a.to_vec()),
1228 })
1229 }
1230
1231 fn detect_authenticator_type(&self, aaguid: Option<&[u8]>) -> Result<AuthenticatorType> {
1233 match aaguid {
1234 Some(guid) if guid == [0u8; 16] => {
1235 Ok(AuthenticatorType::SecurityKey)
1237 }
1238 Some(guid) => {
1239 match guid {
1241 [
1243 0xf8,
1244 0xa0,
1245 0x11,
1246 0xf3,
1247 0x8c,
1248 0x0a,
1249 0x4d,
1250 0x15,
1251 0x80,
1252 0x06,
1253 0x17,
1254 0x11,
1255 0x1f,
1256 0x9e,
1257 0xdc,
1258 0x7d,
1259 ] => Ok(AuthenticatorType::SecurityKey),
1260 [
1262 0x08,
1263 0x98,
1264 0x7d,
1265 0x78,
1266 0x23,
1267 0x88,
1268 0x4d,
1269 0xa9,
1270 0xa6,
1271 0x91,
1272 0xb6,
1273 0xe1,
1274 0x04,
1275 0x5e,
1276 0xd4,
1277 0xd4,
1278 ] => Ok(AuthenticatorType::Platform),
1279 [
1281 0x08,
1282 0x98,
1283 0x7d,
1284 0x78,
1285 0x4e,
1286 0xd4,
1287 0x4d,
1288 0x49,
1289 0xa6,
1290 0x91,
1291 0xb6,
1292 0xe1,
1293 0x04,
1294 0x5e,
1295 0xd4,
1296 0xd4,
1297 ] => Ok(AuthenticatorType::Platform),
1298 _ => {
1299 Ok(AuthenticatorType::CrossPlatform)
1301 }
1302 }
1303 }
1304 None => {
1305 Ok(AuthenticatorType::SecurityKey)
1307 }
1308 }
1309 }
1310
1311 async fn validate_platform_authenticator(
1313 &self,
1314 assertion: &serde_json::Value,
1315 ) -> Result<TypeSpecificValidationResult> {
1316 tracing::debug!("Validating platform authenticator");
1317
1318 let authenticator_data = assertion
1320 .get("response")
1321 .and_then(|r| r.get("authenticatorData"))
1322 .and_then(|a| a.as_str())
1323 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
1324
1325 let auth_data_bytes = URL_SAFE_NO_PAD
1326 .decode(authenticator_data)
1327 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1328
1329 if auth_data_bytes.len() < 33 {
1330 return Err(AuthError::validation("AuthenticatorData too short"));
1331 }
1332
1333 let flags = auth_data_bytes[32];
1334 let user_verified = (flags & 0x04) != 0;
1335
1336 if !user_verified && self.config.user_verification == "required" {
1337 return Err(AuthError::validation(
1338 "User verification required for platform authenticator",
1339 ));
1340 }
1341
1342 Ok(TypeSpecificValidationResult {
1343 user_verified,
1344 attestation_valid: true,
1345 additional_properties: vec![
1346 ("authenticator_class".to_string(), "platform".to_string()),
1347 ("biometric_capable".to_string(), "true".to_string()),
1348 ],
1349 })
1350 }
1351
1352 async fn validate_cross_platform_authenticator(
1354 &self,
1355 _assertion: &serde_json::Value,
1356 ) -> Result<TypeSpecificValidationResult> {
1357 tracing::debug!("Validating cross-platform authenticator");
1358
1359 Ok(TypeSpecificValidationResult {
1361 user_verified: true,
1362 attestation_valid: true,
1363 additional_properties: vec![
1364 (
1365 "authenticator_class".to_string(),
1366 "cross_platform".to_string(),
1367 ),
1368 ("roaming_capable".to_string(), "true".to_string()),
1369 ],
1370 })
1371 }
1372
1373 async fn validate_security_key(
1375 &self,
1376 assertion: &serde_json::Value,
1377 ) -> Result<TypeSpecificValidationResult> {
1378 tracing::debug!("Validating security key");
1379
1380 let authenticator_data = assertion
1382 .get("response")
1383 .and_then(|r| r.get("authenticatorData"))
1384 .and_then(|a| a.as_str())
1385 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
1386
1387 let auth_data_bytes = URL_SAFE_NO_PAD
1388 .decode(authenticator_data)
1389 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1390
1391 if auth_data_bytes.len() < 33 {
1392 return Err(AuthError::validation("AuthenticatorData too short"));
1393 }
1394
1395 let flags = auth_data_bytes[32];
1396 let user_present = (flags & 0x01) != 0;
1397 let user_verified = (flags & 0x04) != 0;
1398
1399 if !user_present {
1400 return Err(AuthError::validation(
1401 "User presence required for security key",
1402 ));
1403 }
1404
1405 Ok(TypeSpecificValidationResult {
1406 user_verified,
1407 attestation_valid: true,
1408 additional_properties: vec![
1409 (
1410 "authenticator_class".to_string(),
1411 "security_key".to_string(),
1412 ),
1413 ("user_presence".to_string(), user_present.to_string()),
1414 ("hardware_backed".to_string(), "true".to_string()),
1415 ],
1416 })
1417 }
1418
1419 fn verify_assertion_signature(
1422 &self,
1423 assertion_response: &str,
1424 auth_data_bytes: &[u8],
1425 decoded_client_data: &[u8],
1426 public_key_jwk: &serde_json::Value,
1427 ) -> Result<()> {
1428 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1429 use ring::digest;
1430
1431 tracing::debug!("Verifying assertion signature");
1433
1434 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1436 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
1437
1438 let signature = assertion
1440 .get("response")
1441 .and_then(|r| r.get("signature"))
1442 .and_then(|s| s.as_str())
1443 .ok_or_else(|| AuthError::validation("Missing signature in assertion response"))?;
1444
1445 let signature_bytes = URL_SAFE_NO_PAD
1446 .decode(signature)
1447 .map_err(|_| AuthError::validation("Invalid signature base64"))?;
1448
1449 let client_data_hash = {
1451 let mut context = digest::Context::new(&digest::SHA256);
1452 context.update(decoded_client_data);
1453 context.finish()
1454 };
1455
1456 let mut signed_data = Vec::new();
1457 signed_data.extend_from_slice(auth_data_bytes);
1458 signed_data.extend_from_slice(client_data_hash.as_ref());
1459
1460 self.verify_webauthn_signature(&signed_data, &signature_bytes, public_key_jwk)?;
1462
1463 Ok(())
1464 }
1465
1466 fn extract_counter_from_assertion(&self, assertion_response: &str) -> Result<u32> {
1468 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1469
1470 tracing::debug!("Extracting counter from assertion response");
1472
1473 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1475 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
1476
1477 let authenticator_data = assertion
1478 .get("response")
1479 .and_then(|r| r.get("authenticatorData"))
1480 .and_then(|a| a.as_str())
1481 .ok_or_else(|| {
1482 AuthError::validation("Missing authenticatorData in assertion response")
1483 })?;
1484
1485 let auth_data_bytes = URL_SAFE_NO_PAD
1487 .decode(authenticator_data)
1488 .map_err(|_| AuthError::validation("authenticatorData is not valid base64url"))?;
1489
1490 if auth_data_bytes.len() < 37 {
1493 return Err(AuthError::validation(
1494 "authenticatorData is too short to contain a counter",
1495 ));
1496 }
1497
1498 let counter_bytes: [u8; 4] = [
1500 auth_data_bytes[33],
1501 auth_data_bytes[34],
1502 auth_data_bytes[35],
1503 auth_data_bytes[36],
1504 ];
1505
1506 let counter = u32::from_be_bytes(counter_bytes);
1507 tracing::debug!("Extracted signature counter: {}", counter);
1508 Ok(counter)
1509 }
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct AdvancedVerificationResult {
1515 pub user_present: bool,
1516 pub user_verified: bool,
1517 pub new_counter: u32,
1518 pub signature_valid: bool,
1519 pub attestation_valid: bool,
1520}
1521
1522#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1524pub enum AuthenticatorType {
1525 Platform,
1527 CrossPlatform,
1529 SecurityKey,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1535pub struct CrossPlatformVerificationResult {
1536 pub authenticator_type: AuthenticatorType,
1537 pub validation_result: TypeSpecificValidationResult,
1538 pub aaguid: Option<Vec<u8>>,
1539}
1540
1541#[derive(Debug, Clone, Serialize, Deserialize)]
1543pub struct TypeSpecificValidationResult {
1544 pub user_verified: bool,
1545 pub attestation_valid: bool,
1546 pub additional_properties: Vec<(String, String)>,
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551 use super::*;
1552 use crate::tokens::TokenManager;
1553
1554 #[tokio::test]
1555 async fn test_passkey_config_validation() {
1556 let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
1557
1558 let config = PasskeyConfig {
1559 rp_id: "example.com".to_string(),
1560 rp_name: "Test App".to_string(),
1561 origin: "https://example.com".to_string(),
1562 timeout_ms: 60000,
1563 user_verification: "preferred".to_string(),
1564 authenticator_attachment: None,
1565 require_resident_key: false,
1566 ..Default::default()
1567 };
1568
1569 let result = PasskeyAuthMethod::new(config, token_manager);
1570
1571 #[cfg(feature = "passkeys")]
1572 {
1573 assert!(result.is_ok());
1574 let method = result.unwrap();
1575 assert!(method.validate_config().is_ok());
1576 }
1577
1578 #[cfg(not(feature = "passkeys"))]
1579 {
1580 assert!(result.is_err());
1581 }
1582 }
1583
1584 #[tokio::test]
1585 async fn test_invalid_passkey_config() {
1586 #[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
1587 let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
1588
1589 #[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
1590 let config = PasskeyConfig {
1591 rp_id: "".to_string(), rp_name: "Test App".to_string(),
1593 origin: "https://example.com".to_string(),
1594 timeout_ms: 60000,
1595 user_verification: "invalid".to_string(), authenticator_attachment: None,
1597 require_resident_key: false,
1598 ..Default::default()
1599 };
1600
1601 #[cfg(feature = "passkeys")]
1602 {
1603 let result = PasskeyAuthMethod::new(config, token_manager);
1604 if let Ok(method) = result {
1605 assert!(method.validate_config().is_err());
1606 }
1607 }
1608 }
1609}