1use 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
121pub 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
282pub 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>], pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)], }
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}
489pub 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}
589pub 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 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}
702pub 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}
731pub 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
801pub 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
846pub 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}
863pub 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;
963pub 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}
1187fn 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 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}
1367fn 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}
1413pub 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}