1use crate::heddle::api::{common as host, v1alpha2 as api};
5use crate::hybrid_codec::{Reject, canonical, field, hash, key_id, record, signing_digest, width};
6use crate::import_authority as import;
7use prost::Message;
8
9pub const GENESIS_DOMAIN: &str = "heddle-native-genesis-authority-v1";
10pub const SIGNED_GENESIS_DOMAIN: &str = "heddle-signed-native-genesis-authority-v1";
11record!(api::NativeGenesisAuthorityV1, format_version:u, identity:m, owner_kind:e,
12 genesis_digest:b, original_signatures_digest:b, creator_public_key:b,
13 creator_authority_envelope_digest:b, owner_chain_digest:b, publisher_key_id:b);
14record!(api::SignedNativeGenesisAuthorityV1, body:m, creator_signature:m);
15record!(api::NativeGenesisWitnessV1, format_version:u, kind:e, binding:m,
16 original_genesis:m, creator_authority_envelope:b, boundary_acceptance:o);
17
18pub fn signed_genesis_digest(v: &api::SignedNativeGenesisAuthorityV1) -> Result<Vec<u8>, Reject> {
19 signing_digest(SIGNED_GENESIS_DOMAIN, v)
20}
21#[derive(serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23enum NativeOwner {
24 Account(Vec<u8>),
25 LocalKey([u8; 32]),
26}
27#[derive(serde::Deserialize)]
28struct GenesisSelectors {
29 version: u16,
30 spool: String,
31 creator: [u8; 32],
32 owner: NativeOwner,
33}
34#[derive(serde::Deserialize)]
35struct ThreadSelector {
36 thread: [u8; 32],
37}
38fn spool_uuid(text: &str) -> Result<Vec<u8>, Reject> {
39 if text.len() != 36
40 || [8, 13, 18, 23].iter().any(|&i| text.as_bytes()[i] != b'-')
41 || text.bytes().enumerate().any(|(i, b)| {
42 ![8, 13, 18, 23].contains(&i) && !b.is_ascii_digit() && !(b'a'..=b'f').contains(&b)
43 })
44 {
45 return Err(Reject::Canonical);
46 }
47 hex::decode(text.replace('-', "")).map_err(|_| Reject::Canonical)
48}
49pub fn verify_genesis_authority(
53 signed: &api::SignedNativeGenesisAuthorityV1,
54 original: &api::SignedRecord,
55 envelope: &[u8],
56) -> Result<(), Reject> {
57 let b = signed.body.as_ref().ok_or(Reject::GenesisBinding)?;
58 if b.format_version != 1 {
59 return Err(Reject::Version);
60 }
61 let id = b.identity.as_ref().ok_or(Reject::Canonical)?;
62 for v in [&id.spool_uuid, &id.owner_account_uuid] {
63 width(v, 16)?;
64 if v.iter().all(|v| *v == 0) {
65 return Err(Reject::Canonical);
66 }
67 }
68 for v in [
69 &id.spool_genesis_digest,
70 &id.owner_id,
71 &id.owner_state_hash,
72 &b.genesis_digest,
73 &b.original_signatures_digest,
74 &b.creator_public_key,
75 &b.creator_authority_envelope_digest,
76 &b.owner_chain_digest,
77 &b.publisher_key_id,
78 ] {
79 width(v, 32)?;
80 }
81 if envelope.len() > import::MAX_RECORD_BYTES {
82 return Err(Reject::Bounds);
83 }
84 import::verify_native(original, "heddle-thread-genesis-v1")?;
85 let g: GenesisSelectors =
86 rmp_serde::from_slice(&original.canonical_record).map_err(|_| Reject::Canonical)?;
87 if g.version != 1
88 || spool_uuid(&g.spool)? != id.spool_uuid
89 || g.creator.as_slice() != b.creator_public_key
90 || original.signatures.len() != 1
91 || original.signatures[0].public_key != b.creator_public_key
92 || import::native_id(original) != b.genesis_digest
93 || import::original_signatures(&[original], &[])? != b.original_signatures_digest
94 || hash(&[envelope]) != b.creator_authority_envelope_digest
95 || key_id(&b.creator_public_key) != b.publisher_key_id
96 {
97 return Err(Reject::GenesisBinding);
98 }
99 match (&g.owner, b.owner_kind) {
100 (NativeOwner::Account(account), 1)
101 if *account == id.owner_account_uuid && !envelope.is_empty() =>
102 {
103 let authority: api::ThreadControlAuthority =
104 crate::hybrid_codec::strict_decode(envelope, import::MAX_RECORD_BYTES)?;
105 if authority.format != 1 {
106 return Err(Reject::Version);
107 }
108 }
109 (NativeOwner::LocalKey(key), 2) if key == &g.creator && envelope.is_empty() => (),
110 _ => return Err(Reject::GenesisBinding),
111 }
112 import::verify_authorization_signature(
113 &b.creator_public_key,
114 GENESIS_DOMAIN,
115 b,
116 signed.creator_signature.as_ref().ok_or(Reject::Signature)?,
117 )
118}
119pub fn verify_genesis_payload(
120 s: &host::HostedWitnessStatementV1,
121 p: &api::NativeGenesisWitnessV1,
122) -> Result<(), Reject> {
123 if p.format_version != 1 || p.kind != 2 {
124 return Err(Reject::Version);
125 }
126 let binding = p.binding.as_ref().ok_or(Reject::GenesisBinding)?;
127 let original = p.original_genesis.as_ref().ok_or(Reject::Canonical)?;
128 verify_genesis_authority(binding, original, &p.creator_authority_envelope)?;
129 let b = binding.body.as_ref().ok_or(Reject::GenesisBinding)?;
130 let id = b.identity.as_ref().ok_or(Reject::Canonical)?;
131 import::match_boundary(
132 s,
133 &p.boundary_acceptance.iter().cloned().collect::<Vec<_>>(),
134 )?;
135 if (s.basis == 2) != p.boundary_acceptance.is_some() {
136 return Err(Reject::BoundaryAcceptance);
137 }
138 if let Some(e) = &p.boundary_acceptance {
139 import::boundary_original(e, original)?;
140 }
141 if s.purpose != 1
142 || s.spool_uuid != id.spool_uuid
143 || s.spool_genesis_digest != id.spool_genesis_digest
144 || (s.basis == 1
145 && (s.owner_id != id.owner_id
146 || s.owner_state_hash != id.owner_state_hash
147 || s.ownership_transfer_sequence != id.ownership_transfer_sequence))
148 || s.canonical_payload != canonical(p)?
149 || s.authority_digest != signed_genesis_digest(binding)?
150 || s.original_signatures_digest != b.original_signatures_digest
151 || s.publisher_key_id != b.publisher_key_id
152 {
153 return Err(Reject::Scope);
154 }
155 if s.canonical_payload.len() > import::MAX_RECORD_BYTES {
156 return Err(Reject::Bounds);
157 }
158 Ok(())
159}
160fn sorted<T>(values: &[T], digest: impl Fn(&T) -> Result<Vec<u8>, Reject>) -> Result<(), Reject> {
161 let mut previous = None;
162 for v in values {
163 let h = digest(v)?;
164 if previous.as_ref().is_some_and(|p| p >= &h) {
165 return Err(Reject::Canonical);
166 }
167 previous = Some(h);
168 }
169 Ok(())
170}
171fn authority_payload_digest(p: &api::ImportAuthorityWitnessV1) -> Result<Vec<u8>, Reject> {
172 signing_digest("heddle-import-authority-witness-payload-v1", p)
173}
174fn thread(record: &api::SignedRecord) -> Result<Vec<u8>, Reject> {
175 if record.format == "heddle-thread-genesis-v1" {
176 return Ok(import::native_id(record));
177 }
178 let value: ThreadSelector =
179 rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Canonical)?;
180 Ok(value.thread.to_vec())
181}
182pub fn validate_public_bundle(b: &api::NativePublicProofBundleV1) -> Result<(), Reject> {
187 if b.format_version != 1 {
188 return Err(Reject::Version);
189 }
190 if b.encoded_len() > import::MAX_BUNDLE_BYTES
191 || b.owner_histories.len() > 64
192 || b.ownership_transfers.len() > 64
193 || b.owner_chains.is_empty()
194 || b.owner_chains.len() > 64
195 || b.policies.len() > 256
196 || b.genesis_witnesses.is_empty()
197 || b.genesis_witnesses.len() > 256
198 || b.authority_witnesses.len() > 256
199 || b.landing_witnesses.len() > 256
200 || b.statements.len() > 1024
201 || b.history_proofs.len() > 1024
202 {
203 return Err(Reject::Bounds);
204 }
205 let owner = b
206 .owner_genesis
207 .as_ref()
208 .and_then(|o| o.genesis.as_ref())
209 .ok_or(Reject::Canonical)?;
210 sorted(&b.owner_chains, import::owner_chain_digest)?;
211 let chain = b.owner_chains.first().ok_or(Reject::Canonical)?;
212 b.witness_set.as_ref().ok_or(Reject::Canonical)?;
213 for retained in &b.owner_chains {
214 if retained.spool_genesis_digest != chain.spool_genesis_digest
215 || retained
216 .owner_state_hashes
217 .iter()
218 .any(|h| !b.owner_histories.iter().any(|o| o.state_hash == *h))
219 || retained.transfer_audit_hashes.iter().any(|h| {
220 !b.ownership_transfers
221 .iter()
222 .any(|t| t.audit_record_hash == *h)
223 })
224 {
225 return Err(Reject::Scope);
226 }
227 }
228 sorted(&b.genesis_witnesses, |p| {
229 signed_genesis_digest(p.binding.as_ref().ok_or(Reject::GenesisBinding)?)
230 })?;
231 sorted(&b.authority_witnesses, authority_payload_digest)?;
232 sorted(&b.landing_witnesses, |p| {
233 signing_digest("heddle-hosted-landing-witness-payload-v1", p)
234 })?;
235 sorted(&b.statements, |s| {
236 crate::witness_trust::statement_signing_digest(s.body.as_ref().ok_or(Reject::Canonical)?)
237 })?;
238 for p in &b.genesis_witnesses {
239 let binding = p.binding.as_ref().ok_or(Reject::GenesisBinding)?;
240 let g = binding.body.as_ref().ok_or(Reject::GenesisBinding)?;
241 let id = g.identity.as_ref().ok_or(Reject::Canonical)?;
242 verify_genesis_authority(
243 binding,
244 p.original_genesis.as_ref().ok_or(Reject::Canonical)?,
245 &p.creator_authority_envelope,
246 )?;
247 if id.spool_uuid != owner.spool_uuid
248 || id.spool_genesis_digest != chain.spool_genesis_digest
249 || !b
250 .owner_chains
251 .iter()
252 .any(|c| import::owner_chain_digest(c).is_ok_and(|h| h == g.owner_chain_digest))
253 || !b.owner_histories.iter().any(|h| {
254 h.state_hash == id.owner_state_hash
255 && h.root
256 .as_ref()
257 .and_then(|r| r.root.as_ref())
258 .is_some_and(|r| {
259 r.owner_id == id.owner_id && r.account_uuid == id.owner_account_uuid
260 })
261 })
262 {
263 return Err(Reject::Scope);
264 }
265 if g.owner_kind == 2
266 && !b.authority_witnesses.iter().any(|a| {
267 a.kind == 2
268 && a.original
269 .as_ref()
270 .is_some_and(|o| thread(o).is_ok_and(|t| t == g.genesis_digest))
271 })
272 {
273 return Err(Reject::Scope);
274 }
275 require_statement(b, 1, &canonical(p)?)?;
276 }
277 for p in &b.authority_witnesses {
278 require_statement(b, 2, &canonical(p)?)?;
279 if !requires_authority(p.original.as_ref().ok_or(Reject::Canonical)?)? {
280 return Err(Reject::Scope);
281 }
282 for original in p.original.iter().chain(p.dependencies.iter()) {
283 if [
284 "heddle-thread-genesis-v1",
285 "heddle-thread-operation-v1",
286 "heddle-thread-ownership-claim-v1",
287 "heddle-thread-ownership-resolution-v1",
288 ]
289 .contains(&original.format.as_str())
290 {
291 let t = thread(original)?;
292 if !b.genesis_witnesses.iter().any(|g| {
293 g.original_genesis
294 .as_ref()
295 .is_some_and(|o| import::native_id(o) == t)
296 }) {
297 return Err(Reject::Scope);
298 }
299 if original.format == "heddle-thread-genesis-v1"
300 && !b
301 .genesis_witnesses
302 .iter()
303 .any(|g| g.original_genesis.as_ref() == Some(original))
304 {
305 return Err(Reject::Scope);
306 }
307 if original.format != "heddle-thread-genesis-v1"
308 && p.original.as_ref() != Some(original)
309 {
310 require_native_dependency(b, original)?;
311 }
312 } else if ![
313 "heddle-original-boundary-acceptance-v1",
314 "heddle-thread-genesis-admission-v2",
315 "heddle-thread-authority-admission-v3",
316 ]
317 .contains(&original.format.as_str())
318 {
319 return Err(Reject::Version);
320 }
321 }
322 }
323 for p in &b.landing_witnesses {
324 require_statement(b, 4, &canonical(p)?)?;
325 let execution = p.execution.as_ref().ok_or(Reject::Canonical)?;
326 let op: OperationSelectors =
327 rmp_serde::from_slice(&execution.canonical_record).map_err(|_| Reject::Canonical)?;
328 if execution.format != "heddle-thread-operation-v1" || op.body.kind != "integration" {
329 return Err(Reject::Scope);
330 }
331 if !b.genesis_witnesses.iter().any(|g| {
332 g.original_genesis
333 .as_ref()
334 .is_some_and(|o| thread(execution).is_ok_and(|t| import::native_id(o) == t))
335 }) {
336 return Err(Reject::Scope);
337 }
338 for original in p.source_operation.iter().chain(p.review_evidence.iter()) {
339 let t = thread(original)?;
340 if !b.genesis_witnesses.iter().any(|g| {
341 g.original_genesis
342 .as_ref()
343 .is_some_and(|o| import::native_id(o) == t)
344 }) {
345 return Err(Reject::Scope);
346 }
347 require_native_dependency(b, original)?;
348 }
349 }
350 for signed in &b.statements {
351 let s = signed.body.as_ref().ok_or(Reject::Canonical)?;
352 if s.spool_uuid != owner.spool_uuid
353 || s.spool_genesis_digest != chain.spool_genesis_digest
354 || !b.owner_histories.iter().any(|h| {
355 h.state_hash == s.owner_state_hash
356 && h.root
357 .as_ref()
358 .and_then(|r| r.root.as_ref())
359 .is_some_and(|r| r.owner_id == s.owner_id)
360 })
361 {
362 return Err(Reject::Scope);
363 }
364 import::require_policy_history(
366 &b.policies,
367 &s.spool_uuid,
368 s.policy_sequence,
369 &s.policy_state_hash,
370 )?;
371 match s.purpose {
372 1 => {
373 let p = b
374 .genesis_witnesses
375 .iter()
376 .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
377 .ok_or(Reject::Scope)?;
378 verify_genesis_payload(s, p)?;
379 }
380 2 => {
381 let p = b
382 .authority_witnesses
383 .iter()
384 .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
385 .ok_or(Reject::Scope)?;
386 import::verify_witness_payload(s, import::WitnessPayload::Authority(p))?;
387 }
388 4 => {
389 let p = b
390 .landing_witnesses
391 .iter()
392 .find(|p| canonical(*p).is_ok_and(|v| v == s.canonical_payload))
393 .ok_or(Reject::Scope)?;
394 import::verify_witness_payload(s, import::WitnessPayload::Landing(p))?;
395 }
396 _ => return Err(Reject::Version),
397 }
398 }
399 Ok(())
400}
401#[derive(serde::Deserialize)]
402struct OperationSelectors {
403 body: OperationBodySelectors,
404}
405#[derive(serde::Deserialize)]
406struct OperationBodySelectors {
407 kind: String,
408}
409#[derive(serde::Deserialize)]
410struct CaptureOperationSelectors {
411 body: CaptureBodySelectors,
412}
413#[derive(serde::Deserialize)]
414struct CaptureBodySelectors {
415 canonical: CaptureSelectors,
416}
417#[derive(serde::Deserialize)]
418struct CaptureSelectors {
419 author: OperationBodySelectors,
420}
421#[derive(serde::Deserialize)]
422struct LocalIntegrationOperationSelectors {
423 body: LocalIntegrationBodySelectors,
424}
425#[derive(serde::Deserialize)]
426struct LocalIntegrationBodySelectors {
427 canonical: Vec<u8>,
428}
429fn requires_authority(record: &api::SignedRecord) -> Result<bool, Reject> {
430 if record.format != "heddle-thread-operation-v1" {
431 return Ok(true);
432 }
433 let op: OperationSelectors =
434 rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Canonical)?;
435 if op.body.kind == "integration" {
436 return Ok(false);
437 }
438 match op.body.kind.as_str() {
439 "capture" => {
440 let op: CaptureOperationSelectors =
441 rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Canonical)?;
442 Ok(op.body.canonical.author.kind != "local_key")
443 }
444 "local_integration" => {
445 let op: LocalIntegrationOperationSelectors =
446 rmp_serde::from_slice(&record.canonical_record).map_err(|_| Reject::Canonical)?;
447 let integration: CaptureSelectors =
448 rmp_serde::from_slice(&op.body.canonical).map_err(|_| Reject::Canonical)?;
449 Ok(integration.author.kind != "local_key")
450 }
451 _ => Ok(true),
452 }
453}
454fn require_native_dependency(
455 b: &api::NativePublicProofBundleV1,
456 original: &api::SignedRecord,
457) -> Result<(), Reject> {
458 if requires_authority(original)? {
459 let p = b
460 .authority_witnesses
461 .iter()
462 .find(|p| p.original.as_ref() == Some(original))
463 .ok_or(Reject::Scope)?;
464 return require_statement(b, 2, &canonical(p)?);
465 }
466 let op: OperationSelectors =
467 rmp_serde::from_slice(&original.canonical_record).map_err(|_| Reject::Canonical)?;
468 if op.body.kind == "integration" {
469 let p = b
470 .landing_witnesses
471 .iter()
472 .find(|p| p.execution.as_ref() == Some(original))
473 .ok_or(Reject::Scope)?;
474 return require_statement(b, 4, &canonical(p)?);
475 }
476 import::verify_native(original, "heddle-thread-operation-v1")?;
481 let t = thread(original)?;
482 let claim = b
483 .authority_witnesses
484 .iter()
485 .find(|p| {
486 p.kind == 2
487 && p.original
488 .as_ref()
489 .is_some_and(|o| thread(o).is_ok_and(|id| id == t))
490 })
491 .ok_or(Reject::Scope)?;
492 require_statement(b, 2, &canonical(claim)?)
493}
494fn require_statement(
495 b: &api::NativePublicProofBundleV1,
496 purpose: i32,
497 payload: &[u8],
498) -> Result<(), Reject> {
499 if b.statements
500 .iter()
501 .filter(|s| {
502 s.body
503 .as_ref()
504 .is_some_and(|s| s.purpose == purpose && s.canonical_payload == payload)
505 })
506 .count()
507 != 1
508 {
509 return Err(Reject::Scope);
510 }
511 Ok(())
512}
513pub fn verify_bundle_witnesses(
516 b: &api::NativePublicProofBundleV1,
517 set: &crate::witness_trust::VerifiedWitnessSet,
518 now_ms: i64,
519) -> Result<(), Reject> {
520 validate_public_bundle(b)?;
521 let carried = b.witness_set.as_ref().ok_or(Reject::Canonical)?;
522 if carried.body.as_ref() != Some(set.body()) || carried.body_digest != set.digest() {
523 return Err(Reject::StaleContext);
524 }
525 for signed in &b.statements {
526 match crate::witness_trust::resolve_statement(set, signed, None, false, now_ms) {
527 Ok(_) => continue,
528 Err(Reject::Proof) => (),
529 Err(e) => return Err(e),
530 }
531 let statement = signed.body.as_ref().ok_or(Reject::Canonical)?;
532 let entry = set
533 .body()
534 .entries
535 .iter()
536 .find(|e| e.executor_id == statement.executor_id)
537 .ok_or(Reject::Root)?;
538 let leaf = crate::witness_trust::leaf_digest(
539 statement.purpose,
540 &canonical(statement)?,
541 &signed.signature,
542 )?;
543 let proof = b
546 .history_proofs
547 .iter()
548 .find(|p| {
549 p.purpose == statement.purpose
550 && crate::witness_trust::verify_inclusion(&leaf, p, entry).is_ok()
551 })
552 .ok_or(Reject::Proof)?;
553 crate::witness_trust::resolve_statement(set, signed, Some(proof), false, now_ms)?;
554 }
555 Ok(())
556}
557pub fn validate_carriers(
560 imported: Option<&api::ImportPublicProofBundleV1>,
561 native: Option<&api::NativePublicProofBundleV1>,
562) -> Result<(), Reject> {
563 match (imported, native) {
564 (Some(_), Some(_)) => Err(Reject::Protocol),
565 (Some(b), None) => import::validate_public_bundle(b),
566 (None, Some(b)) => validate_public_bundle(b),
567 (None, None) => Ok(()), }
569}