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!(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
125pub 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
286pub 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>], pub known_job_associations: &'a [(Vec<u8>, Vec<u8>)], }
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}
448pub 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
467pub 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 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 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}
599pub 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}
699pub 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 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}
812pub 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}
841pub 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
912pub 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
957pub 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}
974pub 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}
1033pub 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#[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;
1292pub 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}
1536fn 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 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}
1717fn 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}
1763pub 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}