use auths_keri::Prefix;
use auths_keri::witness::independence::HonestyCeiling;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::core::Ed25519PublicKey;
use crate::org_bundle::{
AirGappedOrgBundle, AuthorityAtSigning, classify_authority_in_bundle, verify_org_bundle,
};
use crate::tlog::{ConsistencyProof, InclusionProof, MerkleHash, SignedCheckpoint, hash_leaf};
use crate::types::IdentityDID;
pub const EVIDENCE_PACK_SCHEMA_VERSION: u32 = 1;
pub const MAX_PACK_JSON_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum EvidencePackError {
#[error("canonicalization failed: {0}")]
Canonicalize(String),
#[error("decode failed: {0}")]
Decode(String),
#[error("offline verification failed: {0}")]
OfflineVerification(String),
}
impl auths_crypto::AuthsErrorInfo for EvidencePackError {
fn error_code(&self) -> &'static str {
match self {
Self::Canonicalize(_) => "AUTHS-E2301",
Self::Decode(_) => "AUTHS-E2302",
Self::OfflineVerification(_) => "AUTHS-E2303",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::Canonicalize(_) | Self::Decode(_) => Some(
"The file is not a valid evidence pack; re-export it with `auths compliance report`",
),
Self::OfflineVerification(_) => Some(
"The pack failed offline verification; obtain a fresh, untampered pack from the org",
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComplianceFramework {
Slsa,
Sbom,
Cra,
Soc2,
Iso27001,
}
#[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 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, EvidencePackError> {
json_canon::to_string(self).map_err(|e| EvidencePackError::Canonicalize(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, EvidencePackError> {
serde_json::from_str(json).map_err(|e| EvidencePackError::Decode(e.to_string()))
}
pub fn to_intoto_statement(&self) -> Result<String, EvidencePackError> {
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| EvidencePackError::Canonicalize(e.to_string()))
}
}
#[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>,
pub checkpoint_attested: Option<bool>,
}
pub fn verify_transparency_inclusion(t: &TransparencyInclusion) -> Result<(), EvidencePackError> {
t.inclusion_proof.verify(&t.leaf_hash).map_err(|e| {
EvidencePackError::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(EvidencePackError::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(EvidencePackError::OfflineVerification(
"consistency proof new root/size does not match the signed checkpoint".into(),
));
}
c.verify().map_err(|e| {
EvidencePackError::OfflineVerification(format!(
"consistency proof did not verify: {e}"
))
})?;
}
None => {
if t.inclusion_proof.root != checkpoint_root
|| t.inclusion_proof.size != checkpoint_size
{
return Err(EvidencePackError::OfflineVerification(
"inclusion proof is not against the signed checkpoint and no consistency proof was provided".into(),
));
}
}
}
Ok(())
}
fn verify_inclusion_binds_artifact(
artifact_digest: &str,
t: &TransparencyInclusion,
) -> Result<(), EvidencePackError> {
if t.leaf_hash != hash_leaf(artifact_digest.as_bytes()) {
return Err(EvidencePackError::OfflineVerification(format!(
"transparency evidence is not for this artifact: leaf hash does not re-derive from {artifact_digest}"
)));
}
verify_transparency_inclusion(t)
}
pub fn verify_artifact_log_inclusion(
artifact_digest: &str,
t: &TransparencyInclusion,
pinned_log_key: &Ed25519PublicKey,
) -> Result<(), EvidencePackError> {
verify_inclusion_binds_artifact(artifact_digest, t)?;
t.signed_checkpoint
.verify_log_signature(pinned_log_key)
.map_err(|e| {
EvidencePackError::OfflineVerification(format!(
"checkpoint is not attested by the pinned log key: {e}"
))
})
}
pub fn verify_evidence_pack_offline(
pack: &EvidencePack,
pinned_roots: &[IdentityDID],
pinned_log_key: Option<&Ed25519PublicKey>,
) -> Result<Vec<RowVerdict>, EvidencePackError> {
let bundle = pack.org_bundle.as_ref().ok_or_else(|| {
EvidencePackError::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| EvidencePackError::OfflineVerification(e.to_string()))?;
if !report.root_pinned {
return Err(EvidencePackError::OfflineVerification(format!(
"org {} is not in the pinned trust roots",
bundle.org_did.as_str()
)));
}
if report.duplicity_detected {
return Err(EvidencePackError::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_inclusion_binds_artifact(&row.artifact_digest, t).is_ok());
let checkpoint_attested = match (row.transparency.as_ref(), pinned_log_key) {
(Some(t), Some(key)) => Some(t.signed_checkpoint.verify_log_signature(key).is_ok()),
_ => None,
};
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,
checkpoint_attested,
});
}
Ok(verdicts)
}
#[derive(Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
enum PackVerdictJson {
#[serde(rename = "verdicts")]
Verdicts {
rows: Vec<RowVerdict>,
},
#[serde(rename = "error")]
Error {
code: String,
message: String,
},
}
const SERIALIZE_FALLBACK: &str =
r#"{"kind":"error","code":"AUTHS-E2301","message":"verdict serialization failed"}"#;
pub fn verify_evidence_pack_offline_json(
pack_json: &str,
pinned_roots_json: &str,
pinned_log_key_hex: Option<&str>,
) -> String {
use auths_crypto::AuthsErrorInfo;
let envelope = match verify_pack_json_inner(pack_json, pinned_roots_json, pinned_log_key_hex) {
Ok(rows) => PackVerdictJson::Verdicts { rows },
Err(e) => PackVerdictJson::Error {
code: e.error_code().to_string(),
message: e.to_string(),
},
};
serde_json::to_string(&envelope).unwrap_or_else(|_| SERIALIZE_FALLBACK.to_string())
}
pub fn parse_log_key_hex(hex_key: &str) -> Result<Ed25519PublicKey, EvidencePackError> {
let bytes = hex::decode(hex_key.trim())
.map_err(|e| EvidencePackError::Decode(format!("pinned log key: invalid hex: {e}")))?;
Ed25519PublicKey::try_from_slice(&bytes)
.map_err(|e| EvidencePackError::Decode(format!("pinned log key: {e}")))
}
fn verify_pack_json_inner(
pack_json: &str,
pinned_roots_json: &str,
pinned_log_key_hex: Option<&str>,
) -> Result<Vec<RowVerdict>, EvidencePackError> {
if pack_json.len() > MAX_PACK_JSON_BYTES {
return Err(EvidencePackError::Decode(format!(
"pack JSON too large: {} bytes, max {}",
pack_json.len(),
MAX_PACK_JSON_BYTES
)));
}
let pack = EvidencePack::from_json(pack_json)?;
let pinned_roots: Vec<IdentityDID> = serde_json::from_str(pinned_roots_json)
.map_err(|e| EvidencePackError::Decode(format!("pinned roots: {e}")))?;
let pinned_log_key = pinned_log_key_hex.map(parse_log_key_hex).transpose()?;
verify_evidence_pack_offline(&pack, &pinned_roots, pinned_log_key.as_ref())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::core::{Ed25519PublicKey, Ed25519Signature};
use crate::tlog::merkle::{hash_children, hash_leaf};
use crate::tlog::{Checkpoint, LogOrigin};
use auths_keri::witness::independence::EquivocationDetection;
fn fixed_now() -> DateTime<Utc> {
DateTime::parse_from_rfc3339("2026-06-08T00:00:00Z")
.unwrap()
.with_timezone(&Utc)
}
fn single_operator_ceiling() -> HonestyCeiling {
HonestyCeiling {
distinct_operators: 1,
distinct_organizations: 1,
distinct_jurisdictions: 1,
distinct_infra_zones: 1,
policy_met: false,
equivocation: EquivocationDetection::Sampled,
shortfalls: vec!["no independent witness commons".into()],
label: "single-operator — not yet independent".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 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"));
}
#[test]
fn pack_without_bundle_fails_offline_verification_closed() {
let pack = sample_pack();
let roots = vec![IdentityDID::new_unchecked("did:keri:EOrg")];
let err = verify_evidence_pack_offline(&pack, &roots, None).unwrap_err();
assert!(err.to_string().contains("no embedded org bundle"));
}
#[test]
fn pack_json_contract_reports_errors_as_tagged_envelopes() {
let verdict = verify_evidence_pack_offline_json("not json", "[]", None);
let v: serde_json::Value = serde_json::from_str(&verdict).unwrap();
assert_eq!(v["kind"], "error");
assert_eq!(v["code"], "AUTHS-E2302");
}
#[test]
fn pack_json_contract_rejects_a_malformed_pinned_log_key() {
let pack_json = sample_pack().canonicalize().unwrap();
for bad in ["not hex", "abcd"] {
let verdict = verify_evidence_pack_offline_json(&pack_json, "[]", Some(bad));
let v: serde_json::Value = serde_json::from_str(&verdict).unwrap();
assert_eq!(
v["kind"], "error",
"malformed log key '{bad}' must fail closed"
);
assert_eq!(v["code"], "AUTHS-E2302");
}
}
fn signed_checkpoint_at(size: u64, root: MerkleHash) -> SignedCheckpoint {
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() {
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() {
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"
);
}
fn operator_signed_checkpoint(
size: u64,
root: MerkleHash,
signing_key: &ed25519_dalek::SigningKey,
) -> SignedCheckpoint {
use ed25519_dalek::Signer;
let checkpoint = Checkpoint {
origin: LogOrigin::new("auths.dev/log").unwrap(),
size,
root,
timestamp: fixed_now(),
};
let sig = signing_key.sign(checkpoint.to_note_body().as_bytes());
SignedCheckpoint {
checkpoint,
log_signature: Ed25519Signature::from_bytes(sig.to_bytes()),
log_public_key: Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes()),
witnesses: vec![],
ecdsa_checkpoint_signature: None,
ecdsa_checkpoint_key: None,
}
}
fn artifact_evidence(
digest: &str,
signing_key: &ed25519_dalek::SigningKey,
) -> TransparencyInclusion {
let leaf = hash_leaf(digest.as_bytes());
let sibling = hash_leaf(b"sha256:other");
let root = hash_children(&leaf, &sibling);
TransparencyInclusion {
leaf_hash: leaf,
inclusion_proof: InclusionProof {
index: 0,
size: 2,
root,
hashes: vec![sibling],
},
signed_checkpoint: operator_signed_checkpoint(2, root, signing_key),
consistency_proof: None,
}
}
#[test]
fn artifact_log_inclusion_verifies_under_the_pinned_operator() {
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let evidence = artifact_evidence(digest, &key);
let pinned = Ed25519PublicKey::from_bytes(key.verifying_key().to_bytes());
verify_artifact_log_inclusion(digest, &evidence, &pinned)
.expect("bound, included, operator-attested evidence verifies");
}
#[test]
fn artifact_log_inclusion_rejects_evidence_for_a_different_artifact() {
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let evidence = artifact_evidence(digest, &key);
let pinned = Ed25519PublicKey::from_bytes(key.verifying_key().to_bytes());
let other = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let err = verify_artifact_log_inclusion(other, &evidence, &pinned)
.expect_err("a valid proof for some OTHER leaf is a mismatch, not evidence");
assert!(
err.to_string().contains("not for this artifact"),
"rejection must name the binding failure, got: {err}"
);
}
#[test]
fn artifact_log_inclusion_rejects_a_different_pinned_operator() {
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let evidence = artifact_evidence(digest, &key);
let other_operator = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
let pinned = Ed25519PublicKey::from_bytes(other_operator.verifying_key().to_bytes());
let err = verify_artifact_log_inclusion(digest, &evidence, &pinned)
.expect_err("a checkpoint from a different operator must fail the pinned-key check");
assert!(
err.to_string().contains("pinned log key"),
"rejection must name the operator failure, got: {err}"
);
}
}