use webauthn_rs::prelude::{
CreationChallengeResponse, Passkey, PasskeyRegistration, RegisterPublicKeyCredential, Webauthn,
};
use webauthn_rs_proto::PubKeyCredParams;
use crate::error::AppError;
pub const EDDSA_ALG: i64 = -8;
pub fn start_passkey_registration(
webauthn: &Webauthn,
user_unique_id: uuid::Uuid,
user_name: &str,
user_display_name: &str,
exclude_credentials: Option<Vec<webauthn_rs::prelude::CredentialID>>,
) -> Result<(CreationChallengeResponse, PasskeyRegistration), AppError> {
let (mut ccr, reg_state) = webauthn
.start_passkey_registration(
user_unique_id,
user_name,
user_display_name,
exclude_credentials,
)
.map_err(|e| AppError::Internal(format!("webauthn registration start failed: {e}")))?;
extend_ccr_with_eddsa(&mut ccr);
let reg_state = extend_state_with_eddsa(®_state)?;
Ok((ccr, reg_state))
}
pub fn finish_passkey_registration(
webauthn: &Webauthn,
credential: &RegisterPublicKeyCredential,
state: &PasskeyRegistration,
) -> Result<Passkey, AppError> {
webauthn
.finish_passkey_registration(credential, state)
.map_err(|e| AppError::Authentication(format!("passkey registration failed: {e}")))
}
pub(crate) fn extend_ccr_with_eddsa(ccr: &mut CreationChallengeResponse) {
if ccr
.public_key
.pub_key_cred_params
.iter()
.any(|p| p.alg == EDDSA_ALG)
{
return;
}
ccr.public_key.pub_key_cred_params.push(PubKeyCredParams {
type_: "public-key".to_string(),
alg: EDDSA_ALG,
});
}
pub(crate) fn extend_state_with_eddsa(
state: &PasskeyRegistration,
) -> Result<PasskeyRegistration, AppError> {
let mut value = serde_json::to_value(state).map_err(|e| {
AppError::Internal(format!(
"failed to serialise passkey registration state: {e}"
))
})?;
let rs = value
.get_mut("rs")
.and_then(|v| v.as_object_mut())
.ok_or_else(|| {
AppError::Internal("passkey registration state missing 'rs' object".into())
})?;
let algs = rs
.get_mut("credential_algorithms")
.and_then(|v| v.as_array_mut())
.ok_or_else(|| {
AppError::Internal(
"passkey registration state missing 'rs.credential_algorithms'".into(),
)
})?;
let already_present = algs
.iter()
.any(|v| v.as_str().map(|s| s == "EDDSA").unwrap_or(false));
if !already_present {
algs.push(serde_json::json!("EDDSA"));
}
serde_json::from_value(value).map_err(|e| {
AppError::Internal(format!(
"failed to deserialise rewritten passkey registration state: {e}"
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
use vti_common::auth::passkey::build_webauthn;
fn webauthn() -> Webauthn {
build_webauthn("https://vtc.example.com").expect("webauthn builder")
}
#[test]
fn start_advertises_eddsa_alongside_default_algorithms() {
let w = webauthn();
let (ccr, _state) =
start_passkey_registration(&w, Uuid::new_v4(), "did:key:zABC", "did:key:zABC", None)
.unwrap();
assert!(
ccr.public_key
.pub_key_cred_params
.iter()
.any(|p| p.alg == EDDSA_ALG),
"EdDSA missing from pub_key_cred_params: {:?}",
ccr.public_key.pub_key_cred_params
);
assert!(
ccr.public_key.pub_key_cred_params.len() >= 2,
"expected at least default ES256+RS256 plus EdDSA"
);
}
#[test]
fn start_state_credential_algorithms_includes_eddsa() {
let w = webauthn();
let (_ccr, state) =
start_passkey_registration(&w, Uuid::new_v4(), "did:key:zABC", "did:key:zABC", None)
.unwrap();
let json = serde_json::to_value(&state).unwrap();
let algs = json
.get("rs")
.and_then(|rs| rs.get("credential_algorithms"))
.and_then(|v| v.as_array())
.expect("credential_algorithms is an array");
assert!(
algs.iter().any(|v| v.as_str() == Some("EDDSA")),
"credential_algorithms must include EDDSA so the finish-time check accepts Ed25519 credentials: {algs:?}"
);
}
#[test]
fn extend_ccr_is_idempotent_and_additive() {
let w = webauthn();
let (mut ccr, _state) = w
.start_passkey_registration(Uuid::new_v4(), "u", "u", None)
.unwrap();
let before = ccr.public_key.pub_key_cred_params.clone();
assert!(!before.is_empty());
extend_ccr_with_eddsa(&mut ccr);
let eddsa_count = ccr
.public_key
.pub_key_cred_params
.iter()
.filter(|p| p.alg == EDDSA_ALG)
.count();
assert_eq!(eddsa_count, 1);
let after_first = ccr.public_key.pub_key_cred_params.len();
extend_ccr_with_eddsa(&mut ccr);
assert_eq!(ccr.public_key.pub_key_cred_params.len(), after_first);
for p in &before {
assert!(
ccr.public_key
.pub_key_cred_params
.iter()
.any(|q| q.alg == p.alg && q.type_ == p.type_)
);
}
}
#[test]
fn extend_state_round_trips_through_serde() {
let w = webauthn();
let (_ccr, state) = w
.start_passkey_registration(Uuid::new_v4(), "u", "u", None)
.unwrap();
let rewritten = extend_state_with_eddsa(&state).unwrap();
let json = serde_json::to_value(&rewritten).unwrap();
let algs = json["rs"]["credential_algorithms"].as_array().unwrap();
assert!(
algs.iter().any(|v| v.as_str() == Some("EDDSA")),
"EDDSA must be appended: {algs:?}"
);
let original_json = serde_json::to_value(&state).unwrap();
for field in ["policy", "require_resident_key"] {
assert_eq!(
json["rs"][field], original_json["rs"][field],
"field {field} must survive the rewrite",
);
}
}
}