use aws_lc_rs::digest::{self, SHA256};
use serde::{Deserialize, Serialize};
use crate::Passki;
use crate::client_data::{ClientData, ClientDataType};
use crate::types::*;
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<RegistrationExtensions>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RegistrationState {
pub challenge: Vec<u8>,
pub user: UserInfo,
pub user_verification: UserVerificationRequirement,
}
#[derive(Deserialize)]
pub struct RegistrationCredential {
pub credential_id: String,
pub public_key: String,
pub client_data_json: String,
pub client_extension_results: Option<ClientExtensionResults>,
}
#[derive(Debug)]
pub(crate) struct ParsedAttestation {
pub credential_id: Vec<u8>,
pub public_key: Vec<u8>,
pub algorithm: i32,
pub flags: u8,
pub counter: u32,
pub aaguid: [u8; 16],
}
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]>,
extensions: Option<RegistrationExtensions>,
) -> 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: [ALG_EDDSA, ALG_ES256, ALG_ES384, ALG_RS256, ALG_RS384]
.into_iter()
.map(|alg| PubKeyCredParam {
alg,
type_: "public-key".to_string(),
})
.collect(),
timeout,
attestation,
authenticator_selection: AuthenticatorSelection {
resident_key,
user_verification,
},
exclude_credentials,
extensions,
};
let state = RegistrationState {
challenge: challenge.clone(),
user,
user_verification,
};
Ok((challenge_response, state))
}
pub fn finish_passkey_registration(
&self,
credential: &RegistrationCredential,
state: &RegistrationState,
) -> Result<StoredPasskey> {
let client_data_bytes = Self::base64_decode(&credential.client_data_json)?;
let client_data = ClientData::from_bytes(&client_data_bytes)?;
client_data.verify(ClientDataType::Create, &state.challenge, &self.rp_origin)?;
let client_data_hash = digest::digest(&SHA256, &client_data_bytes);
let attestation_bytes = Self::base64_decode(&credential.public_key)?;
let parsed = self.verify_attestation(&attestation_bytes, client_data_hash.as_ref())?;
if (parsed.flags & FLAG_UP) == 0 {
return Err(Box::new(PasskiError::new(
"User not present (UP flag not set)",
)));
}
if state.user_verification == UserVerificationRequirement::Required
&& (parsed.flags & FLAG_UV) == 0
{
return Err(Box::new(PasskiError::new(
"User verification required but UV flag not set",
)));
}
let credential_id = Self::base64_decode(&credential.credential_id)?;
if credential_id != parsed.credential_id {
return Err(Box::new(PasskiError::new(
"Credential ID mismatch between client and attested credential data",
)));
}
let rk = credential
.client_extension_results
.as_ref()
.and_then(|ext| ext.cred_props.as_ref())
.and_then(|cp| cp.rk);
Ok(StoredPasskey {
credential_id: parsed.credential_id,
public_key: parsed.public_key,
counter: parsed.counter,
algorithm: parsed.algorithm,
rk,
})
}
pub(crate) fn split_attestation_object(
attestation_bytes: &[u8],
) -> Result<(Option<String>, Vec<u8>, ciborium::Value)> {
let attestation: ciborium::Value = ciborium::from_reader(attestation_bytes)
.map_err(|e| PasskiError::new(format!("Failed to parse attestation object: {}", e)))?;
let map = attestation
.as_map()
.ok_or_else(|| PasskiError::new("Attestation object is not a map"))?;
let auth_data = map
.iter()
.find(|(k, _)| k.as_text() == Some("authData"))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing authData in attestation"))?
.to_vec();
let fmt = map
.iter()
.find(|(k, _)| k.as_text() == Some("fmt"))
.and_then(|(_, v)| v.as_text())
.map(str::to_string);
let att_stmt = map
.iter()
.find(|(k, _)| k.as_text() == Some("attStmt"))
.map(|(_, v)| v.clone())
.unwrap_or_else(|| ciborium::Value::Map(Vec::new()));
Ok((fmt, auth_data, att_stmt))
}
#[cfg(test)]
pub(crate) fn parse_attestation_object(
&self,
attestation_bytes: &[u8],
) -> Result<ParsedAttestation> {
let (_, auth_data, _) = Self::split_attestation_object(attestation_bytes)?;
self.parse_auth_data(&auth_data)
}
pub(crate) fn parse_auth_data(&self, auth_data_bytes: &[u8]) -> Result<ParsedAttestation> {
if auth_data_bytes.len() < 37 {
return Err(Box::new(PasskiError::new(
"Invalid authenticator data length",
)));
}
let rp_id_hash = digest::digest(&SHA256, self.rp_id.as_bytes());
if &auth_data_bytes[..32] != rp_id_hash.as_ref() {
return Err(Box::new(PasskiError::new("rpId hash mismatch")));
}
let flags = auth_data_bytes[32];
let counter = u32::from_be_bytes([
auth_data_bytes[33],
auth_data_bytes[34],
auth_data_bytes[35],
auth_data_bytes[36],
]);
if (flags & FLAG_AT) == 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 mut aaguid = [0u8; 16];
aaguid.copy_from_slice(&auth_data_bytes[37..53]);
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 credential_id = auth_data_bytes[55..cose_key_offset].to_vec();
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 mut public_key = Vec::new();
ciborium::into_writer(&cose_key_value, &mut public_key)
.map_err(|e| PasskiError::new(format!("Failed to serialize COSE key: {}", e)))?;
Ok(ParsedAttestation {
credential_id,
public_key,
algorithm,
flags,
counter,
aaguid,
})
}
}