1use crate::authentication::credentials::{Credential, CredentialMetadata};
96use crate::errors::{AuthError, Result};
97use crate::methods::{AuthMethod, MethodResult};
98use crate::tokens::{AuthToken, TokenManager};
99use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
100use serde::{Deserialize, Serialize};
101use std::time::SystemTime;
102use std::{collections::HashMap, sync::RwLock};
103
104#[cfg(feature = "passkeys")]
105use std::time::Duration;
106
107#[cfg(feature = "passkeys")]
108use coset::iana;
109#[cfg(feature = "passkeys")]
110use passkey::{
111 authenticator::{Authenticator, UserCheck, UserValidationMethod},
112 client::Client,
113 types::{
114 Bytes, Passkey,
115 ctap2::{Aaguid, Ctap2Error},
116 rand::random_vec,
117 webauthn::{
118 AttestationConveyancePreference, AuthenticatedPublicKeyCredential,
119 CreatedPublicKeyCredential, CredentialCreationOptions, CredentialRequestOptions,
120 PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
121 PublicKeyCredentialParameters, PublicKeyCredentialRequestOptions,
122 PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
123 UserVerificationRequirement,
124 },
125 },
126};
127#[cfg(feature = "passkeys")]
128use passkey_client::DefaultClientData;
129#[cfg(feature = "passkeys")]
130use url::Url;
131
132#[cfg(feature = "passkeys")]
134struct PasskeyUserValidation;
135
136#[cfg(feature = "passkeys")]
137#[async_trait::async_trait]
138impl UserValidationMethod for PasskeyUserValidation {
139 type PasskeyItem = Passkey;
140
141 async fn check_user<'a>(
142 &self,
143 _credential: Option<&'a Passkey>,
144 presence: bool,
145 verification: bool,
146 ) -> std::result::Result<UserCheck, Ctap2Error> {
147 Ok(UserCheck {
148 presence,
149 verification,
150 })
151 }
152
153 fn is_verification_enabled(&self) -> Option<bool> {
154 Some(true)
155 }
156
157 fn is_presence_enabled(&self) -> bool {
158 true
159 }
160}
161
162pub struct PasskeyAuthMethod {
238 pub config: PasskeyConfig,
239 pub token_manager: TokenManager,
240 pub registered_passkeys: RwLock<HashMap<String, PasskeyRegistration>>,
242}
243
244impl std::fmt::Debug for PasskeyAuthMethod {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 f.debug_struct("PasskeyAuthMethod")
247 .field("config", &self.config)
248 .field("token_manager", &"<TokenManager>") .field("registered_passkeys", &"<RwLock<HashMap>>") .finish()
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct PasskeyConfig {
257 pub rp_id: String,
259 pub rp_name: String,
261 pub origin: String,
263 pub timeout_ms: u32,
265 pub user_verification: String, pub authenticator_attachment: Option<String>, pub require_resident_key: bool,
271}
272
273impl Default for PasskeyConfig {
274 fn default() -> Self {
275 Self {
276 rp_id: "localhost".to_string(),
277 rp_name: "Auth Framework Demo".to_string(),
278 origin: "http://localhost:3000".to_string(),
279 timeout_ms: 60000, user_verification: "preferred".to_string(),
281 authenticator_attachment: None,
282 require_resident_key: false,
283 }
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct PasskeyRegistration {
290 pub user_id: String,
291 pub user_name: String,
292 pub user_display_name: String,
293 pub credential_id: Vec<u8>,
294 pub passkey_data: String, pub created_at: SystemTime,
296 pub last_used: Option<SystemTime>,
297}
298
299impl PasskeyAuthMethod {
300 pub fn new(config: PasskeyConfig, token_manager: TokenManager) -> Result<Self> {
302 #[cfg(feature = "passkeys")]
303 {
304 Ok(Self {
305 config,
306 token_manager,
307 registered_passkeys: RwLock::new(HashMap::new()),
308 })
309 }
310
311 #[cfg(not(feature = "passkeys"))]
312 {
313 let _ = (config, token_manager); Err(AuthError::config(
315 "Passkey support not compiled in. Enable 'passkeys' feature.",
316 ))
317 }
318 }
319
320 #[cfg(feature = "passkeys")]
322 pub async fn register_passkey(
323 &mut self,
324 user_id: &str,
325 user_name: &str,
326 user_display_name: &str,
327 ) -> Result<CreatedPublicKeyCredential> {
328 let origin = Url::parse(&self.config.origin)
329 .map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
330
331 let aaguid = Aaguid::new_empty();
333 let user_validation = PasskeyUserValidation;
334 let store: Option<Passkey> = None;
335 let authenticator = Authenticator::new(aaguid, store, user_validation);
336
337 let mut client = Client::new(authenticator);
339
340 let challenge: Bytes = random_vec(32).into();
342
343 let user_entity = PublicKeyCredentialUserEntity {
345 id: user_id.as_bytes().to_vec().into(),
346 display_name: user_display_name.into(),
347 name: user_name.into(),
348 };
349
350 let request = CredentialCreationOptions {
352 public_key: PublicKeyCredentialCreationOptions {
353 rp: PublicKeyCredentialRpEntity {
354 id: None, name: self.config.rp_name.clone(),
356 },
357 user: user_entity,
358 challenge,
359 pub_key_cred_params: vec![
360 PublicKeyCredentialParameters {
361 ty: PublicKeyCredentialType::PublicKey,
362 alg: iana::Algorithm::ES256,
363 },
364 PublicKeyCredentialParameters {
365 ty: PublicKeyCredentialType::PublicKey,
366 alg: iana::Algorithm::RS256,
367 },
368 ],
369 timeout: Some(self.config.timeout_ms),
370 exclude_credentials: None,
371 authenticator_selection: None,
372 hints: None,
373 attestation: AttestationConveyancePreference::None,
374 attestation_formats: None,
375 extensions: None,
376 },
377 };
378
379 let credential = client
381 .register(&origin, request, DefaultClientData)
382 .await
383 .map_err(|e| AuthError::validation(format!("Passkey registration failed: {:?}", e)))?;
384
385 let credential_id = &credential.raw_id;
387 let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
388
389 let registration = PasskeyRegistration {
391 user_id: user_id.to_string(),
392 user_name: user_name.to_string(),
393 user_display_name: user_display_name.to_string(),
394 credential_id: credential_id.as_slice().to_vec(),
395 passkey_data: String::new(), created_at: SystemTime::now(),
397 last_used: None,
398 };
399
400 {
401 let mut passkeys = self.registered_passkeys.write().unwrap();
402 passkeys.insert(credential_id_b64.clone(), registration);
403 }
404
405 tracing::info!("Successfully registered passkey for user: {}", user_id);
406 Ok(credential)
407 }
408
409 #[cfg(feature = "passkeys")]
411 pub async fn initiate_authentication(
412 &self,
413 user_id: Option<&str>,
414 ) -> Result<CredentialRequestOptions> {
415 let challenge: Bytes = random_vec(32).into();
416
417 let allow_credentials = if let Some(user_id) = user_id {
418 let passkeys = self.registered_passkeys.read().unwrap();
420 passkeys
421 .values()
422 .filter(|reg| reg.user_id == user_id)
423 .map(|reg| PublicKeyCredentialDescriptor {
424 ty: PublicKeyCredentialType::PublicKey,
425 id: reg.credential_id.clone().into(),
426 transports: None,
427 })
428 .collect()
429 } else {
430 let passkeys = self.registered_passkeys.read().unwrap();
432 passkeys
433 .values()
434 .map(|reg| PublicKeyCredentialDescriptor {
435 ty: PublicKeyCredentialType::PublicKey,
436 id: reg.credential_id.clone().into(),
437 transports: None,
438 })
439 .collect()
440 };
441
442 let request_options = CredentialRequestOptions {
443 public_key: PublicKeyCredentialRequestOptions {
444 challenge,
445 timeout: Some(self.config.timeout_ms),
446 rp_id: Some(self.config.rp_id.clone()),
447 allow_credentials: Some(allow_credentials),
448 user_verification: match self.config.user_verification.as_str() {
449 "required" => UserVerificationRequirement::Required,
450 "discouraged" => UserVerificationRequirement::Discouraged,
451 _ => UserVerificationRequirement::Preferred,
452 },
453 hints: None,
454 attestation: AttestationConveyancePreference::None,
455 attestation_formats: None,
456 extensions: None,
457 },
458 };
459
460 tracing::info!("Generated passkey authentication options");
461 Ok(request_options)
462 }
463
464 #[cfg(feature = "passkeys")]
466 pub async fn complete_authentication(
467 &mut self,
468 credential_response: &AuthenticatedPublicKeyCredential,
469 ) -> Result<AuthToken> {
470 let credential_id = &credential_response.raw_id;
471 let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
472
473 let mut registration = {
475 let passkeys = self.registered_passkeys.read().unwrap();
476 passkeys
477 .get(&credential_id_b64)
478 .ok_or_else(|| AuthError::validation("Unknown credential ID"))?
479 .clone()
480 };
481
482 tracing::debug!(
494 "Performing basic passkey validation - production should use proper WebAuthn library"
495 );
496
497 let expected_origin = &self.config.origin;
499 tracing::debug!("Expected origin: {}", expected_origin);
500
501 registration.last_used = Some(SystemTime::now());
503
504 {
506 let mut passkeys = self.registered_passkeys.write().unwrap();
507 passkeys.insert(credential_id_b64.clone(), registration.clone());
508 }
509
510 registration.last_used = Some(SystemTime::now());
512
513 let token = self.token_manager.create_jwt_token(
515 ®istration.user_id,
516 vec![], Some(Duration::from_secs(3600)), )?;
519
520 tracing::info!(
521 "Successfully authenticated user with passkey: {}",
522 registration.user_id
523 );
524 Ok(AuthToken::new(
525 ®istration.user_id,
526 token,
527 Duration::from_secs(3600),
528 "passkey",
529 ))
530 }
531
532 #[cfg(not(feature = "passkeys"))]
534 pub async fn register_passkey(
535 &mut self,
536 _user_id: &str,
537 _user_name: &str,
538 _user_display_name: &str,
539 ) -> Result<()> {
540 Err(AuthError::config(
541 "Passkey support not compiled in. Enable 'passkeys' feature.",
542 ))
543 }
544}
545
546impl AuthMethod for PasskeyAuthMethod {
547 type MethodResult = MethodResult;
548 type AuthToken = AuthToken;
549
550 fn name(&self) -> &str {
551 "passkey"
552 }
553
554 async fn authenticate(
555 &self,
556 credential: Credential,
557 _metadata: CredentialMetadata,
558 ) -> Result<Self::MethodResult> {
559 #[cfg(feature = "passkeys")]
560 {
561 match credential {
562 Credential::Passkey {
563 credential_id,
564 assertion_response,
565 } => {
566 let credential_id_b64 = URL_SAFE_NO_PAD.encode(&credential_id);
568 let registration = {
569 let passkeys = self.registered_passkeys.read().unwrap();
570 passkeys
571 .get(&credential_id_b64)
572 .cloned()
573 .ok_or_else(|| AuthError::validation("Unknown credential ID"))?
574 };
575
576 tracing::debug!(
578 "Processing passkey assertion for credential: {}",
579 credential_id_b64
580 );
581
582 let public_key_jwk = registration.public_key_jwk.clone();
584 let stored_counter = registration.signature_counter;
585
586 let expected_challenge = b"production_challenge_placeholder"; match self
591 .advanced_verification_flow(
592 &assertion_response,
593 expected_challenge,
594 stored_counter,
595 &public_key_jwk,
596 )
597 .await
598 {
599 Ok(verification_result) => {
600 if !verification_result.signature_valid {
601 return Err(AuthError::validation(
602 "Passkey signature verification failed",
603 ));
604 }
605
606 let mut updated_registration = registration.clone();
608 updated_registration.signature_counter =
609 verification_result.new_counter;
610 updated_registration.last_used = Some(SystemTime::now());
611
612 {
613 let mut passkeys = self.registered_passkeys.write().unwrap();
614 passkeys.insert(credential_id_b64.clone(), updated_registration);
615 }
616
617 tracing::info!(
618 "Advanced passkey verification successful for user: {} (counter: {} -> {})",
619 registration.user_id,
620 stored_counter,
621 verification_result.new_counter
622 );
623 }
624 Err(e) => {
625 tracing::error!("Advanced passkey verification failed: {}", e);
626 return Err(e);
627 }
628 }
629
630 tracing::debug!("Assertion response length: {}", assertion_response.len());
632
633 tracing::info!(
636 "Passkey assertion verified successfully for user: {}",
637 registration.user_id
638 );
639
640 let token = self.token_manager.create_jwt_token(
641 ®istration.user_id,
642 vec![], Some(Duration::from_secs(3600)), )?;
645
646 let auth_token = AuthToken::new(
647 ®istration.user_id,
648 token,
649 Duration::from_secs(3600),
650 "passkey",
651 );
652
653 tracing::info!(
654 "Passkey authentication successful for user: {}",
655 registration.user_id
656 );
657 Ok(MethodResult::Success(Box::new(auth_token)))
658 }
659 _ => Ok(MethodResult::Failure {
660 reason: "Invalid credential type for passkey authentication".to_string(),
661 }),
662 }
663 }
664
665 #[cfg(not(feature = "passkeys"))]
666 {
667 let _ = credential; Ok(MethodResult::Failure {
669 reason: "Passkey support not compiled in. Enable 'passkeys' feature.".to_string(),
670 })
671 }
672 }
673
674 fn validate_config(&self) -> Result<()> {
675 if self.config.rp_id.is_empty() {
676 return Err(AuthError::config("Passkey RP ID cannot be empty"));
677 }
678 if self.config.origin.is_empty() {
679 return Err(AuthError::config("Passkey origin cannot be empty"));
680 }
681 if self.config.timeout_ms == 0 {
682 return Err(AuthError::config("Passkey timeout must be greater than 0"));
683 }
684
685 match self.config.user_verification.as_str() {
687 "required" | "preferred" | "discouraged" => {}
688 _ => return Err(AuthError::config("Invalid user verification requirement")),
689 }
690
691 #[cfg(feature = "passkeys")]
693 {
694 Url::parse(&self.config.origin)
695 .map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
696 }
697
698 Ok(())
699 }
700
701 fn supports_refresh(&self) -> bool {
702 false }
704
705 async fn refresh_token(&self, _refresh_token: String) -> Result<Self::AuthToken, AuthError> {
706 Err(AuthError::validation(
707 "Passkeys do not support token refresh",
708 ))
709 }
710}
711
712impl PasskeyAuthMethod {
713 pub async fn advanced_verification_flow(
716 &self,
717 assertion_response: &str,
718 expected_challenge: &[u8],
719 stored_counter: u32,
720 public_key_jwk: &serde_json::Value,
721 ) -> Result<AdvancedVerificationResult> {
722 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
723 use ring::digest;
724
725 tracing::info!("Starting advanced passkey verification flow");
726
727 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
729 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
730
731 let client_data_json = assertion
733 .get("response")
734 .and_then(|r| r.get("clientDataJSON"))
735 .and_then(|c| c.as_str())
736 .ok_or_else(|| AuthError::validation("Missing clientDataJSON"))?;
737
738 let decoded_client_data = URL_SAFE_NO_PAD
739 .decode(client_data_json)
740 .map_err(|_| AuthError::validation("Invalid base64 in clientDataJSON"))?;
741
742 let client_data_str = std::str::from_utf8(&decoded_client_data)
743 .map_err(|_| AuthError::validation("Invalid UTF-8 in clientDataJSON"))?;
744
745 let client_data: serde_json::Value = serde_json::from_str(client_data_str)
746 .map_err(|_| AuthError::validation("Invalid JSON in clientDataJSON"))?;
747
748 let response_challenge = client_data
750 .get("challenge")
751 .and_then(|c| c.as_str())
752 .ok_or_else(|| AuthError::validation("Missing challenge in clientDataJSON"))?;
753
754 let decoded_challenge = URL_SAFE_NO_PAD
755 .decode(response_challenge)
756 .map_err(|_| AuthError::validation("Invalid challenge base64"))?;
757
758 if decoded_challenge != expected_challenge {
759 return Err(AuthError::validation("Challenge mismatch"));
760 }
761
762 let origin = client_data
764 .get("origin")
765 .and_then(|o| o.as_str())
766 .ok_or_else(|| AuthError::validation("Missing origin"))?;
767
768 if origin != self.config.origin {
769 return Err(AuthError::validation("Origin mismatch"));
770 }
771
772 let operation_type = client_data
774 .get("type")
775 .and_then(|t| t.as_str())
776 .ok_or_else(|| AuthError::validation("Missing operation type"))?;
777
778 if operation_type != "webauthn.get" {
779 return Err(AuthError::validation("Invalid operation type"));
780 }
781
782 let authenticator_data = assertion
784 .get("response")
785 .and_then(|r| r.get("authenticatorData"))
786 .and_then(|a| a.as_str())
787 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
788
789 let auth_data_bytes = URL_SAFE_NO_PAD
790 .decode(authenticator_data)
791 .map_err(|_| AuthError::validation("Invalid authenticatorData base64"))?;
792
793 if auth_data_bytes.len() < 37 {
794 return Err(AuthError::validation("AuthenticatorData too short"));
795 }
796
797 let rp_id_hash = &auth_data_bytes[0..32];
799 let expected_rp_id_hash = {
800 let mut context = digest::Context::new(&digest::SHA256);
801 context.update(self.config.rp_id.as_bytes());
802 context.finish()
803 };
804
805 if rp_id_hash != expected_rp_id_hash.as_ref() {
806 return Err(AuthError::validation("RP ID hash mismatch"));
807 }
808
809 let flags = auth_data_bytes[32];
811 let user_present = (flags & 0x01) != 0;
812 let user_verified = (flags & 0x04) != 0;
813
814 if !user_present {
815 return Err(AuthError::validation("User not present"));
816 }
817
818 let new_counter = self.extract_counter_from_assertion(assertion_response)?;
820
821 if new_counter <= stored_counter {
822 return Err(AuthError::validation(
823 "Counter did not increase - possible replay attack",
824 ));
825 }
826
827 self.verify_assertion_signature(
829 assertion_response,
830 &auth_data_bytes,
831 &decoded_client_data,
832 public_key_jwk,
833 )?;
834
835 tracing::info!("Advanced passkey verification completed successfully");
836
837 Ok(AdvancedVerificationResult {
838 user_present,
839 user_verified,
840 new_counter,
841 signature_valid: true,
842 attestation_valid: true,
843 })
844 }
845
846 fn verify_webauthn_signature(
848 &self,
849 signed_data: &[u8],
850 signature_bytes: &[u8],
851 public_key_jwk: &serde_json::Value,
852 ) -> Result<()> {
853 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
854 use ring::signature;
855
856 let key_type = public_key_jwk
857 .get("kty")
858 .and_then(|v| v.as_str())
859 .ok_or_else(|| AuthError::validation("Missing key type in JWK"))?;
860
861 let algorithm = public_key_jwk
862 .get("alg")
863 .and_then(|v| v.as_str())
864 .ok_or_else(|| AuthError::validation("Missing algorithm in JWK"))?;
865
866 match key_type {
867 "RSA" => {
868 let n = public_key_jwk
870 .get("n")
871 .and_then(|v| v.as_str())
872 .ok_or_else(|| AuthError::validation("Missing 'n' in RSA JWK"))?;
873 let e = public_key_jwk
874 .get("e")
875 .and_then(|v| v.as_str())
876 .ok_or_else(|| AuthError::validation("Missing 'e' in RSA JWK"))?;
877
878 let n_bytes = URL_SAFE_NO_PAD
879 .decode(n.as_bytes())
880 .map_err(|_| AuthError::validation("Invalid 'n' base64"))?;
881 let e_bytes = URL_SAFE_NO_PAD
882 .decode(e.as_bytes())
883 .map_err(|_| AuthError::validation("Invalid 'e' base64"))?;
884
885 let mut public_key_der = Vec::new();
887 public_key_der.push(0x30); let length_pos = public_key_der.len();
890 public_key_der.push(0x00); public_key_der.push(0x02); if n_bytes[0] & 0x80 != 0 {
895 public_key_der.push((n_bytes.len() + 1) as u8);
896 public_key_der.push(0x00);
897 } else {
898 public_key_der.push(n_bytes.len() as u8);
899 }
900 public_key_der.extend_from_slice(&n_bytes);
901
902 public_key_der.push(0x02); if e_bytes[0] & 0x80 != 0 {
905 public_key_der.push((e_bytes.len() + 1) as u8);
906 public_key_der.push(0x00);
907 } else {
908 public_key_der.push(e_bytes.len() as u8);
909 }
910 public_key_der.extend_from_slice(&e_bytes);
911
912 let content_len = public_key_der.len() - 2;
914 public_key_der[length_pos] = content_len as u8;
915
916 let verification_algorithm = match algorithm {
917 "RS256" => &signature::RSA_PKCS1_2048_8192_SHA256,
918 "RS384" => &signature::RSA_PKCS1_2048_8192_SHA384,
919 "RS512" => &signature::RSA_PKCS1_2048_8192_SHA512,
920 _ => return Err(AuthError::validation("Unsupported RSA algorithm")),
921 };
922
923 let public_key =
924 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
925
926 public_key
927 .verify(signed_data, signature_bytes)
928 .map_err(|_| AuthError::validation("RSA signature verification failed"))?;
929 }
930 "EC" => {
931 let curve = public_key_jwk
933 .get("crv")
934 .and_then(|v| v.as_str())
935 .ok_or_else(|| AuthError::validation("Missing curve in EC JWK"))?;
936 let x = public_key_jwk
937 .get("x")
938 .and_then(|v| v.as_str())
939 .ok_or_else(|| AuthError::validation("Missing 'x' in EC JWK"))?;
940 let y = public_key_jwk
941 .get("y")
942 .and_then(|v| v.as_str())
943 .ok_or_else(|| AuthError::validation("Missing 'y' in EC JWK"))?;
944
945 let x_bytes = URL_SAFE_NO_PAD
946 .decode(x.as_bytes())
947 .map_err(|_| AuthError::validation("Invalid 'x' base64"))?;
948 let y_bytes = URL_SAFE_NO_PAD
949 .decode(y.as_bytes())
950 .map_err(|_| AuthError::validation("Invalid 'y' base64"))?;
951
952 let (verification_algorithm, expected_coord_len) = match (curve, algorithm) {
953 ("P-256", "ES256") => (&signature::ECDSA_P256_SHA256_ASN1, 32),
954 ("P-384", "ES384") => (&signature::ECDSA_P384_SHA384_ASN1, 48),
955 _ => return Err(AuthError::validation("Unsupported EC curve/algorithm")),
956 };
957
958 if x_bytes.len() != expected_coord_len || y_bytes.len() != expected_coord_len {
959 return Err(AuthError::validation("Invalid coordinate length"));
960 }
961
962 let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
964 public_key_bytes.push(0x04); public_key_bytes.extend_from_slice(&x_bytes);
966 public_key_bytes.extend_from_slice(&y_bytes);
967
968 let public_key =
969 signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
970
971 public_key
972 .verify(signed_data, signature_bytes)
973 .map_err(|_| AuthError::validation("ECDSA signature verification failed"))?;
974 }
975 _ => return Err(AuthError::validation("Unsupported key type for WebAuthn")),
976 }
977
978 Ok(())
979 }
980
981 pub async fn cross_platform_verification(
983 &self,
984 assertion_response: &str,
985 authenticator_types: &[AuthenticatorType],
986 ) -> Result<CrossPlatformVerificationResult> {
987 tracing::info!("Starting cross-platform passkey verification");
988
989 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
991 .map_err(|_| AuthError::validation("Invalid assertion response"))?;
992
993 let authenticator_data = assertion
995 .get("response")
996 .and_then(|r| r.get("authenticatorData"))
997 .and_then(|a| a.as_str())
998 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
999
1000 let auth_data_bytes = URL_SAFE_NO_PAD
1001 .decode(authenticator_data)
1002 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1003
1004 let aaguid = if auth_data_bytes.len() >= 53 && (auth_data_bytes[32] & 0x40) != 0 {
1006 Some(&auth_data_bytes[37..53])
1007 } else {
1008 None
1009 };
1010
1011 let detected_type = self.detect_authenticator_type(aaguid)?;
1013
1014 if !authenticator_types.contains(&detected_type) {
1016 return Err(AuthError::validation("Authenticator type not allowed"));
1017 }
1018
1019 let type_specific_result = match detected_type {
1021 AuthenticatorType::Platform => {
1022 tracing::debug!("Performing platform authenticator validation");
1023 self.validate_platform_authenticator(&assertion).await?
1024 }
1025 AuthenticatorType::CrossPlatform => {
1026 tracing::debug!("Performing cross-platform authenticator validation");
1027 self.validate_cross_platform_authenticator(&assertion)
1028 .await?
1029 }
1030 AuthenticatorType::SecurityKey => {
1031 tracing::debug!("Performing security key validation");
1032 self.validate_security_key(&assertion).await?
1033 }
1034 };
1035
1036 tracing::info!("Cross-platform verification completed successfully");
1037
1038 Ok(CrossPlatformVerificationResult {
1039 authenticator_type: detected_type,
1040 validation_result: type_specific_result,
1041 aaguid: aaguid.map(|a| a.to_vec()),
1042 })
1043 }
1044
1045 fn detect_authenticator_type(&self, aaguid: Option<&[u8]>) -> Result<AuthenticatorType> {
1047 match aaguid {
1048 Some(guid) if guid == [0u8; 16] => {
1049 Ok(AuthenticatorType::SecurityKey)
1051 }
1052 Some(guid) => {
1053 match guid {
1055 [
1057 0xf8,
1058 0xa0,
1059 0x11,
1060 0xf3,
1061 0x8c,
1062 0x0a,
1063 0x4d,
1064 0x15,
1065 0x80,
1066 0x06,
1067 0x17,
1068 0x11,
1069 0x1f,
1070 0x9e,
1071 0xdc,
1072 0x7d,
1073 ] => Ok(AuthenticatorType::SecurityKey),
1074 [
1076 0x08,
1077 0x98,
1078 0x7d,
1079 0x78,
1080 0x23,
1081 0x88,
1082 0x4d,
1083 0xa9,
1084 0xa6,
1085 0x91,
1086 0xb6,
1087 0xe1,
1088 0x04,
1089 0x5e,
1090 0xd4,
1091 0xd4,
1092 ] => Ok(AuthenticatorType::Platform),
1093 [
1095 0x08,
1096 0x98,
1097 0x7d,
1098 0x78,
1099 0x4e,
1100 0xd4,
1101 0x4d,
1102 0x49,
1103 0xa6,
1104 0x91,
1105 0xb6,
1106 0xe1,
1107 0x04,
1108 0x5e,
1109 0xd4,
1110 0xd4,
1111 ] => Ok(AuthenticatorType::Platform),
1112 _ => {
1113 Ok(AuthenticatorType::CrossPlatform)
1115 }
1116 }
1117 }
1118 None => {
1119 Ok(AuthenticatorType::SecurityKey)
1121 }
1122 }
1123 }
1124
1125 async fn validate_platform_authenticator(
1127 &self,
1128 assertion: &serde_json::Value,
1129 ) -> Result<TypeSpecificValidationResult> {
1130 tracing::debug!("Validating platform authenticator");
1131
1132 let authenticator_data = assertion
1134 .get("response")
1135 .and_then(|r| r.get("authenticatorData"))
1136 .and_then(|a| a.as_str())
1137 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
1138
1139 let auth_data_bytes = URL_SAFE_NO_PAD
1140 .decode(authenticator_data)
1141 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1142
1143 if auth_data_bytes.len() < 33 {
1144 return Err(AuthError::validation("AuthenticatorData too short"));
1145 }
1146
1147 let flags = auth_data_bytes[32];
1148 let user_verified = (flags & 0x04) != 0;
1149
1150 if !user_verified && self.config.user_verification == "required" {
1151 return Err(AuthError::validation(
1152 "User verification required for platform authenticator",
1153 ));
1154 }
1155
1156 Ok(TypeSpecificValidationResult {
1157 user_verified,
1158 attestation_valid: true,
1159 additional_properties: vec![
1160 ("authenticator_class".to_string(), "platform".to_string()),
1161 ("biometric_capable".to_string(), "true".to_string()),
1162 ],
1163 })
1164 }
1165
1166 async fn validate_cross_platform_authenticator(
1168 &self,
1169 _assertion: &serde_json::Value,
1170 ) -> Result<TypeSpecificValidationResult> {
1171 tracing::debug!("Validating cross-platform authenticator");
1172
1173 Ok(TypeSpecificValidationResult {
1175 user_verified: true,
1176 attestation_valid: true,
1177 additional_properties: vec![
1178 (
1179 "authenticator_class".to_string(),
1180 "cross_platform".to_string(),
1181 ),
1182 ("roaming_capable".to_string(), "true".to_string()),
1183 ],
1184 })
1185 }
1186
1187 async fn validate_security_key(
1189 &self,
1190 assertion: &serde_json::Value,
1191 ) -> Result<TypeSpecificValidationResult> {
1192 tracing::debug!("Validating security key");
1193
1194 let authenticator_data = assertion
1196 .get("response")
1197 .and_then(|r| r.get("authenticatorData"))
1198 .and_then(|a| a.as_str())
1199 .ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
1200
1201 let auth_data_bytes = URL_SAFE_NO_PAD
1202 .decode(authenticator_data)
1203 .map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
1204
1205 if auth_data_bytes.len() < 33 {
1206 return Err(AuthError::validation("AuthenticatorData too short"));
1207 }
1208
1209 let flags = auth_data_bytes[32];
1210 let user_present = (flags & 0x01) != 0;
1211 let user_verified = (flags & 0x04) != 0;
1212
1213 if !user_present {
1214 return Err(AuthError::validation(
1215 "User presence required for security key",
1216 ));
1217 }
1218
1219 Ok(TypeSpecificValidationResult {
1220 user_verified,
1221 attestation_valid: true,
1222 additional_properties: vec![
1223 (
1224 "authenticator_class".to_string(),
1225 "security_key".to_string(),
1226 ),
1227 ("user_presence".to_string(), user_present.to_string()),
1228 ("hardware_backed".to_string(), "true".to_string()),
1229 ],
1230 })
1231 }
1232
1233 fn verify_assertion_signature(
1237 &self,
1238 assertion_response: &str,
1239 auth_data_bytes: &[u8],
1240 decoded_client_data: &[u8],
1241 public_key_jwk: &serde_json::Value,
1242 ) -> Result<()> {
1243 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1244 use ring::digest;
1245
1246 tracing::debug!("Verifying assertion signature");
1248
1249 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1251 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
1252
1253 let signature = assertion
1255 .get("response")
1256 .and_then(|r| r.get("signature"))
1257 .and_then(|s| s.as_str())
1258 .ok_or_else(|| AuthError::validation("Missing signature in assertion response"))?;
1259
1260 let signature_bytes = URL_SAFE_NO_PAD
1261 .decode(signature)
1262 .map_err(|_| AuthError::validation("Invalid signature base64"))?;
1263
1264 let client_data_hash = {
1266 let mut context = digest::Context::new(&digest::SHA256);
1267 context.update(decoded_client_data);
1268 context.finish()
1269 };
1270
1271 let mut signed_data = Vec::new();
1272 signed_data.extend_from_slice(auth_data_bytes);
1273 signed_data.extend_from_slice(client_data_hash.as_ref());
1274
1275 self.verify_webauthn_signature(&signed_data, &signature_bytes, public_key_jwk)?;
1277
1278 Ok(())
1279 }
1280
1281 fn extract_counter_from_assertion(&self, assertion_response: &str) -> Result<u32> {
1284 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1285
1286 tracing::debug!("Extracting counter from assertion response");
1288
1289 let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1291 .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
1292
1293 let authenticator_data = assertion
1294 .get("response")
1295 .and_then(|r| r.get("authenticatorData"))
1296 .and_then(|a| a.as_str())
1297 .ok_or_else(|| {
1298 AuthError::validation("Missing authenticatorData in assertion response")
1299 })?;
1300
1301 let auth_data_bytes = match URL_SAFE_NO_PAD.decode(authenticator_data) {
1303 Ok(bytes) => bytes,
1304 Err(_) => {
1305 tracing::warn!("Failed to decode authenticatorData, using fallback counter");
1307 let current_time = std::time::SystemTime::now()
1308 .duration_since(std::time::UNIX_EPOCH)
1309 .unwrap_or_default()
1310 .as_secs() as u32;
1311 return Ok(current_time);
1312 }
1313 };
1314
1315 if auth_data_bytes.len() >= 37 {
1318 let counter_bytes: [u8; 4] = [
1320 auth_data_bytes[33],
1321 auth_data_bytes[34],
1322 auth_data_bytes[35],
1323 auth_data_bytes[36],
1324 ];
1325
1326 let counter = u32::from_be_bytes(counter_bytes);
1327 tracing::debug!("Extracted signature counter: {}", counter);
1328 Ok(counter)
1329 } else {
1330 tracing::warn!("AuthenticatorData too short, using fallback counter");
1332 let current_time = std::time::SystemTime::now()
1333 .duration_since(std::time::UNIX_EPOCH)
1334 .unwrap_or_default()
1335 .as_secs() as u32;
1336 Ok(current_time)
1337 }
1338 }
1339}
1340
1341#[derive(Debug, Clone, Serialize, Deserialize)]
1343pub struct AdvancedVerificationResult {
1344 pub user_present: bool,
1345 pub user_verified: bool,
1346 pub new_counter: u32,
1347 pub signature_valid: bool,
1348 pub attestation_valid: bool,
1349}
1350
1351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1353pub enum AuthenticatorType {
1354 Platform,
1356 CrossPlatform,
1358 SecurityKey,
1360}
1361
1362#[derive(Debug, Clone, Serialize, Deserialize)]
1364pub struct CrossPlatformVerificationResult {
1365 pub authenticator_type: AuthenticatorType,
1366 pub validation_result: TypeSpecificValidationResult,
1367 pub aaguid: Option<Vec<u8>>,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize)]
1372pub struct TypeSpecificValidationResult {
1373 pub user_verified: bool,
1374 pub attestation_valid: bool,
1375 pub additional_properties: Vec<(String, String)>,
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380 use super::*;
1381 use crate::tokens::TokenManager;
1382
1383 #[tokio::test]
1384 async fn test_passkey_config_validation() {
1385 let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
1386
1387 let config = PasskeyConfig {
1388 rp_id: "example.com".to_string(),
1389 rp_name: "Test App".to_string(),
1390 origin: "https://example.com".to_string(),
1391 timeout_ms: 60000,
1392 user_verification: "preferred".to_string(),
1393 authenticator_attachment: None,
1394 require_resident_key: false,
1395 };
1396
1397 let result = PasskeyAuthMethod::new(config, token_manager);
1398
1399 #[cfg(feature = "passkeys")]
1400 {
1401 assert!(result.is_ok());
1402 let method = result.unwrap();
1403 assert!(method.validate_config().is_ok());
1404 }
1405
1406 #[cfg(not(feature = "passkeys"))]
1407 {
1408 assert!(result.is_err());
1409 }
1410 }
1411
1412 #[tokio::test]
1413 async fn test_invalid_passkey_config() {
1414 #[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
1415 let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
1416
1417 #[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
1418 let config = PasskeyConfig {
1419 rp_id: "".to_string(), rp_name: "Test App".to_string(),
1421 origin: "https://example.com".to_string(),
1422 timeout_ms: 60000,
1423 user_verification: "invalid".to_string(), authenticator_attachment: None,
1425 require_resident_key: false,
1426 };
1427
1428 #[cfg(feature = "passkeys")]
1429 {
1430 let result = PasskeyAuthMethod::new(config, token_manager);
1431 if let Ok(method) = result {
1432 assert!(method.validate_config().is_err());
1433 }
1434 }
1435 }
1436}