use aws_lc_rs::digest::{self, SHA256};
use aws_lc_rs::signature::{
ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, ED25519, EcdsaVerificationAlgorithm,
RSA_PKCS1_2048_8192_SHA256, RSA_PKCS1_2048_8192_SHA384, RsaParameters, RsaPublicKeyComponents,
UnparsedPublicKey,
};
use serde::{Deserialize, Serialize};
use crate::Passki;
use crate::client_data::{ClientData, ClientDataType};
use crate::types::*;
#[derive(Serialize, Debug)]
pub struct AuthenticationChallenge {
pub challenge: String,
pub timeout: u64,
#[serde(rename = "rpId")]
pub rp_id: String,
#[serde(rename = "allowCredentials")]
pub allow_credentials: Vec<AllowCredential>,
#[serde(rename = "userVerification")]
pub user_verification: UserVerificationRequirement,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<AuthenticationExtensions>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct AuthenticationState {
pub challenge: Vec<u8>,
pub allowed_credentials: Vec<Vec<u8>>,
pub user_verification: UserVerificationRequirement,
}
#[derive(Deserialize)]
pub struct AuthenticationCredential {
pub credential_id: String,
pub authenticator_data: String,
pub client_data_json: String,
pub signature: String,
pub user_handle: Option<String>,
pub client_extension_results: Option<ClientExtensionResults>,
}
#[derive(Debug)]
pub struct AuthenticationResult {
pub credential_id: Vec<u8>,
pub counter: u32,
pub user_handle: Option<Vec<u8>>,
pub prf_first: Option<Vec<u8>>,
pub prf_second: Option<Vec<u8>>,
}
impl Passki {
pub fn start_passkey_authentication(
&self,
passkeys: &[StoredPasskey],
timeout: u64,
user_verification: UserVerificationRequirement,
extensions: Option<AuthenticationExtensions>,
) -> (AuthenticationChallenge, AuthenticationState) {
let challenge = Self::generate_challenge();
let challenge_response = AuthenticationChallenge {
challenge: Self::base64_encode(&challenge),
timeout,
rp_id: self.rp_id.clone(),
allow_credentials: passkeys
.iter()
.map(|pk| AllowCredential {
id: Self::base64_encode(&pk.credential_id),
type_: "public-key".to_string(),
})
.collect(),
user_verification,
extensions,
};
let state = AuthenticationState {
challenge: challenge.clone(),
allowed_credentials: passkeys.iter().map(|pk| pk.credential_id.clone()).collect(),
user_verification,
};
(challenge_response, state)
}
pub fn finish_passkey_authentication(
&self,
credential: &AuthenticationCredential,
state: &AuthenticationState,
stored_passkey: &StoredPasskey,
) -> Result<AuthenticationResult> {
let credential_id = Self::base64_decode(&credential.credential_id)?;
if !state.allowed_credentials.is_empty()
&& !state.allowed_credentials.contains(&credential_id)
{
return Err(Box::new(PasskiError::new("Credential not allowed")));
}
let client_data_bytes = Self::base64_decode(&credential.client_data_json)?;
let client_data = ClientData::from_bytes(&client_data_bytes)?;
client_data.verify(ClientDataType::Get, &state.challenge, &self.rp_origin)?;
let authenticator_data = Self::base64_decode(&credential.authenticator_data)?;
if authenticator_data.len() < 37 {
return Err(Box::new(PasskiError::new("Invalid authenticator data")));
}
let rp_id_hash = digest::digest(&SHA256, self.rp_id.as_bytes());
if &authenticator_data[..32] != rp_id_hash.as_ref() {
return Err(Box::new(PasskiError::new("rpId hash mismatch")));
}
let flags = authenticator_data[32];
if (flags & FLAG_UP) == 0 {
return Err(Box::new(PasskiError::new(
"User not present (UP flag not set)",
)));
}
if state.user_verification == UserVerificationRequirement::Required
&& (flags & FLAG_UV) == 0
{
return Err(Box::new(PasskiError::new(
"User verification required but UV flag not set",
)));
}
let counter = u32::from_be_bytes([
authenticator_data[33],
authenticator_data[34],
authenticator_data[35],
authenticator_data[36],
]);
if (counter != 0 || stored_passkey.counter != 0) && counter <= stored_passkey.counter {
return Err(Box::new(PasskiError::new(
"Invalid counter (possible replay attack)",
)));
}
let signature = Self::base64_decode(&credential.signature)?;
let client_data_hash = digest::digest(&SHA256, &client_data_bytes);
let mut signed_data = authenticator_data.clone();
signed_data.extend_from_slice(client_data_hash.as_ref());
Self::verify_signature(
&stored_passkey.public_key,
stored_passkey.algorithm,
&signed_data,
&signature,
)?;
let prf_results = credential
.client_extension_results
.as_ref()
.and_then(|ext| ext.prf.as_ref())
.and_then(|prf| prf.results.as_ref());
let prf_first = prf_results
.and_then(|r| r.first.as_deref())
.map(Self::base64_decode)
.transpose()?;
let prf_second = prf_results
.and_then(|r| r.second.as_deref())
.map(Self::base64_decode)
.transpose()?;
let user_handle = credential
.user_handle
.as_deref()
.map(Self::base64_decode)
.transpose()?;
Ok(AuthenticationResult {
credential_id,
counter,
user_handle,
prf_first,
prf_second,
})
}
#[inline]
pub(crate) fn verify_signature(
cose_key_bytes: &[u8],
algorithm: i32,
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
match algorithm {
ALG_EDDSA => Self::verify_eddsa(cose_key_bytes, signed_data, signature),
ALG_ES256 => Self::verify_ecdsa(
&ECDSA_P256_SHA256_ASN1,
CRV_P256,
"ES256",
cose_key_bytes,
signed_data,
signature,
),
ALG_ES384 => Self::verify_ecdsa(
&ECDSA_P384_SHA384_ASN1,
CRV_P384,
"ES384",
cose_key_bytes,
signed_data,
signature,
),
ALG_RS256 => Self::verify_rsa(
&RSA_PKCS1_2048_8192_SHA256,
"RS256",
cose_key_bytes,
signed_data,
signature,
),
ALG_RS384 => Self::verify_rsa(
&RSA_PKCS1_2048_8192_SHA384,
"RS384",
cose_key_bytes,
signed_data,
signature,
),
_ => Err(Box::new(PasskiError::new(format!(
"Unsupported algorithm: {}",
algorithm
)))),
}
}
pub(crate) fn cose_parse(
cose_key_bytes: &[u8],
) -> Result<Vec<(ciborium::Value, ciborium::Value)>> {
let cose_key_value: ciborium::Value = ciborium::from_reader(cose_key_bytes)
.map_err(|e| PasskiError::new(format!("Failed to parse COSE key: {}", e)))?;
match cose_key_value {
ciborium::Value::Map(map) => Ok(map),
_ => Err(Box::new(PasskiError::new("COSE key is not a map"))),
}
}
pub(crate) fn cose_field<'a>(
cose_map: &'a [(ciborium::Value, ciborium::Value)],
label: i64,
name: &str,
) -> Result<&'a [u8]> {
cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some(label.into()))
.and_then(|(_, v)| v.as_bytes())
.map(Vec::as_slice)
.ok_or_else(|| PasskiError::new(format!("Missing {} in COSE key", name)).into())
}
fn cose_expect(
cose_map: &[(ciborium::Value, ciborium::Value)],
label: i64,
name: &str,
expected: i64,
) -> Result<()> {
let value: i64 = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some(label.into()))
.and_then(|(_, v)| v.as_integer())
.and_then(|i| i.try_into().ok())
.ok_or_else(|| PasskiError::new(format!("Missing {} in COSE key", name)))?;
if value != expected {
return Err(Box::new(PasskiError::new(format!(
"Invalid {} in COSE key: expected {}, got {}",
name, expected, value
))));
}
Ok(())
}
pub(crate) fn verify_eddsa(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
let cose_map = Self::cose_parse(cose_key_bytes)?;
Self::cose_expect(&cose_map, 1, "kty", KTY_OKP)?;
Self::cose_expect(&cose_map, -1, "crv", CRV_ED25519)?;
let x = Self::cose_field(&cose_map, -2, "x coordinate")?;
if x.len() != 32 {
return Err(Box::new(PasskiError::new(
"Invalid Ed25519 public key length",
)));
}
let public_key = UnparsedPublicKey::new(&ED25519, x);
public_key
.verify(signed_data, signature)
.map_err(|_| PasskiError::new("EdDSA signature verification failed"))?;
Ok(())
}
pub(crate) fn verify_ecdsa(
algorithm: &'static EcdsaVerificationAlgorithm,
crv: i64,
name: &str,
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
let cose_map = Self::cose_parse(cose_key_bytes)?;
Self::cose_expect(&cose_map, 1, "kty", KTY_EC2)?;
Self::cose_expect(&cose_map, -1, "crv", crv)?;
let x = Self::cose_field(&cose_map, -2, "x coordinate")?;
let y = Self::cose_field(&cose_map, -3, "y coordinate")?;
let mut public_key_bytes = vec![0x04];
public_key_bytes.extend_from_slice(x);
public_key_bytes.extend_from_slice(y);
let public_key = UnparsedPublicKey::new(algorithm, &public_key_bytes);
public_key
.verify(signed_data, signature)
.map_err(|_| PasskiError::new(format!("{} signature verification failed", name)))?;
Ok(())
}
pub(crate) fn verify_rsa(
algorithm: &'static RsaParameters,
name: &str,
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
let cose_map = Self::cose_parse(cose_key_bytes)?;
Self::cose_expect(&cose_map, 1, "kty", KTY_RSA)?;
let n = Self::cose_field(&cose_map, -1, "n (modulus)")?;
let e = Self::cose_field(&cose_map, -2, "e (exponent)")?;
let public_key = RsaPublicKeyComponents { n, e };
public_key
.verify(algorithm, signed_data, signature)
.map_err(|_| PasskiError::new(format!("{} signature verification failed", name)))?;
Ok(())
}
}