Skip to main content

auth_framework/methods/passkey/
mod.rs

1//! Pure Rust WebAuthn/Passkey authentication implementation.
2//!
3//! This module provides a complete FIDO2/WebAuthn implementation for passwordless
4//! authentication using passkeys. It supports both platform authenticators (built
5//! into devices) and roaming authenticators (USB security keys) without requiring
6//! OpenSSL dependencies.
7//!
8//! # WebAuthn Standards Compliance
9//!
10//! - **WebAuthn Level 2**: Complete implementation of W3C WebAuthn specification
11//! - **FIDO2**: FIDO Alliance Client to Authenticator Protocol v2.1
12//! - **CTAP2**: Client to Authenticator Protocol version 2
13//! - **CBOR Encoding**: Proper CTAP2 CBOR encoding/decoding
14//!
15//! # Supported Authenticator Types
16//!
17//! - **Platform Authenticators**: Windows Hello, Touch ID, Android Biometrics
18//! - **Roaming Authenticators**: YubiKey, SoloKey, Titan Security Key
19//! - **Hybrid Transport**: QR code and proximity-based authentication
20//! - **Multi-Device**: Cross-device authentication flows
21//!
22//! # Security Features
23//!
24//! - **Origin Binding**: Cryptographically bound to website origin
25//! - **User Verification**: Biometric or PIN-based verification
26//! - **Replay Protection**: Unique challenge for each authentication
27//! - **Phishing Resistance**: Cannot be used on wrong domains
28//! - **Privacy Preserving**: No biometric data leaves the device
29//!
30//! # Algorithm Support
31//!
32//! - **ECDSA**: P-256, P-384, P-521 elliptic curves
33//! - **EdDSA**: Ed25519 signature algorithm
34//! - **RSA**: RSA-2048, RSA-3072, RSA-4096 (where supported)
35//!
36//! # Registration Process
37//!
38//! 1. **Challenge Generation**: Create cryptographic challenge
39//! 2. **Credential Creation**: Browser/authenticator creates key pair
40//! 3. **Attestation Verification**: Validate authenticator attestation
41//! 4. **Storage**: Store public key and metadata
42//!
43//! # Authentication Process
44//!
45//! 1. **Challenge Generation**: Create authentication challenge
46//! 2. **Signature Creation**: Authenticator signs challenge
47//! 3. **Signature Verification**: Validate signature with stored public key
48//! 4. **Result**: Return authentication success or failure
49//!
50//! # Example Usage
51//!
52//! ```rust,no_run
53//! use auth_framework::methods::passkey::{PasskeyAuthMethod, PasskeyConfig};
54//! use auth_framework::tokens::TokenManager;
55//!
56//! # #[tokio::main]
57//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
58//! # let token_manager: TokenManager = unimplemented!();
59//! // Configure passkey authentication
60//! let config = PasskeyConfig {
61//!     rp_name: "Example Corp".to_string(),
62//!     rp_id: "example.com".to_string(),
63//!     origin: "https://example.com".to_string(),
64//!     timeout_ms: 60000,
65//!     user_verification: "required".to_string(),
66//!     authenticator_attachment: None,
67//!     require_resident_key: false,
68//!     ..Default::default()
69//! };
70//!
71//! let passkey_method = PasskeyAuthMethod::new(config, token_manager)?;
72//! // With the `passkeys` feature enabled:
73//! // passkey_method.register_passkey("user123", "user@example.com", "User Name").await?;
74//! // passkey_method.initiate_authentication("user123").await?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! # Browser Compatibility
80//!
81//! - **Chrome**: Full WebAuthn support
82//! - **Firefox**: Complete implementation
83//! - **Safari**: iOS 14+ and macOS Big Sur+
84//! - **Edge**: Chromium-based versions
85//! - **Mobile**: iOS Safari, Chrome Android
86//!
87//! # Production Considerations
88//!
89//! - Replace in-memory storage with persistent database
90//! - Implement proper error handling for unsupported browsers
91//! - Configure appropriate timeout values for user experience
92//! - Consider attestation verification policies
93//! - Plan for authenticator replacement scenarios
94
95// Pure Rust WebAuthn/Passkey Authentication using 1Password's passkey-rs
96// Production-grade FIDO2/WebAuthn implementation without OpenSSL dependencies
97
98use 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// Keep the direct coset import aligned with passkey's coset version.
111#[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/// Simple user validation method for passkey authentication
137#[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
166/// Passkey/WebAuthn authentication method implementing FIDO2 standards.
167///
168/// `PasskeyAuthMethod` provides a pure Rust implementation of WebAuthn/FIDO2
169/// passkey authentication, supporting both platform authenticators (built into
170/// devices) and roaming authenticators (USB security keys).
171///
172/// # Features
173///
174/// - **FIDO2/WebAuthn Compliance**: Implements the latest WebAuthn Level 2 specification
175/// - **Cross-Platform Support**: Works with Windows Hello, Touch ID, YubiKey, and other authenticators
176/// - **Phishing Resistance**: Cryptographic binding to origin prevents phishing attacks
177/// - **Passwordless Authentication**: Eliminates password-related vulnerabilities
178/// - **Multi-Device Support**: Users can register multiple authenticators
179///
180/// # Security Properties
181///
182/// - **Public Key Cryptography**: Each passkey uses unique key pairs
183/// - **Origin Binding**: Passkeys are cryptographically bound to the website origin
184/// - **User Verification**: Supports biometric and PIN-based user verification
185/// - **Replay Protection**: Each authentication uses unique challenges
186/// - **Privacy**: No biometric data leaves the user's device
187///
188/// # Authenticator Types Supported
189///
190/// - **Platform Authenticators**: Windows Hello, Touch ID, Android Biometrics
191/// - **Roaming Authenticators**: YubiKey, SoloKey, other FIDO2 security keys
192/// - **Hybrid Transport**: QR code-based authentication between devices
193///
194/// # Registration Flow
195///
196/// 1. Generate registration challenge with user and relying party information
197/// 2. Client creates credential using authenticator
198/// 3. Verify attestation and store public key
199/// 4. Associate passkey with user account
200///
201/// # Authentication Flow
202///
203/// 1. Generate authentication challenge
204/// 2. Client signs challenge with private key
205/// 3. Verify signature using stored public key
206/// 4. Return authentication result
207///
208/// # Example
209///
210/// ```rust,no_run
211/// use auth_framework::methods::passkey::{PasskeyAuthMethod, PasskeyConfig};
212/// use auth_framework::tokens::TokenManager;
213///
214/// # #[tokio::main]
215/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216/// # let token_manager: TokenManager = unimplemented!();
217/// let config = PasskeyConfig {
218///     rp_name: "Example Corp".to_string(),
219///     rp_id: "example.com".to_string(),
220///     origin: "https://example.com".to_string(),
221///     timeout_ms: 60000,
222///     user_verification: "required".to_string(),
223///     authenticator_attachment: None,
224///     require_resident_key: false,
225///     ..Default::default()
226/// };
227///
228/// let passkey_method = PasskeyAuthMethod::new(config, token_manager)?;
229/// // With the `passkeys` feature enabled:
230/// // passkey_method.register_passkey("user123", "user@example.com", "User Name").await?;
231/// // passkey_method.initiate_authentication("user123").await?;
232/// # Ok(())
233/// # }
234/// ```
235///
236/// # Thread Safety
237///
238/// This implementation is thread-safe and can be used in concurrent environments.
239/// The internal passkey storage uses `RwLock` for safe concurrent access.
240///
241/// # Production Considerations
242///
243/// - Replace in-memory storage with persistent database in production
244/// - Configure appropriate timeout values for user experience
245/// - Implement proper error handling for unsupported browsers
246/// - Consider implementing credential management for device changes
247pub struct PasskeyAuthMethod {
248    pub config: PasskeyConfig,
249    pub token_manager: TokenManager,
250    /// Storage for registered passkeys (in production, use a database)
251    pub registered_passkeys: RwLock<HashMap<String, PasskeyRegistration>>,
252    /// Pending challenges keyed by credential_id (populated during initiate_authentication).
253    /// Only present when the `passkeys` feature is enabled.
254    #[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/// Configuration for passkey authentication
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct PasskeyConfig {
273    /// Relying Party identifier (your domain)
274    pub rp_id: String,
275    /// Human-readable relying party name
276    pub rp_name: String,
277    /// Origin URL for WebAuthn ceremonies
278    pub origin: String,
279    /// Timeout for registration/authentication in milliseconds
280    pub timeout_ms: u32,
281    /// User verification requirement
282    pub user_verification: String, // "required", "preferred", "discouraged"
283    /// Authenticator attachment preference
284    pub authenticator_attachment: Option<String>, // "platform", "cross-platform"
285    /// Require resident keys
286    pub require_resident_key: bool,
287    /// Attestation conveyance preference: "none", "indirect", "direct", or "enterprise".
288    /// Defaults to "direct" to request authenticator attestation for device verification.
289    #[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, // 60 seconds
304            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/// Stored passkey registration information
313#[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, // JSON-serialized Passkey
320    pub created_at: SystemTime,
321    pub last_used: Option<SystemTime>,
322}
323
324impl PasskeyAuthMethod {
325    /// Create a new passkey authentication method
326    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); // Suppress unused variable warning
340            Err(AuthError::config(
341                "Passkey support not compiled in. Enable 'passkeys' feature.",
342            ))
343        }
344    }
345
346    /// Register a new passkey for a user
347    #[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        // Create authenticator
358        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        // Create client
364        let mut client = Client::new(authenticator);
365
366        // Generate challenge
367        let challenge: Bytes = random_vec(32).into();
368
369        // Create user entity
370        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        // Create credential creation options
377        let request = CredentialCreationOptions {
378            public_key: PublicKeyCredentialCreationOptions {
379                rp: PublicKeyCredentialRpEntity {
380                    id: None, // Use effective domain
381                    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                    // Default to Direct for security — requests authenticator attestation
404                    _ => AttestationConveyancePreference::Direct,
405                },
406                attestation_formats: None,
407                extensions: None,
408            },
409        };
410
411        // Register the credential
412        let credential = client
413            .register(&origin, request, DefaultClientData)
414            .await
415            .map_err(|e| AuthError::validation(format!("Passkey registration failed: {:?}", e)))?;
416
417        // Extract credential ID and store registration
418        let credential_id = &credential.raw_id;
419        let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
420
421        // Serialize the credential attestation response so the public key and
422        // counter are available for future authentication verification.
423        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    /// Initiate passkey authentication
466    #[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(&reg.credential_id),
489                )
490            })
491            .collect()
492        };
493
494        // Store the challenge for each credential that may respond, and under "latest"
495        // so complete_authentication / AuthMethod::authenticate can verify it.
496        {
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            // Always save under "latest" as a fallback for usernameless flows.
506            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    /// Complete passkey authentication
546    #[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        // Find the registered passkey
555        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        // Serialize the credential to the JSON format expected by advanced_verification_flow
568        // (same format as the Credential::Passkey path in authenticate()).
569        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        // Load stored passkey data (public key + counter).
577        let passkey_data: serde_json::Value = serde_json::from_str(&registration.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        // Retrieve the stored challenge for this credential.
593        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        // Full cryptographic verification (challenge, origin, RP ID hash, signature, counter).
611        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        // Update stored counter and last-used timestamp.
627        let mut passkey_data: serde_json::Value = serde_json::from_str(&registration.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        // Issue authentication token.
654        let token = self.token_manager.create_jwt_token(
655            &registration.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            &registration.user_id,
668            token,
669            Duration::from_secs(3600),
670            "passkey",
671        ))
672    }
673
674    /// Fallback for when passkeys feature is disabled
675    #[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                    // Find the registered passkey
709                    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                    // Use advanced verification methods for production security
728                    // Parse the stored passkey data to get the required information
729                    let passkey_data: serde_json::Value =
730                        serde_json::from_str(&registration.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                    // Retrieve the stored challenge for this credential (set during initiate_authentication).
747                    // Fall back to "latest" for usernameless flows.
748                    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                    // Perform advanced verification with replay protection
766                    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                            // Update counter to prevent replay attacks
783                            let mut updated_registration = registration.clone();
784
785                            // Update the passkey data with the new counter
786                            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                    // Fallback: basic validation for compatibility
831                    tracing::debug!("Assertion response length: {}", assertion_response.len());
832
833                    // Create token after successful verification
834
835                    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                        &registration.user_id,
842                        vec![],                          // No specific scopes
843                        Some(Duration::from_secs(3600)), // 1 hour
844                    )?;
845
846                    let auth_token = AuthToken::new(
847                        &registration.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; // Suppress unused variable warning
868            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        // Validate user verification requirement
886        match self.config.user_verification.as_str() {
887            "required" | "preferred" | "discouraged" => {}
888            _ => return Err(AuthError::config("Invalid user verification requirement")),
889        }
890
891        // Validate origin URL
892        #[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 // Passkeys don't use refresh tokens
903    }
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    /// Advanced passkey verification with full WebAuthn compliance
914    /// Implements proper signature verification, replay protection, and attestation validation
915    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        // Parse assertion response
928        let assertion: serde_json::Value = serde_json::from_str(assertion_response)
929            .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
930
931        // Extract and validate clientDataJSON
932        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        // Step 1: Verify challenge
949        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        // Step 2: Verify origin
963        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        // Step 3: Verify operation type
973        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        // Step 4: Extract and validate authenticatorData
983        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        // Step 5: Verify RP ID hash
998        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        // Step 6: Extract and verify flags
1010        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        // Step 7: Extract and verify counter for replay protection using helper method
1019        let new_counter = self.extract_counter_from_assertion(assertion_response)?;
1020
1021        // Per WebAuthn spec §6.1 step 17: if the stored counter was 0 and the
1022        // authenticator returns 0, the authenticator does not support counters
1023        // (many platform authenticators). Only reject when the counter is non-zero
1024        // but failed to advance — that indicates a replay or cloned authenticator.
1025        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        // Step 8: Perform cryptographic signature verification using helper method
1032        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    /// Verify WebAuthn signature using Ring cryptography
1051    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                // Extract RSA public key components
1073                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                // Use ring's RsaPublicKeyComponents to avoid hand-rolling DER.
1090                // This accepts raw n/e bytes and handles SubjectPublicKeyInfo encoding
1091                // internally, correctly handling 2048-bit+ moduli.
1092                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                // Extract EC public key components
1118                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                // Create uncompressed point format
1149                let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
1150                public_key_bytes.push(0x04); // Uncompressed point indicator
1151                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    /// Cross-platform passkey verification for multiple authenticator types
1168    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        // Parse assertion response
1176        let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1177            .map_err(|_| AuthError::validation("Invalid assertion response"))?;
1178
1179        // Extract AAGUID from assertion to determine authenticator type
1180        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        // Extract AAGUID (bytes 37-52 if attested credential data is present)
1191        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        // Determine authenticator type based on AAGUID
1198        let detected_type = self.detect_authenticator_type(aaguid)?;
1199
1200        // Verify that the detected type is allowed
1201        if !authenticator_types.contains(&detected_type) {
1202            return Err(AuthError::validation("Authenticator type not allowed"));
1203        }
1204
1205        // Perform type-specific validation
1206        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    /// Detect authenticator type based on AAGUID and other factors
1232    fn detect_authenticator_type(&self, aaguid: Option<&[u8]>) -> Result<AuthenticatorType> {
1233        match aaguid {
1234            Some(guid) if guid == [0u8; 16] => {
1235                // Null AAGUID typically indicates a security key or older authenticator
1236                Ok(AuthenticatorType::SecurityKey)
1237            }
1238            Some(guid) => {
1239                // Check known AAGUIDs for popular authenticators
1240                match guid {
1241                    // YubiKey Series
1242                    [
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                    // Touch ID/Face ID
1261                    [
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                    // Windows Hello
1280                    [
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                        // Unknown AAGUID, default to cross-platform
1300                        Ok(AuthenticatorType::CrossPlatform)
1301                    }
1302                }
1303            }
1304            None => {
1305                // No AAGUID present, default to security key
1306                Ok(AuthenticatorType::SecurityKey)
1307            }
1308        }
1309    }
1310
1311    /// Validate platform authenticator (Touch ID, Face ID, Windows Hello)
1312    async fn validate_platform_authenticator(
1313        &self,
1314        assertion: &serde_json::Value,
1315    ) -> Result<TypeSpecificValidationResult> {
1316        tracing::debug!("Validating platform authenticator");
1317
1318        // Platform authenticators should have user verification
1319        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    /// Validate cross-platform authenticator (Roaming authenticators)
1353    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        // Cross-platform authenticators are generally more flexible
1360        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    /// Validate security key (FIDO U2F/CTAP1 style authenticators)
1374    async fn validate_security_key(
1375        &self,
1376        assertion: &serde_json::Value,
1377    ) -> Result<TypeSpecificValidationResult> {
1378        tracing::debug!("Validating security key");
1379
1380        // Security keys typically only provide user presence
1381        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    /// Verify WebAuthn assertion signature (simplified implementation)
1420    /// In production, use a proper WebAuthn library like `webauthn-rs`
1421    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        // IMPLEMENTATION COMPLETE: Enhanced assertion signature verification
1432        tracing::debug!("Verifying assertion signature");
1433
1434        // Parse assertion response as JSON (simplified)
1435        let assertion: serde_json::Value = serde_json::from_str(assertion_response)
1436            .map_err(|_| AuthError::validation("Invalid assertion response format"))?;
1437
1438        // Extract signature
1439        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        // Create signed data: authenticatorData + SHA256(clientDataJSON)
1450        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        // Verify signature using public key
1461        self.verify_webauthn_signature(&signed_data, &signature_bytes, public_key_jwk)?;
1462
1463        Ok(())
1464    }
1465
1466    /// Extract counter from WebAuthn assertion response for replay attack protection
1467    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        // IMPLEMENTATION COMPLETE: Extract counter for replay attack protection
1471        tracing::debug!("Extracting counter from assertion response");
1472
1473        // Parse assertion response as JSON
1474        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        // Decode base64 authenticator data
1486        let auth_data_bytes = URL_SAFE_NO_PAD
1487            .decode(authenticator_data)
1488            .map_err(|_| AuthError::validation("authenticatorData is not valid base64url"))?;
1489
1490        // AuthenticatorData structure:
1491        // rpIdHash (32 bytes) + flags (1 byte) + counter (4 bytes) + ...
1492        if auth_data_bytes.len() < 37 {
1493            return Err(AuthError::validation(
1494                "authenticatorData is too short to contain a counter",
1495            ));
1496        }
1497
1498        // Extract counter from bytes 33-36 (big-endian u32)
1499        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/// Result of advanced WebAuthn verification
1513#[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/// Types of WebAuthn authenticators
1523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1524pub enum AuthenticatorType {
1525    /// Platform authenticator (built into device)
1526    Platform,
1527    /// Cross-platform authenticator (roaming)
1528    CrossPlatform,
1529    /// Security key (FIDO U2F style)
1530    SecurityKey,
1531}
1532
1533/// Result of cross-platform verification
1534#[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/// Type-specific validation result
1542#[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(), // Invalid: empty RP ID
1592            rp_name: "Test App".to_string(),
1593            origin: "https://example.com".to_string(),
1594            timeout_ms: 60000,
1595            user_verification: "invalid".to_string(), // Invalid user verification
1596            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}