use std::collections::HashMap;
use std::time::{Duration, SystemTime};
use ciborium::value::Value;
use ring::rand::{SecureRandom, SystemRandom};
use crate::error::{Result, WebAuthnError};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PublicKey {
ES256 {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
x: Vec<u8>,
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
y: Vec<u8>,
},
ES384 {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
x: Vec<u8>,
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
y: Vec<u8>,
},
EdDSA(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] Vec<u8>),
RS256 {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
n: Vec<u8>,
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
e: Vec<u8>,
},
}
impl PublicKey {
pub fn algorithm(&self) -> i64 {
match self {
PublicKey::ES256 { .. } => crate::algorithm::COSE_ES256,
PublicKey::ES384 { .. } => crate::algorithm::COSE_ES384,
PublicKey::EdDSA(_) => crate::algorithm::COSE_EDDSA,
PublicKey::RS256 { .. } => crate::algorithm::COSE_RS256,
}
}
pub fn key_type(&self) -> &'static str {
match self {
PublicKey::ES256 { .. } => "EC2 P-256",
PublicKey::ES384 { .. } => "EC2 P-384",
PublicKey::EdDSA(_) => "OKP Ed25519",
PublicKey::RS256 { .. } => "RSA 2048",
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Credential {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub id: Vec<u8>,
pub public_key: PublicKey,
pub sign_count: u32,
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub user_id: Vec<u8>,
pub rp_id: String,
pub created_at: SystemTime,
pub backup_eligible: bool,
pub backup_state: bool,
}
#[derive(Debug, Clone)]
pub struct AuthenticatorAttestationResponse {
pub client_data_json: Vec<u8>,
pub attestation_object: Vec<u8>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Challenge {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub bytes: Vec<u8>,
pub created_at: SystemTime,
}
impl Challenge {
pub fn new() -> Result<Self> {
let rng = SystemRandom::new();
let mut bytes = vec![0u8; 32];
rng.fill(&mut bytes).map_err(|_| {
WebAuthnError::InvalidClientData(
"system random number generator failed to produce bytes".to_string(),
)
})?;
Ok(Self {
bytes,
created_at: SystemTime::now(),
})
}
pub fn is_expired(&self, ttl_secs: u64) -> bool {
self.created_at
.elapsed()
.map(|age| age >= Duration::from_secs(ttl_secs))
.unwrap_or(true)
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegistrationResult {
pub credential: Credential,
pub attestation_type: AttestationType,
pub backup_eligible: bool,
pub backup_state: bool,
#[cfg_attr(feature = "serde", serde(skip))]
pub extensions: Option<HashMap<String, Value>>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AuthenticationResult {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub credential_id: Vec<u8>,
pub new_sign_count: u32,
pub user_present: bool,
pub user_verified: bool,
pub backup_eligible: bool,
pub backup_state: bool,
#[cfg_attr(feature = "serde", serde(skip))]
pub extensions: Option<HashMap<String, Value>>,
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AttestationType {
None,
SelfAttestation,
Basic,
BasicVerified,
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use std::time::{Duration, SystemTime};
use super::*;
fn epoch_plus(secs: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(secs)
}
#[test]
fn challenge_round_trips() {
let c = Challenge {
bytes: vec![0xDE, 0xAD, 0xBE, 0xEF],
created_at: epoch_plus(1_700_000_000),
};
let json = serde_json::to_string(&c).expect("test setup");
let back: Challenge = serde_json::from_str(&json).expect("test setup");
assert_eq!(back.bytes, c.bytes);
assert_eq!(back.created_at, c.created_at);
}
#[test]
fn public_key_es256_round_trips() {
let key = PublicKey::ES256 {
x: vec![0x01u8; 32],
y: vec![0x02u8; 32],
};
let json = serde_json::to_string(&key).expect("test setup");
let back: PublicKey = serde_json::from_str(&json).expect("test setup");
match back {
PublicKey::ES256 { x, y } => {
assert_eq!(x, vec![0x01u8; 32]);
assert_eq!(y, vec![0x02u8; 32]);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn public_key_eddsa_round_trips() {
let key = PublicKey::EdDSA(vec![0x03u8; 32]);
let json = serde_json::to_string(&key).expect("test setup");
let back: PublicKey = serde_json::from_str(&json).expect("test setup");
match back {
PublicKey::EdDSA(bytes) => assert_eq!(bytes, vec![0x03u8; 32]),
_ => panic!("wrong variant"),
}
}
#[test]
fn public_key_rs256_round_trips() {
let key = PublicKey::RS256 {
n: vec![0x04u8; 256],
e: vec![0x01, 0x00, 0x01],
};
let json = serde_json::to_string(&key).expect("test setup");
let back: PublicKey = serde_json::from_str(&json).expect("test setup");
match back {
PublicKey::RS256 { n, e } => {
assert_eq!(n, vec![0x04u8; 256]);
assert_eq!(e, vec![0x01, 0x00, 0x01]);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn credential_round_trips() {
let cred = Credential {
id: vec![0xAAu8; 16],
public_key: PublicKey::ES256 {
x: vec![0x01u8; 32],
y: vec![0x02u8; 32],
},
sign_count: 42,
user_id: vec![0xBBu8; 8],
rp_id: "example.com".to_string(),
created_at: epoch_plus(1_700_000_000),
backup_eligible: true,
backup_state: true,
};
let json = serde_json::to_string(&cred).expect("test setup");
let back: Credential = serde_json::from_str(&json).expect("test setup");
assert_eq!(back.id, cred.id);
assert_eq!(back.sign_count, cred.sign_count);
assert_eq!(back.user_id, cred.user_id);
assert_eq!(back.rp_id, cred.rp_id);
assert_eq!(back.created_at, cred.created_at);
assert_eq!(back.backup_eligible, cred.backup_eligible);
assert_eq!(back.backup_state, cred.backup_state);
}
}