use aws_lc_rs::digest::{self, SHA256};
use aws_lc_rs::signature::{
RsaPublicKeyComponents, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1,
ED25519, RSA_PKCS1_2048_8192_SHA256, RSA_PKCS1_2048_8192_SHA384,
};
use serde::{Deserialize, Serialize};
use crate::client_data::{ClientData, ClientDataType};
use crate::types::*;
use crate::Passki;
#[derive(Serialize)]
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,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AuthenticationState {
pub challenge: Vec<u8>,
pub allowed_credentials: Vec<Vec<u8>>,
}
#[derive(Deserialize)]
pub struct AuthenticationCredential {
pub credential_id: String,
pub authenticator_data: String,
pub client_data_json: String,
pub signature: String,
}
#[derive(Debug)]
pub struct AuthenticationResult {
pub credential_id: Vec<u8>,
pub counter: u32,
}
impl Passki {
pub fn start_passkey_authentication(
&self,
passkeys: &[StoredPasskey],
timeout: u64,
user_verification: UserVerificationRequirement,
) -> (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,
};
let state = AuthenticationState {
challenge: challenge.clone(),
allowed_credentials: passkeys.iter().map(|pk| pk.credential_id.clone()).collect(),
};
(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 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,
)?;
Ok(AuthenticationResult {
credential_id,
counter,
})
}
#[inline] pub(crate) fn verify_signature(
cose_key_bytes: &[u8],
algorithm: i32,
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
match algorithm {
-8 => Self::verify_eddsa(cose_key_bytes, signed_data, signature),
-7 => Self::verify_es256(cose_key_bytes, signed_data, signature),
-35 => Self::verify_es384(cose_key_bytes, signed_data, signature),
-257 => Self::verify_rs256(cose_key_bytes, signed_data, signature),
-258 => Self::verify_rs384(cose_key_bytes, signed_data, signature),
_ => Err(Box::new(PasskiError::new(format!(
"Unsupported algorithm: {}",
algorithm
)))),
}
}
pub(crate) fn verify_eddsa(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
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 cose_map = cose_key_value
.as_map()
.ok_or_else(|| PasskiError::new("COSE key is not a map"))?;
let x = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-2).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing x coordinate in COSE key"))?;
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_es256(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
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 cose_map = cose_key_value
.as_map()
.ok_or_else(|| PasskiError::new("COSE key is not a map"))?;
let x = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-2).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing x coordinate in COSE key"))?;
let y = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-3).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing y coordinate in COSE key"))?;
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(&ECDSA_P256_SHA256_ASN1, &public_key_bytes);
public_key
.verify(signed_data, signature)
.map_err(|_| PasskiError::new("ES256 signature verification failed"))?;
Ok(())
}
pub(crate) fn verify_es384(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
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 cose_map = cose_key_value
.as_map()
.ok_or_else(|| PasskiError::new("COSE key is not a map"))?;
let x = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-2).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing x coordinate in COSE key"))?;
let y = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-3).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing y coordinate in COSE key"))?;
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(&ECDSA_P384_SHA384_ASN1, &public_key_bytes);
public_key
.verify(signed_data, signature)
.map_err(|_| PasskiError::new("ES384 signature verification failed"))?;
Ok(())
}
pub(crate) fn verify_rs256(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
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 cose_map = cose_key_value
.as_map()
.ok_or_else(|| PasskiError::new("COSE key is not a map"))?;
let n = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-1).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing n (modulus) in COSE key"))?;
let e = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-2).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing e (exponent) in COSE key"))?;
let public_key = RsaPublicKeyComponents { n, e };
public_key
.verify(&RSA_PKCS1_2048_8192_SHA256, signed_data, signature)
.map_err(|_| PasskiError::new("RS256 signature verification failed"))?;
Ok(())
}
pub(crate) fn verify_rs384(
cose_key_bytes: &[u8],
signed_data: &[u8],
signature: &[u8],
) -> Result<()> {
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 cose_map = cose_key_value
.as_map()
.ok_or_else(|| PasskiError::new("COSE key is not a map"))?;
let n = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-1).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing n (modulus) in COSE key"))?;
let e = cose_map
.iter()
.find(|(k, _)| k.as_integer() == Some((-2).into()))
.and_then(|(_, v)| v.as_bytes())
.ok_or_else(|| PasskiError::new("Missing e (exponent) in COSE key"))?;
let public_key = RsaPublicKeyComponents { n, e };
public_key
.verify(&RSA_PKCS1_2048_8192_SHA384, signed_data, signature)
.map_err(|_| PasskiError::new("RS384 signature verification failed"))?;
Ok(())
}
}