attested_request/
device.rs1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4use p256::{
5 EncodedPoint, PublicKey,
6 ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier as _},
7};
8use serde::Deserialize;
9use sha2::{Digest, Sha256};
10
11use crate::profile::Platform;
12
13const COORDINATE_BYTES: usize = 32;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct DeviceKey(VerifyingKey);
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum DeviceKeyError {
22 #[error("device key is not EC P-256")]
24 UnsupportedKeyType,
25 #[error("device key coordinates are malformed")]
27 MalformedCoordinates,
28 #[error("device key is not a point on P-256")]
30 NotOnCurve,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35pub(crate) struct EcJwk {
36 pub(crate) kty: String,
37 pub(crate) crv: String,
38 pub(crate) x: String,
39 pub(crate) y: String,
40}
41
42impl EcJwk {
43 pub(crate) fn to_verifying_key(&self) -> Result<VerifyingKey, DeviceKeyError> {
44 if self.kty != "EC" || self.crv != "P-256" {
45 return Err(DeviceKeyError::UnsupportedKeyType);
46 }
47 let x = coordinate(&self.x)?;
48 let y = coordinate(&self.y)?;
49 let point = EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), false);
50 let key =
52 PublicKey::from_sec1_bytes(point.as_bytes()).map_err(|_| DeviceKeyError::NotOnCurve)?;
53 Ok(VerifyingKey::from(key))
54 }
55}
56
57fn coordinate(encoded: &str) -> Result<[u8; COORDINATE_BYTES], DeviceKeyError> {
59 let bytes = URL_SAFE_NO_PAD
60 .decode(encoded)
61 .map_err(|_| DeviceKeyError::MalformedCoordinates)?;
62 let padding = COORDINATE_BYTES
63 .checked_sub(bytes.len())
64 .ok_or(DeviceKeyError::MalformedCoordinates)?;
65 let mut coordinate = [0u8; COORDINATE_BYTES];
66 coordinate[padding..].copy_from_slice(&bytes);
67 Ok(coordinate)
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
72pub enum PlatformSignatureError {
73 #[error("iOS signature is not a CBOR App Attest assertion")]
75 MalformedAssertion,
76 #[error("ECDSA signature is not DER")]
78 MalformedEcdsa,
79 #[error("signature does not verify under the device key")]
81 Mismatch,
82}
83
84#[derive(Deserialize)]
86struct Assertion {
87 #[serde(with = "serde_bytes")]
88 signature: Vec<u8>,
89 #[serde(rename = "authenticatorData", with = "serde_bytes")]
90 authenticator_data: Vec<u8>,
91}
92
93impl DeviceKey {
94 #[must_use]
96 pub const fn new(key: VerifyingKey) -> Self {
97 Self(key)
98 }
99
100 #[must_use]
102 pub const fn verifying_key(&self) -> &VerifyingKey {
103 &self.0
104 }
105
106 #[must_use]
109 pub fn thumbprint(&self) -> String {
110 let point = self.0.to_encoded_point(false);
111 let (Some(x), Some(y)) = (point.x(), point.y()) else {
112 unreachable!("an uncompressed point has both coordinates");
113 };
114 let canonical = format!(
115 r#"{{"crv":"P-256","kty":"EC","x":"{}","y":"{}"}}"#,
116 URL_SAFE_NO_PAD.encode(x),
117 URL_SAFE_NO_PAD.encode(y),
118 );
119 URL_SAFE_NO_PAD.encode(Sha256::digest(canonical.as_bytes()))
120 }
121
122 pub fn verify(
133 &self,
134 platform: Platform,
135 client_data_hash: &[u8; 32],
136 signature: &[u8],
137 ) -> Result<(), PlatformSignatureError> {
138 let (digest, der): ([u8; 32], Vec<u8>) = match platform {
139 Platform::Android => (*client_data_hash, signature.to_vec()),
140 Platform::Ios => {
141 let assertion: Assertion = ciborium::from_reader(signature)
142 .map_err(|_| PlatformSignatureError::MalformedAssertion)?;
143 let nonce = Sha256::new()
144 .chain_update(&assertion.authenticator_data)
145 .chain_update(client_data_hash)
146 .finalize();
147 (Sha256::digest(nonce).into(), assertion.signature)
148 }
149 };
150 let signature =
151 Signature::from_der(&der).map_err(|_| PlatformSignatureError::MalformedEcdsa)?;
152 self.0
153 .verify_prehash(&digest, &signature)
154 .map_err(|_| PlatformSignatureError::Mismatch)
155 }
156}