use std::path::Path;
use auths_id::keri::types::Prefix;
use auths_transparency::{
ConsistencyProof, HonestyCeiling, InclusionProof, MerkleHash, SignedCheckpoint, WitnessPolicy,
WitnessPolicyError, ceiling_for_policy_load,
};
use auths_verifier::IdentityDID;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::context::AuthsContext;
use crate::domains::org::audit::{AuthorityAtSigning, classify_authority_at_signing};
use crate::domains::org::bundle::{AirGappedOrgBundle, build_org_bundle};
use crate::domains::org::error::OrgError;
use crate::domains::org::offline_verify::{classify_authority_in_bundle, verify_org_bundle};
pub const EVIDENCE_PACK_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ComplianceQueryError {
#[error("authority classification failed: {0}")]
Authority(#[from] OrgError),
#[error("canonicalization failed: {0}")]
Canonicalize(String),
#[error("org signing failed: {0}")]
Signing(String),
#[error("decode failed: {0}")]
Decode(String),
#[error("verification failed: {0}")]
Verification(String),
#[error("offline verification failed: {0}")]
OfflineVerification(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComplianceFramework {
Slsa,
Sbom,
Cra,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransparencyInclusion {
pub leaf_hash: MerkleHash,
pub inclusion_proof: InclusionProof,
pub signed_checkpoint: SignedCheckpoint,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consistency_proof: Option<ConsistencyProof>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReleaseRecord {
pub artifact_digest: String,
pub signer_prefix: Prefix,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signed_at: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transparency: Option<TransparencyInclusion>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvidenceRow {
pub artifact_digest: String,
pub signer: IdentityDID,
pub authority_at_release: AuthorityAtSigning,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signed_at: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transparency: Option<TransparencyInclusion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidencePack {
pub schema_version: u32,
pub org: IdentityDID,
pub period: String,
pub framework: ComplianceFramework,
pub equivocation_visibility: HonestyCeiling,
pub generated_at: DateTime<Utc>,
pub rows: Vec<EvidenceRow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub org_bundle: Option<AirGappedOrgBundle>,
}
impl EvidencePack {
pub fn canonicalize(&self) -> Result<String, ComplianceQueryError> {
json_canon::to_string(self).map_err(|e| ComplianceQueryError::Canonicalize(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, ComplianceQueryError> {
serde_json::from_str(json).map_err(|e| ComplianceQueryError::Decode(e.to_string()))
}
pub fn to_intoto_statement(&self) -> Result<String, ComplianceQueryError> {
let subject: Vec<serde_json::Value> = self
.rows
.iter()
.map(|r| {
let digest = r
.artifact_digest
.strip_prefix("sha256:")
.unwrap_or(&r.artifact_digest);
serde_json::json!({
"name": r.artifact_digest,
"digest": { "sha256": digest },
})
})
.collect();
let statement = serde_json::json!({
"_type": "https://in-toto.io/Statement/v1",
"subject": subject,
"predicateType": "https://auths.dev/compliance/evidence/v1",
"predicate": self,
});
json_canon::to_string(&statement)
.map_err(|e| ComplianceQueryError::Canonicalize(e.to_string()))
}
}
pub fn load_witness_policy(path: Option<&Path>) -> Result<WitnessPolicy, WitnessPolicyError> {
match path {
Some(p) => WitnessPolicy::load(p),
None => Err(WitnessPolicyError::NotFound {
path: "<AUTHS_WITNESS_POLICY_PATH unset>".into(),
}),
}
}
fn classify_rows(
ctx: &AuthsContext,
org_prefix: &Prefix,
releases: &[ReleaseRecord],
) -> Result<Vec<EvidenceRow>, ComplianceQueryError> {
let mut rows = Vec::with_capacity(releases.len());
for r in releases {
let authority_at_release =
classify_authority_at_signing(ctx, org_prefix, &r.signer_prefix, r.signed_at)?;
let signer = IdentityDID::new_unchecked(format!("did:keri:{}", r.signer_prefix.as_str()));
rows.push(EvidenceRow {
artifact_digest: r.artifact_digest.clone(),
signer,
authority_at_release,
signed_at: r.signed_at,
transparency: r.transparency.clone(),
});
}
Ok(rows)
}
#[allow(clippy::too_many_arguments)]
pub fn build_evidence_pack(
ctx: &AuthsContext,
org: IdentityDID,
org_prefix: &Prefix,
period: impl Into<String>,
framework: ComplianceFramework,
releases: &[ReleaseRecord],
witness_policy: &Result<WitnessPolicy, WitnessPolicyError>,
generated_at: DateTime<Utc>,
) -> Result<EvidencePack, ComplianceQueryError> {
let rows = classify_rows(ctx, org_prefix, releases)?;
Ok(EvidencePack {
schema_version: EVIDENCE_PACK_SCHEMA_VERSION,
org,
period: period.into(),
framework,
equivocation_visibility: ceiling_for_policy_load(witness_policy),
generated_at,
rows,
org_bundle: None,
})
}
#[allow(clippy::too_many_arguments)]
pub fn build_offline_evidence_pack(
ctx: &AuthsContext,
org: IdentityDID,
org_prefix: &Prefix,
period: impl Into<String>,
framework: ComplianceFramework,
releases: &[ReleaseRecord],
witness_policy: &Result<WitnessPolicy, WitnessPolicyError>,
generated_at: DateTime<Utc>,
) -> Result<EvidencePack, ComplianceQueryError> {
let mut pack = build_evidence_pack(
ctx,
org,
org_prefix,
period,
framework,
releases,
witness_policy,
generated_at,
)?;
pack.org_bundle = Some(build_org_bundle(ctx, org_prefix)?);
Ok(pack)
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RowVerdict {
pub artifact_digest: String,
pub signer: IdentityDID,
pub authority_at_release: AuthorityAtSigning,
pub authority_consistent: bool,
pub transparency_verified: Option<bool>,
}
fn verify_transparency_inclusion(t: &TransparencyInclusion) -> Result<(), ComplianceQueryError> {
t.inclusion_proof.verify(&t.leaf_hash).map_err(|e| {
ComplianceQueryError::OfflineVerification(format!("inclusion proof did not verify: {e}"))
})?;
let checkpoint_root = t.signed_checkpoint.checkpoint.root;
let checkpoint_size = t.signed_checkpoint.checkpoint.size;
match &t.consistency_proof {
Some(c) => {
if c.old_root != t.inclusion_proof.root || c.old_size != t.inclusion_proof.size {
return Err(ComplianceQueryError::OfflineVerification(
"consistency proof old root/size does not match the inclusion proof".into(),
));
}
if c.new_root != checkpoint_root || c.new_size != checkpoint_size {
return Err(ComplianceQueryError::OfflineVerification(
"consistency proof new root/size does not match the signed checkpoint".into(),
));
}
c.verify().map_err(|e| {
ComplianceQueryError::OfflineVerification(format!(
"consistency proof did not verify: {e}"
))
})?;
}
None => {
if t.inclusion_proof.root != checkpoint_root
|| t.inclusion_proof.size != checkpoint_size
{
return Err(ComplianceQueryError::OfflineVerification(
"inclusion proof is not against the signed checkpoint and no consistency proof was provided".into(),
));
}
}
}
Ok(())
}
pub fn verify_evidence_pack_offline(
pack: &EvidencePack,
pinned_roots: &[IdentityDID],
) -> Result<Vec<RowVerdict>, ComplianceQueryError> {
let bundle = pack.org_bundle.as_ref().ok_or_else(|| {
ComplianceQueryError::OfflineVerification(
"pack carries no embedded org bundle — not an offline-verifiable pack".into(),
)
})?;
let report = verify_org_bundle(bundle, pinned_roots, None)
.map_err(|e| ComplianceQueryError::OfflineVerification(e.to_string()))?;
if !report.root_pinned {
return Err(ComplianceQueryError::OfflineVerification(format!(
"org {} is not in the pinned trust roots",
bundle.org_did.as_str()
)));
}
if report.duplicity_detected {
return Err(ComplianceQueryError::OfflineVerification(
"org KEL shows duplicity (same-seq divergent SAIDs)".into(),
));
}
let mut verdicts = Vec::with_capacity(pack.rows.len());
for row in &pack.rows {
let signer_prefix = Prefix::new_unchecked(
row.signer
.as_str()
.strip_prefix("did:keri:")
.unwrap_or(row.signer.as_str())
.to_string(),
);
let rederived = classify_authority_in_bundle(bundle, &signer_prefix, row.signed_at);
let authority_consistent = rederived == row.authority_at_release;
let transparency_verified = row
.transparency
.as_ref()
.map(|t| verify_transparency_inclusion(t).is_ok());
verdicts.push(RowVerdict {
artifact_digest: row.artifact_digest.clone(),
signer: row.signer.clone(),
authority_at_release: row.authority_at_release.clone(),
authority_consistent,
transparency_verified,
});
}
Ok(verdicts)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use auths_transparency::types::LogOrigin;
fn fixed_now() -> DateTime<Utc> {
DateTime::parse_from_rfc3339("2026-06-08T00:00:00Z")
.unwrap()
.with_timezone(&Utc)
}
fn single_operator_ceiling() -> HonestyCeiling {
ceiling_for_policy_load(&Err(WitnessPolicyError::NotFound {
path: "<unset>".into(),
}))
}
fn sample_pack() -> EvidencePack {
EvidencePack {
schema_version: EVIDENCE_PACK_SCHEMA_VERSION,
org: IdentityDID::new_unchecked("did:keri:EOrg"),
period: "2026-Q3".into(),
framework: ComplianceFramework::Slsa,
equivocation_visibility: single_operator_ceiling(),
generated_at: fixed_now(),
rows: vec![
EvidenceRow {
artifact_digest: "sha256:aa".into(),
signer: IdentityDID::new_unchecked("did:keri:EAlice"),
authority_at_release: AuthorityAtSigning::AuthorizedBeforeRevocation,
signed_at: Some(7),
transparency: None,
},
EvidenceRow {
artifact_digest: "sha256:bb".into(),
signer: IdentityDID::new_unchecked("did:keri:EBob"),
authority_at_release: AuthorityAtSigning::RejectedRevokedPositionUnknown {
revoked_at: 12,
},
signed_at: None,
transparency: None,
},
],
org_bundle: None,
}
}
#[test]
fn canonicalize_is_deterministic() {
let a = sample_pack().canonicalize().unwrap();
let b = sample_pack().canonicalize().unwrap();
assert_eq!(
a, b,
"same inputs must produce byte-identical canonical bytes"
);
}
#[test]
fn pack_round_trips_through_json() {
let pack = sample_pack();
let json = pack.canonicalize().unwrap();
let back = EvidencePack::from_json(&json).unwrap();
assert_eq!(
json,
back.canonicalize().unwrap(),
"canonical JSON must round-trip byte-identically"
);
}
#[test]
fn pack_embeds_single_operator_ceiling_not_a_bare_flag() {
let pack = sample_pack();
assert!(!pack.equivocation_visibility.policy_met);
let json = pack.canonicalize().unwrap();
assert!(!json.contains("non_equivocation"));
assert!(json.contains("not yet independent"));
}
#[test]
fn position_unknown_is_represented_honestly_not_authorized() {
let json = sample_pack().canonicalize().unwrap();
assert!(json.contains("rejected_revoked_position_unknown"));
let bob_authorized = json.matches("authorized_before_revocation").count();
assert_eq!(
bob_authorized, 1,
"only Alice is authorized-before-revocation"
);
}
#[test]
fn intoto_statement_carries_subjects_and_predicate_type() {
let stmt = sample_pack().to_intoto_statement().unwrap();
assert!(stmt.contains("https://in-toto.io/Statement/v1"));
assert!(stmt.contains("https://auths.dev/compliance/evidence/v1"));
assert!(stmt.contains("\"sha256\":\"aa\""));
assert!(stmt.contains("authority_at_signing"));
}
fn signed_checkpoint_at(size: u64, root: MerkleHash) -> SignedCheckpoint {
use auths_transparency::checkpoint::Checkpoint;
use auths_verifier::{Ed25519PublicKey, Ed25519Signature};
SignedCheckpoint {
checkpoint: Checkpoint {
origin: LogOrigin::new("auths.dev/log").unwrap(),
size,
root,
timestamp: fixed_now(),
},
log_signature: Ed25519Signature::from_bytes([0u8; 64]),
log_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
witnesses: vec![],
ecdsa_checkpoint_signature: None,
ecdsa_checkpoint_key: None,
}
}
#[test]
fn transparency_inclusion_against_checkpoint_verifies() {
use auths_transparency::merkle::{hash_children, hash_leaf};
let a = hash_leaf(b"artifact-a");
let b = hash_leaf(b"artifact-b");
let root = hash_children(&a, &b);
let t = TransparencyInclusion {
leaf_hash: a,
inclusion_proof: InclusionProof {
index: 0,
size: 2,
root,
hashes: vec![b],
},
signed_checkpoint: signed_checkpoint_at(2, root),
consistency_proof: None,
};
verify_transparency_inclusion(&t).expect("inclusion against the checkpoint verifies");
}
#[test]
fn transparency_inclusion_mismatched_checkpoint_fails() {
use auths_transparency::merkle::{hash_children, hash_leaf};
let a = hash_leaf(b"artifact-a");
let b = hash_leaf(b"artifact-b");
let root = hash_children(&a, &b);
let t = TransparencyInclusion {
leaf_hash: a,
inclusion_proof: InclusionProof {
index: 0,
size: 2,
root,
hashes: vec![b],
},
signed_checkpoint: signed_checkpoint_at(2, MerkleHash::from_bytes([0x99; 32])),
consistency_proof: None,
};
assert!(
verify_transparency_inclusion(&t).is_err(),
"inclusion not anchored to the checkpoint must fail closed"
);
}
}