Skip to main content

saddle_core/
generation_pair.rs

1use ring::signature::{ED25519, UnparsedPublicKey};
2use serde::Deserialize;
3use sha2::{Digest, Sha256};
4
5/// Applicability for the repository-external approved five-leaf generation
6/// signing key. Production construction intentionally remains outside this
7/// repository.
8pub 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
16/// Nominal half issued by the signed deployment applicability verifier.
17pub struct VerifiedSignedDeploymentGenerationOwner {
18    generation: u64,
19    source: [u8; 32],
20    build: [u8; 32],
21}
22
23/// Nominal half issued from one approved canonical five-leaf receipt.
24pub 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
41/// Single-use equality receipt. It carries no numeric getter and cannot be
42/// constructed, cloned or serialized by a consumer.
43pub 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
135/// Final consumption is deliberately fact-free: all equality and provenance
136/// checks occurred while both opaque halves were present.
137pub 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
169/// Complete signed input consumed by the only v2 verifier. The bytes and
170/// detached signature are returned intact on every failure.
171pub 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
197/// A checked, by-value request for an external custody service. It is not a
198/// signing authority and intentionally exposes no field or clone operation.
199pub 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
216/// Untrusted detached signer output captured as one opaque value. Authority is
217/// conferred only by `verify_five_leaf_v2_signing_response`.
218pub 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
230/// Approved public-key applicability. This checkpoint deliberately provides
231/// no production issuer: key approval and custody remain external STOP assets.
232pub 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
249/// Single-use request reservation produced in the same receipt transaction as
250/// the signer policy. It has no raw nonce constructor or getter.
251pub 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/// Parses the canonical transport envelope without treating it as authority.
363/// A malformed envelope is returned byte-for-byte for retry or audit.
364#[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/// Checks the exact production observation and policy-bound subject before an
415/// external signing request may leave Gate custody. The policy is returned so
416/// the same non-clone owner must later authorize the response.
417#[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/// The only authority-upgrading response verifier. Every rejection restores
517/// all three opaque inputs for an original-pair retry.
518#[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/// Upgrades canonical signed bytes into the generated nominal half. There is
1123/// no raw generation, digest or caller-expected argument.
1124#[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/// Verifies the only accepted v2 signed subject and returns one indivisible
1183/// owner containing both the aggregate and Observability constituent halves.
1184#[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/// Consumes the signed half and binds it to Admission's actual five-leaf
1388/// composition. No compared fact is returned to the caller.
1389#[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/// Consumes both nominal halves. No raw compare oracle is exposed.
1420#[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/// Verifies the same equality while returning both linear halves. This is
1460/// used only by the later four-receipt atomic transaction, which must be able
1461/// to restore every outer owner when another receipt is crossed.
1462#[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}