use serde::{Deserialize, Serialize};
use crate::client_data::{ClientData, ClientDataType};
use crate::types::*;
use crate::Passki;
#[derive(Serialize, Debug)]
pub struct RegistrationChallenge {
pub rp: RelyingParty,
pub user: UserInfo,
pub challenge: String,
#[serde(rename = "pubKeyCredParams")]
pub pub_key_cred_params: Vec<PubKeyCredParam>,
pub timeout: u64,
pub attestation: AttestationConveyancePreference,
#[serde(rename = "authenticatorSelection")]
pub authenticator_selection: AuthenticatorSelection,
#[serde(rename = "excludeCredentials")]
pub exclude_credentials: Vec<ExcludeCredential>,
}
#[derive(Clone, Debug)]
pub struct RegistrationState {
pub challenge: Vec<u8>,
pub user: UserInfo,
}
#[derive(Deserialize)]
pub struct RegistrationCredential {
pub credential_id: String,
pub public_key: String,
pub client_data_json: String,
}
impl Passki {
#[allow(clippy::too_many_arguments)]
pub fn start_passkey_registration(
&self,
user_id: &[u8],
username: &str,
display_name: &str,
timeout: u64,
attestation: AttestationConveyancePreference,
resident_key: ResidentKeyRequirement,
user_verification: UserVerificationRequirement,
existing_credentials: Option<&[StoredPasskey]>,
) -> Result<(RegistrationChallenge, RegistrationState)> {
if user_id.len() < 16 {
return Err(Box::new(PasskiError::new(
"user_id must be at least 16 bytes",
)));
}
let challenge = Self::generate_challenge();
let user_id_bytes = user_id.to_vec();
let exclude_credentials = existing_credentials
.unwrap_or(&[])
.iter()
.map(|pk| ExcludeCredential {
id: Self::base64_encode(&pk.credential_id),
type_: "public-key".to_string(),
})
.collect();
let user = UserInfo {
id: Self::base64_encode(&user_id_bytes),
name: username.to_string(),
display_name: display_name.to_string(),
};
let challenge_response = RegistrationChallenge {
rp: RelyingParty {
name: self.rp_name.clone(),
id: self.rp_id.clone(),
},
user: user.clone(),
challenge: Self::base64_encode(&challenge),
pub_key_cred_params: vec![
PubKeyCredParam {
alg: -8,
type_: "public-key".to_string(),
},
PubKeyCredParam {
alg: -7,
type_: "public-key".to_string(),
},
PubKeyCredParam {
alg: -35,
type_: "public-key".to_string(),
},
PubKeyCredParam {
alg: -257,
type_: "public-key".to_string(),
},
PubKeyCredParam {
alg: -258,
type_: "public-key".to_string(),
},
],
timeout,
attestation,
authenticator_selection: AuthenticatorSelection {
resident_key,
user_verification,
},
exclude_credentials,
};
let state = RegistrationState {
challenge: challenge.clone(),
user,
};
Ok((challenge_response, state))
}
pub fn finish_passkey_registration(
&self,
credential: &RegistrationCredential,
state: &RegistrationState,
) -> Result<StoredPasskey> {
let client_data = ClientData::from_base64(&credential.client_data_json)?;
client_data.verify(ClientDataType::Create, &state.challenge, &self.rp_origin)?;
let attestation_bytes = Self::base64_decode(&credential.public_key)?;
let (public_key_bytes, algorithm) = Self::parse_attestation_object(&attestation_bytes)?;
Ok(StoredPasskey {
credential_id: Self::base64_decode(&credential.credential_id)?,
public_key: public_key_bytes,
counter: 0,
algorithm,
})
}
pub(crate) fn parse_attestation_object(attestation_bytes: &[u8]) -> Result<(Vec<u8>, i32)> {
let attestation: ciborium::Value = ciborium::from_reader(attestation_bytes)
.map_err(|e| PasskiError::new(format!("Failed to parse attestation object: {}", e)))?;
let auth_data_bytes = attestation
.as_map()
.and_then(|m| m.iter().find(|(k, _)| k.as_text() == Some("authData")))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing authData in attestation"))?;
if auth_data_bytes.len() < 37 {
return Err(Box::new(PasskiError::new(
"Invalid authenticator data length",
)));
}
let flags = auth_data_bytes[32];
if (flags & 0x40) == 0 {
return Err(Box::new(PasskiError::new(
"No attested credential data present",
)));
}
if auth_data_bytes.len() < 55 {
return Err(Box::new(PasskiError::new("Authenticator data too short")));
}
let cred_id_len = u16::from_be_bytes([auth_data_bytes[53], auth_data_bytes[54]]) as usize;
let cose_key_offset = 55 + cred_id_len;
if auth_data_bytes.len() < cose_key_offset {
return Err(Box::new(PasskiError::new(
"Authenticator data too short for credential",
)));
}
let cose_key_bytes = &auth_data_bytes[cose_key_offset..];
let cose_key_value: ciborium::Value = ciborium::from_reader(cose_key_bytes)
.map_err(|e| PasskiError::new(format!("Failed to parse COSE key: {}", e)))?;
let algorithm = cose_key_value
.as_map()
.and_then(|m| m.iter().find(|(k, _)| k.as_integer() == Some(3.into())))
.and_then(|(_, v)| v.as_integer())
.and_then(|i| i.try_into().ok())
.ok_or_else(|| PasskiError::new("Missing or invalid algorithm in COSE key"))?;
let public_key_bytes = cose_key_bytes.to_vec();
Ok((public_key_bytes, algorithm))
}
}