Skip to main content

auths_transparency/
bundle.rs

1use auths_verifier::{CanonicalDid, IdentityDID, Role};
2use serde::{Deserialize, Serialize};
3
4use crate::checkpoint::SignedCheckpoint;
5use crate::entry::{Entry, EntryType};
6use crate::proof::InclusionProof;
7
8/// An offline verification bundle containing an entry, its inclusion proof,
9/// and a signed checkpoint.
10///
11/// Allows clients to verify that an entry was logged without contacting the
12/// transparency log server.
13///
14/// Args:
15/// * `entry` — The log entry being proven.
16/// * `inclusion_proof` — Merkle proof that the entry is included in the log.
17/// * `signed_checkpoint` — Signed checkpoint attesting to the log state.
18/// * `delegation_chain` — Optional chain of delegation links.
19///
20/// Usage:
21/// ```ignore
22/// let bundle = OfflineBundle {
23///     entry,
24///     inclusion_proof: proof,
25///     signed_checkpoint: checkpoint,
26///     delegation_chain: vec![],
27/// };
28/// ```
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[allow(missing_docs)]
31pub struct OfflineBundle {
32    pub entry: Entry,
33    pub inclusion_proof: InclusionProof,
34    pub signed_checkpoint: SignedCheckpoint,
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub delegation_chain: Vec<DelegationChainLink>,
37}
38
39/// A single link in a delegation chain, containing the logged entry
40/// and its Merkle inclusion proof.
41///
42/// Each link proves that a delegation event (e.g., org member add) was
43/// recorded in the transparency log.
44///
45/// Args:
46/// * `link_type` — The type of delegation event this link represents.
47/// * `entry` — The full log entry for the delegation event.
48/// * `inclusion_proof` — Merkle proof that the entry is included in the log.
49///
50/// Usage:
51/// ```ignore
52/// let link = DelegationChainLink {
53///     link_type: EntryType::OrgMemberAdd,
54///     entry,
55///     inclusion_proof: proof,
56/// };
57/// ```
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[allow(missing_docs)]
60pub struct DelegationChainLink {
61    pub link_type: EntryType,
62    pub entry: Entry,
63    pub inclusion_proof: InclusionProof,
64}
65
66/// Result of verifying an [`OfflineBundle`].
67///
68/// Reports the outcome of each verification dimension independently,
69/// allowing consumers to make nuanced trust decisions.
70///
71/// Args:
72/// * `signature` — Whether the entry's actor signature verified.
73/// * `inclusion` — Whether the Merkle inclusion proof verified.
74/// * `checkpoint` — Whether the signed checkpoint verified.
75/// * `witnesses` — Witness cosignature quorum status.
76/// * `namespace` — Whether the actor is authorized for the namespace.
77/// * `delegation` — Delegation chain verification status.
78/// * `warnings` — Non-fatal issues encountered during verification.
79///
80/// Usage:
81/// ```ignore
82/// let report = BundleVerificationReport {
83///     signature: SignatureStatus::Verified,
84///     inclusion: InclusionStatus::Verified,
85///     checkpoint: CheckpointStatus::Verified,
86///     witnesses: WitnessStatus::NotProvided,
87///     namespace: NamespaceStatus::Authorized,
88///     delegation: DelegationStatus::NoDelegationData,
89///     warnings: vec![],
90/// };
91/// assert!(report.is_valid());
92/// ```
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94#[allow(missing_docs)]
95pub struct BundleVerificationReport {
96    pub signature: SignatureStatus,
97    pub inclusion: InclusionStatus,
98    pub checkpoint: CheckpointStatus,
99    pub witnesses: WitnessStatus,
100    pub namespace: NamespaceStatus,
101    pub delegation: DelegationStatus,
102    pub warnings: Vec<String>,
103}
104
105impl BundleVerificationReport {
106    /// Whether the bundle passed all verification checks.
107    ///
108    /// Returns `true` when signature, inclusion, and checkpoint are all verified,
109    /// witnesses meet quorum (or are not provided), namespace is authorized or owned,
110    /// and delegation is not broken.
111    ///
112    /// **Trust note:** `NoDelegationData` is accepted as valid because many bundles
113    /// (e.g., direct-signing, early Epic 1 bundles) lack delegation chains. This is
114    /// a weaker trust signal than `ChainVerified` — callers needing full provenance
115    /// should check `delegation` explicitly rather than relying solely on `is_valid()`.
116    pub fn is_valid(&self) -> bool {
117        let sig_ok = matches!(self.signature, SignatureStatus::Verified);
118        let inc_ok = matches!(self.inclusion, InclusionStatus::Verified);
119        let chk_ok = matches!(
120            self.checkpoint,
121            CheckpointStatus::Verified | CheckpointStatus::NotProvided
122        );
123        let wit_ok = matches!(
124            self.witnesses,
125            WitnessStatus::Quorum { .. } | WitnessStatus::NotProvided
126        );
127        let ns_ok = matches!(
128            self.namespace,
129            NamespaceStatus::Authorized | NamespaceStatus::Owned
130        );
131        let del_ok = !matches!(self.delegation, DelegationStatus::ChainBroken { .. });
132
133        sig_ok && inc_ok && chk_ok && wit_ok && ns_ok && del_ok
134    }
135}
136
137/// Outcome of verifying the entry's actor signature.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(tag = "status", rename_all = "snake_case")]
140#[non_exhaustive]
141pub enum SignatureStatus {
142    /// The signature verified against the actor's public key.
143    Verified,
144    /// The signature did not verify.
145    Failed {
146        /// Description of the failure.
147        reason: String,
148    },
149    /// No signature data was available for verification.
150    NotProvided,
151}
152
153/// Outcome of verifying the Merkle inclusion proof.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(tag = "status", rename_all = "snake_case")]
156#[non_exhaustive]
157pub enum InclusionStatus {
158    /// The inclusion proof verified against the checkpoint root.
159    Verified,
160    /// The inclusion proof did not verify.
161    Failed {
162        /// Description of the failure.
163        reason: String,
164    },
165    /// No inclusion proof was available for verification.
166    NotProvided,
167}
168
169/// Outcome of verifying the signed checkpoint.
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(tag = "status", rename_all = "snake_case")]
172#[non_exhaustive]
173pub enum CheckpointStatus {
174    /// The checkpoint signature verified against the log's public key.
175    Verified,
176    /// The checkpoint signature did not verify.
177    InvalidSignature,
178    /// The trust config declared `EcdsaP256` but the checkpoint did not
179    /// carry the ECDSA signature bytes. Distinct from `InvalidSignature`
180    /// so operators can tell a protocol mis-configuration apart from a
181    /// crypto failure.
182    MissingEcdsaSignature,
183    /// The trust config declared `EcdsaP256` but the checkpoint did not
184    /// carry the ECDSA public-key bytes.
185    MissingEcdsaKey,
186    /// No checkpoint was available for verification.
187    NotProvided,
188}
189
190/// Outcome of verifying witness cosignatures.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(tag = "status", rename_all = "snake_case")]
193#[non_exhaustive]
194pub enum WitnessStatus {
195    /// Witness quorum was met.
196    Quorum {
197        /// Number of witnesses that verified.
198        verified: usize,
199        /// Number of witnesses required for quorum.
200        required: usize,
201    },
202    /// Witness quorum was not met.
203    Insufficient {
204        /// Number of witnesses that verified.
205        verified: usize,
206        /// Number of witnesses required for quorum.
207        required: usize,
208    },
209    /// The cosigning quorum met the count but is NOT independent — too few
210    /// distinct organizations / jurisdictions / infrastructure zones among the
211    /// witnesses that actually cosigned. A count-met-but-correlated quorum is not
212    /// a non-equivocation guarantee, so this fails verification.
213    NotIndependent {
214        /// Number of witnesses that verified (count was met).
215        verified: usize,
216        /// Number of witnesses required for the count.
217        required: usize,
218        /// Which diversity thresholds the actual cosigners fell short of.
219        shortfalls: Vec<String>,
220    },
221    /// No witness data was available for verification.
222    NotProvided,
223}
224
225/// Outcome of verifying the actor's namespace authorization.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(tag = "status", rename_all = "snake_case")]
228#[non_exhaustive]
229pub enum NamespaceStatus {
230    /// The actor is authorized for the namespace via delegation.
231    Authorized,
232    /// The actor owns the namespace directly.
233    Owned,
234    /// The namespace has no owner on record.
235    Unowned,
236    /// The actor is not authorized for the namespace.
237    Unauthorized,
238}
239
240/// Outcome of verifying the delegation chain.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(tag = "status", rename_all = "snake_case")]
243#[non_exhaustive]
244pub enum DelegationStatus {
245    /// The actor signed directly (no delegation needed).
246    Direct,
247    /// The delegation chain verified successfully.
248    ChainVerified {
249        /// The organization identity that issued the delegation.
250        org_did: IdentityDID,
251        /// The member identity that received the delegation.
252        member_did: IdentityDID,
253        /// The role granted to the member.
254        member_role: Role,
255        /// The device that performed the action on behalf of the member.
256        device_did: CanonicalDid,
257    },
258    /// The delegation chain could not be verified.
259    ChainBroken {
260        /// Description of why the chain is broken.
261        reason: String,
262    },
263    /// No delegation data was present in the bundle.
264    NoDelegationData,
265}
266
267#[cfg(test)]
268#[allow(clippy::disallowed_methods)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn signature_status_serializes() {
274        let status = SignatureStatus::Verified;
275        let json = serde_json::to_string(&status).unwrap();
276        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
277        assert_eq!(parsed["status"], "verified");
278    }
279
280    #[test]
281    fn witness_status_quorum_serializes() {
282        let status = WitnessStatus::Quorum {
283            verified: 3,
284            required: 2,
285        };
286        let json = serde_json::to_string(&status).unwrap();
287        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
288        assert_eq!(parsed["status"], "quorum");
289        assert_eq!(parsed["verified"], 3);
290        assert_eq!(parsed["required"], 2);
291    }
292
293    #[test]
294    fn witness_status_insufficient_serializes() {
295        let status = WitnessStatus::Insufficient {
296            verified: 1,
297            required: 3,
298        };
299        let json = serde_json::to_string(&status).unwrap();
300        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
301        assert_eq!(parsed["status"], "insufficient");
302        assert_eq!(parsed["verified"], 1);
303        assert_eq!(parsed["required"], 3);
304    }
305
306    #[test]
307    fn report_is_valid_all_verified() {
308        let report = BundleVerificationReport {
309            signature: SignatureStatus::Verified,
310            inclusion: InclusionStatus::Verified,
311            checkpoint: CheckpointStatus::Verified,
312            witnesses: WitnessStatus::Quorum {
313                verified: 2,
314                required: 2,
315            },
316            namespace: NamespaceStatus::Authorized,
317            delegation: DelegationStatus::Direct,
318            warnings: vec![],
319        };
320        assert!(report.is_valid());
321    }
322
323    #[test]
324    fn report_is_valid_with_not_provided_optionals() {
325        let report = BundleVerificationReport {
326            signature: SignatureStatus::Verified,
327            inclusion: InclusionStatus::Verified,
328            checkpoint: CheckpointStatus::NotProvided,
329            witnesses: WitnessStatus::NotProvided,
330            namespace: NamespaceStatus::Owned,
331            delegation: DelegationStatus::NoDelegationData,
332            warnings: vec![],
333        };
334        assert!(report.is_valid());
335    }
336
337    #[test]
338    fn report_invalid_on_failed_signature() {
339        let report = BundleVerificationReport {
340            signature: SignatureStatus::Failed {
341                reason: "bad sig".into(),
342            },
343            inclusion: InclusionStatus::Verified,
344            checkpoint: CheckpointStatus::Verified,
345            witnesses: WitnessStatus::NotProvided,
346            namespace: NamespaceStatus::Authorized,
347            delegation: DelegationStatus::Direct,
348            warnings: vec![],
349        };
350        assert!(!report.is_valid());
351    }
352
353    #[test]
354    fn report_invalid_on_broken_delegation() {
355        let report = BundleVerificationReport {
356            signature: SignatureStatus::Verified,
357            inclusion: InclusionStatus::Verified,
358            checkpoint: CheckpointStatus::Verified,
359            witnesses: WitnessStatus::NotProvided,
360            namespace: NamespaceStatus::Authorized,
361            delegation: DelegationStatus::ChainBroken {
362                reason: "missing link".into(),
363            },
364            warnings: vec![],
365        };
366        assert!(!report.is_valid());
367    }
368
369    #[test]
370    fn delegation_status_chain_verified_serializes() {
371        let status = DelegationStatus::ChainVerified {
372            org_did: IdentityDID::new_unchecked("did:keri:EOrg123"),
373            member_did: IdentityDID::new_unchecked("did:keri:EMember456"),
374            member_role: Role::Admin,
375            device_did: CanonicalDid::new_unchecked("did:key:z6MkDevice789"),
376        };
377        let json = serde_json::to_string(&status).unwrap();
378        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
379        assert_eq!(parsed["status"], "chain_verified");
380        assert_eq!(parsed["org_did"], "did:keri:EOrg123");
381        assert_eq!(parsed["member_did"], "did:keri:EMember456");
382    }
383}