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