alpine/
attestation.rs

1use std::collections::HashMap;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use ed25519_dalek::{Signature, Verifier, VerifyingKey};
5use serde::Deserialize;
6use serde_cbor::Value;
7use thiserror::Error;
8
9use crate::messages::DiscoveryReply;
10
11#[derive(Debug, Error)]
12pub enum AttestationError {
13    #[error("device identity attestation missing")]
14    Missing,
15    #[error("attestation decode failed: {0}")]
16    Decode(String),
17    #[error("attestation missing field: {0}")]
18    MissingField(&'static str),
19    #[error("attestation invalid field: {0}")]
20    InvalidField(&'static str),
21    #[error("attestation unsupported algorithm: {0}")]
22    UnsupportedAlgorithm(String),
23    #[error("attestation signer not trusted: {0}")]
24    UnknownSigner(String),
25    #[error("attestation signature invalid")]
26    InvalidSignature,
27    #[error("attestation expired")]
28    Expired,
29    #[error("attestation identity mismatch: {0}")]
30    IdentityMismatch(&'static str),
31    #[error("device identity pubkey missing from discovery")]
32    MissingDeviceIdentityPubkey,
33}
34
35#[derive(Debug, Clone, Default)]
36pub struct AttesterRegistry {
37    keys: HashMap<String, VerifyingKey>,
38}
39
40impl AttesterRegistry {
41    pub fn new() -> Self {
42        Self {
43            keys: HashMap::new(),
44        }
45    }
46
47    pub fn insert(&mut self, kid: impl Into<String>, key: VerifyingKey) {
48        self.keys.insert(kid.into(), key);
49    }
50
51    pub fn insert_key_bytes(
52        &mut self,
53        kid: impl Into<String>,
54        key_bytes: &[u8],
55    ) -> Result<(), AttestationError> {
56        let key: [u8; 32] = key_bytes
57            .try_into()
58            .map_err(|_| AttestationError::InvalidField("signer_kid_pubkey"))?;
59        let verifying = VerifyingKey::from_bytes(&key)
60            .map_err(|_| AttestationError::InvalidField("signer_kid_pubkey"))?;
61        self.insert(kid, verifying);
62        Ok(())
63    }
64
65    pub fn get(&self, kid: &str) -> Option<&VerifyingKey> {
66        self.keys.get(kid)
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.keys.is_empty()
71    }
72}
73
74#[derive(Debug)]
75pub struct VerifiedDeviceIdentityAttestation {
76    pub signer_kid: String,
77    pub expires_at: Option<u64>,
78}
79
80#[derive(Debug, Error)]
81pub enum AttesterBundleError {
82    #[error("attesters bundle decode failed: {0}")]
83    Decode(String),
84    #[error("attesters bundle missing field: {0}")]
85    MissingField(&'static str),
86    #[error("attesters bundle invalid field: {0}")]
87    InvalidField(&'static str),
88    #[error("attesters bundle unsupported algorithm: {0}")]
89    UnsupportedAlgorithm(String),
90    #[error("attesters bundle signature invalid")]
91    InvalidSignature,
92    #[error("attesters bundle expired")]
93    Expired,
94    #[error("root key invalid")]
95    InvalidRootKey,
96}
97
98#[derive(Debug, Clone, Deserialize)]
99pub struct AttesterRecord {
100    pub kid: String,
101    #[serde(with = "serde_bytes")]
102    pub pubkey: Vec<u8>,
103    pub alg: String,
104    pub status: String,
105    #[serde(default)]
106    pub revoked_at: Option<u64>,
107    #[serde(default)]
108    pub expires_at: Option<u64>,
109}
110
111#[derive(Debug, Clone)]
112pub struct VerifiedAttesterBundle {
113    pub issued_at: u64,
114    pub expires_at: u64,
115    pub signer_kid: Option<String>,
116    pub attesters: Vec<AttesterRecord>,
117    pub registry: AttesterRegistry,
118}
119
120#[derive(Debug, Deserialize)]
121struct DeviceIdentityPayload {
122    device_id: String,
123    #[serde(rename = "mfg")]
124    manufacturer_id: String,
125    #[serde(rename = "model")]
126    model_id: String,
127    #[serde(rename = "hw_rev")]
128    hardware_rev: String,
129    #[serde(rename = "pub_ed25519", with = "serde_bytes")]
130    pub_ed25519: Vec<u8>,
131    #[serde(default)]
132    expires_at: Option<u64>,
133}
134
135fn value_as_text(value: &Value) -> Option<String> {
136    match value {
137        Value::Text(text) => Some(text.clone()),
138        Value::Bytes(bytes) => Some(hex::encode(bytes)),
139        _ => None,
140    }
141}
142
143fn value_as_u64(value: &Value) -> Option<u64> {
144    match value {
145        Value::Integer(int) if *int >= 0 => Some(*int as u64),
146        Value::Float(float) if *float >= 0.0 => Some(*float as u64),
147        _ => None,
148    }
149}
150
151fn value_as_bytes(value: &Value) -> Option<Vec<u8>> {
152    match value {
153        Value::Bytes(bytes) => Some(bytes.clone()),
154        _ => None,
155    }
156}
157
158fn map_lookup<'a>(
159    map: &'a std::collections::BTreeMap<Value, Value>,
160    key: &str,
161) -> Option<&'a Value> {
162    map.iter().find_map(|(k, v)| match k {
163        Value::Text(text) if text == key => Some(v),
164        _ => None,
165    })
166}
167
168pub fn verify_device_identity_attestation(
169    reply: &DiscoveryReply,
170    registry: &AttesterRegistry,
171    now: SystemTime,
172) -> Result<VerifiedDeviceIdentityAttestation, AttestationError> {
173    if reply.device_identity_attestation.is_empty() {
174        return Err(AttestationError::Missing);
175    }
176    if reply.device_identity_pubkey.is_empty() {
177        return Err(AttestationError::MissingDeviceIdentityPubkey);
178    }
179
180    let envelope: Value = serde_cbor::from_slice(&reply.device_identity_attestation)
181        .map_err(|err| AttestationError::Decode(err.to_string()))?;
182    let map = match envelope {
183        Value::Map(map) => map,
184        _ => return Err(AttestationError::InvalidField("attestation_map")),
185    };
186
187    let payload_bytes = map_lookup(&map, "payload")
188        .and_then(value_as_bytes)
189        .ok_or(AttestationError::MissingField("payload"))?;
190    let sig_bytes = map_lookup(&map, "sig")
191        .and_then(value_as_bytes)
192        .ok_or(AttestationError::MissingField("sig"))?;
193    let alg = map_lookup(&map, "alg")
194        .and_then(value_as_text)
195        .ok_or(AttestationError::MissingField("alg"))?;
196    let signer_kid = map_lookup(&map, "signer_kid")
197        .and_then(value_as_text)
198        .ok_or(AttestationError::MissingField("signer_kid"))?;
199    let expires_at = map_lookup(&map, "expires_at").and_then(value_as_u64);
200
201    let alg_lower = alg.to_ascii_lowercase();
202    if alg_lower != "ed25519" && alg_lower != "eddsa" {
203        return Err(AttestationError::UnsupportedAlgorithm(alg));
204    }
205
206    let attester_key = registry
207        .get(&signer_kid)
208        .ok_or_else(|| AttestationError::UnknownSigner(signer_kid.clone()))?;
209
210    let signature =
211        Signature::from_slice(&sig_bytes).map_err(|_| AttestationError::InvalidSignature)?;
212    attester_key
213        .verify(&payload_bytes, &signature)
214        .map_err(|_| AttestationError::InvalidSignature)?;
215
216    let payload: DeviceIdentityPayload = serde_cbor::from_slice(&payload_bytes)
217        .map_err(|err| AttestationError::Decode(err.to_string()))?;
218
219    if payload.device_id != reply.device_id {
220        return Err(AttestationError::IdentityMismatch("device_id"));
221    }
222    if payload.manufacturer_id != reply.manufacturer_id {
223        return Err(AttestationError::IdentityMismatch("manufacturer_id"));
224    }
225    if payload.model_id != reply.model_id {
226        return Err(AttestationError::IdentityMismatch("model_id"));
227    }
228    if payload.hardware_rev != reply.hardware_rev {
229        return Err(AttestationError::IdentityMismatch("hardware_rev"));
230    }
231    if payload.pub_ed25519 != reply.device_identity_pubkey {
232        return Err(AttestationError::IdentityMismatch("device_identity_pubkey"));
233    }
234
235    let now_secs = now
236        .duration_since(UNIX_EPOCH)
237        .map(|d| d.as_secs())
238        .unwrap_or_default();
239    let expires = expires_at.or(payload.expires_at);
240    if let Some(expires_at) = expires {
241        if expires_at <= now_secs {
242            return Err(AttestationError::Expired);
243        }
244    }
245
246    Ok(VerifiedDeviceIdentityAttestation {
247        signer_kid,
248        expires_at: expires,
249    })
250}
251
252#[derive(Debug, Deserialize)]
253struct AttesterBundleEnvelope {
254    v: u8,
255    #[serde(with = "serde_bytes")]
256    payload: Vec<u8>,
257    #[serde(with = "serde_bytes")]
258    sig: Vec<u8>,
259    alg: String,
260    #[serde(default)]
261    signer_kid: Option<String>,
262}
263
264#[derive(Debug, Deserialize)]
265struct AttesterBundlePayload {
266    v: u8,
267    issued_at: u64,
268    expires_at: u64,
269    attesters: Vec<AttesterRecord>,
270}
271
272pub fn verify_attester_bundle(
273    bundle_bytes: &[u8],
274    root_pubkey: &[u8],
275    now: SystemTime,
276) -> Result<VerifiedAttesterBundle, AttesterBundleError> {
277    let key_bytes: [u8; 32] = root_pubkey
278        .try_into()
279        .map_err(|_| AttesterBundleError::InvalidRootKey)?;
280    let root_key =
281        VerifyingKey::from_bytes(&key_bytes).map_err(|_| AttesterBundleError::InvalidRootKey)?;
282
283    let envelope: AttesterBundleEnvelope = serde_cbor::from_slice(bundle_bytes)
284        .map_err(|err| AttesterBundleError::Decode(err.to_string()))?;
285    if envelope.v == 0 {
286        return Err(AttesterBundleError::InvalidField("v"));
287    }
288
289    let alg_lower = envelope.alg.to_ascii_lowercase();
290    if alg_lower != "ed25519" && alg_lower != "eddsa" {
291        return Err(AttesterBundleError::UnsupportedAlgorithm(envelope.alg));
292    }
293
294    let signature =
295        Signature::from_slice(&envelope.sig).map_err(|_| AttesterBundleError::InvalidSignature)?;
296    root_key
297        .verify(&envelope.payload, &signature)
298        .map_err(|_| AttesterBundleError::InvalidSignature)?;
299
300    let payload: AttesterBundlePayload = serde_cbor::from_slice(&envelope.payload)
301        .map_err(|err| AttesterBundleError::Decode(err.to_string()))?;
302
303    if payload.v == 0 {
304        return Err(AttesterBundleError::InvalidField("payload.v"));
305    }
306
307    let now_secs = now
308        .duration_since(UNIX_EPOCH)
309        .map(|d| d.as_secs())
310        .unwrap_or_default();
311    if payload.expires_at <= now_secs {
312        return Err(AttesterBundleError::Expired);
313    }
314
315    let mut registry = AttesterRegistry::new();
316    let mut active_attesters = Vec::new();
317
318    for attester in payload.attesters.into_iter() {
319        let status_lower = attester.status.to_ascii_lowercase();
320        if status_lower != "active" {
321            continue;
322        }
323        if let Some(revoked_at) = attester.revoked_at {
324            if revoked_at <= now_secs {
325                continue;
326            }
327        }
328        if let Some(expires_at) = attester.expires_at {
329            if expires_at <= now_secs {
330                continue;
331            }
332        }
333        let alg_lower = attester.alg.to_ascii_lowercase();
334        if alg_lower != "ed25519" && alg_lower != "eddsa" {
335            return Err(AttesterBundleError::UnsupportedAlgorithm(attester.alg));
336        }
337        registry
338            .insert_key_bytes(attester.kid.clone(), &attester.pubkey)
339            .map_err(|_| AttesterBundleError::InvalidField("attester.pubkey"))?;
340        active_attesters.push(attester);
341    }
342
343    Ok(VerifiedAttesterBundle {
344        issued_at: payload.issued_at,
345        expires_at: payload.expires_at,
346        signer_kid: envelope.signer_kid,
347        attesters: active_attesters,
348        registry,
349    })
350}