Skip to main content

heddle_api/
descriptor_trust.rs

1//! Canonical two-layer root-attested ephemeral endpoint-descriptor contract.
2//!
3//! The deployment descriptor **root** signs an attestation binding an ephemeral
4//! key. That ephemeral key signs its own [`SignedEndpointDescriptor`] (relay and
5//! direct addresses). Consumers MUST verify both layers and bind them; unsigned
6//! address hints are not part of this contract.
7
8use ed25519_dalek::{Signature, VerifyingKey};
9use prost::Message;
10use serde::{Deserialize, Serialize};
11
12use crate::heddle::api::v1alpha1::{EndpointDescriptor, SignedEndpointDescriptor};
13use crate::signing::{canonical_with_domain, endpoint_descriptor_bytes};
14
15/// Document version for [`EndpointDescriptorSetDocument`].
16pub const SET_VERSION: u8 = 1;
17/// Domain separator for root-key attestations of ephemeral descriptor keys.
18/// Distinct from [`crate::signing::TRANSPORT_BOOTSTRAP_SIGNING_V1_DOMAIN`] so an
19/// endpoint-descriptor signature cannot verify as an attestation and vice versa.
20pub const ROOT_ATTESTATION_V1_DOMAIN: &str = "heddle-descriptor-root-attestation-v1";
21/// Canonical `kind=` label inside [`ephemeral_attestation_bytes`].
22pub const ROOT_ATTESTATION_KIND: &str = "descriptor-ephemeral-attestation-v1";
23
24/// Published set of root-attested ephemeral endpoint descriptors.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct EndpointDescriptorSetDocument {
27    pub version: u8,
28    pub root_key_id: String,
29    pub entries: Vec<AttestedEndpointDescriptorEntry>,
30}
31
32/// One root-attested ephemeral key plus the descriptor that key signed.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct AttestedEndpointDescriptorEntry {
35    pub ephemeral_key_id: String,
36    /// 32-byte Ed25519 public key as 64 lowercase hex characters.
37    pub ephemeral_public_key: String,
38    pub not_before_unix_millis: i64,
39    pub not_after_unix_millis: i64,
40    pub region: String,
41    /// 64-byte Ed25519 signature as 128 lowercase hex characters.
42    pub attestation_signature: String,
43    /// Hex-encoded [`SignedEndpointDescriptor`] protobuf.
44    pub signed_descriptor: String,
45}
46
47/// Endpoint that passed both attestation and descriptor-signature layers.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct VerifiedEndpoint {
50    pub ephemeral_public_key: [u8; 32],
51    pub endpoint_descriptor: EndpointDescriptor,
52    pub not_before_unix_millis: i64,
53    pub not_after_unix_millis: i64,
54    pub region: String,
55}
56
57/// Why a set entry was not promoted to [`VerifiedEndpoint`].
58#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
59pub enum EntryReject {
60    #[error("missing attestation signature")]
61    MissingSignature,
62    #[error("invalid attestation signature")]
63    InvalidSignature,
64    #[error("unattested ephemeral key")]
65    Unattested,
66    #[error("invalid endpoint-descriptor signature")]
67    InvalidDescriptorSignature,
68    #[error("descriptor identity does not match the attested ephemeral key")]
69    DescriptorKeyMismatch,
70    #[error("attestation is not yet valid")]
71    NotYetValid,
72    #[error("attestation has expired")]
73    Expired,
74    #[error("attestation validity window is inverted or empty")]
75    InvalidWindow,
76    #[error("invalid ephemeral public key")]
77    InvalidKey,
78    #[error("malformed signed endpoint descriptor")]
79    MalformedDescriptor,
80    #[error("unsupported descriptor set version")]
81    UnsupportedVersion,
82    #[error("unsupported endpoint descriptor version")]
83    UnsupportedDescriptorVersion,
84    #[error("descriptor is not yet valid")]
85    DescriptorNotYetValid,
86    #[error("descriptor has expired")]
87    DescriptorExpired,
88}
89
90/// Errors from [`parse_endpoint_descriptor_set`].
91#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
92pub enum DescriptorSetError {
93    #[error("malformed endpoint descriptor set: {0}")]
94    Malformed(String),
95    #[error("unsupported endpoint descriptor set version {0}")]
96    UnsupportedVersion(u8),
97}
98
99/// Canonical bytes the deployment-descriptor root signs to attest an ephemeral key.
100///
101/// Field order is load-bearing:
102/// `ephemeral_key_id` (utf8), `ephemeral_public_key` (raw 32 bytes),
103/// `not_before_unix_millis` / `not_after_unix_millis` (i64 big-endian),
104/// `region` (utf8). Integers are fixed-width; the public key is never hex.
105pub fn ephemeral_attestation_bytes(
106    ephemeral_key_id: &str,
107    ephemeral_public_key: &[u8; 32],
108    not_before_unix_millis: i64,
109    not_after_unix_millis: i64,
110    region: &str,
111) -> Vec<u8> {
112    canonical_with_domain(
113        ROOT_ATTESTATION_V1_DOMAIN,
114        ROOT_ATTESTATION_KIND,
115        &[
116            ("ephemeral_key_id", ephemeral_key_id.as_bytes().to_vec()),
117            ("ephemeral_public_key", ephemeral_public_key.to_vec()),
118            (
119                "not_before_unix_millis",
120                not_before_unix_millis.to_be_bytes().to_vec(),
121            ),
122            (
123                "not_after_unix_millis",
124                not_after_unix_millis.to_be_bytes().to_vec(),
125            ),
126            ("region", region.as_bytes().to_vec()),
127        ],
128    )
129}
130
131/// Verify both trust layers of one attested entry and bind them.
132///
133/// Layer 1: the `root_public_key` signed [`ephemeral_attestation_bytes`].
134/// Layer 2: the attested ephemeral key signed the inner
135/// [`SignedEndpointDescriptor`] over [`endpoint_descriptor_bytes`].
136/// Bind: `descriptor.endpoint_id` is the hex of the attested key, and
137/// `SignedEndpointDescriptor.key_id` equals `entry.ephemeral_key_id`.
138pub fn verify_ephemeral_attestation(
139    root_public_key: &[u8; 32],
140    entry: &AttestedEndpointDescriptorEntry,
141    now_unix_millis: i64,
142) -> Result<VerifiedEndpoint, EntryReject> {
143    if entry.ephemeral_key_id.trim().is_empty() {
144        return Err(EntryReject::InvalidKey);
145    }
146    if entry.not_before_unix_millis >= entry.not_after_unix_millis {
147        return Err(EntryReject::InvalidWindow);
148    }
149    let ephemeral_public_key: [u8; 32] =
150        decode_lowercase_hex(&entry.ephemeral_public_key).ok_or(EntryReject::InvalidKey)?;
151    if entry.attestation_signature.is_empty() {
152        return Err(EntryReject::MissingSignature);
153    }
154    let attestation_signature: [u8; 64] =
155        decode_lowercase_hex(&entry.attestation_signature).ok_or(EntryReject::InvalidSignature)?;
156    if attestation_signature == [0u8; 64] {
157        return Err(EntryReject::Unattested);
158    }
159
160    let root =
161        VerifyingKey::from_bytes(root_public_key).map_err(|_| EntryReject::InvalidSignature)?;
162    let attestation_bytes = ephemeral_attestation_bytes(
163        &entry.ephemeral_key_id,
164        &ephemeral_public_key,
165        entry.not_before_unix_millis,
166        entry.not_after_unix_millis,
167        &entry.region,
168    );
169    root.verify_strict(
170        &attestation_bytes,
171        &Signature::from_bytes(&attestation_signature),
172    )
173    .map_err(|_| EntryReject::InvalidSignature)?;
174
175    if now_unix_millis < entry.not_before_unix_millis {
176        return Err(EntryReject::NotYetValid);
177    }
178    if now_unix_millis >= entry.not_after_unix_millis {
179        return Err(EntryReject::Expired);
180    }
181
182    if !is_lowercase_hex(&entry.signed_descriptor) {
183        return Err(EntryReject::MalformedDescriptor);
184    }
185    let signed_bytes =
186        hex::decode(&entry.signed_descriptor).map_err(|_| EntryReject::MalformedDescriptor)?;
187    let signed = SignedEndpointDescriptor::decode(signed_bytes.as_slice())
188        .map_err(|_| EntryReject::MalformedDescriptor)?;
189    let descriptor = signed.descriptor.ok_or(EntryReject::MalformedDescriptor)?;
190    let descriptor_signature: [u8; 64] = signed
191        .signature
192        .as_slice()
193        .try_into()
194        .map_err(|_| EntryReject::InvalidDescriptorSignature)?;
195    let ephemeral =
196        VerifyingKey::from_bytes(&ephemeral_public_key).map_err(|_| EntryReject::InvalidKey)?;
197    ephemeral
198        .verify_strict(
199            &endpoint_descriptor_bytes(&descriptor),
200            &Signature::from_bytes(&descriptor_signature),
201        )
202        .map_err(|_| EntryReject::InvalidDescriptorSignature)?;
203
204    if descriptor.endpoint_id != hex::encode(ephemeral_public_key) {
205        return Err(EntryReject::DescriptorKeyMismatch);
206    }
207    if signed.key_id != entry.ephemeral_key_id {
208        return Err(EntryReject::DescriptorKeyMismatch);
209    }
210    if descriptor.version != 1 {
211        return Err(EntryReject::UnsupportedDescriptorVersion);
212    }
213    if now_unix_millis < descriptor.issued_at_unix_millis {
214        return Err(EntryReject::DescriptorNotYetValid);
215    }
216    if now_unix_millis >= descriptor.expires_at_unix_millis {
217        return Err(EntryReject::DescriptorExpired);
218    }
219
220    Ok(VerifiedEndpoint {
221        ephemeral_public_key,
222        endpoint_descriptor: descriptor,
223        not_before_unix_millis: entry.not_before_unix_millis,
224        not_after_unix_millis: entry.not_after_unix_millis,
225        region: entry.region.clone(),
226    })
227}
228
229/// Deserialize an [`EndpointDescriptorSetDocument`] and reject unknown versions.
230pub fn parse_endpoint_descriptor_set(
231    body: &[u8],
232) -> Result<EndpointDescriptorSetDocument, DescriptorSetError> {
233    let set: EndpointDescriptorSetDocument = serde_json::from_slice(body)
234        .map_err(|error| DescriptorSetError::Malformed(error.to_string()))?;
235    if set.version != SET_VERSION {
236        return Err(DescriptorSetError::UnsupportedVersion(set.version));
237    }
238    Ok(set)
239}
240
241/// Verify every entry and drop rejects. Never returns a [`VerifiedEndpoint`]
242/// unless both layers verified and bound.
243pub fn trusted_live_entries(
244    set: &EndpointDescriptorSetDocument,
245    root_public_key: &[u8; 32],
246    now_unix_millis: i64,
247) -> (Vec<VerifiedEndpoint>, Vec<EntryReject>) {
248    if set.version != SET_VERSION {
249        return (vec![], vec![EntryReject::UnsupportedVersion]);
250    }
251    let mut live = Vec::new();
252    let mut rejects = Vec::new();
253    for entry in &set.entries {
254        match verify_ephemeral_attestation(root_public_key, entry, now_unix_millis) {
255            Ok(verified) => live.push(verified),
256            Err(reject) => rejects.push(reject),
257        }
258    }
259    (live, rejects)
260}
261
262fn is_lowercase_hex(value: &str) -> bool {
263    !value.is_empty()
264        && value.len().is_multiple_of(2)
265        && value
266            .bytes()
267            .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
268}
269
270fn decode_lowercase_hex<const N: usize>(value: &str) -> Option<[u8; N]> {
271    if value.len() != N * 2 {
272        return None;
273    }
274    if !value
275        .bytes()
276        .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
277    {
278        return None;
279    }
280    let mut out = [0u8; N];
281    hex::decode_to_slice(value.as_bytes(), &mut out).ok()?;
282    Some(out)
283}
284
285/// Fixed published vector both weft and heddle pin their tests to.
286pub mod conformance {
287    /// 32-byte Ed25519 seed for the deployment-descriptor root.
288    pub const ROOT_SECRET_SEED: [u8; 32] = [
289        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
290        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
291        0x1e, 0x1f,
292    ];
293    /// 32-byte Ed25519 seed for the attested ephemeral key.
294    pub const EPHEMERAL_SECRET_SEED: [u8; 32] = [
295        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
296        0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d,
297        0x3e, 0x3f,
298    ];
299    pub const ROOT_SECRET_SEED_HEX: &str =
300        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
301    pub const EPHEMERAL_SECRET_SEED_HEX: &str =
302        "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f";
303    pub const EPHEMERAL_KEY_ID: &str = "ephemeral-us-west-1";
304    pub const NOT_BEFORE_UNIX_MILLIS: i64 = 1_700_000_000_000;
305    pub const NOT_AFTER_UNIX_MILLIS: i64 = 1_800_000_000_000;
306    pub const REGION: &str = "us-west";
307    pub const RELAY_URL: &str = "https://relay.example.test";
308    pub const DIRECT_ADDRESS: &str = "203.0.113.7:4433";
309
310    pub const ROOT_PUBLIC_KEY_HEX: &str =
311        "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
312    pub const EPHEMERAL_PUBLIC_KEY_HEX: &str =
313        "29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7";
314    pub const CANONICAL_BYTES_HEX: &str = "686564646c652d64657363726970746f722d726f6f742d6174746573746174696f6e2d76310a6b696e643d33353a64657363726970746f722d657068656d6572616c2d6174746573746174696f6e2d76310a657068656d6572616c5f6b65795f69643d31393a657068656d6572616c2d75732d776573742d310a657068656d6572616c5f7075626c69635f6b65793d33323a29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd70a6e6f745f6265666f72655f756e69785f6d696c6c69733d383a0000018bcfe568000a6e6f745f61667465725f756e69785f6d696c6c69733d383a000001a3185c50000a726567696f6e3d373a75732d77657374";
315    pub const ATTESTATION_SIGNATURE_HEX: &str = "ca633a465754c149d13e65eaa20d885c241cd1ff4202bb26f060d51ecc307d18c040f5ecf9ee85e8d96567bb869274477e69aece3366ee2ae7b00c8ddac12e0f";
316    pub const SIGNED_DESCRIPTOR_HEX: &str = "0a8e0108011240323961636261653134316263636166306232326531613934643334643062633733363165353236643062666531326338393739346263393332323936366464371a1a68747470733a2f2f72656c61792e6578616d706c652e74657374220c686564646c652d6170692f312a103230332e302e3131332e373a343433333080d095ffbc313880a0f1c2b1341213657068656d6572616c2d75732d776573742d311a407bfee156bb5e68572682c111803c7fc10d7cd18597e33ef8ee512ee24fe0118abad7c30f135b9ba6afdec2985537d0b564261067d25d36a9b9bfa09434d4cf05";
317}
318
319#[cfg(test)]
320mod tests {
321    use super::conformance::{
322        ATTESTATION_SIGNATURE_HEX, CANONICAL_BYTES_HEX, DIRECT_ADDRESS, EPHEMERAL_KEY_ID,
323        EPHEMERAL_PUBLIC_KEY_HEX, EPHEMERAL_SECRET_SEED, EPHEMERAL_SECRET_SEED_HEX,
324        NOT_AFTER_UNIX_MILLIS, NOT_BEFORE_UNIX_MILLIS, REGION, RELAY_URL, ROOT_PUBLIC_KEY_HEX,
325        ROOT_SECRET_SEED, ROOT_SECRET_SEED_HEX, SIGNED_DESCRIPTOR_HEX,
326    };
327    use super::*;
328    use crate::HOSTED_ALPN_V1;
329    use ed25519_dalek::{Signer, SigningKey};
330
331    struct Regenerated {
332        root_public_key: [u8; 32],
333        ephemeral_public_key: [u8; 32],
334        canonical_bytes: Vec<u8>,
335        attestation_signature: [u8; 64],
336        signed_descriptor_hex: String,
337        entry: AttestedEndpointDescriptorEntry,
338    }
339
340    fn regenerate() -> Regenerated {
341        let root = SigningKey::from_bytes(&ROOT_SECRET_SEED);
342        let ephemeral = SigningKey::from_bytes(&EPHEMERAL_SECRET_SEED);
343        let root_public_key = root.verifying_key().to_bytes();
344        let ephemeral_public_key = ephemeral.verifying_key().to_bytes();
345        let canonical_bytes = ephemeral_attestation_bytes(
346            EPHEMERAL_KEY_ID,
347            &ephemeral_public_key,
348            NOT_BEFORE_UNIX_MILLIS,
349            NOT_AFTER_UNIX_MILLIS,
350            REGION,
351        );
352        let attestation_signature = root.sign(&canonical_bytes).to_bytes();
353        let descriptor = conformance_descriptor(&ephemeral_public_key);
354        let signed = sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, descriptor);
355        let signed_descriptor_hex = hex::encode(signed.encode_to_vec());
356        let entry = AttestedEndpointDescriptorEntry {
357            ephemeral_key_id: EPHEMERAL_KEY_ID.to_string(),
358            ephemeral_public_key: hex::encode(ephemeral_public_key),
359            not_before_unix_millis: NOT_BEFORE_UNIX_MILLIS,
360            not_after_unix_millis: NOT_AFTER_UNIX_MILLIS,
361            region: REGION.to_string(),
362            attestation_signature: hex::encode(attestation_signature),
363            signed_descriptor: signed_descriptor_hex.clone(),
364        };
365        Regenerated {
366            root_public_key,
367            ephemeral_public_key,
368            canonical_bytes,
369            attestation_signature,
370            signed_descriptor_hex,
371            entry,
372        }
373    }
374
375    fn conformance_descriptor(ephemeral_public_key: &[u8; 32]) -> EndpointDescriptor {
376        EndpointDescriptor {
377            version: 1,
378            endpoint_id: hex::encode(ephemeral_public_key),
379            relay_urls: vec![RELAY_URL.to_string()],
380            supported_alpns: vec![HOSTED_ALPN_V1.to_vec()],
381            direct_addresses: vec![DIRECT_ADDRESS.to_string()],
382            issued_at_unix_millis: NOT_BEFORE_UNIX_MILLIS,
383            expires_at_unix_millis: NOT_AFTER_UNIX_MILLIS,
384            rotation: None,
385        }
386    }
387
388    fn sign_descriptor(
389        ephemeral: &SigningKey,
390        key_id: &str,
391        descriptor: EndpointDescriptor,
392    ) -> SignedEndpointDescriptor {
393        let signature = ephemeral
394            .sign(&endpoint_descriptor_bytes(&descriptor))
395            .to_bytes()
396            .to_vec();
397        SignedEndpointDescriptor {
398            descriptor: Some(descriptor),
399            key_id: key_id.to_string(),
400            signature,
401        }
402    }
403
404    fn committed_entry() -> AttestedEndpointDescriptorEntry {
405        AttestedEndpointDescriptorEntry {
406            ephemeral_key_id: EPHEMERAL_KEY_ID.to_string(),
407            ephemeral_public_key: EPHEMERAL_PUBLIC_KEY_HEX.to_string(),
408            not_before_unix_millis: NOT_BEFORE_UNIX_MILLIS,
409            not_after_unix_millis: NOT_AFTER_UNIX_MILLIS,
410            region: REGION.to_string(),
411            attestation_signature: ATTESTATION_SIGNATURE_HEX.to_string(),
412            signed_descriptor: SIGNED_DESCRIPTOR_HEX.to_string(),
413        }
414    }
415
416    fn parse_hex_32(value: &str) -> [u8; 32] {
417        decode_lowercase_hex(value).expect("32-byte lowercase hex")
418    }
419
420    fn now() -> i64 {
421        NOT_BEFORE_UNIX_MILLIS
422    }
423
424    #[test]
425    fn domains_are_separated_from_transport_bootstrap() {
426        assert_eq!(SET_VERSION, 1);
427        assert_eq!(
428            ROOT_ATTESTATION_V1_DOMAIN,
429            "heddle-descriptor-root-attestation-v1"
430        );
431        assert_eq!(ROOT_ATTESTATION_KIND, "descriptor-ephemeral-attestation-v1");
432        assert_ne!(
433            ROOT_ATTESTATION_V1_DOMAIN,
434            crate::signing::TRANSPORT_BOOTSTRAP_SIGNING_V1_DOMAIN
435        );
436        assert_eq!(hex::encode(ROOT_SECRET_SEED), ROOT_SECRET_SEED_HEX);
437        assert_eq!(
438            hex::encode(EPHEMERAL_SECRET_SEED),
439            EPHEMERAL_SECRET_SEED_HEX
440        );
441    }
442
443    #[test]
444    fn attestation_bytes_use_length_prefixed_framing_and_be_integers() {
445        let pk = [0xaa; 32];
446        let bytes = ephemeral_attestation_bytes("ab", &pk, 1, 2, "c");
447        let expected_prefix = format!(
448            "{ROOT_ATTESTATION_V1_DOMAIN}\nkind={}:{}",
449            ROOT_ATTESTATION_KIND.len(),
450            ROOT_ATTESTATION_KIND
451        );
452        assert!(bytes.starts_with(expected_prefix.as_bytes()));
453        assert!(bytes.windows(32).any(|window| window == pk));
454        assert!(bytes.windows(8).any(|window| window == 1_i64.to_be_bytes()));
455        assert!(bytes.windows(8).any(|window| window == 2_i64.to_be_bytes()));
456        assert!(
457            !bytes
458                .windows(64)
459                .any(|window| window == hex::encode(pk).as_bytes())
460        );
461        let shifted_utf8 = ephemeral_attestation_bytes("a", &pk, 1, 2, "bc");
462        assert_ne!(bytes, shifted_utf8);
463        let shifted_window = ephemeral_attestation_bytes("ab", &pk, 0x0102, 0x03, "c");
464        let original_window = ephemeral_attestation_bytes("ab", &pk, 0x01, 0x0203, "c");
465        assert_ne!(shifted_window, original_window);
466    }
467
468    #[test]
469    fn verify_accepts_the_committed_conformance_vector() {
470        let regenerated = regenerate();
471        assert_eq!(
472            hex::encode(regenerated.root_public_key),
473            ROOT_PUBLIC_KEY_HEX
474        );
475        assert_eq!(
476            hex::encode(regenerated.ephemeral_public_key),
477            EPHEMERAL_PUBLIC_KEY_HEX
478        );
479        assert_eq!(
480            hex::encode(&regenerated.canonical_bytes),
481            CANONICAL_BYTES_HEX
482        );
483        assert_eq!(
484            hex::encode(regenerated.attestation_signature),
485            ATTESTATION_SIGNATURE_HEX
486        );
487        assert_eq!(regenerated.signed_descriptor_hex, SIGNED_DESCRIPTOR_HEX);
488
489        let root = parse_hex_32(ROOT_PUBLIC_KEY_HEX);
490        let verified = verify_ephemeral_attestation(&root, &committed_entry(), now())
491            .expect("committed vector must verify");
492        assert_eq!(
493            verified.ephemeral_public_key,
494            regenerated.ephemeral_public_key
495        );
496        assert_eq!(
497            verified.endpoint_descriptor.endpoint_id,
498            EPHEMERAL_PUBLIC_KEY_HEX
499        );
500        assert_eq!(verified.region, REGION);
501        assert_eq!(verified.not_before_unix_millis, NOT_BEFORE_UNIX_MILLIS);
502        assert_eq!(verified.not_after_unix_millis, NOT_AFTER_UNIX_MILLIS);
503    }
504
505    #[test]
506    fn foreign_root_attestation_is_rejected() {
507        let regenerated = regenerate();
508        let foreign = SigningKey::from_bytes(&[0x41; 32])
509            .verifying_key()
510            .to_bytes();
511        assert_eq!(
512            verify_ephemeral_attestation(&foreign, &regenerated.entry, now()),
513            Err(EntryReject::InvalidSignature)
514        );
515    }
516
517    #[test]
518    fn tampering_any_attested_tuple_field_invalidates_the_signature() {
519        let regenerated = regenerate();
520        let root = regenerated.root_public_key;
521
522        let mut pubkey = regenerated.entry.clone();
523        let mut pk = parse_hex_32(&pubkey.ephemeral_public_key);
524        pk[0] ^= 0x01;
525        pubkey.ephemeral_public_key = hex::encode(pk);
526
527        let mut key_id = regenerated.entry.clone();
528        key_id.ephemeral_key_id.push('x');
529
530        let mut not_before = regenerated.entry.clone();
531        not_before.not_before_unix_millis += 1;
532
533        let mut not_after = regenerated.entry.clone();
534        not_after.not_after_unix_millis -= 1;
535
536        let mut region = regenerated.entry.clone();
537        region.region.push('x');
538
539        for (label, entry) in [
540            ("pubkey", pubkey),
541            ("key_id", key_id),
542            ("not_before", not_before),
543            ("not_after", not_after),
544            ("region", region),
545        ] {
546            assert_eq!(
547                verify_ephemeral_attestation(&root, &entry, now()),
548                Err(EntryReject::InvalidSignature),
549                "{label} must be covered by the attestation signature"
550            );
551        }
552    }
553
554    #[test]
555    fn descriptor_signed_by_a_different_key_is_rejected() {
556        let regenerated = regenerate();
557        let other = SigningKey::from_bytes(&[0x42; 32]);
558        let descriptor = regenerated.entry_descriptor();
559        let mut entry = regenerated.entry.clone();
560        entry.signed_descriptor =
561            hex::encode(sign_descriptor(&other, EPHEMERAL_KEY_ID, descriptor).encode_to_vec());
562        assert_eq!(
563            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
564            Err(EntryReject::InvalidDescriptorSignature)
565        );
566    }
567
568    #[test]
569    fn descriptor_endpoint_id_must_equal_the_attested_public_key() {
570        let regenerated = regenerate();
571        let ephemeral = SigningKey::from_bytes(&EPHEMERAL_SECRET_SEED);
572        let mut descriptor = regenerated.entry_descriptor();
573        descriptor.endpoint_id = "not-the-attested-key".to_string();
574        let mut entry = regenerated.entry.clone();
575        entry.signed_descriptor =
576            hex::encode(sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, descriptor).encode_to_vec());
577        assert_eq!(
578            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
579            Err(EntryReject::DescriptorKeyMismatch)
580        );
581    }
582
583    #[test]
584    fn signed_descriptor_key_id_must_equal_ephemeral_key_id() {
585        let regenerated = regenerate();
586        let ephemeral = SigningKey::from_bytes(&EPHEMERAL_SECRET_SEED);
587        let descriptor = regenerated.entry_descriptor();
588        let mut entry = regenerated.entry.clone();
589        entry.signed_descriptor =
590            hex::encode(sign_descriptor(&ephemeral, "other-key-id", descriptor).encode_to_vec());
591        assert_eq!(
592            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
593            Err(EntryReject::DescriptorKeyMismatch)
594        );
595    }
596
597    #[test]
598    fn endpoint_descriptor_signature_cannot_verify_as_attestation() {
599        let regenerated = regenerate();
600        let root = SigningKey::from_bytes(&ROOT_SECRET_SEED);
601        let descriptor = regenerated.entry_descriptor();
602        let mut entry = regenerated.entry.clone();
603        entry.attestation_signature = hex::encode(
604            root.sign(&endpoint_descriptor_bytes(&descriptor))
605                .to_bytes(),
606        );
607        assert_eq!(
608            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
609            Err(EntryReject::InvalidSignature)
610        );
611    }
612
613    #[test]
614    fn attestation_signature_cannot_verify_as_descriptor_signature() {
615        let regenerated = regenerate();
616        let ephemeral = SigningKey::from_bytes(&EPHEMERAL_SECRET_SEED);
617        let descriptor = regenerated.entry_descriptor();
618        let mut signed = sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, descriptor);
619        signed.signature = ephemeral
620            .sign(&regenerated.canonical_bytes)
621            .to_bytes()
622            .to_vec();
623        let mut entry = regenerated.entry.clone();
624        entry.signed_descriptor = hex::encode(signed.encode_to_vec());
625        assert_eq!(
626            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
627            Err(EntryReject::InvalidDescriptorSignature)
628        );
629    }
630
631    #[test]
632    fn expired_and_not_yet_valid_entries_are_excluded() {
633        let regenerated = regenerate();
634        let root = regenerated.root_public_key;
635        assert_eq!(
636            verify_ephemeral_attestation(&root, &regenerated.entry, NOT_BEFORE_UNIX_MILLIS - 1),
637            Err(EntryReject::NotYetValid)
638        );
639        assert_eq!(
640            verify_ephemeral_attestation(&root, &regenerated.entry, NOT_AFTER_UNIX_MILLIS),
641            Err(EntryReject::Expired)
642        );
643        assert_eq!(
644            verify_ephemeral_attestation(&root, &regenerated.entry, NOT_AFTER_UNIX_MILLIS - 1)
645                .map(|verified| verified.ephemeral_public_key),
646            Ok(regenerated.ephemeral_public_key)
647        );
648    }
649
650    #[test]
651    fn inverted_window_all_zero_signature_and_malformed_descriptor_are_rejected() {
652        let regenerated = regenerate();
653        let root = regenerated.root_public_key;
654        let mut inverted = regenerated.entry.clone();
655        inverted.not_before_unix_millis = NOT_AFTER_UNIX_MILLIS;
656        inverted.not_after_unix_millis = NOT_BEFORE_UNIX_MILLIS;
657        assert_eq!(
658            verify_ephemeral_attestation(&root, &inverted, now()),
659            Err(EntryReject::InvalidWindow)
660        );
661        let mut missing = regenerated.entry.clone();
662        missing.attestation_signature.clear();
663        assert_eq!(
664            verify_ephemeral_attestation(&root, &missing, now()),
665            Err(EntryReject::MissingSignature)
666        );
667        let mut unattested = regenerated.entry.clone();
668        unattested.attestation_signature = "00".repeat(64);
669        assert_eq!(
670            verify_ephemeral_attestation(&root, &unattested, now()),
671            Err(EntryReject::Unattested)
672        );
673        let mut malformed = regenerated.entry.clone();
674        malformed.signed_descriptor = "zz".to_string();
675        assert_eq!(
676            verify_ephemeral_attestation(&root, &malformed, now()),
677            Err(EntryReject::MalformedDescriptor)
678        );
679        let mut empty_key = regenerated.entry.clone();
680        empty_key.ephemeral_key_id.clear();
681        assert_eq!(
682            verify_ephemeral_attestation(&root, &empty_key, now()),
683            Err(EntryReject::InvalidKey)
684        );
685        let mut uppercase = regenerated.entry.clone();
686        uppercase.ephemeral_public_key = uppercase.ephemeral_public_key.to_uppercase();
687        assert_eq!(
688            verify_ephemeral_attestation(&root, &uppercase, now()),
689            Err(EntryReject::InvalidKey)
690        );
691    }
692
693    #[test]
694    fn trusted_live_entries_drops_rejects_and_keeps_only_two_layer_successes() {
695        let regenerated = regenerate();
696        let mut tampered = regenerated.entry.clone();
697        tampered.region.push('x');
698        let set = EndpointDescriptorSetDocument {
699            version: SET_VERSION,
700            root_key_id: "root-1".to_string(),
701            entries: vec![regenerated.entry.clone(), tampered],
702        };
703        let (live, rejects) = trusted_live_entries(&set, &regenerated.root_public_key, now());
704        assert_eq!(live.len(), 1);
705        assert_eq!(
706            live[0].ephemeral_public_key,
707            regenerated.ephemeral_public_key
708        );
709        assert_eq!(rejects, vec![EntryReject::InvalidSignature]);
710
711        let (live, rejects) =
712            trusted_live_entries(&set, &regenerated.root_public_key, NOT_AFTER_UNIX_MILLIS);
713        assert!(live.is_empty());
714        assert_eq!(
715            rejects,
716            vec![EntryReject::Expired, EntryReject::InvalidSignature]
717        );
718    }
719
720    #[test]
721    fn mismatched_document_version_is_rejected_fail_closed() {
722        let regenerated = regenerate();
723        for version in [0_u8, 2, 255] {
724            let set = EndpointDescriptorSetDocument {
725                version,
726                root_key_id: "root-1".to_string(),
727                entries: vec![regenerated.entry.clone()],
728            };
729            let (live, rejects) = trusted_live_entries(&set, &regenerated.root_public_key, now());
730            assert!(
731                live.is_empty(),
732                "version {version} must not promote entries"
733            );
734            assert_eq!(rejects, vec![EntryReject::UnsupportedVersion]);
735            let body = serde_json::to_vec(&set).expect("serialize set");
736            assert_eq!(
737                parse_endpoint_descriptor_set(&body),
738                Err(DescriptorSetError::UnsupportedVersion(version))
739            );
740        }
741
742        let v1 = EndpointDescriptorSetDocument {
743            version: SET_VERSION,
744            root_key_id: "root-1".to_string(),
745            entries: vec![regenerated.entry.clone()],
746        };
747        let parsed = parse_endpoint_descriptor_set(&serde_json::to_vec(&v1).expect("serialize v1"))
748            .expect("version 1 must parse");
749        assert_eq!(parsed, v1);
750    }
751
752    #[test]
753    fn inner_descriptor_version_and_validity_window_are_enforced() {
754        let regenerated = regenerate();
755        let ephemeral = SigningKey::from_bytes(&EPHEMERAL_SECRET_SEED);
756        let root = regenerated.root_public_key;
757
758        let mut expired = regenerated.entry_descriptor();
759        expired.expires_at_unix_millis = now();
760        let mut expired_entry = regenerated.entry.clone();
761        expired_entry.signed_descriptor =
762            hex::encode(sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, expired).encode_to_vec());
763        assert_eq!(
764            verify_ephemeral_attestation(&root, &expired_entry, now()),
765            Err(EntryReject::DescriptorExpired)
766        );
767
768        let mut not_yet = regenerated.entry_descriptor();
769        not_yet.issued_at_unix_millis = now() + 1;
770        let mut not_yet_entry = regenerated.entry.clone();
771        not_yet_entry.signed_descriptor =
772            hex::encode(sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, not_yet).encode_to_vec());
773        assert_eq!(
774            verify_ephemeral_attestation(&root, &not_yet_entry, now()),
775            Err(EntryReject::DescriptorNotYetValid)
776        );
777
778        let mut unsupported = regenerated.entry_descriptor();
779        unsupported.version = 0;
780        let mut unsupported_entry = regenerated.entry.clone();
781        unsupported_entry.signed_descriptor =
782            hex::encode(sign_descriptor(&ephemeral, EPHEMERAL_KEY_ID, unsupported).encode_to_vec());
783        assert_eq!(
784            verify_ephemeral_attestation(&root, &unsupported_entry, now()),
785            Err(EntryReject::UnsupportedDescriptorVersion)
786        );
787    }
788
789    #[test]
790    fn signed_descriptor_hex_must_be_lowercase() {
791        let regenerated = regenerate();
792        let mut entry = regenerated.entry.clone();
793        entry.signed_descriptor = entry.signed_descriptor.to_uppercase();
794        assert_eq!(
795            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
796            Err(EntryReject::MalformedDescriptor)
797        );
798    }
799
800    #[test]
801    fn whitespace_only_ephemeral_key_id_is_rejected() {
802        let regenerated = regenerate();
803        let mut entry = regenerated.entry.clone();
804        entry.ephemeral_key_id = " \t ".to_string();
805        assert_eq!(
806            verify_ephemeral_attestation(&regenerated.root_public_key, &entry, now()),
807            Err(EntryReject::InvalidKey)
808        );
809    }
810
811    impl Regenerated {
812        fn entry_descriptor(&self) -> EndpointDescriptor {
813            let signed_bytes = hex::decode(&self.entry.signed_descriptor).unwrap();
814            SignedEndpointDescriptor::decode(signed_bytes.as_slice())
815                .unwrap()
816                .descriptor
817                .unwrap()
818        }
819    }
820}