Skip to main content

heddle_api/
import_authority.rs

1//! HYBRID canonical import authority. The existing heddle capability verifier
2//! supplies independently selected owner history/current permission. This
3//! module verifies the NEW typed parent grant and its bounded job child;
4//! it neither enrolls incoming roots nor substitutes for owner verification.
5use crate::heddle::api::v1alpha2::*;
6pub use crate::hybrid_codec::{Reject, strict_decode};
7use crate::hybrid_codec::{canonical, field, hash, key_id, record, signing_digest, verify, width};
8
9pub const PERMISSION_DOMAIN: &str = "heddle-import-member-permission-v1";
10pub const GENESIS_DOMAIN: &str = "heddle-import-genesis-authority-v1";
11pub const DELEGATION_DOMAIN: &str = "heddle-import-job-delegation-v1";
12pub const RENEWAL_DOMAIN: &str = "heddle-import-job-renewal-v1";
13pub const OPERATION_DOMAIN: &str = "heddle-delegated-import-operation-v1";
14pub const MANIFEST_DOMAIN: &str = "heddle-import-result-manifest-v1";
15pub const PUBLICATION_DOMAIN: &str = "heddle-import-publication-payload-v1";
16pub const MAX_BRANCHES: usize = 256;
17pub const MAX_RECORD_BYTES: usize = 64 * 1024;
18pub const MAX_BUNDLE_BYTES: usize = 1024 * 1024;
19pub const MAX_RESULT_BYTES: u64 = 1 << 30;
20pub const CANCELLATION_NAMESPACE: &str = "heddle-import-cancel-v1";
21
22record!(AuthorizationSignature, signer_key_id:b, signature:b);
23record!(RecordSignature, public_key:b, signature:b);
24record!(SignedRecord, format:s, canonical_record:b, signatures:l);
25record!(ImportFrontierV1, format_version:u, thread_id:b, operation_ids:h);
26record!(ImportContentV1, format_version:u, canonical_capture:b);
27record!(ImportGenesisWitnessV1, format_version:u, binding:m, original_genesis:m, creator_authority_envelope:b);
28record!(ImportAuthorityWitnessV1, format_version:u, kind:e, original:m, dependencies:l, authority_envelope:b);
29record!(HostedLandingRequestProofV1, format_version:u, signing_identity:s, method_path:s, timestamp_millis:u, nonce:b, request_body:b, signature:m);
30record!(HostedLandingWitnessV1, format_version:u, execution:m, request:m, source_operation:m, review_evidence:l, authority_envelope:b);
31record!(ImportJobCasStateV1, format_version:u, logical_job_id:b, retry_lineage_id:b, active_predecessor:m, authority_epoch:u, committed_manifest:m);
32record!(ImportIdentityV1, spool_uuid:b, spool_genesis_digest:b, owner_id:b,
33    owner_account_uuid:b, owner_state_hash:b, ownership_transfer_sequence:u);
34record!(ImportOwnerChainV1, spool_genesis_digest:b, owner_state_hashes:q, transfer_audit_hashes:q);
35record!(ImportBranchLimitV1, ref_name:s, hash_algorithm:e, ref_mode:e, pinned_commit_oid:b,
36    genesis_digest:b, target_thread_id:b, expected_frontier_digest:b, slot_id:u, max_result_bytes:u);
37record!(ImportPermissionScopeV1, provider:s, source_url:s, branches:l, destination_version:b,
38    options_digest:b, converter_version:s, max_operations:u, max_result_bytes:u);
39record!(ImportMemberPermissionV1, format_version:u, identity:m, logical_job_id:b,
40    retry_lineage_id:b, subject_public_key:b, purpose:e, scope:m, not_before_unix_seconds:u,
41    expires_at_unix_seconds:u, cancellation_id:b, owner_chain_digest:b, nonce:b);
42record!(SignedImportMemberPermissionV1, body:m, owner_signature:m);
43record!(ImportGenesisAuthorityV1, format_version:u, identity:m, genesis_digest:b,
44    original_creator_signature:b, creator_public_key:b, creator_authority_envelope_digest:b,
45    parent_permission_digest:b, owner_chain_digest:b);
46record!(SignedImportGenesisAuthorityV1, body:m, creator_signature:m);
47record!(ImportBranchManifestV1, limit:m, genesis_authority_digest:b);
48record!(ImportJobDelegationV1, format_version:u, identity:m, delegation_id:b, logical_job_id:b,
49    retry_lineage_id:b, job_public_key:b, job_key_id:b, delegating_public_key:b,
50    parent_permission_digest:b, owner_chain_digest:b, purpose:e, scope:m, branch_manifest:l,
51    not_before_unix_seconds:u, expires_at_unix_seconds:u, cancellation_id:b, predecessor_delegation_digest:b);
52record!(SignedImportJobDelegationV1, body:m, delegating_signature:m);
53record!(ImportCommittedSlotV1, ref_name:s, slot_id:u, signed_operation_digest:b,
54    resulting_frontier_digest:b, result_bytes:u);
55record!(ImportResultManifestV1, format_version:u, logical_job_id:b, retry_lineage_id:b, slots:l);
56record!(ImportJobRenewalV1, format_version:u, predecessor_delegation_digest:b,
57    expected_authority_epoch:u, committed_manifest_digest:b, replacement:m);
58record!(SignedImportJobRenewalV1, body:m, delegating_signature:m);
59record!(DelegatedImportOperationV1, format_version:u, spool_uuid:b, spool_genesis_digest:b,
60    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, delegation_digest:b,
61    ref_name:s, slot_id:u, hash_algorithm:e, observed_commit_oid:b, genesis_digest:b,
62    target_thread_id:b, expected_frontier_digest:b, resulting_frontier_digest:b,
63    resulting_content_digest:b, result_bytes:u, options_digest:b, converter_version:s);
64record!(SignedDelegatedImportOperationV1, body:m, job_signature:m);
65record!(ImportPublicationWitnessV1, format_version:u, signed_operation_digest:b, delegation_digest:b,
66    logical_job_id:b, retry_lineage_id:b, physical_operation_id:b, ref_name:s, slot_id:u,
67    hash_algorithm:e, observed_commit_oid:b, expected_frontier_digest:b, resulting_frontier_digest:b,
68    terminal_manifest_digest:b);
69
70pub fn signed_permission_digest(v: &SignedImportMemberPermissionV1) -> Result<Vec<u8>, Reject> {
71    signing_digest("heddle-signed-import-member-permission-v1", v)
72}
73pub fn owner_chain_digest(v: &ImportOwnerChainV1) -> Result<Vec<u8>, Reject> {
74    width(&v.spool_genesis_digest, 32)?;
75    if v.owner_state_hashes.is_empty()
76        || v.owner_state_hashes.len() > 64
77        || v.transfer_audit_hashes.len() > 64
78    {
79        return Err(Reject::Bounds);
80    }
81    for h in v.owner_state_hashes.iter().chain(&v.transfer_audit_hashes) {
82        width(h, 32)?;
83    }
84    if v.owner_state_hashes.windows(2).any(|w| w[0] >= w[1]) {
85        return Err(Reject::Canonical);
86    }
87    signing_digest("heddle-import-owner-chain-v1", v)
88}
89pub fn signed_genesis_digest(v: &SignedImportGenesisAuthorityV1) -> Result<Vec<u8>, Reject> {
90    signing_digest("heddle-signed-import-genesis-authority-v1", v)
91}
92pub fn signed_delegation_digest(v: &SignedImportJobDelegationV1) -> Result<Vec<u8>, Reject> {
93    signing_digest("heddle-signed-import-job-delegation-v1", v)
94}
95pub fn signed_operation_digest(v: &SignedDelegatedImportOperationV1) -> Result<Vec<u8>, Reject> {
96    signing_digest("heddle-signed-delegated-import-operation-v1", v)
97}
98pub fn manifest_digest(v: &ImportResultManifestV1) -> Result<Vec<u8>, Reject> {
99    signing_digest(MANIFEST_DOMAIN, v)
100}
101pub fn verify_authorization_signature(
102    key: &[u8],
103    domain: &str,
104    body: &impl crate::hybrid_codec::Canonical,
105    signature: &AuthorizationSignature,
106) -> Result<(), Reject> {
107    if signature.signer_key_id != key_id(key) {
108        return Err(Reject::Signature);
109    }
110    let bytes = canonical(body)?;
111    if bytes.len() > MAX_RECORD_BYTES {
112        return Err(Reject::Bounds);
113    }
114    verify(
115        key,
116        &hash(&[domain.as_bytes(), &bytes]),
117        &signature.signature,
118    )
119}
120
121/// Conservative canonical URL grammar for v1: lowercase DNS HTTPS host, no
122/// userinfo/query/fragment/port/escapes, ASCII unreserved path segments. No
123/// implicit URL normalization is performed by a signing implementation.
124pub fn canonical_https(value: &str, origin: bool) -> Result<(), Reject> {
125    if value.len() > 2048 {
126        return Err(Reject::Bounds);
127    }
128    let rest = value.strip_prefix("https://").ok_or(Reject::Canonical)?;
129    let (host, path) = rest.split_once('/').unwrap_or((rest, ""));
130    if host.is_empty()
131        || host.len() > 253
132        || host.split('.').any(|part| {
133            part.is_empty()
134                || part.len() > 63
135                || part.starts_with('-')
136                || part.ends_with('-')
137                || !part
138                    .bytes()
139                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
140        })
141        || (origin && rest != host)
142        || (!origin && path.is_empty())
143        || path.split('/').any(|p| {
144            p.is_empty() && !origin
145                || p == "."
146                || p == ".."
147                || !p
148                    .bytes()
149                    .all(|b| b.is_ascii_alphanumeric() || b"-._~".contains(&b))
150        })
151    {
152        return Err(Reject::Canonical);
153    }
154    Ok(())
155}
156fn identity(value: &ImportIdentityV1) -> Result<(), Reject> {
157    for v in [&value.spool_uuid, &value.owner_account_uuid] {
158        width(v, 16)?;
159        if v.iter().all(|b| *b == 0) {
160            return Err(Reject::Canonical);
161        }
162    }
163    for v in [
164        &value.spool_genesis_digest,
165        &value.owner_id,
166        &value.owner_state_hash,
167    ] {
168        width(v, 32)?;
169    }
170    Ok(())
171}
172fn interval(start: i64, end: i64, now: i64) -> Result<(), Reject> {
173    if start < 0 || end <= start {
174        return Err(Reject::Semantic);
175    }
176    if now < start || now >= end {
177        return Err(Reject::Expired);
178    }
179    Ok(())
180}
181fn branch(value: &ImportBranchLimitV1) -> Result<(), Reject> {
182    if !value.ref_name.starts_with("refs/heads/")
183        || value.ref_name.len() > 1024
184        || value.ref_name.ends_with('/')
185        || value.ref_name.ends_with('.')
186        || value.ref_name.contains("..")
187        || value.ref_name.contains("//")
188        || value.ref_name.contains("@{")
189        || value
190            .ref_name
191            .split('/')
192            .any(|p| p.starts_with('.') || p.ends_with(".lock"))
193        || !value
194            .ref_name
195            .bytes()
196            .all(|b| b.is_ascii_alphanumeric() || b"/_-.".contains(&b))
197    {
198        return Err(Reject::Canonical);
199    }
200    let size = match value.hash_algorithm {
201        1 => 20,
202        2 => 32,
203        _ => return Err(Reject::Version),
204    };
205    match value.ref_mode {
206        1 => width(&value.pinned_commit_oid, size)?,
207        2 if value.pinned_commit_oid.is_empty() => (),
208        _ => return Err(Reject::Semantic),
209    }
210    for v in [
211        &value.genesis_digest,
212        &value.target_thread_id,
213        &value.expected_frontier_digest,
214    ] {
215        width(v, 32)?;
216    }
217    if value.max_result_bytes == 0 || value.max_result_bytes > MAX_RESULT_BYTES {
218        return Err(Reject::Bounds);
219    }
220    Ok(())
221}
222pub fn validate_scope(value: &ImportPermissionScopeV1) -> Result<(), Reject> {
223    canonical_https(&value.source_url, false)?;
224    if value.provider.is_empty()
225        || value.provider.len() > 64
226        || !value
227            .provider
228            .bytes()
229            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
230        || value.converter_version.is_empty()
231        || value.converter_version.len() > 128
232        || !value.converter_version.is_ascii()
233    {
234        return Err(Reject::Canonical);
235    }
236    width(&value.destination_version, 32)?;
237    width(&value.options_digest, 32)?;
238    if value.branches.is_empty()
239        || value.branches.len() > MAX_BRANCHES
240        || value.max_operations == 0
241        || value.max_operations as usize > MAX_BRANCHES
242        || value.max_result_bytes == 0
243        || value.max_result_bytes > MAX_RESULT_BYTES
244    {
245        return Err(Reject::Bounds);
246    }
247    if (value.max_operations as usize) < value.branches.len() {
248        return Err(Reject::Scope);
249    }
250    let mut total = 0_u64;
251    for (i, b) in value.branches.iter().enumerate() {
252        branch(b)?;
253        if i > 0 && value.branches[i - 1].ref_name.as_bytes() >= b.ref_name.as_bytes() {
254            return Err(Reject::Canonical);
255        }
256        total = total
257            .checked_add(b.max_result_bytes)
258            .ok_or(Reject::Bounds)?;
259    }
260    if total > value.max_result_bytes {
261        return Err(Reject::Scope);
262    }
263    Ok(())
264}
265fn scope_subset(child: &ImportPermissionScopeV1, parent: &ImportPermissionScopeV1) -> bool {
266    child.provider == parent.provider
267        && child.source_url == parent.source_url
268        && child.destination_version == parent.destination_version
269        && child.options_digest == parent.options_digest
270        && child.converter_version == parent.converter_version
271        && child.max_operations <= parent.max_operations
272        && child.max_result_bytes <= parent.max_result_bytes
273        && child.branches.iter().all(|c| {
274            parent.branches.iter().any(|p| {
275                let mut limit = c.clone();
276                limit.max_result_bytes = p.max_result_bytes;
277                limit == *p && c.max_result_bytes <= p.max_result_bytes
278            })
279        })
280}
281
282/// Public context from the existing owner/keyring verifier, not from fields in
283/// the incoming bundle. now is receiver/host time for new work or independently
284/// verified witness observation time for retained history, NEVER author time.
285pub struct ImportOwnerExpectation<'a> {
286    pub identity: &'a ImportIdentityV1,
287    pub owner_public_key: &'a [u8],
288    pub owner_chain_digest: &'a [u8],
289    pub authority_expires_at_seconds: i64,
290    pub now_unix_seconds: i64,
291    pub forbidden_job_keys: &'a [Vec<u8>], // Every user/root/witness key, including tombstones.
292    pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)], // key -> logical job.
293}
294pub fn verify_member_permission(
295    signed: &SignedImportMemberPermissionV1,
296    expected: &ImportOwnerExpectation<'_>,
297) -> Result<(), Reject> {
298    let p = signed.body.as_ref().ok_or(Reject::ImportPermission)?;
299    if p.format_version != 1 || p.purpose != 1 {
300        return Err(Reject::ImportPermission);
301    }
302    identity(p.identity.as_ref().ok_or(Reject::Canonical)?)?;
303    if p.identity.as_ref() != Some(expected.identity)
304        || p.owner_chain_digest != expected.owner_chain_digest
305    {
306        return Err(Reject::Root);
307    }
308    width(&p.logical_job_id, 16)?;
309    width(&p.retry_lineage_id, 16)?;
310    width(&p.subject_public_key, 32)?;
311    width(&p.cancellation_id, 32)?;
312    width(&p.nonce, 32)?;
313    width(&p.owner_chain_digest, 32)?;
314    validate_scope(p.scope.as_ref().ok_or(Reject::Canonical)?)?;
315    interval(
316        p.not_before_unix_seconds,
317        p.expires_at_unix_seconds,
318        expected.now_unix_seconds,
319    )?;
320    if p.expires_at_unix_seconds > expected.authority_expires_at_seconds {
321        return Err(Reject::Scope);
322    }
323    verify_authorization_signature(
324        expected.owner_public_key,
325        PERMISSION_DOMAIN,
326        p,
327        signed.owner_signature.as_ref().ok_or(Reject::Signature)?,
328    )
329}
330
331#[derive(Debug, Clone, PartialEq)]
332pub struct VerifiedImportDelegation {
333    body: ImportJobDelegationV1,
334    digest: Vec<u8>,
335}
336impl VerifiedImportDelegation {
337    pub fn body(&self) -> &ImportJobDelegationV1 {
338        &self.body
339    }
340    pub fn digest(&self) -> &[u8] {
341        &self.digest
342    }
343}
344pub fn verify_delegation(
345    signed: &SignedImportJobDelegationV1,
346    member: Option<&SignedImportMemberPermissionV1>,
347    expected: &ImportOwnerExpectation<'_>,
348) -> Result<VerifiedImportDelegation, Reject> {
349    let d = signed.body.as_ref().ok_or(Reject::Canonical)?;
350    if d.format_version != 1 || d.purpose != 1 {
351        return Err(Reject::Version);
352    }
353    identity(d.identity.as_ref().ok_or(Reject::Canonical)?)?;
354    if d.identity.as_ref() != Some(expected.identity)
355        || d.owner_chain_digest != expected.owner_chain_digest
356    {
357        return Err(Reject::Root);
358    }
359    for v in [&d.delegation_id, &d.logical_job_id, &d.retry_lineage_id] {
360        width(v, 16)?;
361        if v.iter().all(|b| *b == 0) {
362            return Err(Reject::Canonical);
363        }
364    }
365    for v in [
366        &d.job_public_key,
367        &d.job_key_id,
368        &d.delegating_public_key,
369        &d.parent_permission_digest,
370        &d.owner_chain_digest,
371        &d.cancellation_id,
372        &d.predecessor_delegation_digest,
373    ] {
374        width(v, 32)?;
375    }
376    if d.job_key_id != key_id(&d.job_public_key) {
377        return Err(Reject::Canonical);
378    }
379    if d.job_public_key == d.delegating_public_key
380        || d.job_public_key == expected.owner_public_key
381        || expected.forbidden_job_keys.contains(&d.job_public_key)
382    {
383        return Err(Reject::KeyRole);
384    }
385    if expected
386        .known_job_associations
387        .iter()
388        .any(|(k, j)| k == &d.job_public_key && j != &d.logical_job_id)
389    {
390        return Err(Reject::Scope);
391    }
392    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
393    validate_scope(scope)?;
394    if d.branch_manifest.len() != scope.branches.len() {
395        return Err(Reject::Scope);
396    }
397    for (m, b) in d.branch_manifest.iter().zip(&scope.branches) {
398        width(&m.genesis_authority_digest, 32)?;
399        if m.limit.as_ref() != Some(b) {
400            return Err(Reject::Scope);
401        }
402    }
403    interval(
404        d.not_before_unix_seconds,
405        d.expires_at_unix_seconds,
406        expected.now_unix_seconds,
407    )?;
408    if d.expires_at_unix_seconds > expected.authority_expires_at_seconds {
409        return Err(Reject::Scope);
410    }
411    if d.delegating_public_key == expected.owner_public_key {
412        if member.is_some() || d.parent_permission_digest != vec![0; 32] {
413            return Err(Reject::ImportPermission);
414        }
415    } else {
416        let member = member.ok_or(Reject::ImportPermission)?;
417        verify_member_permission(member, expected)?;
418        let p = member.body.as_ref().ok_or(Reject::ImportPermission)?;
419        if d.parent_permission_digest != signed_permission_digest(member)?
420            || d.delegating_public_key != p.subject_public_key
421            || d.logical_job_id != p.logical_job_id
422            || d.retry_lineage_id != p.retry_lineage_id
423            || d.not_before_unix_seconds < p.not_before_unix_seconds
424            || d.expires_at_unix_seconds > p.expires_at_unix_seconds
425            || !scope_subset(scope, p.scope.as_ref().ok_or(Reject::Canonical)?)
426        {
427            return Err(Reject::Scope);
428        }
429    }
430    verify_authorization_signature(
431        &d.delegating_public_key,
432        DELEGATION_DOMAIN,
433        d,
434        signed
435            .delegating_signature
436            .as_ref()
437            .ok_or(Reject::Signature)?,
438    )?;
439    Ok(VerifiedImportDelegation {
440        body: d.clone(),
441        digest: signed_delegation_digest(signed)?,
442    })
443}
444pub fn verify_genesis_authority(
445    signed: &SignedImportGenesisAuthorityV1,
446    delegation: &VerifiedImportDelegation,
447    original_genesis_digest: &[u8],
448    original_signature: &[u8],
449    envelope_digest: &[u8],
450) -> Result<(), Reject> {
451    let g = signed.body.as_ref().ok_or(Reject::Canonical)?;
452    let d = &delegation.body;
453    if g.format_version != 1 {
454        return Err(Reject::Version);
455    }
456    width(&g.original_creator_signature, 64)?;
457    for v in [
458        &g.genesis_digest,
459        &g.creator_public_key,
460        &g.creator_authority_envelope_digest,
461        &g.parent_permission_digest,
462        &g.owner_chain_digest,
463    ] {
464        width(v, 32)?;
465    }
466    if g.identity != d.identity
467        || g.creator_public_key != d.delegating_public_key
468        || g.parent_permission_digest != d.parent_permission_digest
469        || g.owner_chain_digest != d.owner_chain_digest
470        || g.genesis_digest != original_genesis_digest
471        || g.original_creator_signature != original_signature
472        || g.creator_authority_envelope_digest != envelope_digest
473        || !d.branch_manifest.iter().any(|m| {
474            m.limit
475                .as_ref()
476                .is_some_and(|b| b.genesis_digest == g.genesis_digest)
477                && signed_genesis_digest(signed).is_ok_and(|h| h == m.genesis_authority_digest)
478        })
479    {
480        return Err(Reject::Scope);
481    }
482    verify_authorization_signature(
483        &g.creator_public_key,
484        GENESIS_DOMAIN,
485        g,
486        signed.creator_signature.as_ref().ok_or(Reject::Signature)?,
487    )
488}
489/// Structural signature + scoped operation only. This does not establish
490/// publication, current policy, cancellation, leases or conversion correctness.
491pub fn verify_operation(
492    signed: &SignedDelegatedImportOperationV1,
493    delegation: &VerifiedImportDelegation,
494) -> Result<(), Reject> {
495    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
496    let d = &delegation.body;
497    if o.format_version != 1 {
498        return Err(Reject::Version);
499    }
500    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
501    width(&o.physical_operation_id, 16)?;
502    for v in [
503        &o.spool_genesis_digest,
504        &o.delegation_digest,
505        &o.genesis_digest,
506        &o.target_thread_id,
507        &o.expected_frontier_digest,
508        &o.resulting_frontier_digest,
509        &o.resulting_content_digest,
510        &o.options_digest,
511    ] {
512        width(v, 32)?;
513    }
514    width(&o.spool_uuid, 16)?;
515    width(&o.logical_job_id, 16)?;
516    width(&o.retry_lineage_id, 16)?;
517    let scope = d.scope.as_ref().ok_or(Reject::Canonical)?;
518    let b = scope
519        .branches
520        .iter()
521        .find(|b| b.ref_name == o.ref_name && b.slot_id == o.slot_id)
522        .ok_or(Reject::Scope)?;
523    let oid_len = match o.hash_algorithm {
524        1 => 20,
525        2 => 32,
526        _ => return Err(Reject::Version),
527    };
528    width(&o.observed_commit_oid, oid_len)?;
529    if o.spool_uuid != id.spool_uuid
530        || o.spool_genesis_digest != id.spool_genesis_digest
531        || o.logical_job_id != d.logical_job_id
532        || o.retry_lineage_id != d.retry_lineage_id
533        || o.delegation_digest != delegation.digest
534        || o.hash_algorithm != b.hash_algorithm
535        || (b.ref_mode == 1 && o.observed_commit_oid != b.pinned_commit_oid)
536        || o.genesis_digest != b.genesis_digest
537        || o.target_thread_id != b.target_thread_id
538        || o.expected_frontier_digest != b.expected_frontier_digest
539        || o.result_bytes > b.max_result_bytes
540        || o.result_bytes == 0
541        || o.options_digest != scope.options_digest
542        || o.converter_version != scope.converter_version
543    {
544        return Err(Reject::Scope);
545    }
546    verify_authorization_signature(
547        &d.job_public_key,
548        OPERATION_DOMAIN,
549        o,
550        signed.job_signature.as_ref().ok_or(Reject::Signature)?,
551    )
552}
553pub fn verify_new_operation(
554    signed: &SignedDelegatedImportOperationV1,
555    delegation: &VerifiedImportDelegation,
556    now_seconds: i64,
557) -> Result<(), Reject> {
558    interval(
559        delegation.body.not_before_unix_seconds,
560        delegation.body.expires_at_unix_seconds,
561        now_seconds,
562    )?;
563    verify_operation(signed, delegation)
564}
565pub fn validate_manifest(m: &ImportResultManifestV1) -> Result<(), Reject> {
566    if m.format_version != 1 {
567        return Err(Reject::Version);
568    }
569    width(&m.logical_job_id, 16)?;
570    width(&m.retry_lineage_id, 16)?;
571    if m.slots.len() > MAX_BRANCHES {
572        return Err(Reject::Bounds);
573    }
574    for (i, s) in m.slots.iter().enumerate() {
575        width(&s.signed_operation_digest, 32)?;
576        width(&s.resulting_frontier_digest, 32)?;
577        if !s.ref_name.starts_with("refs/heads/") || !s.ref_name.is_ascii() {
578            return Err(Reject::Canonical);
579        }
580        if s.ref_name.len() > 1024 || s.result_bytes == 0 || s.result_bytes > MAX_RESULT_BYTES {
581            return Err(Reject::Bounds);
582        }
583        if i > 0 && (&m.slots[i - 1].ref_name, m.slots[i - 1].slot_id) >= (&s.ref_name, s.slot_id) {
584            return Err(Reject::Canonical);
585        }
586    }
587    Ok(())
588}
589/// CAS state MUST be receiver-owned and held under the publication/renewal
590/// transaction fence. Returns replacement only after all remaining-slot checks.
591pub fn verify_renewal(
592    signed: &SignedImportJobRenewalV1,
593    previous: &VerifiedImportDelegation,
594    committed: &ImportResultManifestV1,
595    authority_epoch: u64,
596    member: Option<&SignedImportMemberPermissionV1>,
597    expected: &ImportOwnerExpectation<'_>,
598) -> Result<VerifiedImportDelegation, Reject> {
599    let r = signed.body.as_ref().ok_or(Reject::Canonical)?;
600    if r.format_version != 1 {
601        return Err(Reject::Version);
602    }
603    validate_manifest(committed)?;
604    if r.expected_authority_epoch != authority_epoch {
605        return Err(Reject::StaleContext);
606    }
607    if r.predecessor_delegation_digest != previous.digest {
608        return Err(Reject::RenewalFork);
609    }
610    if r.committed_manifest_digest != manifest_digest(committed)? {
611        return Err(Reject::StaleManifest);
612    }
613    let signed_next = r.replacement.as_ref().ok_or(Reject::Canonical)?;
614    let next = verify_delegation(signed_next, member, expected)?;
615    let before = &previous.body;
616    let after = &next.body;
617    let before_id = before.identity.as_ref().ok_or(Reject::Canonical)?;
618    let after_id = after.identity.as_ref().ok_or(Reject::Canonical)?;
619    if after.logical_job_id != before.logical_job_id
620        || after.retry_lineage_id != before.retry_lineage_id
621        || committed.logical_job_id != before.logical_job_id
622        || committed.retry_lineage_id != before.retry_lineage_id
623        || after_id.spool_uuid != before_id.spool_uuid
624        || after_id.spool_genesis_digest != before_id.spool_genesis_digest
625        || after.predecessor_delegation_digest != previous.digest
626        || after.job_public_key == before.job_public_key
627        || after.delegation_id == before.delegation_id
628    {
629        return Err(Reject::RenewalFork);
630    }
631    let old_scope = before.scope.as_ref().ok_or(Reject::Canonical)?;
632    let new_scope = after.scope.as_ref().ok_or(Reject::Canonical)?;
633    if !scope_subset(new_scope, old_scope) {
634        return Err(Reject::RenewalFork);
635    }
636    let old_slots = &old_scope.branches;
637    let mut consumed = 0_u64;
638    for slot in &committed.slots {
639        // Old certificates may already omit previously committed slots. Only
640        // charge all committed slots against the original logical-job budgets;
641        // the host's initial manifest/limits remain durable across renewals.
642        if let Some(b) = old_slots
643            .iter()
644            .find(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
645        {
646            if slot.result_bytes > b.max_result_bytes {
647                return Err(Reject::RenewalFork);
648            }
649            consumed = consumed
650                .checked_add(slot.result_bytes)
651                .ok_or(Reject::Bounds)?;
652        }
653        if new_scope
654            .branches
655            .iter()
656            .any(|b| b.ref_name == slot.ref_name && b.slot_id == slot.slot_id)
657        {
658            return Err(Reject::CommittedSlot);
659        }
660    }
661    let removed = old_slots
662        .iter()
663        .filter(|b| {
664            committed
665                .slots
666                .iter()
667                .any(|s| s.ref_name == b.ref_name && s.slot_id == b.slot_id)
668        })
669        .count();
670    if new_scope.max_operations as usize > old_scope.max_operations as usize - removed
671        || new_scope.max_result_bytes
672            > old_scope
673                .max_result_bytes
674                .checked_sub(consumed)
675                .ok_or(Reject::RenewalFork)?
676        || after.branch_manifest.iter().any(|m| {
677            !before.branch_manifest.iter().any(|old| {
678                old.genesis_authority_digest == m.genesis_authority_digest
679                    && old
680                        .limit
681                        .as_ref()
682                        .zip(m.limit.as_ref())
683                        .is_some_and(|(a, b)| {
684                            a.ref_name == b.ref_name && a.genesis_digest == b.genesis_digest
685                        })
686            })
687        })
688    {
689        return Err(Reject::RenewalFork);
690    }
691    verify_authorization_signature(
692        &after.delegating_public_key,
693        RENEWAL_DOMAIN,
694        r,
695        signed
696            .delegating_signature
697            .as_ref()
698            .ok_or(Reject::Signature)?,
699    )?;
700    Ok(next)
701}
702/// Persistent unique slot identity: logical job/ref/slot. Exact replay returns
703/// the old receipt/manifest, never another publication or fresh witness.
704pub fn check_slot_replay(
705    committed: &ImportResultManifestV1,
706    signed: &SignedDelegatedImportOperationV1,
707) -> Result<bool, Reject> {
708    validate_manifest(committed)?;
709    let o = signed.body.as_ref().ok_or(Reject::Canonical)?;
710    if committed.logical_job_id != o.logical_job_id
711        || committed.retry_lineage_id != o.retry_lineage_id
712    {
713        return Err(Reject::Scope);
714    }
715    match committed
716        .slots
717        .iter()
718        .find(|s| s.ref_name == o.ref_name && s.slot_id == o.slot_id)
719    {
720        Some(s)
721            if s.signed_operation_digest == signed_operation_digest(signed)?
722                && s.resulting_frontier_digest == o.resulting_frontier_digest
723                && s.result_bytes == o.result_bytes =>
724        {
725            Ok(true)
726        }
727        Some(_) => Err(Reject::SlotConflict),
728        None => Ok(false),
729    }
730}
731/// Verify exact committed publication in addition to job signature/scope. The
732/// owner/keyring verifier must resolve the statement's accepted state/order;
733/// witness signature alone cannot establish that user authority or disclosure.
734pub fn verify_publication(
735    operation: &SignedDelegatedImportOperationV1,
736    delegation: &VerifiedImportDelegation,
737    manifest: &ImportResultManifestV1,
738    statement: &crate::heddle::api::common::SignedHostedWitnessStatementV1,
739    set: &crate::witness_trust::VerifiedWitnessSet,
740    proof: Option<&crate::heddle::api::common::HostedWitnessHistoryProofV1>,
741    now_ms: i64,
742) -> Result<crate::witness_trust::ResolvedWitnessStatement, Reject> {
743    verify_operation(operation, delegation)?;
744    if !check_slot_replay(manifest, operation)? {
745        return Err(Reject::Scope);
746    }
747    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
748    let d = &delegation.body;
749    let id = d.identity.as_ref().ok_or(Reject::Canonical)?;
750    let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
751    let payload = ImportPublicationWitnessV1 {
752        format_version: 1,
753        signed_operation_digest: signed_operation_digest(operation)?,
754        delegation_digest: delegation.digest.clone(),
755        logical_job_id: o.logical_job_id.clone(),
756        retry_lineage_id: o.retry_lineage_id.clone(),
757        physical_operation_id: o.physical_operation_id.clone(),
758        ref_name: o.ref_name.clone(),
759        slot_id: o.slot_id,
760        hash_algorithm: o.hash_algorithm,
761        observed_commit_oid: o.observed_commit_oid.clone(),
762        expected_frontier_digest: o.expected_frontier_digest.clone(),
763        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
764        terminal_manifest_digest: manifest_digest(manifest)?,
765    };
766    if s.purpose != 3
767        || s.spool_uuid != id.spool_uuid
768        || s.spool_genesis_digest != id.spool_genesis_digest
769        || s.owner_id != id.owner_id
770        || s.owner_state_hash != id.owner_state_hash
771        || s.ownership_transfer_sequence != id.ownership_transfer_sequence
772        || s.authority_digest != delegation.digest
773        || s.original_signatures_digest
774            != hash(&[&operation
775                .job_signature
776                .as_ref()
777                .ok_or(Reject::Signature)?
778                .signature])
779        || s.canonical_payload != canonical(&payload)?
780        || s.basis != 1
781    {
782        return Err(Reject::Scope);
783    }
784    interval(
785        d.not_before_unix_seconds,
786        d.expires_at_unix_seconds,
787        s.observed_at_unix_millis / 1000,
788    )?;
789    crate::witness_trust::resolve_statement(set, statement, proof, false, now_ms)
790}
791pub fn require_hybrid_peer(
792    protocol: Option<&crate::heddle::api::common::ProtocolCompatibility>,
793) -> Result<(), Reject> {
794    let protocol = protocol.ok_or(Reject::Protocol)?;
795    if protocol.protocol_version != 2 || protocol.mandatory_features != [1] {
796        return Err(Reject::Protocol);
797    }
798    Ok(())
799}
800
801/// Producer-owned logical-job/lease fence for RetryImportSource and final
802/// publication. The physical retry row never supplies a new logical identity.
803pub fn check_job_fence(
804    logical_job_id: &[u8],
805    active_delegation_digest: &[u8],
806    expected_epoch: u64,
807    active: &VerifiedImportDelegation,
808    durable_epoch: u64,
809) -> Result<(), Reject> {
810    if logical_job_id != active.body.logical_job_id {
811        return Err(Reject::Scope);
812    }
813    if expected_epoch != durable_epoch || active_delegation_digest != active.digest {
814        return Err(Reject::StaleContext);
815    }
816    Ok(())
817}
818pub fn validate_public_bundle(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
819    use prost::Message;
820    if bundle.format_version != 1 {
821        return Err(Reject::Version);
822    }
823    if bundle.encoded_len() > MAX_BUNDLE_BYTES
824        || bundle.owner_histories.len() > 64
825        || bundle.ownership_transfers.len() > 64
826        || bundle.genesis_authorities.len() > MAX_BRANCHES
827        || bundle.delegations.len() > 64
828        || bundle.renewals.len() > 63
829        || bundle.operations.len() > MAX_BRANCHES
830        || bundle.statements.len() > 1024
831        || bundle.history_proofs.len() > 1024
832        || bundle.policies.len() > 256
833        || bundle.original_geneses.len() > MAX_BRANCHES
834        || bundle.creator_authority_envelopes.len() > MAX_BRANCHES
835        || bundle.member_permissions.len() > 64
836        || bundle.manifests.len() > 320
837        || bundle.genesis_witnesses.len() > 256
838        || bundle.authority_witnesses.len() > 256
839        || bundle.landing_witnesses.len() > 256
840    {
841        return Err(Reject::Bounds);
842    }
843    validate_bundle_history(bundle)
844}
845
846/// Typed adapter boundary: an authentic unrelated capability/online role must
847/// never be selected as the parent of an import certificate.
848pub enum ImportPermissionEvidence<'a> {
849    Import(&'a SignedImportMemberPermissionV1),
850    OwnerCapability(&'a SignedOwnerCapability),
851    OnlineRole(&'a str),
852}
853pub fn select_import_permission(
854    evidence: ImportPermissionEvidence<'_>,
855) -> Result<&SignedImportMemberPermissionV1, Reject> {
856    match evidence {
857        ImportPermissionEvidence::Import(p) => Ok(p),
858        ImportPermissionEvidence::OwnerCapability(_) | ImportPermissionEvidence::OnlineRole(_) => {
859            Err(Reject::ImportPermission)
860        }
861    }
862}
863/// Hybrid dispatch has no legacy execution arm, even for an authentic witness.
864pub fn require_import_operation_format(format: &str) -> Result<(), Reject> {
865    if format != OPERATION_DOMAIN {
866        return Err(Reject::Protocol);
867    }
868    Ok(())
869}
870pub fn frontier_digest(frontier: &ImportFrontierV1) -> Result<Vec<u8>, Reject> {
871    if frontier.format_version != 1 {
872        return Err(Reject::Version);
873    }
874    width(&frontier.thread_id, 32)?;
875    if frontier.operation_ids.len() > 128 {
876        return Err(Reject::Bounds);
877    }
878    for id in &frontier.operation_ids {
879        width(id, 32)?;
880    }
881    if frontier.operation_ids.windows(2).any(|w| w[0] >= w[1]) {
882        return Err(Reject::Canonical);
883    }
884    signing_digest("heddle-import-frontier-v1", frontier)
885}
886pub fn content_digest(content: &ImportContentV1) -> Result<Vec<u8>, Reject> {
887    if content.format_version != 1 {
888        return Err(Reject::Version);
889    }
890    if content.canonical_capture.is_empty()
891        || content.canonical_capture.len() > MAX_RESULT_BYTES as usize
892    {
893        return Err(Reject::Bounds);
894    }
895    signing_digest("heddle-import-content-v1", content)
896}
897pub fn signed_native_digest(record: &SignedRecord) -> Result<Vec<u8>, Reject> {
898    signing_digest("heddle-signed-native-record-v1", record)
899}
900fn verify_native(record: &SignedRecord, format: &str) -> Result<(), Reject> {
901    if record.format != format {
902        return Err(Reject::Version);
903    }
904    if record.canonical_record.is_empty()
905        || record.canonical_record.len() > MAX_RECORD_BYTES
906        || record.signatures.is_empty()
907        || record.signatures.len() > 16
908    {
909        return Err(Reject::Bounds);
910    }
911    let mut previous: Option<&[u8]> = None;
912    let input = [format.as_bytes(), b"\0", &record.canonical_record].concat();
913    for s in &record.signatures {
914        if previous.is_some_and(|p| p >= s.public_key.as_slice()) {
915            return Err(Reject::Canonical);
916        }
917        verify(&s.public_key, &input, &s.signature)?;
918        previous = Some(&s.public_key);
919    }
920    Ok(())
921}
922fn native_dependencies(records: &[SignedRecord]) -> Result<(), Reject> {
923    if records.len() > 128 {
924        return Err(Reject::Bounds);
925    }
926    let mut previous = None;
927    for record in records {
928        if ![
929            "heddle-thread-genesis-v1",
930            "heddle-thread-operation-v1",
931            "heddle-thread-ownership-claim-v1",
932            "heddle-thread-ownership-resolution-v1",
933        ]
934        .contains(&record.format.as_str())
935        {
936            return Err(Reject::Version);
937        }
938        verify_native(record, &record.format)?;
939        let digest = signed_native_digest(record)?;
940        if previous.as_ref().is_some_and(|p| p >= &digest) {
941            return Err(Reject::Canonical);
942        }
943        previous = Some(digest);
944    }
945    Ok(())
946}
947fn original_signatures(
948    records: &[&SignedRecord],
949    extra: &[RecordSignature],
950) -> Result<Vec<u8>, Reject> {
951    let signatures = records
952        .iter()
953        .flat_map(|r| r.signatures.iter())
954        .chain(extra.iter())
955        .collect::<Vec<_>>();
956    let mut out = (signatures.len() as u32).to_be_bytes().to_vec();
957    for s in signatures {
958        s.write(&mut out)?;
959    }
960    Ok(hash(&[b"heddle-hosted-original-signatures-v1", &out]))
961}
962use crate::hybrid_codec::Canonical;
963/// Caller constructs this from independently verified native originals and
964/// accepted owner/policy/landing context. This matching layer verifies original
965/// signatures and exact payload commitments separately from witness trust; it
966/// does not replace native causal, authority or landing-model verification.
967pub enum WitnessPayload<'a> {
968    Genesis(&'a ImportGenesisWitnessV1),
969    Authority(&'a ImportAuthorityWitnessV1),
970    Landing(&'a HostedLandingWitnessV1),
971}
972pub fn verify_witness_payload(
973    statement: &crate::heddle::api::common::HostedWitnessStatementV1,
974    payload: WitnessPayload<'_>,
975) -> Result<(), Reject> {
976    let (purpose, bytes, authority, signatures, publisher) = match payload {
977        WitnessPayload::Genesis(p) => {
978            if p.format_version != 1 {
979                return Err(Reject::Version);
980            }
981            let original = p.original_genesis.as_ref().ok_or(Reject::Canonical)?;
982            let binding = p.binding.as_ref().ok_or(Reject::Canonical)?;
983            let b = binding.body.as_ref().ok_or(Reject::Canonical)?;
984            verify_native(original, "heddle-thread-genesis-v1")?;
985            if native_id(original) != b.genesis_digest {
986                return Err(Reject::Scope);
987            }
988            if let Some(id) = &b.identity {
989                if statement.spool_uuid != id.spool_uuid
990                    || statement.spool_genesis_digest != id.spool_genesis_digest
991                    || statement.owner_id != id.owner_id
992                    || statement.owner_state_hash != id.owner_state_hash
993                    || statement.ownership_transfer_sequence != id.ownership_transfer_sequence
994                {
995                    return Err(Reject::Scope);
996                }
997            } else {
998                return Err(Reject::Canonical);
999            }
1000            let creator = original
1001                .signatures
1002                .iter()
1003                .find(|s| s.public_key == b.creator_public_key)
1004                .ok_or(Reject::Signature)?;
1005            if b.original_creator_signature != creator.signature
1006                || b.creator_authority_envelope_digest != hash(&[&p.creator_authority_envelope])
1007            {
1008                return Err(Reject::Scope);
1009            }
1010            verify_authorization_signature(
1011                &b.creator_public_key,
1012                GENESIS_DOMAIN,
1013                b,
1014                binding
1015                    .creator_signature
1016                    .as_ref()
1017                    .ok_or(Reject::Signature)?,
1018            )?;
1019            (
1020                1,
1021                canonical(p)?,
1022                signed_genesis_digest(binding)?,
1023                original_signatures(&[original], &[])?,
1024                key_id(&b.creator_public_key),
1025            )
1026        }
1027        WitnessPayload::Authority(p) => {
1028            if p.format_version != 1 {
1029                return Err(Reject::Version);
1030            }
1031            let original = p.original.as_ref().ok_or(Reject::Canonical)?;
1032            let format = match p.kind {
1033                1 => "heddle-thread-operation-v1",
1034                2 => "heddle-thread-ownership-claim-v1",
1035                3 => "heddle-thread-ownership-resolution-v1",
1036                _ => return Err(Reject::Version),
1037            };
1038            verify_native(original, format)?;
1039            if (p.kind == 2 || p.kind == 3) && original.signatures.len() != 2 {
1040                return Err(Reject::Signature);
1041            }
1042            if !original
1043                .signatures
1044                .iter()
1045                .any(|s| key_id(&s.public_key) == statement.publisher_key_id)
1046            {
1047                return Err(Reject::Signature);
1048            }
1049            if p.authority_envelope.is_empty() || p.authority_envelope.len() > MAX_RECORD_BYTES {
1050                return Err(Reject::Bounds);
1051            }
1052            native_dependencies(&p.dependencies)?;
1053            let records = std::iter::once(original)
1054                .chain(p.dependencies.iter())
1055                .collect::<Vec<_>>();
1056            (
1057                2,
1058                canonical(p)?,
1059                hash(&[
1060                    b"heddle-hosted-authority-envelope-v1",
1061                    &(p.authority_envelope.len() as u32).to_be_bytes(),
1062                    &p.authority_envelope,
1063                ]),
1064                original_signatures(&records, &[])?,
1065                statement.publisher_key_id.clone(),
1066            )
1067        }
1068        WitnessPayload::Landing(p) => {
1069            if p.format_version != 1 {
1070                return Err(Reject::Version);
1071            }
1072            let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
1073            let source = p.source_operation.as_ref().ok_or(Reject::Canonical)?;
1074            let request = p.request.as_ref().ok_or(Reject::Canonical)?;
1075            if request.format_version != 1
1076                || request.method_path != "/heddle.api.v1alpha2.ThreadService/LandThread"
1077            {
1078                return Err(Reject::Version);
1079            }
1080            verify_native(execution, "heddle-thread-operation-v1")?;
1081            verify_native(source, "heddle-thread-operation-v1")?;
1082            native_dependencies(&p.review_evidence)?;
1083            let signature = request.signature.as_ref().ok_or(Reject::Signature)?;
1084            if request.signing_identity
1085                != format!(
1086                    "principal:device-key:{}",
1087                    hex::encode(&signature.public_key)
1088                )
1089            {
1090                return Err(Reject::Signature);
1091            }
1092            width(&request.nonce, 16)?;
1093            if request.timestamp_millis <= 0
1094                || request.request_body.is_empty()
1095                || request.request_body.len() > MAX_RECORD_BYTES
1096                || p.authority_envelope.is_empty()
1097                || p.authority_envelope.len() > MAX_RECORD_BYTES
1098            {
1099                return Err(Reject::Bounds);
1100            }
1101            let input = crate::signing::unary_bytes(
1102                &request.signing_identity,
1103                &request.method_path,
1104                request.timestamp_millis,
1105                &request.nonce,
1106                &request.request_body,
1107            );
1108            verify(&signature.public_key, &input, &signature.signature)?;
1109            let records = [execution, source]
1110                .into_iter()
1111                .chain(p.review_evidence.iter())
1112                .collect::<Vec<_>>();
1113            (
1114                4,
1115                canonical(p)?,
1116                hash(&[
1117                    b"heddle-hosted-authority-envelope-v1",
1118                    &(p.authority_envelope.len() as u32).to_be_bytes(),
1119                    &p.authority_envelope,
1120                ]),
1121                original_signatures(&records, std::slice::from_ref(signature))?,
1122                key_id(&signature.public_key),
1123            )
1124        }
1125    };
1126    if bytes.len() > MAX_RECORD_BYTES {
1127        return Err(Reject::Bounds);
1128    }
1129    if statement.purpose != purpose
1130        || statement.canonical_payload != bytes
1131        || statement.authority_digest != authority
1132        || statement.original_signatures_digest != signatures
1133        || statement.publisher_key_id != publisher
1134    {
1135        return Err(Reject::Scope);
1136    }
1137    Ok(())
1138}
1139
1140pub fn resolve_bundle_permission<'a>(
1141    bundle: &'a ImportPublicProofBundleV1,
1142    digest: &[u8],
1143) -> Result<Option<&'a SignedImportMemberPermissionV1>, Reject> {
1144    width(digest, 32)?;
1145    if digest == [0; 32] {
1146        return Ok(None);
1147    }
1148    bundle
1149        .member_permissions
1150        .iter()
1151        .find(|p| signed_permission_digest(p).is_ok_and(|d| d == digest))
1152        .map(Some)
1153        .ok_or(Reject::ImportPermission)
1154}
1155pub fn resolve_bundle_manifest<'a>(
1156    bundle: &'a ImportPublicProofBundleV1,
1157    digest: &[u8],
1158) -> Result<&'a ImportResultManifestV1, Reject> {
1159    width(digest, 32)?;
1160    bundle
1161        .manifests
1162        .iter()
1163        .find(|m| manifest_digest(m).is_ok_and(|d| d == digest))
1164        .ok_or(Reject::StaleManifest)
1165}
1166pub fn publication_payload(
1167    operation: &SignedDelegatedImportOperationV1,
1168    manifest: &ImportResultManifestV1,
1169) -> Result<ImportPublicationWitnessV1, Reject> {
1170    let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
1171    Ok(ImportPublicationWitnessV1 {
1172        format_version: 1,
1173        signed_operation_digest: signed_operation_digest(operation)?,
1174        delegation_digest: o.delegation_digest.clone(),
1175        logical_job_id: o.logical_job_id.clone(),
1176        retry_lineage_id: o.retry_lineage_id.clone(),
1177        physical_operation_id: o.physical_operation_id.clone(),
1178        ref_name: o.ref_name.clone(),
1179        slot_id: o.slot_id,
1180        hash_algorithm: o.hash_algorithm,
1181        observed_commit_oid: o.observed_commit_oid.clone(),
1182        expected_frontier_digest: o.expected_frontier_digest.clone(),
1183        resulting_frontier_digest: o.resulting_frontier_digest.clone(),
1184        terminal_manifest_digest: manifest_digest(manifest)?,
1185    })
1186}
1187/// Completeness and digest addressing only. Trust/signature verification still
1188/// uses independently selected owner contexts at each witnessed historical time.
1189fn validate_bundle_history(bundle: &ImportPublicProofBundleV1) -> Result<(), Reject> {
1190    fn sorted<T>(
1191        values: &[T],
1192        digest: impl Fn(&T) -> Result<Vec<u8>, Reject>,
1193    ) -> Result<(), Reject> {
1194        let mut previous = None;
1195        for value in values {
1196            let d = digest(value)?;
1197            if previous.as_ref().is_some_and(|p| p >= &d) {
1198                return Err(Reject::Canonical);
1199            }
1200            previous = Some(d);
1201        }
1202        Ok(())
1203    }
1204    for statement in &bundle.statements {
1205        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
1206        require_policy_history(
1207            bundle,
1208            &s.spool_uuid,
1209            s.policy_sequence,
1210            &s.policy_state_hash,
1211        )?;
1212    }
1213    sorted(&bundle.member_permissions, signed_permission_digest)?;
1214    sorted(&bundle.manifests, manifest_digest)?;
1215    if let Some(p) = &bundle.member_permission
1216        && resolve_bundle_permission(bundle, &signed_permission_digest(p)?)? != Some(p)
1217    {
1218        return Err(Reject::ImportPermission);
1219    }
1220    let terminal = bundle.terminal_manifest.as_ref().ok_or(Reject::Canonical)?;
1221    if resolve_bundle_manifest(bundle, &manifest_digest(terminal)?)? != terminal {
1222        return Err(Reject::Canonical);
1223    }
1224    if bundle.delegations.is_empty() || bundle.renewals.len() + 1 != bundle.delegations.len() {
1225        return Err(Reject::Canonical);
1226    }
1227    for (i, d) in bundle.delegations.iter().enumerate() {
1228        let body = d.body.as_ref().ok_or(Reject::Canonical)?;
1229        resolve_bundle_permission(bundle, &body.parent_permission_digest)?;
1230        if i == 0 {
1231            if body.predecessor_delegation_digest != [0; 32] {
1232                return Err(Reject::RenewalFork);
1233            }
1234        } else {
1235            let r = bundle.renewals[i - 1]
1236                .body
1237                .as_ref()
1238                .ok_or(Reject::Canonical)?;
1239            if r.replacement.as_ref() != Some(d)
1240                || r.predecessor_delegation_digest
1241                    != signed_delegation_digest(&bundle.delegations[i - 1])?
1242                || body.predecessor_delegation_digest != r.predecessor_delegation_digest
1243                || r.expected_authority_epoch != i as u64
1244            {
1245                return Err(Reject::RenewalFork);
1246            }
1247            resolve_bundle_manifest(bundle, &r.committed_manifest_digest)?;
1248        }
1249        for branch in &body.branch_manifest {
1250            let g = bundle
1251                .genesis_authorities
1252                .iter()
1253                .find(|g| {
1254                    signed_genesis_digest(g).is_ok_and(|h| h == branch.genesis_authority_digest)
1255                })
1256                .ok_or(Reject::Scope)?;
1257            let b = g.body.as_ref().ok_or(Reject::Canonical)?;
1258            resolve_bundle_permission(bundle, &b.parent_permission_digest)?;
1259            if !bundle
1260                .original_geneses
1261                .iter()
1262                .any(|o| native_id(o) == b.genesis_digest)
1263                || !bundle
1264                    .creator_authority_envelopes
1265                    .iter()
1266                    .any(|e| hash(&[e]) == b.creator_authority_envelope_digest)
1267            {
1268                return Err(Reject::Scope);
1269            }
1270            // Every branch needs its original admission, not merely its
1271            // binding. Proof-only retirement lookup cannot recover a payload.
1272            if !bundle.genesis_witnesses.iter().any(|payload| {
1273                payload.binding.as_ref() == Some(g)
1274                    && payload.original_genesis.as_ref().is_some_and(|o| {
1275                        native_id(o) == b.genesis_digest && bundle.original_geneses.contains(o)
1276                    })
1277                    && hash(&[&payload.creator_authority_envelope])
1278                        == b.creator_authority_envelope_digest
1279                    && canonical(payload).is_ok_and(|bytes| {
1280                        bundle.statements.iter().any(|s| {
1281                            s.body
1282                                .as_ref()
1283                                .is_some_and(|s| s.purpose == 1 && s.canonical_payload == bytes)
1284                        })
1285                    })
1286            }) {
1287                return Err(Reject::Scope);
1288            }
1289        }
1290    }
1291    for manifest in &bundle.manifests {
1292        validate_manifest(manifest)?;
1293        if manifest.logical_job_id != terminal.logical_job_id
1294            || manifest.retry_lineage_id != terminal.retry_lineage_id
1295        {
1296            return Err(Reject::Scope);
1297        }
1298        for slot in &manifest.slots {
1299            let operation = bundle
1300                .operations
1301                .iter()
1302                .find(|o| {
1303                    signed_operation_digest(o).is_ok_and(|d| d == slot.signed_operation_digest)
1304                })
1305                .ok_or(Reject::Scope)?;
1306            if !check_slot_replay(manifest, operation)? || !check_slot_replay(terminal, operation)?
1307            {
1308                return Err(Reject::Scope);
1309            }
1310        }
1311    }
1312    for operation in &bundle.operations {
1313        let o = operation.body.as_ref().ok_or(Reject::Canonical)?;
1314        if !bundle
1315            .delegations
1316            .iter()
1317            .any(|d| signed_delegation_digest(d).is_ok_and(|h| h == o.delegation_digest))
1318            || !check_slot_replay(terminal, operation)?
1319        {
1320            return Err(Reject::Scope);
1321        }
1322        if !bundle.manifests.iter().any(|m| {
1323            check_slot_replay(m, operation) == Ok(true)
1324                && publication_payload(operation, m)
1325                    .and_then(|p| canonical(&p))
1326                    .is_ok_and(|p| {
1327                        bundle.statements.iter().any(|s| {
1328                            s.body
1329                                .as_ref()
1330                                .is_some_and(|s| s.purpose == 3 && s.canonical_payload == p)
1331                        })
1332                    })
1333        }) {
1334            return Err(Reject::Scope);
1335        }
1336    }
1337    for statement in &bundle.statements {
1338        let s = statement.body.as_ref().ok_or(Reject::Canonical)?;
1339        let found = match s.purpose {
1340            1 => bundle
1341                .genesis_witnesses
1342                .iter()
1343                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
1344            2 => bundle
1345                .authority_witnesses
1346                .iter()
1347                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
1348            3 => bundle.operations.iter().any(|o| {
1349                bundle.manifests.iter().any(|m| {
1350                    publication_payload(o, m)
1351                        .and_then(|p| canonical(&p))
1352                        .is_ok_and(|p| p == s.canonical_payload)
1353                })
1354            }),
1355            4 => bundle
1356                .landing_witnesses
1357                .iter()
1358                .any(|p| canonical(p).is_ok_and(|p| p == s.canonical_payload)),
1359            _ => return Err(Reject::Version),
1360        };
1361        if !found {
1362            return Err(Reject::Scope);
1363        }
1364    }
1365    Ok(())
1366}
1367/// Reference completeness only. Native verification must authenticate every
1368/// selected policy, its owner context and the receipt before using its time.
1369fn require_policy_history(
1370    bundle: &ImportPublicProofBundleV1,
1371    spool: &[u8],
1372    mut sequence: u64,
1373    state_hash: &[u8],
1374) -> Result<(), Reject> {
1375    let mut state_hash = state_hash.to_vec();
1376    for _ in 0..=bundle.policies.len() {
1377        width(&state_hash, 32)?;
1378        if sequence == 0 {
1379            return if state_hash == [0; 32] {
1380                Ok(())
1381            } else {
1382                Err(Reject::Scope)
1383            };
1384        }
1385        let mut matches = bundle
1386            .policies
1387            .iter()
1388            .filter_map(|p| p.body.as_ref())
1389            .filter(|p| {
1390                p.spool_uuid == spool && p.sequence == sequence && p.policy_state_hash == state_hash
1391            });
1392        let policy = matches.next().ok_or(Reject::Scope)?;
1393        if matches.next().is_some() {
1394            return Err(Reject::Canonical);
1395        }
1396        let head = policy.expected_head.as_ref().ok_or(Reject::Canonical)?;
1397        if head.sequence.checked_add(1) != Some(sequence) {
1398            return Err(Reject::Scope);
1399        }
1400        sequence = head.sequence;
1401        state_hash = head.state_hash.clone();
1402    }
1403    Err(Reject::Scope)
1404}
1405fn native_id(record: &SignedRecord) -> Vec<u8> {
1406    let mut h = blake3::Hasher::new();
1407    h.update(record.format.as_bytes());
1408    h.update(&(record.canonical_record.len() as u64).to_le_bytes());
1409    h.update(b"\0");
1410    h.update(&record.canonical_record);
1411    h.finalize().as_bytes().to_vec()
1412}
1413/// Prepare response is a coherent proposal, never authority. Caller must compare
1414/// the IDs, predecessor and manifest before asking its device to sign renewal.
1415pub fn validate_renewal_preparation(response: &PrepareImportJobResponse) -> Result<(), Reject> {
1416    let state = response.renewal_state.as_ref().ok_or(Reject::Canonical)?;
1417    let proposal = response.proposal.as_ref().ok_or(Reject::Canonical)?;
1418    let previous = state.active_predecessor.as_ref().ok_or(Reject::Canonical)?;
1419    let p = previous.body.as_ref().ok_or(Reject::Canonical)?;
1420    let manifest = state.committed_manifest.as_ref().ok_or(Reject::Canonical)?;
1421    validate_manifest(manifest)?;
1422    if state.format_version != 1
1423        || state.authority_epoch == 0
1424        || state.logical_job_id != p.logical_job_id
1425        || state.retry_lineage_id != p.retry_lineage_id
1426        || proposal.logical_job_id != state.logical_job_id
1427        || proposal.retry_lineage_id != state.retry_lineage_id
1428        || manifest.logical_job_id != state.logical_job_id
1429        || manifest.retry_lineage_id != state.retry_lineage_id
1430        || proposal.predecessor_delegation_digest != signed_delegation_digest(previous)?
1431    {
1432        return Err(Reject::StaleContext);
1433    }
1434    Ok(())
1435}