use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use p256::{
EncodedPoint, PublicKey,
ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier as _},
};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use crate::profile::Platform;
const COORDINATE_BYTES: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceKey(VerifyingKey);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DeviceKeyError {
#[error("device key is not EC P-256")]
UnsupportedKeyType,
#[error("device key coordinates are malformed")]
MalformedCoordinates,
#[error("device key is not a point on P-256")]
NotOnCurve,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct EcJwk {
pub(crate) kty: String,
pub(crate) crv: String,
pub(crate) x: String,
pub(crate) y: String,
}
impl EcJwk {
pub(crate) fn to_verifying_key(&self) -> Result<VerifyingKey, DeviceKeyError> {
if self.kty != "EC" || self.crv != "P-256" {
return Err(DeviceKeyError::UnsupportedKeyType);
}
let x = coordinate(&self.x)?;
let y = coordinate(&self.y)?;
let point = EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), false);
let key =
PublicKey::from_sec1_bytes(point.as_bytes()).map_err(|_| DeviceKeyError::NotOnCurve)?;
Ok(VerifyingKey::from(key))
}
}
fn coordinate(encoded: &str) -> Result<[u8; COORDINATE_BYTES], DeviceKeyError> {
let bytes = URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|_| DeviceKeyError::MalformedCoordinates)?;
let padding = COORDINATE_BYTES
.checked_sub(bytes.len())
.ok_or(DeviceKeyError::MalformedCoordinates)?;
let mut coordinate = [0u8; COORDINATE_BYTES];
coordinate[padding..].copy_from_slice(&bytes);
Ok(coordinate)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PlatformSignatureError {
#[error("iOS signature is not a CBOR App Attest assertion")]
MalformedAssertion,
#[error("ECDSA signature is not DER")]
MalformedEcdsa,
#[error("signature does not verify under the device key")]
Mismatch,
}
#[derive(Deserialize)]
struct Assertion {
#[serde(with = "serde_bytes")]
signature: Vec<u8>,
#[serde(rename = "authenticatorData", with = "serde_bytes")]
authenticator_data: Vec<u8>,
}
impl DeviceKey {
#[must_use]
pub const fn new(key: VerifyingKey) -> Self {
Self(key)
}
#[must_use]
pub const fn verifying_key(&self) -> &VerifyingKey {
&self.0
}
#[must_use]
pub fn thumbprint(&self) -> String {
let point = self.0.to_encoded_point(false);
let (Some(x), Some(y)) = (point.x(), point.y()) else {
unreachable!("an uncompressed point has both coordinates");
};
let canonical = format!(
r#"{{"crv":"P-256","kty":"EC","x":"{}","y":"{}"}}"#,
URL_SAFE_NO_PAD.encode(x),
URL_SAFE_NO_PAD.encode(y),
);
URL_SAFE_NO_PAD.encode(Sha256::digest(canonical.as_bytes()))
}
pub fn verify(
&self,
platform: Platform,
client_data_hash: &[u8; 32],
signature: &[u8],
) -> Result<(), PlatformSignatureError> {
let (digest, der): ([u8; 32], Vec<u8>) = match platform {
Platform::Android => (*client_data_hash, signature.to_vec()),
Platform::Ios => {
let assertion: Assertion = ciborium::from_reader(signature)
.map_err(|_| PlatformSignatureError::MalformedAssertion)?;
let nonce = Sha256::new()
.chain_update(&assertion.authenticator_data)
.chain_update(client_data_hash)
.finalize();
(Sha256::digest(nonce).into(), assertion.signature)
}
};
let signature =
Signature::from_der(&der).map_err(|_| PlatformSignatureError::MalformedEcdsa)?;
self.0
.verify_prehash(&digest, &signature)
.map_err(|_| PlatformSignatureError::Mismatch)
}
}