1use ring::signature::{ED25519, UnparsedPublicKey};
2use serde::Deserialize;
3use sha2::{Digest, Sha256};
4
5pub struct VerifiedGenerationKeyApplicabilityReceipt {
9 public_key: [u8; 32],
10 key_identity: [u8; 32],
11 source: [u8; 32],
12 build: [u8; 32],
13 machine: [u8; 32],
14}
15
16pub struct VerifiedSignedDeploymentGenerationOwner {
18 generation: u64,
19 source: [u8; 32],
20 build: [u8; 32],
21}
22
23pub struct VerifiedGeneratedComponentGenerationOwner {
25 generation: u64,
26 source: [u8; 32],
27 build: [u8; 32],
28 machine: [u8; 32],
29 root: [u8; 32],
30 actual_composed: Option<[u8; 32]>,
31 signed_subject: [u8; 32],
32 issuance_nonce: [u8; 32],
33}
34
35impl core::fmt::Debug for VerifiedGeneratedComponentGenerationOwner {
36 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37 f.write_str("VerifiedGeneratedComponentGenerationOwner(<redacted>)")
38 }
39}
40
41pub struct VerifiedComponentGenerationPairingReceipt {
44 binding: [u8; 32],
45}
46
47pub(crate) fn deployment_generation_half(
48 generation: u64,
49 source: [u8; 32],
50 build: [u8; 32],
51) -> VerifiedSignedDeploymentGenerationOwner {
52 VerifiedSignedDeploymentGenerationOwner {
53 generation,
54 source,
55 build,
56 }
57}
58
59#[cfg(test)]
60pub(crate) fn fixture_generated_generation_half(
61 generation: u64,
62) -> VerifiedGeneratedComponentGenerationOwner {
63 VerifiedGeneratedComponentGenerationOwner {
64 generation,
65 source: [5; 32],
66 build: [4; 32],
67 machine: [0x41; 32],
68 root: [0x42; 32],
69 actual_composed: Some([0x43; 32]),
70 signed_subject: [0x44; 32],
71 issuance_nonce: [0x45; 32],
72 }
73}
74
75#[cfg(test)]
76pub(crate) fn fixture_generated_generation_v2_owner(
77 generation: u64,
78) -> VerifiedGeneratedComponentGenerationV2Owner {
79 let subject = [0xc1; 32];
80 let nonce = [0xc2; 32];
81 VerifiedGeneratedComponentGenerationV2Owner {
82 aggregate: VerifiedGeneratedComponentGenerationOwner {
83 generation,
84 source: [5; 32],
85 build: [4; 32],
86 machine: [0x33; 32],
87 root: [0x34; 32],
88 actual_composed: Some([0x35; 32]),
89 signed_subject: subject,
90 issuance_nonce: nonce,
91 },
92 observability: VerifiedObservabilityPairedReceipt {
93 build: VerifiedObservabilityBuildStaticOwner {
94 subject,
95 issuance_nonce: nonce,
96 constituent: [0xc3; 32],
97 },
98 half: VerifiedFiveLeafObservabilityV2Half {
99 half: VerifiedObservabilityConstituentGenerationHalf {
100 subject,
101 issuance_nonce: nonce,
102 constituent: [0xc3; 32],
103 leaf: [0xc4; 32],
104 profile: [1; 6],
105 component_identities: [[0xc5; 32]; 4],
106 config_schema: [0xc6; 32],
107 config: [0xc7; 32],
108 config_values: [1; 5],
109 work: [0xc8; 32],
110 sizes: [1; 4],
111 alignments: [1; 4],
112 offsets: [0; 4],
113 aggregate_layout: [1, 1],
114 payload_machine_bytes: 1,
115 source: [5; 32],
116 build: [4; 32],
117 machine: [0x33; 32],
118 root: [0x34; 32],
119 generation,
120 },
121 },
122 },
123 }
124}
125
126#[cfg(test)]
127pub(crate) fn fixture_upgrade_generated_generation_v2(
128 aggregate: VerifiedGeneratedComponentGenerationOwner,
129) -> VerifiedGeneratedComponentGenerationV2FinalTransaction {
130 let mut owner = fixture_generated_generation_v2_owner(1);
131 owner.aggregate = aggregate;
132 begin_generated_component_generation_v2_final_transaction(owner)
133}
134
135pub fn consume_component_generation_pairing(
138 receipt: VerifiedComponentGenerationPairingReceipt,
139) -> Result<(), VerifiedComponentGenerationPairingReceipt> {
140 if receipt.binding != [0; 32] {
141 Ok(())
142 } else {
143 Err(receipt)
144 }
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub enum GenerationPairError {
149 Encoding,
150 Signature,
151 Foreign,
152}
153
154#[cfg(test)]
155#[derive(Deserialize)]
156#[serde(deny_unknown_fields)]
157struct FiveLeafGenerationDocument {
158 schema: String,
159 domain: String,
160 usage: String,
161 key_identity: String,
162 source_identity: String,
163 build_identity: String,
164 machine_identity: String,
165 root_identity: String,
166 owner_generation: u64,
167}
168
169pub struct CanonicalSignedFiveLeafGenerationV2Input {
172 canonical: Vec<u8>,
173 signature: Vec<u8>,
174}
175
176impl CanonicalSignedFiveLeafGenerationV2Input {
177 #[cfg(test)]
178 fn new(canonical: Vec<u8>, signature: Vec<u8>) -> Self {
179 Self {
180 canonical,
181 signature,
182 }
183 }
184}
185
186pub(crate) const FIVE_LEAF_V2_DOMAIN: &[u8] = b"saddle/f08/five-leaf-generation/v2";
187pub(crate) const FIVE_LEAF_V2_USAGE: &[u8] = b"capacity.five-leaf.generation";
188pub(crate) const APPROVED_OBSERVATION_SHA256: [u8; 32] = [
189 0x6e, 0x6b, 0x0f, 0x8e, 0xf9, 0xed, 0xfa, 0x53, 0x0b, 0x0c, 0xfe, 0x32, 0x6f, 0xc5, 0xd3, 0x83,
190 0x04, 0x1c, 0xa3, 0x27, 0x73, 0x2a, 0x79, 0xb4, 0x8e, 0xda, 0x15, 0xaa, 0xb7, 0xe7, 0x5f, 0x13,
191];
192const APPROVED_OBSERVABILITY_LEAF: [u8; 32] = [
193 0xb4, 0x67, 0x26, 0xfd, 0x1a, 0x64, 0xc8, 0xb9, 0xbd, 0x50, 0xa8, 0x87, 0x0c, 0xcd, 0x1c, 0x3a,
194 0x3a, 0xee, 0xdb, 0x48, 0x1c, 0xb0, 0x12, 0x22, 0xbc, 0xb7, 0xf5, 0xe6, 0xe1, 0xe1, 0x4e, 0xe8,
195];
196
197pub struct FiveLeafV2SigningRequest {
200 canonical: Vec<u8>,
201 request_nonce: [u8; 32],
202 observation_sha256: [u8; 32],
203 approved_lock: [u8; 32],
204 source: [u8; 32],
205 build: [u8; 32],
206 machine: [u8; 32],
207 root: [u8; 32],
208 generation: u64,
209 key_identity: [u8; 32],
210 domain: [u8; 32],
211 usage: [u8; 32],
212 approval_identity: [u8; 32],
213 custody_policy_identity: [u8; 32],
214}
215
216pub struct FiveLeafV2SignerResponse {
219 canonical: Vec<u8>,
220 request_nonce: [u8; 32],
221 signature: Vec<u8>,
222 public_key: [u8; 32],
223 key_identity: [u8; 32],
224 domain: [u8; 32],
225 usage: [u8; 32],
226 approval_identity: [u8; 32],
227 custody_policy_identity: [u8; 32],
228}
229
230pub struct VerifiedFiveLeafV2SignerPolicyOwner {
233 public_key: [u8; 32],
234 key_identity: [u8; 32],
235 approved_lock: [u8; 32],
236 source: [u8; 32],
237 build: [u8; 32],
238 machine: [u8; 32],
239 root: [u8; 32],
240 generation: u64,
241 observation_sha256: [u8; 32],
242 domain: [u8; 32],
243 usage: [u8; 32],
244 approval_identity: [u8; 32],
245 custody_policy_identity: [u8; 32],
246 request_nonce: [u8; 32],
247}
248
249pub struct VerifiedFiveLeafV2SigningRequestApprovalOwner {
252 request_nonce: [u8; 32],
253 approval_identity: [u8; 32],
254 custody_policy_identity: [u8; 32],
255 approved_lock: [u8; 32],
256 observation_sha256: [u8; 32],
257 source: [u8; 32],
258 build: [u8; 32],
259 machine: [u8; 32],
260 root: [u8; 32],
261 generation: u64,
262}
263
264pub(crate) struct FiveLeafV2ApprovedSignerFacts {
265 pub(crate) public_key: [u8; 32],
266 pub(crate) key_identity: [u8; 32],
267 pub(crate) approved_lock: [u8; 32],
268 pub(crate) source: [u8; 32],
269 pub(crate) build: [u8; 32],
270 pub(crate) machine: [u8; 32],
271 pub(crate) root: [u8; 32],
272 pub(crate) generation: u64,
273 pub(crate) observation_sha256: [u8; 32],
274 pub(crate) domain: [u8; 32],
275 pub(crate) usage: [u8; 32],
276 pub(crate) approval_identity: [u8; 32],
277 pub(crate) custody_policy_identity: [u8; 32],
278 pub(crate) request_nonce: [u8; 32],
279}
280
281pub(crate) fn issue_five_leaf_v2_signer_policy(
282 facts: FiveLeafV2ApprovedSignerFacts,
283) -> (
284 VerifiedFiveLeafV2SignerPolicyOwner,
285 VerifiedFiveLeafV2SigningRequestApprovalOwner,
286) {
287 (
288 VerifiedFiveLeafV2SignerPolicyOwner {
289 public_key: facts.public_key,
290 key_identity: facts.key_identity,
291 approved_lock: facts.approved_lock,
292 source: facts.source,
293 build: facts.build,
294 machine: facts.machine,
295 root: facts.root,
296 generation: facts.generation,
297 observation_sha256: facts.observation_sha256,
298 domain: facts.domain,
299 usage: facts.usage,
300 approval_identity: facts.approval_identity,
301 custody_policy_identity: facts.custody_policy_identity,
302 request_nonce: facts.request_nonce,
303 },
304 VerifiedFiveLeafV2SigningRequestApprovalOwner {
305 request_nonce: facts.request_nonce,
306 approval_identity: facts.approval_identity,
307 custody_policy_identity: facts.custody_policy_identity,
308 approved_lock: facts.approved_lock,
309 observation_sha256: facts.observation_sha256,
310 source: facts.source,
311 build: facts.build,
312 machine: facts.machine,
313 root: facts.root,
314 generation: facts.generation,
315 },
316 )
317}
318
319macro_rules! redacted_signing_debug {
320 ($type:ty, $name:literal) => {
321 impl core::fmt::Debug for $type {
322 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323 f.write_str(concat!($name, "(<redacted>)"))
324 }
325 }
326 };
327}
328
329redacted_signing_debug!(FiveLeafV2SigningRequest, "FiveLeafV2SigningRequest");
330redacted_signing_debug!(FiveLeafV2SignerResponse, "FiveLeafV2SignerResponse");
331redacted_signing_debug!(
332 VerifiedFiveLeafV2SignerPolicyOwner,
333 "VerifiedFiveLeafV2SignerPolicyOwner"
334);
335redacted_signing_debug!(
336 VerifiedFiveLeafV2SigningRequestApprovalOwner,
337 "VerifiedFiveLeafV2SigningRequestApprovalOwner"
338);
339
340#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341pub enum FiveLeafV2SigningError {
342 Encoding,
343 Foreign,
344 Signature,
345}
346
347#[derive(Deserialize)]
348#[serde(deny_unknown_fields)]
349struct FiveLeafV2SignerResponseDocument {
350 schema: String,
351 canonical: Vec<u8>,
352 request_nonce: String,
353 signature: Vec<u8>,
354 public_key: String,
355 key_identity: String,
356 domain: String,
357 usage: String,
358 approval_identity: String,
359 custody_policy_identity: String,
360}
361
362#[doc(hidden)]
365pub fn capture_untrusted_five_leaf_v2_signer_response(
366 envelope: Vec<u8>,
367) -> Result<FiveLeafV2SignerResponse, (FiveLeafV2SigningError, Vec<u8>)> {
368 if !canonical_json(&envelope) {
369 return Err((FiveLeafV2SigningError::Encoding, envelope));
370 }
371 let document: FiveLeafV2SignerResponseDocument = match serde_json::from_slice(&envelope) {
372 Ok(value) => value,
373 Err(_) => return Err((FiveLeafV2SigningError::Encoding, envelope)),
374 };
375 let Some(request_nonce) = decode_identity(&document.request_nonce) else {
376 return Err((FiveLeafV2SigningError::Encoding, envelope));
377 };
378 let Some(public_key) = decode_identity(&document.public_key) else {
379 return Err((FiveLeafV2SigningError::Encoding, envelope));
380 };
381 let Some(key_identity) = decode_identity(&document.key_identity) else {
382 return Err((FiveLeafV2SigningError::Encoding, envelope));
383 };
384 let Some(domain) = decode_identity(&document.domain) else {
385 return Err((FiveLeafV2SigningError::Encoding, envelope));
386 };
387 let Some(usage) = decode_identity(&document.usage) else {
388 return Err((FiveLeafV2SigningError::Encoding, envelope));
389 };
390 let Some(approval_identity) = decode_identity(&document.approval_identity) else {
391 return Err((FiveLeafV2SigningError::Encoding, envelope));
392 };
393 let Some(custody_policy_identity) = decode_identity(&document.custody_policy_identity) else {
394 return Err((FiveLeafV2SigningError::Encoding, envelope));
395 };
396 if document.schema != "saddle-f08-five-leaf-v2-signer-response/1"
397 || document.signature.len() != 64
398 {
399 return Err((FiveLeafV2SigningError::Encoding, envelope));
400 }
401 Ok(FiveLeafV2SignerResponse {
402 canonical: document.canonical,
403 request_nonce,
404 signature: document.signature,
405 public_key,
406 key_identity,
407 domain,
408 usage,
409 approval_identity,
410 custody_policy_identity,
411 })
412}
413
414#[allow(clippy::result_large_err)]
418pub fn prepare_five_leaf_v2_signing_request(
419 canonical: Vec<u8>,
420 production_observation: Vec<u8>,
421 policy: VerifiedFiveLeafV2SignerPolicyOwner,
422 approval: VerifiedFiveLeafV2SigningRequestApprovalOwner,
423) -> Result<
424 (
425 FiveLeafV2SigningRequest,
426 VerifiedFiveLeafV2SignerPolicyOwner,
427 ),
428 (
429 FiveLeafV2SigningError,
430 Vec<u8>,
431 Vec<u8>,
432 VerifiedFiveLeafV2SignerPolicyOwner,
433 VerifiedFiveLeafV2SigningRequestApprovalOwner,
434 ),
435> {
436 macro_rules! fail {
437 ($error:expr) => {
438 return Err(($error, canonical, production_observation, policy, approval))
439 };
440 }
441 if approval.request_nonce == [0; 32]
442 || sha256(&production_observation) != APPROVED_OBSERVATION_SHA256
443 || approval.approved_lock == [0; 32]
444 || approval.approved_lock != policy.approved_lock
445 || approval.observation_sha256 != policy.observation_sha256
446 || approval.source != policy.source
447 || approval.build != policy.build
448 || approval.machine != policy.machine
449 || approval.root != policy.root
450 || approval.generation != policy.generation
451 || approval.approval_identity != policy.approval_identity
452 || approval.custody_policy_identity != policy.custody_policy_identity
453 || approval.request_nonce != policy.request_nonce
454 || !canonical_json(&canonical)
455 {
456 fail!(FiveLeafV2SigningError::Foreign);
457 }
458 let doc: FiveLeafGenerationV2Document = match serde_json::from_slice(&canonical) {
459 Ok(value) => value,
460 Err(_) => fail!(FiveLeafV2SigningError::Encoding),
461 };
462 let Some(source) = decode_identity(&doc.source_identity) else {
463 fail!(FiveLeafV2SigningError::Encoding);
464 };
465 let Some(build) = decode_identity(&doc.build_identity) else {
466 fail!(FiveLeafV2SigningError::Encoding);
467 };
468 let Some(machine) = decode_identity(&doc.machine_identity) else {
469 fail!(FiveLeafV2SigningError::Encoding);
470 };
471 let Some(root) = decode_identity(&doc.root_identity) else {
472 fail!(FiveLeafV2SigningError::Encoding);
473 };
474 let Some(key_identity) = decode_identity(&doc.key_identity) else {
475 fail!(FiveLeafV2SigningError::Encoding);
476 };
477 if doc.schema != "saddle-f08-five-leaf-generation/2"
478 || doc.domain != hex(&sha256(FIVE_LEAF_V2_DOMAIN))
479 || doc.usage != hex(&sha256(FIVE_LEAF_V2_USAGE))
480 || decode_identity(&doc.observability.leaf_identity) != Some(APPROVED_OBSERVABILITY_LEAF)
481 || source != policy.source
482 || build != policy.build
483 || machine != policy.machine
484 || root != policy.root
485 || doc.owner_generation != policy.generation
486 || policy.observation_sha256 != APPROVED_OBSERVATION_SHA256
487 || key_identity != policy.key_identity
488 || policy.key_identity != sha256(&policy.public_key)
489 || policy.domain != sha256(FIVE_LEAF_V2_DOMAIN)
490 || policy.usage != sha256(FIVE_LEAF_V2_USAGE)
491 || doc.owner_generation == 0
492 {
493 fail!(FiveLeafV2SigningError::Foreign);
494 }
495 Ok((
496 FiveLeafV2SigningRequest {
497 canonical,
498 request_nonce: approval.request_nonce,
499 observation_sha256: APPROVED_OBSERVATION_SHA256,
500 approved_lock: approval.approved_lock,
501 source,
502 build,
503 machine,
504 root,
505 generation: doc.owner_generation,
506 key_identity,
507 domain: policy.domain,
508 usage: policy.usage,
509 approval_identity: policy.approval_identity,
510 custody_policy_identity: policy.custody_policy_identity,
511 },
512 policy,
513 ))
514}
515
516#[allow(clippy::result_large_err)]
519pub fn verify_five_leaf_v2_signing_response(
520 request: FiveLeafV2SigningRequest,
521 response: FiveLeafV2SignerResponse,
522 policy: VerifiedFiveLeafV2SignerPolicyOwner,
523) -> Result<
524 (
525 CanonicalSignedFiveLeafGenerationV2Input,
526 VerifiedGenerationKeyApplicabilityReceipt,
527 ),
528 (
529 FiveLeafV2SigningError,
530 FiveLeafV2SigningRequest,
531 FiveLeafV2SignerResponse,
532 VerifiedFiveLeafV2SignerPolicyOwner,
533 ),
534> {
535 macro_rules! fail {
536 ($error:expr) => {
537 return Err(($error, request, response, policy))
538 };
539 }
540 if request.canonical != response.canonical
541 || request.request_nonce != response.request_nonce
542 || request.observation_sha256 != APPROVED_OBSERVATION_SHA256
543 || request.approved_lock != policy.approved_lock
544 || request.source != policy.source
545 || request.build != policy.build
546 || request.machine != policy.machine
547 || request.root != policy.root
548 || request.generation != policy.generation
549 || request.observation_sha256 != policy.observation_sha256
550 || request.key_identity != response.key_identity
551 || response.key_identity != policy.key_identity
552 || sha256(&response.public_key) != policy.key_identity
553 || response.public_key != policy.public_key
554 || request.domain != response.domain
555 || response.domain != policy.domain
556 || request.usage != response.usage
557 || response.usage != policy.usage
558 || request.approval_identity != response.approval_identity
559 || response.approval_identity != policy.approval_identity
560 || request.custody_policy_identity != response.custody_policy_identity
561 || response.custody_policy_identity != policy.custody_policy_identity
562 {
563 fail!(FiveLeafV2SigningError::Foreign);
564 }
565 if UnparsedPublicKey::new(&ED25519, response.public_key)
566 .verify(&request.canonical, &response.signature)
567 .is_err()
568 {
569 fail!(FiveLeafV2SigningError::Signature);
570 }
571 Ok((
572 CanonicalSignedFiveLeafGenerationV2Input {
573 canonical: request.canonical,
574 signature: response.signature,
575 },
576 VerifiedGenerationKeyApplicabilityReceipt {
577 public_key: policy.public_key,
578 key_identity: policy.key_identity,
579 source: policy.source,
580 build: policy.build,
581 machine: policy.machine,
582 },
583 ))
584}
585
586#[derive(Deserialize)]
587#[serde(deny_unknown_fields)]
588struct FiveLeafGenerationV2Document {
589 schema: String,
590 domain: String,
591 usage: String,
592 key_identity: String,
593 source_identity: String,
594 build_identity: String,
595 machine_identity: String,
596 root_identity: String,
597 owner_generation: u64,
598 observability: ObservabilityConstituentDocument,
599}
600
601#[derive(Deserialize)]
602#[serde(deny_unknown_fields)]
603struct ObservabilityConstituentDocument {
604 domain: String,
605 leaf_identity: String,
606 profile_identity: String,
607 profile: [u64; 6],
608 component_identities: [String; 4],
609 config_schema_identity: String,
610 config_identity: String,
611 config_values: [u64; 5],
612 work_identity: String,
613 sizes: [u64; 4],
614 alignments: [u64; 4],
615 offsets: [u64; 4],
616 aggregate_layout: [u64; 2],
617 payload_machine_bytes: u64,
618 source_identity: String,
619 build_identity: String,
620 machine_identity: String,
621 root_identity: String,
622 owner_generation: u64,
623 constituent_identity: String,
624}
625
626struct VerifiedObservabilityConstituentGenerationHalf {
627 subject: [u8; 32],
628 issuance_nonce: [u8; 32],
629 constituent: [u8; 32],
630 leaf: [u8; 32],
631 profile: [u64; 6],
632 component_identities: [[u8; 32]; 4],
633 config_schema: [u8; 32],
634 config: [u8; 32],
635 config_values: [u64; 5],
636 work: [u8; 32],
637 sizes: [u64; 4],
638 alignments: [u64; 4],
639 offsets: [u64; 4],
640 aggregate_layout: [u64; 2],
641 payload_machine_bytes: u64,
642 source: [u8; 32],
643 build: [u8; 32],
644 machine: [u8; 32],
645 root: [u8; 32],
646 generation: u64,
647}
648
649pub struct VerifiedFiveLeafGenerationV2Owner {
650 aggregate: VerifiedGeneratedComponentGenerationOwner,
651 observability_build: VerifiedObservabilityBuildStaticOwner,
652 observability: VerifiedObservabilityConstituentGenerationHalf,
653 subject: [u8; 32],
654 issuance_nonce: [u8; 32],
655}
656
657pub struct VerifiedFiveLeafGenerationV2AggregateHalf {
658 owner: VerifiedGeneratedComponentGenerationOwner,
659}
660
661pub struct VerifiedObservabilityBuildStaticOwner {
662 subject: [u8; 32],
663 issuance_nonce: [u8; 32],
664 constituent: [u8; 32],
665}
666
667pub struct VerifiedFiveLeafObservabilityV2Half {
668 half: VerifiedObservabilityConstituentGenerationHalf,
669}
670
671pub struct VerifiedObservabilityPairedReceipt {
672 build: VerifiedObservabilityBuildStaticOwner,
673 half: VerifiedFiveLeafObservabilityV2Half,
674}
675
676pub struct VerifiedFiveLeafGenerationV2PairedOwner {
677 aggregate: VerifiedFiveLeafGenerationV2AggregateHalf,
678 observability: VerifiedObservabilityPairedReceipt,
679 view: FiveLeafGenerationV2NonAuthorityView,
680}
681
682pub struct VerifiedObservabilityCapacityView {
683 leaf: [u8; 32],
684 profile: [u64; 6],
685 component_identities: [[u8; 32]; 4],
686 sizes: [u64; 4],
687 alignments: [u64; 4],
688 offsets: [u64; 4],
689 aggregate_layout: [u64; 2],
690 payload_machine_bytes: u64,
691 work: [u8; 32],
692 machine: [u8; 32],
693 generation: u64,
694}
695
696pub struct VerifiedGeneratedComponentGenerationV2Owner {
697 #[allow(dead_code)]
698 aggregate: VerifiedGeneratedComponentGenerationOwner,
699 #[allow(dead_code)]
700 observability: VerifiedObservabilityPairedReceipt,
701}
702
703pub struct VerifiedGeneratedComponentGenerationV2FinalTransaction {
704 owner: VerifiedGeneratedComponentGenerationV2Owner,
705}
706
707pub struct FiveLeafGenerationV2NonAuthorityView {
708 subject: [u8; 32],
709 issuance_nonce: [u8; 32],
710}
711
712macro_rules! redacted_debug {
713 ($($ty:ty),+ $(,)?) => {$ (
714 impl core::fmt::Debug for $ty {
715 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
716 f.write_str(concat!(stringify!($ty), "(<redacted>)"))
717 }
718 }
719 )+};
720}
721redacted_debug!(
722 VerifiedGenerationKeyApplicabilityReceipt,
723 CanonicalSignedFiveLeafGenerationV2Input,
724 VerifiedFiveLeafGenerationV2Owner,
725 VerifiedFiveLeafGenerationV2AggregateHalf,
726 VerifiedObservabilityBuildStaticOwner,
727 VerifiedFiveLeafObservabilityV2Half,
728 VerifiedObservabilityPairedReceipt,
729 VerifiedFiveLeafGenerationV2PairedOwner,
730 VerifiedObservabilityCapacityView,
731 VerifiedGeneratedComponentGenerationV2Owner,
732 VerifiedGeneratedComponentGenerationV2FinalTransaction,
733 FiveLeafGenerationV2NonAuthorityView,
734);
735
736impl VerifiedFiveLeafGenerationV2PairedOwner {
737 pub fn observability_capacity_view(&self) -> VerifiedObservabilityCapacityView {
738 let half = &self.observability.half.half;
739 VerifiedObservabilityCapacityView {
740 leaf: half.leaf,
741 profile: half.profile,
742 component_identities: half.component_identities,
743 sizes: half.sizes,
744 alignments: half.alignments,
745 offsets: half.offsets,
746 aggregate_layout: half.aggregate_layout,
747 payload_machine_bytes: half.payload_machine_bytes,
748 work: half.work,
749 machine: half.machine,
750 generation: half.generation,
751 }
752 }
753}
754
755pub fn begin_generated_component_generation_v2_final_transaction(
756 owner: VerifiedGeneratedComponentGenerationV2Owner,
757) -> VerifiedGeneratedComponentGenerationV2FinalTransaction {
758 VerifiedGeneratedComponentGenerationV2FinalTransaction { owner }
759}
760
761pub fn restore_generated_component_generation_v2_final_transaction(
762 transaction: VerifiedGeneratedComponentGenerationV2FinalTransaction,
763) -> VerifiedGeneratedComponentGenerationV2Owner {
764 transaction.owner
765}
766
767impl VerifiedGeneratedComponentGenerationV2FinalTransaction {
768 pub(crate) fn pair_subject_recovering(
769 self,
770 subject: crate::VerifiedSignedDirectorySubjectContinuation,
771 ) -> Result<
772 (
773 Self,
774 crate::VerifiedSignedDirectorySubjectContinuation,
775 VerifiedComponentGenerationPairingReceipt,
776 ),
777 (Self, crate::VerifiedSignedDirectorySubjectContinuation),
778 > {
779 let VerifiedGeneratedComponentGenerationV2Owner {
780 aggregate,
781 observability,
782 } = self.owner;
783 match subject.authority.pair_generation_recovering(aggregate) {
784 Ok((authority, aggregate, receipt)) => Ok((
785 Self {
786 owner: VerifiedGeneratedComponentGenerationV2Owner {
787 aggregate,
788 observability,
789 },
790 },
791 crate::VerifiedSignedDirectorySubjectContinuation { authority },
792 receipt,
793 )),
794 Err((authority, aggregate)) => Err((
795 Self {
796 owner: VerifiedGeneratedComponentGenerationV2Owner {
797 aggregate,
798 observability,
799 },
800 },
801 crate::VerifiedSignedDirectorySubjectContinuation { authority },
802 )),
803 }
804 }
805}
806
807impl VerifiedObservabilityCapacityView {
808 pub fn leaf_identity(&self) -> [u8; 32] {
809 self.leaf
810 }
811 pub fn profile(&self) -> [u64; 6] {
812 self.profile
813 }
814 pub fn component_identities(&self) -> [[u8; 32]; 4] {
815 self.component_identities
816 }
817 pub fn sizes(&self) -> [u64; 4] {
818 self.sizes
819 }
820 pub fn alignments(&self) -> [u64; 4] {
821 self.alignments
822 }
823 pub fn offsets(&self) -> [u64; 4] {
824 self.offsets
825 }
826 pub fn aggregate_layout(&self) -> [u64; 2] {
827 self.aggregate_layout
828 }
829 pub fn payload_machine_bytes(&self) -> u64 {
830 self.payload_machine_bytes
831 }
832 pub fn work_identity(&self) -> [u8; 32] {
833 self.work
834 }
835 pub fn machine_identity(&self) -> [u8; 32] {
836 self.machine
837 }
838 pub fn owner_generation(&self) -> u64 {
839 self.generation
840 }
841}
842
843#[allow(clippy::result_large_err)]
844pub fn pair_verified_five_leaf_generation_v2(
845 owner: VerifiedFiveLeafGenerationV2Owner,
846) -> Result<VerifiedFiveLeafGenerationV2PairedOwner, VerifiedFiveLeafGenerationV2Owner> {
847 if owner.aggregate.signed_subject != owner.subject
848 || owner.aggregate.issuance_nonce != owner.issuance_nonce
849 || owner.observability_build.subject != owner.subject
850 || owner.observability_build.issuance_nonce != owner.issuance_nonce
851 || owner.observability.subject != owner.subject
852 || owner.observability.issuance_nonce != owner.issuance_nonce
853 || owner.observability_build.constituent != owner.observability.constituent
854 {
855 return Err(owner);
856 }
857 let VerifiedFiveLeafGenerationV2Owner {
858 aggregate,
859 observability_build,
860 observability,
861 subject,
862 issuance_nonce,
863 } = owner;
864 Ok(VerifiedFiveLeafGenerationV2PairedOwner {
865 aggregate: VerifiedFiveLeafGenerationV2AggregateHalf { owner: aggregate },
866 observability: VerifiedObservabilityPairedReceipt {
867 build: observability_build,
868 half: VerifiedFiveLeafObservabilityV2Half {
869 half: observability,
870 },
871 },
872 view: FiveLeafGenerationV2NonAuthorityView {
873 subject,
874 issuance_nonce,
875 },
876 })
877}
878
879#[allow(clippy::result_large_err, clippy::too_many_arguments)]
880pub fn bind_generated_component_generation_v2(
881 mut paired: VerifiedFiveLeafGenerationV2PairedOwner,
882 actual_source: [u8; 32],
883 actual_machine: [u8; 32],
884 actual_root: [u8; 32],
885 actual_generation: u64,
886 actual_composed: [u8; 32],
887) -> Result<VerifiedGeneratedComponentGenerationV2Owner, VerifiedFiveLeafGenerationV2PairedOwner> {
888 let aggregate = &mut paired.aggregate.owner;
889 let observability = &paired.observability.half.half;
890 let build = &paired.observability.build;
891 if aggregate.source == actual_source
892 && aggregate.machine == actual_machine
893 && aggregate.root == actual_root
894 && aggregate.generation == actual_generation
895 && aggregate.signed_subject == observability.subject
896 && aggregate.issuance_nonce == observability.issuance_nonce
897 && paired.view.subject == observability.subject
898 && paired.view.issuance_nonce == observability.issuance_nonce
899 && build.subject == observability.subject
900 && build.issuance_nonce == observability.issuance_nonce
901 && build.constituent == observability.constituent
902 && observability.config_schema != [0; 32]
903 && observability.config != [0; 32]
904 && !observability.config_values.contains(&0)
905 && observability.build != [0; 32]
906 && observability.source == actual_source
907 && observability.machine == actual_machine
908 && observability.root == actual_root
909 && observability.generation == actual_generation
910 && actual_composed != [0; 32]
911 {
912 aggregate.actual_composed = Some(actual_composed);
913 Ok(VerifiedGeneratedComponentGenerationV2Owner {
914 aggregate: paired.aggregate.owner,
915 observability: paired.observability,
916 })
917 } else {
918 Err(paired)
919 }
920}
921
922#[cfg(test)]
923pub(crate) fn split_verified_five_leaf_generation_v2(
924 owner: VerifiedFiveLeafGenerationV2Owner,
925) -> (
926 VerifiedFiveLeafGenerationV2AggregateHalf,
927 VerifiedObservabilityBuildStaticOwner,
928 VerifiedFiveLeafObservabilityV2Half,
929 FiveLeafGenerationV2NonAuthorityView,
930) {
931 let VerifiedFiveLeafGenerationV2Owner {
932 aggregate,
933 observability_build,
934 observability,
935 subject,
936 issuance_nonce,
937 } = owner;
938 (
939 VerifiedFiveLeafGenerationV2AggregateHalf { owner: aggregate },
940 observability_build,
941 VerifiedFiveLeafObservabilityV2Half {
942 half: observability,
943 },
944 FiveLeafGenerationV2NonAuthorityView {
945 subject,
946 issuance_nonce,
947 },
948 )
949}
950
951#[cfg(test)]
952pub(crate) fn restore_verified_five_leaf_generation_v2(
953 aggregate: VerifiedFiveLeafGenerationV2AggregateHalf,
954 observability_build: VerifiedObservabilityBuildStaticOwner,
955 observability: VerifiedFiveLeafObservabilityV2Half,
956 view: FiveLeafGenerationV2NonAuthorityView,
957) -> Result<
958 VerifiedFiveLeafGenerationV2Owner,
959 (
960 VerifiedFiveLeafGenerationV2AggregateHalf,
961 VerifiedObservabilityBuildStaticOwner,
962 VerifiedFiveLeafObservabilityV2Half,
963 FiveLeafGenerationV2NonAuthorityView,
964 ),
965> {
966 if aggregate.owner.signed_subject == view.subject
967 && aggregate.owner.issuance_nonce == view.issuance_nonce
968 && observability_build.subject == view.subject
969 && observability_build.issuance_nonce == view.issuance_nonce
970 && observability.half.subject == view.subject
971 && observability.half.issuance_nonce == view.issuance_nonce
972 {
973 Ok(VerifiedFiveLeafGenerationV2Owner {
974 aggregate: aggregate.owner,
975 observability_build,
976 observability: observability.half,
977 subject: view.subject,
978 issuance_nonce: view.issuance_nonce,
979 })
980 } else {
981 Err((aggregate, observability_build, observability, view))
982 }
983}
984
985#[cfg(test)]
986pub(crate) fn pair_observability_build_static_v2(
987 build: VerifiedObservabilityBuildStaticOwner,
988 half: VerifiedFiveLeafObservabilityV2Half,
989) -> Result<
990 VerifiedObservabilityPairedReceipt,
991 (
992 VerifiedObservabilityBuildStaticOwner,
993 VerifiedFiveLeafObservabilityV2Half,
994 ),
995> {
996 if build.subject == half.half.subject
997 && build.issuance_nonce == half.half.issuance_nonce
998 && build.constituent == half.half.constituent
999 {
1000 Ok(VerifiedObservabilityPairedReceipt { build, half })
1001 } else {
1002 Err((build, half))
1003 }
1004}
1005
1006struct FixedLayoutDigest([u64; 4]);
1007
1008impl FixedLayoutDigest {
1009 fn new() -> Self {
1010 Self([
1011 0xcbf2_9ce4_8422_2325,
1012 0x9e37_79b9_7f4a_7c15,
1013 0x6a09_e667_f3bc_c909,
1014 0xbb67_ae85_84ca_a73b,
1015 ])
1016 }
1017 fn write(&mut self, bytes: &[u8]) {
1018 const PRIMES: [u64; 4] = [
1019 0x0000_0100_0000_01b3,
1020 0x9e37_79b1_85eb_ca87,
1021 0xc2b2_ae3d_27d4_eb4f,
1022 0x1656_67b1_9e37_79f9,
1023 ];
1024 for (index, byte) in bytes.iter().copied().enumerate() {
1025 for (lane, prime) in self.0.iter_mut().zip(PRIMES) {
1026 *lane ^= u64::from(byte).wrapping_add(index as u64);
1027 *lane = lane.wrapping_mul(prime);
1028 *lane ^= *lane >> 29;
1029 }
1030 }
1031 }
1032 fn finish(self) -> [u8; 32] {
1033 let mut output = [0; 32];
1034 for (index, lane) in self.0.into_iter().enumerate() {
1035 output[index * 8..(index + 1) * 8].copy_from_slice(&lane.to_le_bytes());
1036 }
1037 output
1038 }
1039}
1040
1041fn approved_observability_layout(
1042 components: [[u8; 32]; 4],
1043 profile: [u64; 6],
1044 config_schema: [u8; 32],
1045 config: [u8; 32],
1046 config_values: [u64; 5],
1047 sizes: [u64; 4],
1048 alignments: [u64; 4],
1049 offsets: [u64; 4],
1050 aggregate: [u64; 2],
1051 payload: u64,
1052) -> Option<[u8; 32]> {
1053 const EXPECTED_COMPONENTS: [&str; 4] = [
1054 "4d4a1129bb1531eaed4b3d511684c97d989d7fc5e8238cd7f18c45c95c2990ed",
1055 "893bb585b55ce3288f706a155aa74f018f366e66e680c4ea105706d44cf1dbad",
1056 "0451a5bb65e79cd900a0ae7bfcb6118611f0e596e0a9d883edb82de9ea24bc54",
1057 "6d9816b9977cd88a63234fc003786bd05eb3702a70380bbc57a38a239a673e0f",
1058 ];
1059 let expected = EXPECTED_COMPONENTS.map(|value| decode_identity(value).unwrap());
1060 let mut schema_digest = FixedLayoutDigest::new();
1061 schema_digest.write(b"saddle.observability.fixed-file-config.schema.v1");
1062 schema_digest.write(b"saddle.observability.fixed-file-config/1;rotate_bytes,retained_files,retention_age_ms,sync_bytes,sync_interval_ms");
1063 if components != expected
1064 || config_schema != schema_digest.finish()
1065 || profile != [4, 1_024, 6, 256, 4_096, 32]
1066 || config_values != [134_217_728, 8, 604_800_000, 4_194_304, 1_000]
1067 || sizes != [4_856, 1_528, 13_880, 720]
1068 || alignments != [8; 4]
1069 || offsets[0] != 0
1070 || payload != profile[0].checked_mul(profile[1])?
1071 || payload > sizes[0]
1072 {
1073 return None;
1074 }
1075 let mut cursor = 0_u64;
1076 let mut maximum_alignment = 1_u64;
1077 for index in 0..4 {
1078 let alignment = alignments[index];
1079 if !alignment.is_power_of_two() || sizes[index] == 0 {
1080 return None;
1081 }
1082 maximum_alignment = maximum_alignment.max(alignment);
1083 let aligned = cursor.checked_add(alignment - 1)? & !(alignment - 1);
1084 if offsets[index] != aligned {
1085 return None;
1086 }
1087 cursor = offsets[index].checked_add(sizes[index])?;
1088 }
1089 let aggregate_size = cursor.checked_add(maximum_alignment - 1)? & !(maximum_alignment - 1);
1090 if aggregate != [aggregate_size, maximum_alignment] {
1091 return None;
1092 }
1093 let mut config_digest = FixedLayoutDigest::new();
1094 config_digest.write(b"saddle.observability.fixed-file-config.identity.v1");
1095 config_digest.write(&config_schema);
1096 for value in config_values {
1097 config_digest.write(&value.to_le_bytes());
1098 }
1099 if config_digest.finish() != config {
1100 return None;
1101 }
1102 let mut leaf = FixedLayoutDigest::new();
1103 leaf.write(b"saddle.observability.generated-layout.leaf.v1");
1104 for identity in components {
1105 leaf.write(&identity);
1106 }
1107 leaf.write(&config_schema);
1108 leaf.write(&config);
1109 for value in config_values
1110 .into_iter()
1111 .chain(profile)
1112 .chain(sizes)
1113 .chain(alignments)
1114 .chain(offsets)
1115 .chain(aggregate)
1116 {
1117 leaf.write(&value.to_le_bytes());
1118 }
1119 Some(leaf.finish())
1120}
1121
1122#[allow(clippy::result_large_err)]
1125#[cfg(test)]
1126fn verify_approved_five_leaf_generation_v1_fixture(
1127 applicability: VerifiedGenerationKeyApplicabilityReceipt,
1128 canonical: &[u8],
1129 signature: &[u8],
1130) -> Result<
1131 VerifiedGeneratedComponentGenerationOwner,
1132 (
1133 GenerationPairError,
1134 VerifiedGenerationKeyApplicabilityReceipt,
1135 ),
1136> {
1137 macro_rules! fail {
1138 ($e:expr) => {
1139 return Err(($e, applicability))
1140 };
1141 }
1142 if !canonical_json(canonical)
1143 || UnparsedPublicKey::new(&ED25519, applicability.public_key)
1144 .verify(canonical, signature)
1145 .is_err()
1146 {
1147 fail!(GenerationPairError::Signature);
1148 }
1149 let doc: FiveLeafGenerationDocument = match serde_json::from_slice(canonical) {
1150 Ok(value) => value,
1151 Err(_) => fail!(GenerationPairError::Encoding),
1152 };
1153 let decode = |value: &str| decode_identity(value);
1154 if doc.schema != "saddle-f08-five-leaf-generation/1"
1155 || doc.domain != hex(&sha256(b"saddle/f08/five-leaf-generation/v1"))
1156 || doc.usage != hex(&sha256(b"capacity.five-leaf.generation"))
1157 || decode(&doc.key_identity) != Some(applicability.key_identity)
1158 || decode(&doc.source_identity) != Some(applicability.source)
1159 || decode(&doc.build_identity) != Some(applicability.build)
1160 || decode(&doc.machine_identity) != Some(applicability.machine)
1161 || doc.owner_generation == 0
1162 {
1163 fail!(GenerationPairError::Foreign);
1164 }
1165 let Some(root) = decode(&doc.root_identity) else {
1166 fail!(GenerationPairError::Encoding);
1167 };
1168 Ok(VerifiedGeneratedComponentGenerationOwner {
1169 generation: doc.owner_generation,
1170 source: applicability.source,
1171 build: applicability.build,
1172 machine: applicability.machine,
1173 root,
1174 actual_composed: None,
1175 signed_subject: sha256(canonical),
1176 issuance_nonce: sha256(
1177 &[applicability.key_identity.as_slice(), canonical, signature].concat(),
1178 ),
1179 })
1180}
1181
1182#[allow(clippy::result_large_err)]
1185pub fn verify_approved_five_leaf_generation_v2(
1186 applicability: VerifiedGenerationKeyApplicabilityReceipt,
1187 input: CanonicalSignedFiveLeafGenerationV2Input,
1188) -> Result<
1189 VerifiedFiveLeafGenerationV2Owner,
1190 (
1191 GenerationPairError,
1192 VerifiedGenerationKeyApplicabilityReceipt,
1193 CanonicalSignedFiveLeafGenerationV2Input,
1194 ),
1195> {
1196 macro_rules! fail {
1197 ($error:expr) => {
1198 return Err(($error, applicability, input))
1199 };
1200 }
1201 if !canonical_json(&input.canonical)
1202 || UnparsedPublicKey::new(&ED25519, applicability.public_key)
1203 .verify(&input.canonical, &input.signature)
1204 .is_err()
1205 {
1206 fail!(GenerationPairError::Signature);
1207 }
1208 let doc: FiveLeafGenerationV2Document = match serde_json::from_slice(&input.canonical) {
1209 Ok(value) => value,
1210 Err(_) => fail!(GenerationPairError::Encoding),
1211 };
1212 let decode = |value: &str| decode_identity(value);
1213 const SCHEMA: &str = "saddle-f08-five-leaf-generation/2";
1214 const DOMAIN: &[u8] = b"saddle/f08/five-leaf-generation/v2";
1215 const OBS_DOMAIN: &[u8] = b"saddle/f08/observability-constituent/v1";
1216 let ids = [
1217 decode(&doc.observability.leaf_identity),
1218 decode(&doc.observability.profile_identity),
1219 decode(&doc.observability.config_schema_identity),
1220 decode(&doc.observability.config_identity),
1221 decode(&doc.observability.work_identity),
1222 decode(&doc.observability.source_identity),
1223 decode(&doc.observability.build_identity),
1224 decode(&doc.observability.machine_identity),
1225 decode(&doc.observability.root_identity),
1226 ];
1227 if ids.iter().any(Option::is_none) || doc.observability.profile.contains(&0) {
1228 fail!(GenerationPairError::Encoding);
1229 }
1230 let [
1231 leaf,
1232 profile_identity,
1233 config_schema,
1234 config,
1235 work,
1236 obs_source,
1237 obs_build,
1238 obs_machine,
1239 obs_root,
1240 ] = ids.map(Option::unwrap);
1241 let Some(component_identities) = doc
1242 .observability
1243 .component_identities
1244 .iter()
1245 .map(|value| decode(value))
1246 .collect::<Option<Vec<_>>>()
1247 .and_then(|values| values.try_into().ok())
1248 else {
1249 fail!(GenerationPairError::Encoding);
1250 };
1251 let source = decode(&doc.source_identity);
1252 let build = decode(&doc.build_identity);
1253 let machine = decode(&doc.machine_identity);
1254 let root = decode(&doc.root_identity);
1255 if doc.schema != SCHEMA
1256 || doc.domain != hex(&sha256(DOMAIN))
1257 || doc.usage != hex(&sha256(b"capacity.five-leaf.generation"))
1258 || decode(&doc.key_identity) != Some(applicability.key_identity)
1259 || source != Some(applicability.source)
1260 || build != Some(applicability.build)
1261 || machine != Some(applicability.machine)
1262 || root.is_none()
1263 || doc.owner_generation == 0
1264 || obs_source != applicability.source
1265 || obs_build != applicability.build
1266 || obs_machine != applicability.machine
1267 || Some(obs_root) != root
1268 || doc.observability.owner_generation != doc.owner_generation
1269 || doc.observability.domain != hex(&sha256(OBS_DOMAIN))
1270 {
1271 fail!(GenerationPairError::Foreign);
1272 }
1273 let Some(recomputed_leaf) = approved_observability_layout(
1274 component_identities,
1275 doc.observability.profile,
1276 config_schema,
1277 config,
1278 doc.observability.config_values,
1279 doc.observability.sizes,
1280 doc.observability.alignments,
1281 doc.observability.offsets,
1282 doc.observability.aggregate_layout,
1283 doc.observability.payload_machine_bytes,
1284 ) else {
1285 fail!(GenerationPairError::Foreign);
1286 };
1287 if leaf != recomputed_leaf || profile_identity != component_identities[2] {
1288 fail!(GenerationPairError::Foreign);
1289 }
1290 let profile_bytes = doc.observability.profile.map(u64::to_le_bytes).concat();
1291 let components_bytes = component_identities.concat();
1292 let config_values_bytes = doc
1293 .observability
1294 .config_values
1295 .map(u64::to_le_bytes)
1296 .concat();
1297 let sizes_bytes = doc.observability.sizes.map(u64::to_le_bytes).concat();
1298 let alignments_bytes = doc.observability.alignments.map(u64::to_le_bytes).concat();
1299 let offsets_bytes = doc.observability.offsets.map(u64::to_le_bytes).concat();
1300 let aggregate_bytes = doc
1301 .observability
1302 .aggregate_layout
1303 .map(u64::to_le_bytes)
1304 .concat();
1305 let constituent = sha256(
1306 &[
1307 OBS_DOMAIN,
1308 leaf.as_slice(),
1309 profile_identity.as_slice(),
1310 profile_bytes.as_slice(),
1311 components_bytes.as_slice(),
1312 config_schema.as_slice(),
1313 config.as_slice(),
1314 config_values_bytes.as_slice(),
1315 work.as_slice(),
1316 sizes_bytes.as_slice(),
1317 alignments_bytes.as_slice(),
1318 offsets_bytes.as_slice(),
1319 aggregate_bytes.as_slice(),
1320 &doc.observability.payload_machine_bytes.to_le_bytes(),
1321 obs_source.as_slice(),
1322 obs_build.as_slice(),
1323 obs_machine.as_slice(),
1324 obs_root.as_slice(),
1325 &doc.owner_generation.to_le_bytes(),
1326 ]
1327 .concat(),
1328 );
1329 if decode(&doc.observability.constituent_identity) != Some(constituent) {
1330 fail!(GenerationPairError::Foreign);
1331 }
1332 let subject = sha256(&input.canonical);
1333 let issuance_nonce = sha256(
1334 &[
1335 b"saddle/f08/five-leaf-generation/v2/issuance".as_slice(),
1336 applicability.key_identity.as_slice(),
1337 subject.as_slice(),
1338 input.signature.as_slice(),
1339 ]
1340 .concat(),
1341 );
1342 let aggregate = VerifiedGeneratedComponentGenerationOwner {
1343 generation: doc.owner_generation,
1344 source: applicability.source,
1345 build: applicability.build,
1346 machine: applicability.machine,
1347 root: root.unwrap(),
1348 actual_composed: None,
1349 signed_subject: subject,
1350 issuance_nonce,
1351 };
1352 let observability = VerifiedObservabilityConstituentGenerationHalf {
1353 subject,
1354 issuance_nonce,
1355 constituent,
1356 leaf,
1357 profile: doc.observability.profile,
1358 component_identities,
1359 config_schema,
1360 config,
1361 config_values: doc.observability.config_values,
1362 work,
1363 sizes: doc.observability.sizes,
1364 alignments: doc.observability.alignments,
1365 offsets: doc.observability.offsets,
1366 aggregate_layout: doc.observability.aggregate_layout,
1367 payload_machine_bytes: doc.observability.payload_machine_bytes,
1368 source: obs_source,
1369 build: obs_build,
1370 machine: obs_machine,
1371 root: obs_root,
1372 generation: doc.owner_generation,
1373 };
1374 Ok(VerifiedFiveLeafGenerationV2Owner {
1375 aggregate,
1376 observability_build: VerifiedObservabilityBuildStaticOwner {
1377 subject,
1378 issuance_nonce,
1379 constituent,
1380 },
1381 observability,
1382 subject,
1383 issuance_nonce,
1384 })
1385}
1386
1387#[allow(clippy::result_large_err, clippy::too_many_arguments)]
1390pub fn bind_generated_component_generation(
1391 mut generated: VerifiedGeneratedComponentGenerationOwner,
1392 actual_source: [u8; 32],
1393 actual_build: [u8; 32],
1394 actual_machine: [u8; 32],
1395 actual_root: [u8; 32],
1396 actual_generation: u64,
1397 actual_composed: [u8; 32],
1398) -> Result<
1399 VerifiedGeneratedComponentGenerationOwner,
1400 (
1401 GenerationPairError,
1402 VerifiedGeneratedComponentGenerationOwner,
1403 ),
1404> {
1405 if generated.source == actual_source
1406 && generated.build == actual_build
1407 && generated.machine == actual_machine
1408 && generated.root == actual_root
1409 && generated.generation == actual_generation
1410 && actual_composed != [0; 32]
1411 {
1412 generated.actual_composed = Some(actual_composed);
1413 Ok(generated)
1414 } else {
1415 Err((GenerationPairError::Foreign, generated))
1416 }
1417}
1418
1419#[allow(clippy::result_large_err)]
1421pub fn pair_component_generation(
1422 deployment: VerifiedSignedDeploymentGenerationOwner,
1423 generated: VerifiedGeneratedComponentGenerationOwner,
1424) -> Result<
1425 VerifiedComponentGenerationPairingReceipt,
1426 (
1427 GenerationPairError,
1428 VerifiedSignedDeploymentGenerationOwner,
1429 VerifiedGeneratedComponentGenerationOwner,
1430 ),
1431> {
1432 let actual_composed = match generated.actual_composed {
1433 Some(identity) => identity,
1434 None => return Err((GenerationPairError::Foreign, deployment, generated)),
1435 };
1436 if deployment.generation == generated.generation
1437 && deployment.source == generated.source
1438 && deployment.build == generated.build
1439 && generated.machine != [0; 32]
1440 && generated.root != [0; 32]
1441 {
1442 Ok(VerifiedComponentGenerationPairingReceipt {
1443 binding: sha256(
1444 &[
1445 deployment.source.as_slice(),
1446 deployment.build.as_slice(),
1447 generated.machine.as_slice(),
1448 generated.root.as_slice(),
1449 actual_composed.as_slice(),
1450 ]
1451 .concat(),
1452 ),
1453 })
1454 } else {
1455 Err((GenerationPairError::Foreign, deployment, generated))
1456 }
1457}
1458
1459#[allow(clippy::result_large_err)]
1463pub(crate) fn pair_component_generation_recovering(
1464 deployment: VerifiedSignedDeploymentGenerationOwner,
1465 generated: VerifiedGeneratedComponentGenerationOwner,
1466) -> Result<
1467 (
1468 VerifiedSignedDeploymentGenerationOwner,
1469 VerifiedGeneratedComponentGenerationOwner,
1470 VerifiedComponentGenerationPairingReceipt,
1471 ),
1472 (
1473 GenerationPairError,
1474 VerifiedSignedDeploymentGenerationOwner,
1475 VerifiedGeneratedComponentGenerationOwner,
1476 ),
1477> {
1478 let actual_composed = match generated.actual_composed {
1479 Some(identity) => identity,
1480 None => return Err((GenerationPairError::Foreign, deployment, generated)),
1481 };
1482 if deployment.generation == generated.generation
1483 && deployment.source == generated.source
1484 && deployment.build == generated.build
1485 && generated.machine != [0; 32]
1486 && generated.root != [0; 32]
1487 {
1488 let receipt = VerifiedComponentGenerationPairingReceipt {
1489 binding: sha256(
1490 &[
1491 deployment.source.as_slice(),
1492 deployment.build.as_slice(),
1493 generated.machine.as_slice(),
1494 generated.root.as_slice(),
1495 actual_composed.as_slice(),
1496 ]
1497 .concat(),
1498 ),
1499 };
1500 Ok((deployment, generated, receipt))
1501 } else {
1502 Err((GenerationPairError::Foreign, deployment, generated))
1503 }
1504}
1505
1506fn canonical_json(value: &[u8]) -> bool {
1507 serde_json::from_slice::<serde_json::Value>(value)
1508 .ok()
1509 .and_then(|v| serde_json::to_vec(&v).ok())
1510 .is_some_and(|v| v == value)
1511}
1512fn sha256(value: &[u8]) -> [u8; 32] {
1513 Sha256::digest(value).into()
1514}
1515fn decode_identity(value: &str) -> Option<[u8; 32]> {
1516 if value.len() != 64 {
1517 return None;
1518 }
1519 let mut out = [0; 32];
1520 for (i, pair) in value.as_bytes().chunks_exact(2).enumerate() {
1521 out[i] = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
1522 }
1523 Some(out)
1524}
1525fn hex(value: &[u8; 32]) -> String {
1526 value.iter().map(|b| format!("{b:02x}")).collect()
1527}
1528
1529#[cfg(test)]
1530mod tests {
1531 use super::*;
1532 use ring::{
1533 rand::SystemRandom,
1534 signature::{Ed25519KeyPair, KeyPair},
1535 };
1536
1537 const PRODUCTION_OBSERVATION: &[u8] = b"component_identities=4d4a1129bb1531eaed4b3d511684c97d989d7fc5e8238cd7f18c45c95c2990ed,893bb585b55ce3288f706a155aa74f018f366e66e680c4ea105706d44cf1dbad,0451a5bb65e79cd900a0ae7bfcb6118611f0e596e0a9d883edb82de9ea24bc54,6d9816b9977cd88a63234fc003786bd05eb3702a70380bbc57a38a239a673e0f\nprofile=4,1024,6,256,4096,32\nconfig_schema_identity=64b4654edbd2fe07912a7180eefb20594fbd59028ffaa025407139d5a2ab5058\nconfig_identity=b12186b503b78674b8b0426a66356ec8a9fac1a9e0b322a0aeb425803e7077bb\nconfig_values=134217728,8,604800000,4194304,1000\nsizes=4856,1528,13880,720\nalignments=8,8,8,8\noffsets=0,4856,6384,20264\naggregate_size=20984\naggregate_alignment=8\ncanonical_leaf_identity=b46726fd1a64c8b9bd50a8870ccd1c3a3aeedb481cb01222bcb7f5e6e1e14ee8\n";
1538
1539 fn signing_policy(
1540 key: &Ed25519KeyPair,
1541 approved_lock: [u8; 32],
1542 request_nonce: [u8; 32],
1543 ) -> (
1544 VerifiedFiveLeafV2SignerPolicyOwner,
1545 VerifiedFiveLeafV2SigningRequestApprovalOwner,
1546 ) {
1547 let public_key: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
1548 issue_five_leaf_v2_signer_policy(FiveLeafV2ApprovedSignerFacts {
1549 public_key,
1550 key_identity: sha256(&public_key),
1551 approved_lock,
1552 source: [31; 32],
1553 build: [32; 32],
1554 machine: [33; 32],
1555 root: [34; 32],
1556 generation: 41,
1557 observation_sha256: APPROVED_OBSERVATION_SHA256,
1558 domain: sha256(FIVE_LEAF_V2_DOMAIN),
1559 usage: sha256(FIVE_LEAF_V2_USAGE),
1560 approval_identity: [72; 32],
1561 custody_policy_identity: [73; 32],
1562 request_nonce,
1563 })
1564 }
1565
1566 fn prepared_signing(
1567 key: &Ed25519KeyPair,
1568 nonce: [u8; 32],
1569 ) -> (
1570 FiveLeafV2SigningRequest,
1571 FiveLeafV2SignerResponse,
1572 VerifiedFiveLeafV2SignerPolicyOwner,
1573 ) {
1574 let input = v2_input(key, [34; 32], 41);
1575 let canonical = input.canonical;
1576 let (policy, approval) = signing_policy(key, [71; 32], nonce);
1577 let (request, policy) = prepare_five_leaf_v2_signing_request(
1578 canonical.clone(),
1579 PRODUCTION_OBSERVATION.to_vec(),
1580 policy,
1581 approval,
1582 )
1583 .unwrap();
1584 let public_key: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
1585 let envelope = serde_json::to_vec(&serde_json::json!({
1586 "schema":"saddle-f08-five-leaf-v2-signer-response/1",
1587 "canonical":canonical.clone(),
1588 "request_nonce":hex(&nonce),
1589 "signature":key.sign(&request.canonical).as_ref(),
1590 "public_key":hex(&public_key),
1591 "key_identity":hex(&sha256(&public_key)),
1592 "domain":hex(&sha256(FIVE_LEAF_V2_DOMAIN)),
1593 "usage":hex(&sha256(FIVE_LEAF_V2_USAGE)),
1594 "approval_identity":hex(&[72;32]),
1595 "custody_policy_identity":hex(&[73;32]),
1596 }))
1597 .unwrap();
1598 let response = capture_untrusted_five_leaf_v2_signer_response(envelope).unwrap();
1599 (request, response, policy)
1600 }
1601
1602 fn signed_half(generation: u64, root: [u8; 32]) -> VerifiedGeneratedComponentGenerationOwner {
1603 let bytes = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
1604 let key = Ed25519KeyPair::from_pkcs8(bytes.as_ref()).unwrap();
1605 let public: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
1606 let applicability = VerifiedGenerationKeyApplicabilityReceipt {
1607 public_key: public,
1608 key_identity: sha256(&public),
1609 source: [31; 32],
1610 build: [32; 32],
1611 machine: [33; 32],
1612 };
1613 let canonical = serde_json::to_vec(&serde_json::json!({
1614 "schema":"saddle-f08-five-leaf-generation/1",
1615 "domain":hex(&sha256(b"saddle/f08/five-leaf-generation/v1")),
1616 "usage":hex(&sha256(b"capacity.five-leaf.generation")),
1617 "key_identity":hex(&sha256(&public)),
1618 "source_identity":hex(&[31;32]),"build_identity":hex(&[32;32]),
1619 "machine_identity":hex(&[33;32]),"root_identity":hex(&root),
1620 "owner_generation":generation,
1621 }))
1622 .unwrap();
1623 match verify_approved_five_leaf_generation_v1_fixture(
1624 applicability,
1625 &canonical,
1626 key.sign(&canonical).as_ref(),
1627 ) {
1628 Ok(owner) => owner,
1629 Err(_) => panic!("valid signed receipt must verify"),
1630 }
1631 }
1632
1633 fn v2_input(
1634 key: &Ed25519KeyPair,
1635 root: [u8; 32],
1636 generation: u64,
1637 ) -> CanonicalSignedFiveLeafGenerationV2Input {
1638 let profile = [4_u64, 1024, 6, 256, 4096, 32];
1639 let components = [
1640 "4d4a1129bb1531eaed4b3d511684c97d989d7fc5e8238cd7f18c45c95c2990ed",
1641 "893bb585b55ce3288f706a155aa74f018f366e66e680c4ea105706d44cf1dbad",
1642 "0451a5bb65e79cd900a0ae7bfcb6118611f0e596e0a9d883edb82de9ea24bc54",
1643 "6d9816b9977cd88a63234fc003786bd05eb3702a70380bbc57a38a239a673e0f",
1644 ]
1645 .map(|value| decode_identity(value).unwrap());
1646 let config_values: [u64; 5] = [134_217_728, 8, 604_800_000, 4_194_304, 1_000];
1647 let sizes: [u64; 4] = [4_856, 1_528, 13_880, 720];
1648 let alignments: [u64; 4] = [8; 4];
1649 let offsets: [u64; 4] = [0, 4_856, 6_384, 20_264];
1650 let aggregate_layout: [u64; 2] = [20_984, 8];
1651 let payload_machine_bytes: u64 = 4_096;
1652 let mut config_schema_digest = FixedLayoutDigest::new();
1653 config_schema_digest.write(b"saddle.observability.fixed-file-config.schema.v1");
1654 config_schema_digest.write(b"saddle.observability.fixed-file-config/1;rotate_bytes,retained_files,retention_age_ms,sync_bytes,sync_interval_ms");
1655 let config_schema = config_schema_digest.finish();
1656 let mut config_digest = FixedLayoutDigest::new();
1657 config_digest.write(b"saddle.observability.fixed-file-config.identity.v1");
1658 config_digest.write(&config_schema);
1659 for value in config_values {
1660 config_digest.write(&value.to_le_bytes());
1661 }
1662 let config = config_digest.finish();
1663 let recomputed_leaf = approved_observability_layout(
1664 components,
1665 profile,
1666 config_schema,
1667 config,
1668 config_values,
1669 sizes,
1670 alignments,
1671 offsets,
1672 aggregate_layout,
1673 payload_machine_bytes,
1674 )
1675 .unwrap();
1676 let leaf = recomputed_leaf;
1677 assert_eq!(
1678 hex(&leaf),
1679 "b46726fd1a64c8b9bd50a8870ccd1c3a3aeedb481cb01222bcb7f5e6e1e14ee8"
1680 );
1681 let profile_bytes = profile.map(u64::to_le_bytes).concat();
1682 let components_bytes = components.concat();
1683 let config_values_bytes = config_values.map(u64::to_le_bytes).concat();
1684 let sizes_bytes = sizes.map(u64::to_le_bytes).concat();
1685 let alignments_bytes = alignments.map(u64::to_le_bytes).concat();
1686 let offsets_bytes = offsets.map(u64::to_le_bytes).concat();
1687 let aggregate_bytes = aggregate_layout.map(u64::to_le_bytes).concat();
1688 let work = [45; 32];
1689 let constituent = sha256(
1690 &[
1691 b"saddle/f08/observability-constituent/v1".as_slice(),
1692 leaf.as_slice(),
1693 components[2].as_slice(),
1694 profile_bytes.as_slice(),
1695 components_bytes.as_slice(),
1696 config_schema.as_slice(),
1697 config.as_slice(),
1698 config_values_bytes.as_slice(),
1699 work.as_slice(),
1700 sizes_bytes.as_slice(),
1701 alignments_bytes.as_slice(),
1702 offsets_bytes.as_slice(),
1703 aggregate_bytes.as_slice(),
1704 &payload_machine_bytes.to_le_bytes(),
1705 [31; 32].as_slice(),
1706 [32; 32].as_slice(),
1707 [33; 32].as_slice(),
1708 root.as_slice(),
1709 &generation.to_le_bytes(),
1710 ]
1711 .concat(),
1712 );
1713 let public: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
1714 let canonical = serde_json::to_vec(&serde_json::json!({
1715 "schema":"saddle-f08-five-leaf-generation/2",
1716 "domain":hex(&sha256(b"saddle/f08/five-leaf-generation/v2")),
1717 "usage":hex(&sha256(b"capacity.five-leaf.generation")),
1718 "key_identity":hex(&sha256(&public)),
1719 "source_identity":hex(&[31;32]),"build_identity":hex(&[32;32]),
1720 "machine_identity":hex(&[33;32]),"root_identity":hex(&root),
1721 "owner_generation":generation,
1722 "observability":{
1723 "domain":hex(&sha256(b"saddle/f08/observability-constituent/v1")),
1724 "leaf_identity":hex(&leaf),"profile_identity":hex(&components[2]),
1725 "profile":profile,"component_identities":components.map(|value| hex(&value)),
1726 "config_schema_identity":hex(&config_schema),
1727 "config_identity":hex(&config),"config_values":config_values,
1728 "work_identity":hex(&work),"sizes":sizes,"alignments":alignments,
1729 "offsets":offsets,"aggregate_layout":aggregate_layout,
1730 "payload_machine_bytes":payload_machine_bytes,
1731 "source_identity":hex(&[31;32]),"build_identity":hex(&[32;32]),
1732 "machine_identity":hex(&[33;32]),"root_identity":hex(&root),
1733 "owner_generation":generation,"constituent_identity":hex(&constituent)
1734 }
1735 }))
1736 .unwrap();
1737 let signature = key.sign(&canonical).as_ref().to_vec();
1738 CanonicalSignedFiveLeafGenerationV2Input::new(canonical, signature)
1739 }
1740
1741 fn v2_applicability(key: &Ed25519KeyPair) -> VerifiedGenerationKeyApplicabilityReceipt {
1742 let public: [u8; 32] = key.public_key().as_ref().try_into().unwrap();
1743 VerifiedGenerationKeyApplicabilityReceipt {
1744 public_key: public,
1745 key_identity: sha256(&public),
1746 source: [31; 32],
1747 build: [32; 32],
1748 machine: [33; 32],
1749 }
1750 }
1751
1752 #[test]
1753 fn production_signing_mechanism_is_linear_recoverable_and_domain_separated() {
1754 assert_eq!(sha256(PRODUCTION_OBSERVATION), APPROVED_OBSERVATION_SHA256);
1755 let key = key_pair();
1756 let (request, mut response, policy) = prepared_signing(&key, [81; 32]);
1757 response.request_nonce = [82; 32];
1758 let (error, request, mut response, policy) =
1759 verify_five_leaf_v2_signing_response(request, response, policy).unwrap_err();
1760 assert_eq!(error, FiveLeafV2SigningError::Foreign);
1761 response.request_nonce = [81; 32];
1762 let (input, applicability) =
1763 verify_five_leaf_v2_signing_response(request, response, policy).unwrap();
1764 assert!(verify_approved_five_leaf_generation_v2(applicability, input).is_ok());
1765
1766 let foreign = key_pair();
1767 let (request, _, policy) = prepared_signing(&key, [83; 32]);
1768 let foreign_public: [u8; 32] = foreign.public_key().as_ref().try_into().unwrap();
1769 let foreign_envelope = serde_json::to_vec(&serde_json::json!({
1770 "schema":"saddle-f08-five-leaf-v2-signer-response/1",
1771 "canonical":request.canonical.clone(),
1772 "request_nonce":hex(&request.request_nonce),
1773 "signature":foreign.sign(&request.canonical).as_ref(),
1774 "public_key":hex(&foreign_public),
1775 "key_identity":hex(&sha256(&foreign_public)),
1776 "domain":hex(&request.domain),
1777 "usage":hex(&request.usage),
1778 "approval_identity":hex(&request.approval_identity),
1779 "custody_policy_identity":hex(&request.custody_policy_identity),
1780 }))
1781 .unwrap();
1782 let foreign_response =
1783 capture_untrusted_five_leaf_v2_signer_response(foreign_envelope).unwrap();
1784 let (error, ..) =
1785 verify_five_leaf_v2_signing_response(request, foreign_response, policy).unwrap_err();
1786 assert_eq!(error, FiveLeafV2SigningError::Foreign);
1787
1788 let (request, mut response, policy) = prepared_signing(&key, [84; 32]);
1789 response.canonical.push(b' ');
1790 let (error, ..) =
1791 verify_five_leaf_v2_signing_response(request, response, policy).unwrap_err();
1792 assert_eq!(error, FiveLeafV2SigningError::Foreign);
1793
1794 let (request, mut response, policy) = prepared_signing(&key, [85; 32]);
1795 response.usage = sha256(b"calibration.signing");
1796 let (error, ..) =
1797 verify_five_leaf_v2_signing_response(request, response, policy).unwrap_err();
1798 assert_eq!(error, FiveLeafV2SigningError::Foreign);
1799
1800 let (request, mut response, policy) = prepared_signing(&key, [86; 32]);
1801 response.signature[0] ^= 1;
1802 let (error, ..) =
1803 verify_five_leaf_v2_signing_response(request, response, policy).unwrap_err();
1804 assert_eq!(error, FiveLeafV2SigningError::Signature);
1805 }
1806
1807 #[test]
1808 fn v2_owner_rejects_v1_and_cross_restore_but_original_restores() {
1809 let key_a = key_pair();
1810 let key_b = key_pair();
1811 let owner_a = verify_approved_five_leaf_generation_v2(
1812 v2_applicability(&key_a),
1813 v2_input(&key_a, [34; 32], 41),
1814 )
1815 .unwrap();
1816 let owner_b = verify_approved_five_leaf_generation_v2(
1817 v2_applicability(&key_b),
1818 v2_input(&key_b, [35; 32], 42),
1819 )
1820 .unwrap();
1821 let (aggregate_a, build_a, obs_a, view_a) = split_verified_five_leaf_generation_v2(owner_a);
1822 let (aggregate_b, build_b, obs_b, view_b) = split_verified_five_leaf_generation_v2(owner_b);
1823 let (aggregate_a, build_a, obs_b, view_a) =
1824 restore_verified_five_leaf_generation_v2(aggregate_a, build_a, obs_b, view_a)
1825 .unwrap_err();
1826 let (aggregate_b, build_b, obs_a, view_b) =
1827 restore_verified_five_leaf_generation_v2(aggregate_b, build_b, obs_a, view_b)
1828 .unwrap_err();
1829 let owner_a =
1830 restore_verified_five_leaf_generation_v2(aggregate_a, build_a, obs_a, view_a).unwrap();
1831 let owner_b =
1832 restore_verified_five_leaf_generation_v2(aggregate_b, build_b, obs_b, view_b).unwrap();
1833 let (_, build_a, half_a, _) = split_verified_five_leaf_generation_v2(owner_a);
1834 let (_, build_b, half_b, _) = split_verified_five_leaf_generation_v2(owner_b);
1835 let (build_a, half_b) = pair_observability_build_static_v2(build_a, half_b).unwrap_err();
1836 let (build_b, half_a) = pair_observability_build_static_v2(build_b, half_a).unwrap_err();
1837 assert!(pair_observability_build_static_v2(build_a, half_a).is_ok());
1838 assert!(pair_observability_build_static_v2(build_b, half_b).is_ok());
1839
1840 let legacy = serde_json::to_vec(&serde_json::json!({
1841 "schema":"saddle-f08-five-leaf-generation/1"
1842 }))
1843 .unwrap();
1844 let legacy_input = CanonicalSignedFiveLeafGenerationV2Input::new(
1845 legacy.clone(),
1846 key_a.sign(&legacy).as_ref().to_vec(),
1847 );
1848 assert!(
1849 verify_approved_five_leaf_generation_v2(v2_applicability(&key_a), legacy_input,)
1850 .is_err()
1851 );
1852 }
1853
1854 #[test]
1855 fn v2_rejects_each_observability_physical_fact_drift_and_restores_input() {
1856 let bytes = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
1857 let key = Ed25519KeyPair::from_pkcs8(bytes.as_ref()).unwrap();
1858 let root = [77; 32];
1859 for drift in [
1860 "component",
1861 "size",
1862 "alignment",
1863 "offset",
1864 "aggregate",
1865 "payload",
1866 "config",
1867 ] {
1868 let original = v2_input(&key, root, 9);
1869 let mut value: serde_json::Value = serde_json::from_slice(&original.canonical).unwrap();
1870 let obs = value.get_mut("observability").unwrap();
1871 match drift {
1872 "component" => obs["component_identities"][0] = serde_json::json!(hex(&[99; 32])),
1873 "size" => obs["sizes"][0] = serde_json::json!(4_857),
1874 "alignment" => obs["alignments"][0] = serde_json::json!(16),
1875 "offset" => obs["offsets"][1] = serde_json::json!(4_864),
1876 "aggregate" => obs["aggregate_layout"][0] = serde_json::json!(20_928),
1877 "payload" => obs["payload_machine_bytes"] = serde_json::json!(4_095),
1878 "config" => obs["config_values"][0] = serde_json::json!(134_217_729_u64),
1879 _ => unreachable!(),
1880 }
1881 let canonical = serde_json::to_vec(&value).unwrap();
1882 let input = CanonicalSignedFiveLeafGenerationV2Input::new(
1883 canonical.clone(),
1884 key.sign(&canonical).as_ref().to_vec(),
1885 );
1886 let (_, applicability, returned) =
1887 verify_approved_five_leaf_generation_v2(v2_applicability(&key), input)
1888 .expect_err("independently signed physical drift must fail closed");
1889 assert_eq!(returned.canonical, canonical);
1890 assert!(
1891 verify_approved_five_leaf_generation_v2(applicability, v2_input(&key, root, 9))
1892 .is_ok()
1893 );
1894 }
1895 }
1896
1897 #[test]
1898 fn v2_whole_pair_and_generation_bind_are_recoverable() {
1899 for drift in ["source", "machine", "root", "generation", "composed"] {
1900 let key = key_pair();
1901 let root = [34; 32];
1902 let owner = verify_approved_five_leaf_generation_v2(
1903 v2_applicability(&key),
1904 v2_input(&key, root, 41),
1905 )
1906 .unwrap();
1907 let paired = pair_verified_five_leaf_generation_v2(owner).unwrap();
1908 let view = paired.observability_capacity_view();
1909 assert_eq!(view.profile(), [4, 1_024, 6, 256, 4_096, 32]);
1910 assert_eq!(view.owner_generation(), 41);
1911 let mut actual = ([31; 32], [33; 32], root, 41, [35; 32]);
1912 match drift {
1913 "source" => actual.0 = [99; 32],
1914 "machine" => actual.1 = [99; 32],
1915 "root" => actual.2 = [99; 32],
1916 "generation" => actual.3 = 42,
1917 "composed" => actual.4 = [0; 32],
1918 _ => unreachable!(),
1919 }
1920 let paired = bind_generated_component_generation_v2(
1921 paired, actual.0, actual.1, actual.2, actual.3, actual.4,
1922 )
1923 .unwrap_err();
1924 assert!(
1925 bind_generated_component_generation_v2(
1926 paired, [31; 32], [33; 32], root, 41, [35; 32],
1927 )
1928 .is_ok()
1929 );
1930 }
1931
1932 let key_a = key_pair();
1933 let key_b = key_pair();
1934 let paired_a = pair_verified_five_leaf_generation_v2(
1935 verify_approved_five_leaf_generation_v2(
1936 v2_applicability(&key_a),
1937 v2_input(&key_a, [34; 32], 41),
1938 )
1939 .unwrap(),
1940 )
1941 .unwrap();
1942 let paired_b = pair_verified_five_leaf_generation_v2(
1943 verify_approved_five_leaf_generation_v2(
1944 v2_applicability(&key_b),
1945 v2_input(&key_b, [35; 32], 42),
1946 )
1947 .unwrap(),
1948 )
1949 .unwrap();
1950 let paired_a = bind_generated_component_generation_v2(
1951 paired_a, [31; 32], [33; 32], [35; 32], 42, [36; 32],
1952 )
1953 .unwrap_err();
1954 let paired_b = bind_generated_component_generation_v2(
1955 paired_b, [31; 32], [33; 32], [34; 32], 41, [36; 32],
1956 )
1957 .unwrap_err();
1958 assert!(
1959 bind_generated_component_generation_v2(
1960 paired_a, [31; 32], [33; 32], [34; 32], 41, [36; 32],
1961 )
1962 .is_ok()
1963 );
1964 assert!(
1965 bind_generated_component_generation_v2(
1966 paired_b, [31; 32], [33; 32], [35; 32], 42, [36; 32],
1967 )
1968 .is_ok()
1969 );
1970 }
1971
1972 fn key_pair() -> Ed25519KeyPair {
1973 let bytes = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
1974 Ed25519KeyPair::from_pkcs8(bytes.as_ref()).unwrap()
1975 }
1976
1977 #[test]
1978 fn two_signed_generations_pair_only_with_same_deployment_generation() {
1979 let generated = bind_generated_component_generation(
1980 signed_half(41, [34; 32]),
1981 [31; 32],
1982 [32; 32],
1983 [33; 32],
1984 [34; 32],
1985 41,
1986 [35; 32],
1987 )
1988 .unwrap();
1989 let deployment = deployment_generation_half(42, [31; 32], [32; 32]);
1990 let generated = match pair_component_generation(deployment, generated) {
1991 Err((_, _, generated)) => generated,
1992 Ok(_) => panic!("foreign generation must fail"),
1993 };
1994 let receipt = match pair_component_generation(
1995 deployment_generation_half(41, [31; 32], [32; 32]),
1996 generated,
1997 ) {
1998 Ok(receipt) => receipt,
1999 Err(_) => panic!("same signed generation and provenance pairs"),
2000 };
2001 assert!(consume_component_generation_pairing(receipt).is_ok());
2002 }
2003
2004 #[test]
2005 fn signed_roots_cross_reject_and_original_halves_retry() {
2006 let root_a = [0xa1; 32];
2007 let root_b = [0xb2; 32];
2008 let half_a = match bind_generated_component_generation(
2009 signed_half(41, root_a),
2010 [31; 32],
2011 [32; 32],
2012 [33; 32],
2013 root_b,
2014 41,
2015 [0xbb; 32],
2016 ) {
2017 Err((GenerationPairError::Foreign, half)) => half,
2018 _ => panic!("A half must reject B root"),
2019 };
2020 let half_b = match bind_generated_component_generation(
2021 signed_half(41, root_b),
2022 [31; 32],
2023 [32; 32],
2024 [33; 32],
2025 root_a,
2026 41,
2027 [0xaa; 32],
2028 ) {
2029 Err((GenerationPairError::Foreign, half)) => half,
2030 _ => panic!("B half must reject A root"),
2031 };
2032 assert!(
2033 bind_generated_component_generation(
2034 half_a, [31; 32], [32; 32], [33; 32], root_a, 41, [0xaa; 32],
2035 )
2036 .is_ok()
2037 );
2038 assert!(
2039 bind_generated_component_generation(
2040 half_b, [31; 32], [32; 32], [33; 32], root_b, 41, [0xbb; 32],
2041 )
2042 .is_ok()
2043 );
2044 }
2045}