use crate::authentication::credentials::{Credential, CredentialMetadata};
use crate::errors::{AuthError, Result};
use crate::methods::{AuthMethod, MethodResult};
use crate::tokens::{AuthToken, TokenManager};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
use std::{collections::HashMap, sync::RwLock};
#[cfg(feature = "passkeys")]
use std::time::Duration;
#[cfg(feature = "passkeys")]
use coset::iana;
#[cfg(feature = "passkeys")]
use passkey::{
authenticator::{Authenticator, UserCheck, UserValidationMethod},
client::Client,
types::{
Bytes, Passkey,
ctap2::{Aaguid, Ctap2Error},
rand::random_vec,
webauthn::{
AttestationConveyancePreference, AuthenticatedPublicKeyCredential,
CreatedPublicKeyCredential, CredentialCreationOptions, CredentialRequestOptions,
PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
PublicKeyCredentialParameters, PublicKeyCredentialRequestOptions,
PublicKeyCredentialRpEntity, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
UserVerificationRequirement,
},
},
};
#[cfg(feature = "passkeys")]
use passkey_client::DefaultClientData;
#[cfg(feature = "passkeys")]
use url::Url;
#[cfg(feature = "passkeys")]
struct PasskeyUserValidation;
#[cfg(feature = "passkeys")]
#[async_trait::async_trait]
impl UserValidationMethod for PasskeyUserValidation {
type PasskeyItem = Passkey;
async fn check_user<'a>(
&self,
_credential: Option<&'a Passkey>,
presence: bool,
verification: bool,
) -> std::result::Result<UserCheck, Ctap2Error> {
Ok(UserCheck {
presence,
verification,
})
}
fn is_verification_enabled(&self) -> Option<bool> {
Some(true)
}
fn is_presence_enabled(&self) -> bool {
true
}
}
pub struct PasskeyAuthMethod {
pub config: PasskeyConfig,
pub token_manager: TokenManager,
pub registered_passkeys: RwLock<HashMap<String, PasskeyRegistration>>,
}
impl std::fmt::Debug for PasskeyAuthMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasskeyAuthMethod")
.field("config", &self.config)
.field("token_manager", &"<TokenManager>") .field("registered_passkeys", &"<RwLock<HashMap>>") .finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasskeyConfig {
pub rp_id: String,
pub rp_name: String,
pub origin: String,
pub timeout_ms: u32,
pub user_verification: String, pub authenticator_attachment: Option<String>, pub require_resident_key: bool,
}
impl Default for PasskeyConfig {
fn default() -> Self {
Self {
rp_id: "localhost".to_string(),
rp_name: "Auth Framework Demo".to_string(),
origin: "http://localhost:3000".to_string(),
timeout_ms: 60000, user_verification: "preferred".to_string(),
authenticator_attachment: None,
require_resident_key: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasskeyRegistration {
pub user_id: String,
pub user_name: String,
pub user_display_name: String,
pub credential_id: Vec<u8>,
pub passkey_data: String, pub created_at: SystemTime,
pub last_used: Option<SystemTime>,
}
impl PasskeyAuthMethod {
pub fn new(config: PasskeyConfig, token_manager: TokenManager) -> Result<Self> {
#[cfg(feature = "passkeys")]
{
Ok(Self {
config,
token_manager,
registered_passkeys: RwLock::new(HashMap::new()),
})
}
#[cfg(not(feature = "passkeys"))]
{
let _ = (config, token_manager); Err(AuthError::config(
"Passkey support not compiled in. Enable 'passkeys' feature.",
))
}
}
#[cfg(feature = "passkeys")]
pub async fn register_passkey(
&mut self,
user_id: &str,
user_name: &str,
user_display_name: &str,
) -> Result<CreatedPublicKeyCredential> {
let origin = Url::parse(&self.config.origin)
.map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
let aaguid = Aaguid::new_empty();
let user_validation = PasskeyUserValidation;
let store: Option<Passkey> = None;
let authenticator = Authenticator::new(aaguid, store, user_validation);
let mut client = Client::new(authenticator);
let challenge: Bytes = random_vec(32).into();
let user_entity = PublicKeyCredentialUserEntity {
id: user_id.as_bytes().to_vec().into(),
display_name: user_display_name.into(),
name: user_name.into(),
};
let request = CredentialCreationOptions {
public_key: PublicKeyCredentialCreationOptions {
rp: PublicKeyCredentialRpEntity {
id: None, name: self.config.rp_name.clone(),
},
user: user_entity,
challenge,
pub_key_cred_params: vec![
PublicKeyCredentialParameters {
ty: PublicKeyCredentialType::PublicKey,
alg: iana::Algorithm::ES256,
},
PublicKeyCredentialParameters {
ty: PublicKeyCredentialType::PublicKey,
alg: iana::Algorithm::RS256,
},
],
timeout: Some(self.config.timeout_ms),
exclude_credentials: None,
authenticator_selection: None,
hints: None,
attestation: AttestationConveyancePreference::None,
attestation_formats: None,
extensions: None,
},
};
let credential = client
.register(&origin, request, DefaultClientData)
.await
.map_err(|e| AuthError::validation(format!("Passkey registration failed: {:?}", e)))?;
let credential_id = &credential.raw_id;
let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
let registration = PasskeyRegistration {
user_id: user_id.to_string(),
user_name: user_name.to_string(),
user_display_name: user_display_name.to_string(),
credential_id: credential_id.as_slice().to_vec(),
passkey_data: String::new(), created_at: SystemTime::now(),
last_used: None,
};
{
let mut passkeys = self.registered_passkeys.write().unwrap();
passkeys.insert(credential_id_b64.clone(), registration);
}
tracing::info!("Successfully registered passkey for user: {}", user_id);
Ok(credential)
}
#[cfg(feature = "passkeys")]
pub async fn initiate_authentication(
&self,
user_id: Option<&str>,
) -> Result<CredentialRequestOptions> {
let challenge: Bytes = random_vec(32).into();
let allow_credentials = if let Some(user_id) = user_id {
let passkeys = self.registered_passkeys.read().unwrap();
passkeys
.values()
.filter(|reg| reg.user_id == user_id)
.map(|reg| PublicKeyCredentialDescriptor {
ty: PublicKeyCredentialType::PublicKey,
id: reg.credential_id.clone().into(),
transports: None,
})
.collect()
} else {
let passkeys = self.registered_passkeys.read().unwrap();
passkeys
.values()
.map(|reg| PublicKeyCredentialDescriptor {
ty: PublicKeyCredentialType::PublicKey,
id: reg.credential_id.clone().into(),
transports: None,
})
.collect()
};
let request_options = CredentialRequestOptions {
public_key: PublicKeyCredentialRequestOptions {
challenge,
timeout: Some(self.config.timeout_ms),
rp_id: Some(self.config.rp_id.clone()),
allow_credentials: Some(allow_credentials),
user_verification: match self.config.user_verification.as_str() {
"required" => UserVerificationRequirement::Required,
"discouraged" => UserVerificationRequirement::Discouraged,
_ => UserVerificationRequirement::Preferred,
},
hints: None,
attestation: AttestationConveyancePreference::None,
attestation_formats: None,
extensions: None,
},
};
tracing::info!("Generated passkey authentication options");
Ok(request_options)
}
#[cfg(feature = "passkeys")]
pub async fn complete_authentication(
&mut self,
credential_response: &AuthenticatedPublicKeyCredential,
) -> Result<AuthToken> {
let credential_id = &credential_response.raw_id;
let credential_id_b64 = URL_SAFE_NO_PAD.encode(credential_id.as_slice());
let mut registration = {
let passkeys = self.registered_passkeys.read().unwrap();
passkeys
.get(&credential_id_b64)
.ok_or_else(|| AuthError::validation("Unknown credential ID"))?
.clone()
};
tracing::debug!(
"Performing basic passkey validation - production should use proper WebAuthn library"
);
let expected_origin = &self.config.origin;
tracing::debug!("Expected origin: {}", expected_origin);
registration.last_used = Some(SystemTime::now());
{
let mut passkeys = self.registered_passkeys.write().unwrap();
passkeys.insert(credential_id_b64.clone(), registration.clone());
}
registration.last_used = Some(SystemTime::now());
let token = self.token_manager.create_jwt_token(
®istration.user_id,
vec![], Some(Duration::from_secs(3600)), )?;
tracing::info!(
"Successfully authenticated user with passkey: {}",
registration.user_id
);
Ok(AuthToken::new(
®istration.user_id,
token,
Duration::from_secs(3600),
"passkey",
))
}
#[cfg(not(feature = "passkeys"))]
pub async fn register_passkey(
&mut self,
_user_id: &str,
_user_name: &str,
_user_display_name: &str,
) -> Result<()> {
Err(AuthError::config(
"Passkey support not compiled in. Enable 'passkeys' feature.",
))
}
}
impl AuthMethod for PasskeyAuthMethod {
type MethodResult = MethodResult;
type AuthToken = AuthToken;
fn name(&self) -> &str {
"passkey"
}
async fn authenticate(
&self,
credential: Credential,
_metadata: CredentialMetadata,
) -> Result<Self::MethodResult> {
#[cfg(feature = "passkeys")]
{
match credential {
Credential::Passkey {
credential_id,
assertion_response,
} => {
let credential_id_b64 = URL_SAFE_NO_PAD.encode(&credential_id);
let registration = {
let passkeys = self.registered_passkeys.read().unwrap();
passkeys
.get(&credential_id_b64)
.cloned()
.ok_or_else(|| AuthError::validation("Unknown credential ID"))?
};
tracing::debug!(
"Processing passkey assertion for credential: {}",
credential_id_b64
);
let passkey_data: serde_json::Value =
serde_json::from_str(®istration.passkey_data).map_err(|e| {
AuthError::InvalidCredential {
credential_type: "passkey".to_string(),
message: format!("Failed to parse stored passkey data: {}", e),
}
})?;
let public_key_jwk = passkey_data
.get("public_key")
.cloned()
.unwrap_or(serde_json::Value::Null);
let stored_counter = passkey_data
.get("signature_counter")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let expected_challenge = b"production_challenge_placeholder";
match self
.advanced_verification_flow(
&assertion_response,
expected_challenge,
stored_counter,
&public_key_jwk,
)
.await
{
Ok(verification_result) => {
if !verification_result.signature_valid {
return Err(AuthError::validation(
"Passkey signature verification failed",
));
}
let mut updated_registration = registration.clone();
let mut passkey_data: serde_json::Value = serde_json::from_str(
&updated_registration.passkey_data,
)
.map_err(|e| AuthError::InvalidCredential {
credential_type: "passkey".to_string(),
message: format!("Failed to parse stored passkey data: {}", e),
})?;
passkey_data["signature_counter"] = serde_json::Value::Number(
serde_json::Number::from(verification_result.new_counter),
);
updated_registration.passkey_data =
serde_json::to_string(&passkey_data).map_err(|e| {
AuthError::InvalidCredential {
credential_type: "passkey".to_string(),
message: format!(
"Failed to serialize updated passkey data: {}",
e
),
}
})?;
updated_registration.last_used = Some(SystemTime::now());
{
let mut passkeys = self.registered_passkeys.write().unwrap();
passkeys.insert(credential_id_b64.clone(), updated_registration);
}
tracing::info!(
"Advanced passkey verification successful for user: {} (counter: {} -> {})",
registration.user_id,
stored_counter,
verification_result.new_counter
);
}
Err(e) => {
tracing::error!("Advanced passkey verification failed: {}", e);
return Err(e);
}
}
tracing::debug!("Assertion response length: {}", assertion_response.len());
tracing::info!(
"Passkey assertion verified successfully for user: {}",
registration.user_id
);
let token = self.token_manager.create_jwt_token(
®istration.user_id,
vec![], Some(Duration::from_secs(3600)), )?;
let auth_token = AuthToken::new(
®istration.user_id,
token,
Duration::from_secs(3600),
"passkey",
);
tracing::info!(
"Passkey authentication successful for user: {}",
registration.user_id
);
Ok(MethodResult::Success(Box::new(auth_token)))
}
_ => Ok(MethodResult::Failure {
reason: "Invalid credential type for passkey authentication".to_string(),
}),
}
}
#[cfg(not(feature = "passkeys"))]
{
let _ = credential; Ok(MethodResult::Failure {
reason: "Passkey support not compiled in. Enable 'passkeys' feature.".to_string(),
})
}
}
fn validate_config(&self) -> Result<()> {
if self.config.rp_id.is_empty() {
return Err(AuthError::config("Passkey RP ID cannot be empty"));
}
if self.config.origin.is_empty() {
return Err(AuthError::config("Passkey origin cannot be empty"));
}
if self.config.timeout_ms == 0 {
return Err(AuthError::config("Passkey timeout must be greater than 0"));
}
match self.config.user_verification.as_str() {
"required" | "preferred" | "discouraged" => {}
_ => return Err(AuthError::config("Invalid user verification requirement")),
}
#[cfg(feature = "passkeys")]
{
Url::parse(&self.config.origin)
.map_err(|e| AuthError::config(format!("Invalid origin URL: {}", e)))?;
}
Ok(())
}
fn supports_refresh(&self) -> bool {
false }
async fn refresh_token(&self, _refresh_token: String) -> Result<Self::AuthToken, AuthError> {
Err(AuthError::validation(
"Passkeys do not support token refresh",
))
}
}
impl PasskeyAuthMethod {
pub async fn advanced_verification_flow(
&self,
assertion_response: &str,
expected_challenge: &[u8],
stored_counter: u32,
public_key_jwk: &serde_json::Value,
) -> Result<AdvancedVerificationResult> {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ring::digest;
tracing::info!("Starting advanced passkey verification flow");
let assertion: serde_json::Value = serde_json::from_str(assertion_response)
.map_err(|_| AuthError::validation("Invalid assertion response format"))?;
let client_data_json = assertion
.get("response")
.and_then(|r| r.get("clientDataJSON"))
.and_then(|c| c.as_str())
.ok_or_else(|| AuthError::validation("Missing clientDataJSON"))?;
let decoded_client_data = URL_SAFE_NO_PAD
.decode(client_data_json)
.map_err(|_| AuthError::validation("Invalid base64 in clientDataJSON"))?;
let client_data_str = std::str::from_utf8(&decoded_client_data)
.map_err(|_| AuthError::validation("Invalid UTF-8 in clientDataJSON"))?;
let client_data: serde_json::Value = serde_json::from_str(client_data_str)
.map_err(|_| AuthError::validation("Invalid JSON in clientDataJSON"))?;
let response_challenge = client_data
.get("challenge")
.and_then(|c| c.as_str())
.ok_or_else(|| AuthError::validation("Missing challenge in clientDataJSON"))?;
let decoded_challenge = URL_SAFE_NO_PAD
.decode(response_challenge)
.map_err(|_| AuthError::validation("Invalid challenge base64"))?;
if decoded_challenge != expected_challenge {
return Err(AuthError::validation("Challenge mismatch"));
}
let origin = client_data
.get("origin")
.and_then(|o| o.as_str())
.ok_or_else(|| AuthError::validation("Missing origin"))?;
if origin != self.config.origin {
return Err(AuthError::validation("Origin mismatch"));
}
let operation_type = client_data
.get("type")
.and_then(|t| t.as_str())
.ok_or_else(|| AuthError::validation("Missing operation type"))?;
if operation_type != "webauthn.get" {
return Err(AuthError::validation("Invalid operation type"));
}
let authenticator_data = assertion
.get("response")
.and_then(|r| r.get("authenticatorData"))
.and_then(|a| a.as_str())
.ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
let auth_data_bytes = URL_SAFE_NO_PAD
.decode(authenticator_data)
.map_err(|_| AuthError::validation("Invalid authenticatorData base64"))?;
if auth_data_bytes.len() < 37 {
return Err(AuthError::validation("AuthenticatorData too short"));
}
let rp_id_hash = &auth_data_bytes[0..32];
let expected_rp_id_hash = {
let mut context = digest::Context::new(&digest::SHA256);
context.update(self.config.rp_id.as_bytes());
context.finish()
};
if rp_id_hash != expected_rp_id_hash.as_ref() {
return Err(AuthError::validation("RP ID hash mismatch"));
}
let flags = auth_data_bytes[32];
let user_present = (flags & 0x01) != 0;
let user_verified = (flags & 0x04) != 0;
if !user_present {
return Err(AuthError::validation("User not present"));
}
let new_counter = self.extract_counter_from_assertion(assertion_response)?;
if new_counter <= stored_counter {
return Err(AuthError::validation(
"Counter did not increase - possible replay attack",
));
}
self.verify_assertion_signature(
assertion_response,
&auth_data_bytes,
&decoded_client_data,
public_key_jwk,
)?;
tracing::info!("Advanced passkey verification completed successfully");
Ok(AdvancedVerificationResult {
user_present,
user_verified,
new_counter,
signature_valid: true,
attestation_valid: true,
})
}
fn verify_webauthn_signature(
&self,
signed_data: &[u8],
signature_bytes: &[u8],
public_key_jwk: &serde_json::Value,
) -> Result<()> {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ring::signature;
let key_type = public_key_jwk
.get("kty")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing key type in JWK"))?;
let algorithm = public_key_jwk
.get("alg")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing algorithm in JWK"))?;
match key_type {
"RSA" => {
let n = public_key_jwk
.get("n")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing 'n' in RSA JWK"))?;
let e = public_key_jwk
.get("e")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing 'e' in RSA JWK"))?;
let n_bytes = URL_SAFE_NO_PAD
.decode(n.as_bytes())
.map_err(|_| AuthError::validation("Invalid 'n' base64"))?;
let e_bytes = URL_SAFE_NO_PAD
.decode(e.as_bytes())
.map_err(|_| AuthError::validation("Invalid 'e' base64"))?;
let mut public_key_der = Vec::new();
public_key_der.push(0x30);
let length_pos = public_key_der.len();
public_key_der.push(0x00);
public_key_der.push(0x02); if n_bytes[0] & 0x80 != 0 {
public_key_der.push((n_bytes.len() + 1) as u8);
public_key_der.push(0x00);
} else {
public_key_der.push(n_bytes.len() as u8);
}
public_key_der.extend_from_slice(&n_bytes);
public_key_der.push(0x02); if e_bytes[0] & 0x80 != 0 {
public_key_der.push((e_bytes.len() + 1) as u8);
public_key_der.push(0x00);
} else {
public_key_der.push(e_bytes.len() as u8);
}
public_key_der.extend_from_slice(&e_bytes);
let content_len = public_key_der.len() - 2;
public_key_der[length_pos] = content_len as u8;
let verification_algorithm = match algorithm {
"RS256" => &signature::RSA_PKCS1_2048_8192_SHA256,
"RS384" => &signature::RSA_PKCS1_2048_8192_SHA384,
"RS512" => &signature::RSA_PKCS1_2048_8192_SHA512,
_ => return Err(AuthError::validation("Unsupported RSA algorithm")),
};
let public_key =
signature::UnparsedPublicKey::new(verification_algorithm, &public_key_der);
public_key
.verify(signed_data, signature_bytes)
.map_err(|_| AuthError::validation("RSA signature verification failed"))?;
}
"EC" => {
let curve = public_key_jwk
.get("crv")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing curve in EC JWK"))?;
let x = public_key_jwk
.get("x")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing 'x' in EC JWK"))?;
let y = public_key_jwk
.get("y")
.and_then(|v| v.as_str())
.ok_or_else(|| AuthError::validation("Missing 'y' in EC JWK"))?;
let x_bytes = URL_SAFE_NO_PAD
.decode(x.as_bytes())
.map_err(|_| AuthError::validation("Invalid 'x' base64"))?;
let y_bytes = URL_SAFE_NO_PAD
.decode(y.as_bytes())
.map_err(|_| AuthError::validation("Invalid 'y' base64"))?;
let (verification_algorithm, expected_coord_len) = match (curve, algorithm) {
("P-256", "ES256") => (&signature::ECDSA_P256_SHA256_ASN1, 32),
("P-384", "ES384") => (&signature::ECDSA_P384_SHA384_ASN1, 48),
_ => return Err(AuthError::validation("Unsupported EC curve/algorithm")),
};
if x_bytes.len() != expected_coord_len || y_bytes.len() != expected_coord_len {
return Err(AuthError::validation("Invalid coordinate length"));
}
let mut public_key_bytes = Vec::with_capacity(1 + expected_coord_len * 2);
public_key_bytes.push(0x04); public_key_bytes.extend_from_slice(&x_bytes);
public_key_bytes.extend_from_slice(&y_bytes);
let public_key =
signature::UnparsedPublicKey::new(verification_algorithm, &public_key_bytes);
public_key
.verify(signed_data, signature_bytes)
.map_err(|_| AuthError::validation("ECDSA signature verification failed"))?;
}
_ => return Err(AuthError::validation("Unsupported key type for WebAuthn")),
}
Ok(())
}
pub async fn cross_platform_verification(
&self,
assertion_response: &str,
authenticator_types: &[AuthenticatorType],
) -> Result<CrossPlatformVerificationResult> {
tracing::info!("Starting cross-platform passkey verification");
let assertion: serde_json::Value = serde_json::from_str(assertion_response)
.map_err(|_| AuthError::validation("Invalid assertion response"))?;
let authenticator_data = assertion
.get("response")
.and_then(|r| r.get("authenticatorData"))
.and_then(|a| a.as_str())
.ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
let auth_data_bytes = URL_SAFE_NO_PAD
.decode(authenticator_data)
.map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
let aaguid = if auth_data_bytes.len() >= 53 && (auth_data_bytes[32] & 0x40) != 0 {
Some(&auth_data_bytes[37..53])
} else {
None
};
let detected_type = self.detect_authenticator_type(aaguid)?;
if !authenticator_types.contains(&detected_type) {
return Err(AuthError::validation("Authenticator type not allowed"));
}
let type_specific_result = match detected_type {
AuthenticatorType::Platform => {
tracing::debug!("Performing platform authenticator validation");
self.validate_platform_authenticator(&assertion).await?
}
AuthenticatorType::CrossPlatform => {
tracing::debug!("Performing cross-platform authenticator validation");
self.validate_cross_platform_authenticator(&assertion)
.await?
}
AuthenticatorType::SecurityKey => {
tracing::debug!("Performing security key validation");
self.validate_security_key(&assertion).await?
}
};
tracing::info!("Cross-platform verification completed successfully");
Ok(CrossPlatformVerificationResult {
authenticator_type: detected_type,
validation_result: type_specific_result,
aaguid: aaguid.map(|a| a.to_vec()),
})
}
fn detect_authenticator_type(&self, aaguid: Option<&[u8]>) -> Result<AuthenticatorType> {
match aaguid {
Some(guid) if guid == [0u8; 16] => {
Ok(AuthenticatorType::SecurityKey)
}
Some(guid) => {
match guid {
[
0xf8,
0xa0,
0x11,
0xf3,
0x8c,
0x0a,
0x4d,
0x15,
0x80,
0x06,
0x17,
0x11,
0x1f,
0x9e,
0xdc,
0x7d,
] => Ok(AuthenticatorType::SecurityKey),
[
0x08,
0x98,
0x7d,
0x78,
0x23,
0x88,
0x4d,
0xa9,
0xa6,
0x91,
0xb6,
0xe1,
0x04,
0x5e,
0xd4,
0xd4,
] => Ok(AuthenticatorType::Platform),
[
0x08,
0x98,
0x7d,
0x78,
0x4e,
0xd4,
0x4d,
0x49,
0xa6,
0x91,
0xb6,
0xe1,
0x04,
0x5e,
0xd4,
0xd4,
] => Ok(AuthenticatorType::Platform),
_ => {
Ok(AuthenticatorType::CrossPlatform)
}
}
}
None => {
Ok(AuthenticatorType::SecurityKey)
}
}
}
async fn validate_platform_authenticator(
&self,
assertion: &serde_json::Value,
) -> Result<TypeSpecificValidationResult> {
tracing::debug!("Validating platform authenticator");
let authenticator_data = assertion
.get("response")
.and_then(|r| r.get("authenticatorData"))
.and_then(|a| a.as_str())
.ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
let auth_data_bytes = URL_SAFE_NO_PAD
.decode(authenticator_data)
.map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
if auth_data_bytes.len() < 33 {
return Err(AuthError::validation("AuthenticatorData too short"));
}
let flags = auth_data_bytes[32];
let user_verified = (flags & 0x04) != 0;
if !user_verified && self.config.user_verification == "required" {
return Err(AuthError::validation(
"User verification required for platform authenticator",
));
}
Ok(TypeSpecificValidationResult {
user_verified,
attestation_valid: true,
additional_properties: vec![
("authenticator_class".to_string(), "platform".to_string()),
("biometric_capable".to_string(), "true".to_string()),
],
})
}
async fn validate_cross_platform_authenticator(
&self,
_assertion: &serde_json::Value,
) -> Result<TypeSpecificValidationResult> {
tracing::debug!("Validating cross-platform authenticator");
Ok(TypeSpecificValidationResult {
user_verified: true,
attestation_valid: true,
additional_properties: vec![
(
"authenticator_class".to_string(),
"cross_platform".to_string(),
),
("roaming_capable".to_string(), "true".to_string()),
],
})
}
async fn validate_security_key(
&self,
assertion: &serde_json::Value,
) -> Result<TypeSpecificValidationResult> {
tracing::debug!("Validating security key");
let authenticator_data = assertion
.get("response")
.and_then(|r| r.get("authenticatorData"))
.and_then(|a| a.as_str())
.ok_or_else(|| AuthError::validation("Missing authenticatorData"))?;
let auth_data_bytes = URL_SAFE_NO_PAD
.decode(authenticator_data)
.map_err(|_| AuthError::validation("Invalid authenticatorData"))?;
if auth_data_bytes.len() < 33 {
return Err(AuthError::validation("AuthenticatorData too short"));
}
let flags = auth_data_bytes[32];
let user_present = (flags & 0x01) != 0;
let user_verified = (flags & 0x04) != 0;
if !user_present {
return Err(AuthError::validation(
"User presence required for security key",
));
}
Ok(TypeSpecificValidationResult {
user_verified,
attestation_valid: true,
additional_properties: vec![
(
"authenticator_class".to_string(),
"security_key".to_string(),
),
("user_presence".to_string(), user_present.to_string()),
("hardware_backed".to_string(), "true".to_string()),
],
})
}
fn verify_assertion_signature(
&self,
assertion_response: &str,
auth_data_bytes: &[u8],
decoded_client_data: &[u8],
public_key_jwk: &serde_json::Value,
) -> Result<()> {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ring::digest;
tracing::debug!("Verifying assertion signature");
let assertion: serde_json::Value = serde_json::from_str(assertion_response)
.map_err(|_| AuthError::validation("Invalid assertion response format"))?;
let signature = assertion
.get("response")
.and_then(|r| r.get("signature"))
.and_then(|s| s.as_str())
.ok_or_else(|| AuthError::validation("Missing signature in assertion response"))?;
let signature_bytes = URL_SAFE_NO_PAD
.decode(signature)
.map_err(|_| AuthError::validation("Invalid signature base64"))?;
let client_data_hash = {
let mut context = digest::Context::new(&digest::SHA256);
context.update(decoded_client_data);
context.finish()
};
let mut signed_data = Vec::new();
signed_data.extend_from_slice(auth_data_bytes);
signed_data.extend_from_slice(client_data_hash.as_ref());
self.verify_webauthn_signature(&signed_data, &signature_bytes, public_key_jwk)?;
Ok(())
}
fn extract_counter_from_assertion(&self, assertion_response: &str) -> Result<u32> {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
tracing::debug!("Extracting counter from assertion response");
let assertion: serde_json::Value = serde_json::from_str(assertion_response)
.map_err(|_| AuthError::validation("Invalid assertion response format"))?;
let authenticator_data = assertion
.get("response")
.and_then(|r| r.get("authenticatorData"))
.and_then(|a| a.as_str())
.ok_or_else(|| {
AuthError::validation("Missing authenticatorData in assertion response")
})?;
let auth_data_bytes = match URL_SAFE_NO_PAD.decode(authenticator_data) {
Ok(bytes) => bytes,
Err(_) => {
tracing::warn!("Failed to decode authenticatorData, using fallback counter");
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
return Ok(current_time);
}
};
if auth_data_bytes.len() >= 37 {
let counter_bytes: [u8; 4] = [
auth_data_bytes[33],
auth_data_bytes[34],
auth_data_bytes[35],
auth_data_bytes[36],
];
let counter = u32::from_be_bytes(counter_bytes);
tracing::debug!("Extracted signature counter: {}", counter);
Ok(counter)
} else {
tracing::warn!("AuthenticatorData too short, using fallback counter");
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
Ok(current_time)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedVerificationResult {
pub user_present: bool,
pub user_verified: bool,
pub new_counter: u32,
pub signature_valid: bool,
pub attestation_valid: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthenticatorType {
Platform,
CrossPlatform,
SecurityKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossPlatformVerificationResult {
pub authenticator_type: AuthenticatorType,
pub validation_result: TypeSpecificValidationResult,
pub aaguid: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeSpecificValidationResult {
pub user_verified: bool,
pub attestation_valid: bool,
pub additional_properties: Vec<(String, String)>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tokens::TokenManager;
#[tokio::test]
async fn test_passkey_config_validation() {
let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
let config = PasskeyConfig {
rp_id: "example.com".to_string(),
rp_name: "Test App".to_string(),
origin: "https://example.com".to_string(),
timeout_ms: 60000,
user_verification: "preferred".to_string(),
authenticator_attachment: None,
require_resident_key: false,
};
let result = PasskeyAuthMethod::new(config, token_manager);
#[cfg(feature = "passkeys")]
{
assert!(result.is_ok());
let method = result.unwrap();
assert!(method.validate_config().is_ok());
}
#[cfg(not(feature = "passkeys"))]
{
assert!(result.is_err());
}
}
#[tokio::test]
async fn test_invalid_passkey_config() {
#[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
let token_manager = TokenManager::new_hmac(b"test-secret", "test-issuer", "test-audience");
#[cfg_attr(not(feature = "passkeys"), allow(unused_variables))]
let config = PasskeyConfig {
rp_id: "".to_string(), rp_name: "Test App".to_string(),
origin: "https://example.com".to_string(),
timeout_ms: 60000,
user_verification: "invalid".to_string(), authenticator_attachment: None,
require_resident_key: false,
};
#[cfg(feature = "passkeys")]
{
let result = PasskeyAuthMethod::new(config, token_manager);
if let Ok(method) = result {
assert!(method.validate_config().is_err());
}
}
}
}