Skip to main content

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}