auths_verifier/org_bundle/bundle.rs
1//! Air-gapped org provenance bundle — a self-contained, URL-free artifact.
2//!
3//! Packs everything an **offline** verifier needs to reproduce a first-party
4//! org/member provenance verdict with the network cable unplugged: the org KEL
5//! (which carries the delegation `KeyEvent` seals, delegator-anchored scope seals,
6//! and revocation seals), each delegated member's own KEL, the durable off-boarding
7//! records, and the pinned trust roots. The *builder* (which walks a live registry)
8//! lives in `auths-sdk`; the wire types and their pure methods live here so every
9//! verifier surface — native, FFI, browser WASM — shares one contract.
10//!
11//! **URL-free:** the bundle contains no registry / OOBI / witness URLs — air-gapped
12//! verification cannot phone home. Identities are typed ([`IdentityDID`] /
13//! [`Prefix`]) so a tampered or malformed identifier fails closed at
14//! deserialization rather than flowing through as an opaque string.
15
16use auths_keri::{Event, Prefix};
17use serde::{Deserialize, Serialize};
18
19use super::error::OrgBundleError;
20use super::record::SignedOffboardingRecord;
21use crate::types::IdentityDID;
22
23/// Schema version of the air-gapped org bundle wire format. Bump on any
24/// breaking change to [`AirGappedOrgBundle`].
25pub const AIR_GAPPED_ORG_BUNDLE_SCHEMA_VERSION: u32 = 1;
26
27/// One identifier's KEL plus its per-event signature attachments.
28///
29/// `attachments[i]` is the hex-encoded CESR attachment for `events[i]` (empty when
30/// the backend exposes none for that position). Carried so an offline verifier can
31/// check signatures, not just recompute SAIDs.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct BundledKel {
34 /// The identifier's KEL prefix (validated on deserialize).
35 pub prefix: Prefix,
36 /// The KEL events, oldest first.
37 pub events: Vec<Event>,
38 /// Hex-encoded CESR signature attachments, parallel to `events`.
39 pub attachments: Vec<String>,
40}
41
42/// A self-contained, URL-free bundle for offline first-party org/member provenance.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AirGappedOrgBundle {
45 /// Wire-format schema version ([`AIR_GAPPED_ORG_BUNDLE_SCHEMA_VERSION`]).
46 pub schema_version: u32,
47 /// The org's `did:keri:` (validated on deserialize).
48 pub org_did: IdentityDID,
49 /// The org's KEL prefix.
50 pub org_prefix: Prefix,
51 /// The org KEL sequence at build time — the offline verifier reports
52 /// "verified as-of KEL position X".
53 pub built_at_org_seq: u128,
54 /// Build timestamp (RFC 3339). Provenance only — authority ordering is by KEL
55 /// position, never this wall-clock value.
56 pub built_at: String,
57 /// The org's KEL (delegation, scope, and revocation seals all ride here).
58 pub org_kel: BundledKel,
59 /// Each delegated member's own KEL (live and revoked members both included so
60 /// the verifier can classify any artifact).
61 pub member_kels: Vec<BundledKel>,
62 /// Durable, signed off-boarding records.
63 pub offboarding_records: Vec<SignedOffboardingRecord>,
64 /// Pinned trust roots — for a first-party org bundle, the org itself. The
65 /// verifier trusts these DIDs and reads their keys from the bundled KEL.
66 pub pinned_roots: Vec<IdentityDID>,
67}
68
69impl AirGappedOrgBundle {
70 /// Serialize the bundle to deterministic, canonical JSON (RFC 8785 via
71 /// `json-canon`) — the on-disk/wire form.
72 ///
73 /// Usage:
74 /// ```ignore
75 /// std::fs::write(out, bundle.to_canonical_json()?)?;
76 /// ```
77 pub fn to_canonical_json(&self) -> Result<String, OrgBundleError> {
78 json_canon::to_string(self)
79 .map_err(|e| OrgBundleError::Canonicalize(format!("org bundle: {e}")))
80 }
81
82 /// Parse a bundle from its JSON form. Typed identifiers fail closed on malformed
83 /// input.
84 ///
85 /// Usage:
86 /// ```ignore
87 /// let bundle = AirGappedOrgBundle::from_json(&std::fs::read_to_string(path)?)?;
88 /// ```
89 pub fn from_json(json: &str) -> Result<Self, OrgBundleError> {
90 serde_json::from_str(json).map_err(|e| OrgBundleError::Parse(format!("org bundle: {e}")))
91 }
92
93 /// The org's authenticated key state, derived from the bundled org KEL alone.
94 ///
95 /// Authenticates the embedded org KEL (RT-002 — every event SAID-correct AND
96 /// signed by the controlling key-state, not merely structurally valid) and
97 /// returns the resolved [`auths_keri::KeyState`]. The org KEL is the root of
98 /// trust, so it authenticates with no delegator lookup.
99 ///
100 /// This is the one public call for "give me the org's authenticated key state
101 /// from evidence alone" — the only trust-rooted source of the org's *current*
102 /// verkey available offline (e.g. to verify a DSSE envelope the org signed).
103 /// Every downstream verifier (CI gate, browser widget, third-party audit tool)
104 /// shares it instead of re-implementing the authenticate-then-resolve path.
105 ///
106 /// Fails closed ([`OrgBundleError::Integrity`]) on a tampered event, a length
107 /// mismatch between events and attachments, an unparseable attachment, or a
108 /// signature that does not verify.
109 ///
110 /// Usage:
111 /// ```ignore
112 /// let state = bundle.authenticated_org_state()?;
113 /// let org_verkey = state.current_key();
114 /// ```
115 pub fn authenticated_org_state(&self) -> Result<auths_keri::KeyState, OrgBundleError> {
116 super::verify::authenticate_bundled_kel(&self.org_kel, None)
117 }
118}