1use 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
15pub const SET_VERSION: u8 = 1;
17pub const ROOT_ATTESTATION_V1_DOMAIN: &str = "heddle-descriptor-root-attestation-v1";
21pub const ROOT_ATTESTATION_KIND: &str = "descriptor-ephemeral-attestation-v1";
23
24#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct AttestedEndpointDescriptorEntry {
35 pub ephemeral_key_id: String,
36 pub ephemeral_public_key: String,
38 pub not_before_unix_millis: i64,
39 pub not_after_unix_millis: i64,
40 pub region: String,
41 pub attestation_signature: String,
43 pub signed_descriptor: String,
45}
46
47#[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#[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#[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
99pub 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
131pub 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
229pub 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
241pub 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
285pub mod conformance {
287 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 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(®enerated.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, ®enerated.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(®enerated.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(®enerated.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(®enerated.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(®enerated.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(®enerated.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(®enerated.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, ®enerated.entry, NOT_BEFORE_UNIX_MILLIS - 1),
637 Err(EntryReject::NotYetValid)
638 );
639 assert_eq!(
640 verify_ephemeral_attestation(&root, ®enerated.entry, NOT_AFTER_UNIX_MILLIS),
641 Err(EntryReject::Expired)
642 );
643 assert_eq!(
644 verify_ephemeral_attestation(&root, ®enerated.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, ®enerated.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, ®enerated.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, ®enerated.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, ¬_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(®enerated.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(®enerated.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}