Skip to main content

heddle_api/
native_witness.rs

1//! Job-free native witness closure. This layer checks transport commitments and
2//! signatures; callers still verify native models, independently selected owner
3//! authority, policy, causal closure and boundary intent before durable mutation.
4use 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}
49/// Exact creator signature, original signature, immutable owner variant and
50/// envelope match. Verify the returned binding against separately selected
51/// native authority using StartThread's method; no import permission is read.
52pub 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}
182/// Reference completeness, including a statement for every sidecar, a witnessed
183/// genesis for every original/dependency and an explicit claim for LocalKey.
184/// Presence is never permission. Call verify_bundle_witnesses with a
185/// separately authenticated fresh set, then native owner/model verification.
186pub 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        // Reuse the unchanged policy chain completeness contract.
365        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    // Local captures and LocalKey integrations retain native proof and the
477    // thread's exact hosted ownership claim, never an authority/landing receipt.
478    // This establishes reference closure only. Native authorization must bind
479    // publisher to genesis.owner.local_key and enforce the selected signed cutoff.
480    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}
513/// Authenticate every exact statement against a separately selected fresh set.
514/// RETIRED statements require exact proofs; carrier sets cannot select roots.
515pub 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        // Search exact Merkle paths without repeating signature verification
544        // for every candidate. Only the matching proof is resolved finally.
545        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}
557/// Transport dispatch is explicit and rejects dual arms before staging. Missing
558/// import delegation remains an import rejection, never native fallback.
559pub 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(()), // Purely local; hosted mandatory gate is a caller gate.
568    }
569}