use serde_json::Value;
use crate::audit_bundle::{self, AuditBundle, BundleError};
use crate::error::SchemaError;
use crate::hashing;
use crate::receipt::PolicyReceipt;
use crate::signer::VerifyError;
#[derive(Debug, thiserror::Error)]
pub enum CapsuleVerifyError {
#[error("capsule.schema_invalid: {0}")]
SchemaInvalid(#[from] SchemaError),
#[error(
"capsule.deny_with_execution: deny capsule must have execution.status=\"not_called\" \
and execution.execution_ref=null; got status={status:?} execution_ref={execution_ref:?}"
)]
DenyWithExecution {
status: String,
execution_ref: Option<String>,
},
#[error(
"capsule.live_without_evidence: execution.mode=\"live\" requires non-null \
execution.live_evidence with at least one of transport/response_ref/block_ref"
)]
LiveWithoutEvidence,
#[error(
"capsule.mock_with_live_evidence: execution.mode=\"mock\" must have null \
execution.live_evidence; live_evidence on a mock execution is a mislabel"
)]
MockWithLiveEvidence,
#[error(
"capsule.request_hash_mismatch: request.request_hash={outer} but \
decision.receipt.request_hash={receipt}"
)]
RequestHashMismatch { outer: String, receipt: String },
#[error(
"capsule.policy_hash_mismatch: policy.policy_hash={outer} but \
decision.receipt.policy_hash={receipt}"
)]
PolicyHashMismatch { outer: String, receipt: String },
#[error(
"capsule.decision_result_mismatch: decision.result={outer} but \
decision.receipt.decision={receipt}"
)]
DecisionResultMismatch { outer: String, receipt: String },
#[error(
"capsule.agent_id_mismatch: agent.agent_id={outer} but \
decision.receipt.agent_id={receipt}"
)]
AgentIdMismatch { outer: String, receipt: String },
#[error(
"capsule.audit_event_id_mismatch: audit.audit_event_id={outer} but \
decision.receipt.audit_event_id={receipt}"
)]
AuditEventIdMismatch { outer: String, receipt: String },
#[error(
"capsule.checkpoint_event_hash_mismatch: audit.event_hash={outer} but \
audit.checkpoint.latest_event_hash={checkpoint}"
)]
CheckpointEventHashMismatch { outer: String, checkpoint: String },
#[error(
"capsule.audit_segment_too_large: capsule.audit.audit_segment is {bytes} bytes \
(cap is {cap_bytes} bytes / 1 MiB); verifier refuses to deserialise"
)]
AuditSegmentTooLarge { bytes: usize, cap_bytes: usize },
#[error("capsule.malformed: {detail}")]
Malformed { detail: String },
}
impl CapsuleVerifyError {
pub fn code(&self) -> &'static str {
match self {
Self::SchemaInvalid(_) => "capsule.schema_invalid",
Self::DenyWithExecution { .. } => "capsule.deny_with_execution",
Self::LiveWithoutEvidence => "capsule.live_without_evidence",
Self::MockWithLiveEvidence => "capsule.mock_with_live_evidence",
Self::RequestHashMismatch { .. } => "capsule.request_hash_mismatch",
Self::PolicyHashMismatch { .. } => "capsule.policy_hash_mismatch",
Self::DecisionResultMismatch { .. } => "capsule.decision_result_mismatch",
Self::AgentIdMismatch { .. } => "capsule.agent_id_mismatch",
Self::AuditEventIdMismatch { .. } => "capsule.audit_event_id_mismatch",
Self::CheckpointEventHashMismatch { .. } => "capsule.checkpoint_event_hash_mismatch",
Self::AuditSegmentTooLarge { .. } => "capsule.audit_segment_too_large",
Self::Malformed { .. } => "capsule.malformed",
}
}
}
pub const AUDIT_SEGMENT_BYTE_CAP: usize = 1024 * 1024;
pub fn verify_capsule(value: &Value) -> std::result::Result<(), CapsuleVerifyError> {
crate::schema::validate_passport_capsule(value)?;
let decision = value
.get("decision")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "decision missing after schema-pass".into(),
})?;
let execution = value
.get("execution")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "execution missing after schema-pass".into(),
})?;
let request = value
.get("request")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "request missing after schema-pass".into(),
})?;
let policy = value
.get("policy")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "policy missing after schema-pass".into(),
})?;
let agent = value
.get("agent")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "agent missing after schema-pass".into(),
})?;
let audit = value
.get("audit")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "audit missing after schema-pass".into(),
})?;
let receipt = decision
.get("receipt")
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: "decision.receipt missing after schema-pass".into(),
})?;
let decision_result = string_field(decision, "result")?;
if decision_result == "deny" {
let status = string_field(execution, "status")?;
let execution_ref = execution
.get("execution_ref")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if status != "not_called" || execution_ref.is_some() {
return Err(CapsuleVerifyError::DenyWithExecution {
status,
execution_ref,
});
}
}
let mode = string_field(execution, "mode")?;
let live_evidence = execution.get("live_evidence");
let evidence_present = live_evidence.map(|v| !v.is_null()).unwrap_or(false);
let concrete_evidence_present = live_evidence_has_concrete_ref(live_evidence);
match (mode.as_str(), evidence_present, concrete_evidence_present) {
("live", _, false) => return Err(CapsuleVerifyError::LiveWithoutEvidence),
("mock", true, _) => return Err(CapsuleVerifyError::MockWithLiveEvidence),
_ => {}
}
let outer_request_hash = string_field(request, "request_hash")?;
let receipt_request_hash = string_field(receipt, "request_hash")?;
if outer_request_hash != receipt_request_hash {
return Err(CapsuleVerifyError::RequestHashMismatch {
outer: outer_request_hash,
receipt: receipt_request_hash,
});
}
let outer_policy_hash = string_field(policy, "policy_hash")?;
let receipt_policy_hash = string_field(receipt, "policy_hash")?;
if outer_policy_hash != receipt_policy_hash {
return Err(CapsuleVerifyError::PolicyHashMismatch {
outer: outer_policy_hash,
receipt: receipt_policy_hash,
});
}
let receipt_decision = string_field(receipt, "decision")?;
if decision_result != receipt_decision {
return Err(CapsuleVerifyError::DecisionResultMismatch {
outer: decision_result,
receipt: receipt_decision,
});
}
let outer_agent_id = string_field(agent, "agent_id")?;
let receipt_agent_id = string_field(receipt, "agent_id")?;
if outer_agent_id != receipt_agent_id {
return Err(CapsuleVerifyError::AgentIdMismatch {
outer: outer_agent_id,
receipt: receipt_agent_id,
});
}
let outer_audit_event_id = string_field(audit, "audit_event_id")?;
let receipt_audit_event_id = string_field(receipt, "audit_event_id")?;
if outer_audit_event_id != receipt_audit_event_id {
return Err(CapsuleVerifyError::AuditEventIdMismatch {
outer: outer_audit_event_id,
receipt: receipt_audit_event_id,
});
}
if let Some(checkpoint) = audit.get("checkpoint") {
if !checkpoint.is_null() {
let outer_event_hash = string_field(audit, "event_hash")?;
let cp_latest = string_field(checkpoint, "latest_event_hash")?;
if outer_event_hash != cp_latest {
return Err(CapsuleVerifyError::CheckpointEventHashMismatch {
outer: outer_event_hash,
checkpoint: cp_latest,
});
}
}
}
Ok(())
}
fn string_field(parent: &Value, key: &str) -> std::result::Result<String, CapsuleVerifyError> {
parent
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| CapsuleVerifyError::Malformed {
detail: format!("expected string field {key:?} after schema-pass"),
})
}
fn live_evidence_has_concrete_ref(value: Option<&Value>) -> bool {
let Some(object) = value.and_then(|v| v.as_object()) else {
return false;
};
["transport", "response_ref", "block_ref"]
.iter()
.any(|key| {
object
.get(*key)
.and_then(|v| v.as_str())
.is_some_and(|s| !s.is_empty())
})
}
#[derive(Default, Debug)]
pub struct StrictVerifyOpts<'a> {
pub receipt_pubkey_hex: Option<&'a str>,
pub audit_bundle: Option<&'a AuditBundle>,
pub policy_json: Option<&'a Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckOutcome {
Passed,
Skipped(String),
Failed(String),
}
impl CheckOutcome {
pub fn is_passed(&self) -> bool {
matches!(self, Self::Passed)
}
pub fn is_failed(&self) -> bool {
matches!(self, Self::Failed(_))
}
pub fn is_skipped(&self) -> bool {
matches!(self, Self::Skipped(_))
}
}
#[derive(Debug, Clone)]
pub struct StrictVerifyReport {
pub structural: CheckOutcome,
pub request_hash_recompute: CheckOutcome,
pub policy_hash_recompute: CheckOutcome,
pub receipt_signature: CheckOutcome,
pub audit_chain: CheckOutcome,
pub audit_event_link: CheckOutcome,
}
impl StrictVerifyReport {
pub fn is_ok(&self) -> bool {
self.iter().all(|c| !c.is_failed())
}
pub fn is_fully_ok(&self) -> bool {
self.iter().all(|c| c.is_passed())
}
pub fn iter(&self) -> impl Iterator<Item = &CheckOutcome> {
[
&self.structural,
&self.request_hash_recompute,
&self.policy_hash_recompute,
&self.receipt_signature,
&self.audit_chain,
&self.audit_event_link,
]
.into_iter()
}
pub fn labels() -> [&'static str; 6] {
[
"structural",
"request_hash_recompute",
"policy_hash_recompute",
"receipt_signature",
"audit_chain",
"audit_event_link",
]
}
}
pub fn verify_capsule_strict(value: &Value, opts: &StrictVerifyOpts) -> StrictVerifyReport {
let structural = match verify_capsule(value) {
Ok(()) => CheckOutcome::Passed,
Err(e) => CheckOutcome::Failed(format!("{} ({})", e, e.code())),
};
if structural.is_failed() {
let skip = CheckOutcome::Skipped(
"skipped: structural verify failed; crypto checks not meaningful".into(),
);
return StrictVerifyReport {
structural,
request_hash_recompute: skip.clone(),
policy_hash_recompute: skip.clone(),
receipt_signature: skip.clone(),
audit_chain: skip.clone(),
audit_event_link: skip,
};
}
let embedded_policy = value.pointer("/policy/policy_snapshot");
let policy_for_check = opts
.policy_json
.or_else(|| embedded_policy.filter(|v| !v.is_null()));
let request_hash_recompute = check_request_hash_recompute(value);
let policy_hash_recompute = check_policy_hash_recompute(value, policy_for_check);
if let Some(caller_bundle) = opts.audit_bundle {
let receipt_pubkey_owned: Option<String> = match opts.receipt_pubkey_hex {
Some(s) => Some(s.to_string()),
None => Some(
caller_bundle
.verification_keys
.receipt_signer_pubkey_hex
.clone(),
),
};
let receipt_pubkey_for_check = receipt_pubkey_owned.as_deref();
let receipt_signature = check_receipt_signature(value, receipt_pubkey_for_check);
let audit_chain = check_audit_chain(Some(caller_bundle));
let audit_event_link = check_audit_event_link(value, Some(caller_bundle));
return StrictVerifyReport {
structural,
request_hash_recompute,
policy_hash_recompute,
receipt_signature,
audit_chain,
audit_event_link,
};
}
let embedded_segment_raw = value
.pointer("/audit/audit_segment")
.filter(|v| !v.is_null());
let embedded_segment = match decode_embedded_segment(embedded_segment_raw) {
Ok(maybe) => maybe,
Err(e) => {
let fail = CheckOutcome::Failed(format!(
"audit_segment invalid: {e}; provide --audit-bundle <path> to override"
));
return StrictVerifyReport {
structural,
request_hash_recompute,
policy_hash_recompute,
receipt_signature: fail.clone(),
audit_chain: fail.clone(),
audit_event_link: fail,
};
}
};
let receipt_pubkey_owned: Option<String> = match opts.receipt_pubkey_hex {
Some(s) => Some(s.to_string()),
None => embedded_segment
.as_ref()
.map(|b| b.verification_keys.receipt_signer_pubkey_hex.clone()),
};
let receipt_pubkey_for_check = receipt_pubkey_owned.as_deref();
let bundle_for_check: Option<&AuditBundle> = embedded_segment.as_ref();
let receipt_signature = check_receipt_signature(value, receipt_pubkey_for_check);
let audit_chain = check_audit_chain(bundle_for_check);
let audit_event_link = check_audit_event_link(value, bundle_for_check);
StrictVerifyReport {
structural,
request_hash_recompute,
policy_hash_recompute,
receipt_signature,
audit_chain,
audit_event_link,
}
}
fn decode_embedded_segment(
embedded: Option<&Value>,
) -> std::result::Result<Option<AuditBundle>, CapsuleVerifyError> {
let Some(value) = embedded else {
return Ok(None);
};
let serialised = serde_json::to_vec(value).map_err(|e| CapsuleVerifyError::Malformed {
detail: format!("audit_segment serialise: {e}"),
})?;
if serialised.len() > AUDIT_SEGMENT_BYTE_CAP {
return Err(CapsuleVerifyError::AuditSegmentTooLarge {
bytes: serialised.len(),
cap_bytes: AUDIT_SEGMENT_BYTE_CAP,
});
}
let bundle: AuditBundle =
serde_json::from_value(value.clone()).map_err(|e| CapsuleVerifyError::Malformed {
detail: format!("audit_segment is not a valid sbo3l.audit_bundle.v1: {e}"),
})?;
Ok(Some(bundle))
}
pub const VERIFIER_MODE_SELF_CONTAINED: &str = "verifier-mode: self-contained";
pub const VERIFIER_MODE_AUX_REQUIRED: &str = "verifier-mode: aux-required";
pub fn capsule_is_self_contained(capsule: &Value) -> bool {
let policy_present = capsule
.pointer("/policy/policy_snapshot")
.is_some_and(|v| !v.is_null());
let segment_present = capsule
.pointer("/audit/audit_segment")
.is_some_and(|v| !v.is_null());
policy_present && segment_present
}
fn check_request_hash_recompute(capsule: &Value) -> CheckOutcome {
let Some(aprp) = capsule.pointer("/request/aprp") else {
return CheckOutcome::Failed("capsule.request.aprp missing".into());
};
let recomputed = match hashing::request_hash(aprp) {
Ok(h) => h,
Err(e) => return CheckOutcome::Failed(format!("JCS canonicalization failed: {e}")),
};
let outer = capsule
.pointer("/request/request_hash")
.and_then(|v| v.as_str())
.unwrap_or("");
let receipt = capsule
.pointer("/decision/receipt/request_hash")
.and_then(|v| v.as_str())
.unwrap_or("");
if outer != recomputed {
return CheckOutcome::Failed(format!(
"capsule.request.request_hash={outer} but recomputed JCS+SHA-256 of \
capsule.request.aprp = {recomputed}"
));
}
if receipt != recomputed {
return CheckOutcome::Failed(format!(
"capsule.decision.receipt.request_hash={receipt} but recomputed JCS+SHA-256 of \
capsule.request.aprp = {recomputed}"
));
}
CheckOutcome::Passed
}
fn check_policy_hash_recompute(capsule: &Value, policy_json: Option<&Value>) -> CheckOutcome {
let Some(policy) = policy_json else {
return CheckOutcome::Skipped(
"skipped: --policy <path> not supplied; policy_hash recompute requires the canonical \
policy JSON snapshot"
.into(),
);
};
let bytes = match hashing::canonical_json(policy) {
Ok(b) => b,
Err(e) => return CheckOutcome::Failed(format!("policy JCS canonicalization failed: {e}")),
};
let recomputed = hashing::sha256_hex(&bytes);
let claimed = capsule
.pointer("/policy/policy_hash")
.and_then(|v| v.as_str())
.unwrap_or("");
if claimed != recomputed {
return CheckOutcome::Failed(format!(
"capsule.policy.policy_hash={claimed} but recomputed JCS+SHA-256 of supplied \
policy snapshot = {recomputed}"
));
}
CheckOutcome::Passed
}
fn check_receipt_signature(capsule: &Value, pubkey_hex: Option<&str>) -> CheckOutcome {
let Some(pubkey) = pubkey_hex else {
return CheckOutcome::Skipped(
"skipped: --receipt-pubkey <hex> not supplied; Ed25519 signature verification \
requires the receipt signer's public key"
.into(),
);
};
let Some(receipt_value) = capsule.pointer("/decision/receipt") else {
return CheckOutcome::Failed("capsule.decision.receipt missing".into());
};
let receipt: PolicyReceipt = match serde_json::from_value(receipt_value.clone()) {
Ok(r) => r,
Err(e) => {
return CheckOutcome::Failed(format!(
"capsule.decision.receipt could not be deserialized as PolicyReceipt: {e}"
))
}
};
match receipt.verify(pubkey) {
Ok(()) => CheckOutcome::Passed,
Err(VerifyError::BadPublicKey) => {
CheckOutcome::Failed("supplied receipt-pubkey is not a valid Ed25519 public key".into())
}
Err(VerifyError::BadSignature) => CheckOutcome::Failed(
"capsule.decision.receipt.signature.signature_hex is not a valid Ed25519 signature \
(wrong length or non-hex)"
.into(),
),
Err(VerifyError::Invalid) => CheckOutcome::Failed(
"Ed25519 signature did not verify against supplied receipt-pubkey over the \
canonical receipt body"
.into(),
),
Err(VerifyError::Hex(e)) => CheckOutcome::Failed(format!(
"capsule.decision.receipt.signature.signature_hex (or supplied receipt-pubkey) \
failed hex decoding: {e}"
)),
}
}
fn check_audit_chain(bundle: Option<&AuditBundle>) -> CheckOutcome {
let Some(b) = bundle else {
return CheckOutcome::Skipped(
"skipped: --audit-bundle <path> not supplied; chain walk requires the \
sbo3l.audit_bundle.v1 artefact for the capsule's audit event"
.into(),
);
};
match audit_bundle::verify(b) {
Ok(_) => CheckOutcome::Passed,
Err(BundleError::ReceiptSignatureInvalid) => CheckOutcome::Failed(
"audit_bundle::verify: receipt signature does not verify under the bundle's \
receipt-signer pubkey"
.into(),
),
Err(BundleError::AuditEventSignatureInvalid) => CheckOutcome::Failed(
"audit_bundle::verify: audit event signature does not verify under the bundle's \
audit-signer pubkey"
.into(),
),
Err(BundleError::Chain(e)) => {
CheckOutcome::Failed(format!("audit chain verify failed: {e}"))
}
Err(e) => CheckOutcome::Failed(format!("audit_bundle::verify: {e}")),
}
}
fn check_audit_event_link(capsule: &Value, bundle: Option<&AuditBundle>) -> CheckOutcome {
let Some(b) = bundle else {
return CheckOutcome::Skipped(
"skipped: --audit-bundle <path> not supplied; audit-event-id linkage requires \
the bundle"
.into(),
);
};
let capsule_id = capsule
.pointer("/audit/audit_event_id")
.and_then(|v| v.as_str())
.unwrap_or("");
let bundle_id = b.summary.audit_event_id.as_str();
if capsule_id != bundle_id {
return CheckOutcome::Failed(format!(
"capsule.audit.audit_event_id={capsule_id} but \
bundle.summary.audit_event_id={bundle_id} — wrong bundle for this capsule"
));
}
let in_chain = b
.audit_chain_segment
.iter()
.any(|e| e.event.id == capsule_id);
if !in_chain {
return CheckOutcome::Failed(format!(
"capsule.audit.audit_event_id={capsule_id} not present in bundle.audit_chain_segment"
));
}
CheckOutcome::Passed
}
#[cfg(test)]
mod tests {
use super::*;
fn load(path: &str) -> Value {
let raw = std::fs::read_to_string(path).unwrap();
serde_json::from_str(&raw).unwrap()
}
fn corpus(name: &str) -> Value {
let path =
concat!(env!("CARGO_MANIFEST_DIR"), "/../../test-corpus/passport/").to_string() + name;
load(&path)
}
#[test]
fn golden_allow_capsule_verifies() {
let v = corpus("golden_001_allow_keeperhub_mock.json");
verify_capsule(&v).expect("golden capsule must verify");
}
#[test]
fn tampered_deny_with_execution_ref_is_rejected() {
let v = corpus("tampered_001_deny_with_execution_ref.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.deny_with_execution", "{err}");
}
#[test]
fn tampered_mock_anchor_marked_live_is_rejected_by_schema() {
let v = corpus("tampered_002_mock_anchor_marked_live.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.schema_invalid", "{err}");
}
#[test]
fn tampered_live_mode_without_evidence_is_rejected() {
let v = corpus("tampered_003_live_mode_without_evidence.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.live_without_evidence", "{err}");
}
#[test]
fn tampered_live_mode_empty_evidence_is_rejected() {
let v = corpus("tampered_008_live_mode_empty_evidence.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.schema_invalid", "{err}");
}
#[test]
fn live_mode_with_concrete_evidence_verifies() {
let mut v = corpus("golden_001_allow_keeperhub_mock.json");
let execution = v["execution"].as_object_mut().unwrap();
execution.insert("mode".into(), Value::String("live".into()));
execution.insert(
"live_evidence".into(),
serde_json::json!({
"transport": "https",
"response_ref": "keeperhub-execution-01HTAWX5K3R8YV9NQB7C6P2DGS"
}),
);
verify_capsule(&v).expect("live capsule with concrete evidence must verify");
}
#[test]
fn mock_mode_with_concrete_live_evidence_is_rejected() {
let mut v = corpus("golden_001_allow_keeperhub_mock.json");
let execution = v["execution"].as_object_mut().unwrap();
execution.insert(
"live_evidence".into(),
serde_json::json!({
"response_ref": "keeperhub-execution-01HTAWX5K3R8YV9NQB7C6P2DGS"
}),
);
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.mock_with_live_evidence", "{err}");
}
#[test]
fn tampered_request_hash_mismatch_is_rejected() {
let v = corpus("tampered_004_request_hash_mismatch.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.request_hash_mismatch", "{err}");
}
#[test]
fn tampered_policy_hash_mismatch_is_rejected() {
let v = corpus("tampered_005_policy_hash_mismatch.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.policy_hash_mismatch", "{err}");
}
#[test]
fn tampered_malformed_checkpoint_is_rejected_by_schema() {
let v = corpus("tampered_006_malformed_checkpoint.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.schema_invalid", "{err}");
}
#[test]
fn tampered_unknown_field_is_rejected_by_schema() {
let v = corpus("tampered_007_unknown_field.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.schema_invalid", "{err}");
}
#[test]
fn executor_evidence_null_accepted() {
let v_missing = corpus("golden_001_allow_keeperhub_mock.json");
verify_capsule(&v_missing).expect("golden (executor_evidence missing) must verify");
let mut v_null = corpus("golden_001_allow_keeperhub_mock.json");
v_null["execution"]
.as_object_mut()
.unwrap()
.insert("executor_evidence".into(), Value::Null);
verify_capsule(&v_null).expect("explicit executor_evidence: null must verify");
}
#[test]
fn executor_evidence_arbitrary_object_accepted() {
let mut v_min = corpus("golden_001_allow_keeperhub_mock.json");
v_min["execution"].as_object_mut().unwrap().insert(
"executor_evidence".into(),
serde_json::json!({ "quote_id": "x" }),
);
verify_capsule(&v_min).expect("single-key executor_evidence must verify");
let mut v_uni = corpus("golden_001_allow_keeperhub_mock.json");
v_uni["execution"].as_object_mut().unwrap().insert(
"executor_evidence".into(),
serde_json::json!({
"quote_id": "mock-uniswap-quote-X",
"quote_source": "mock-uniswap-v3-router",
"input_token": { "symbol": "USDC", "address": "0x0" },
"output_token": { "symbol": "ETH", "address": "0x1" },
"route_tokens": [],
"notional_in": "0.05",
"slippage_cap_bps": 50,
"quote_timestamp_unix": 1_700_000_000,
"quote_freshness_seconds": 30,
"recipient_address": "0x1111111111111111111111111111111111111111"
}),
);
verify_capsule(&v_uni).expect("uniswap-shaped executor_evidence must verify");
}
#[test]
fn tampered_executor_evidence_empty_object_is_rejected_by_schema() {
let v = corpus("tampered_009_executor_evidence_empty_object.json");
let err = verify_capsule(&v).expect_err("must fail");
assert_eq!(err.code(), "capsule.schema_invalid", "{err}");
}
#[test]
fn schema_compiles() {
let _ = crate::schema::PASSPORT_CAPSULE_SCHEMA_JSON;
let v: serde_json::Value =
serde_json::from_str(crate::schema::PASSPORT_CAPSULE_SCHEMA_JSON).unwrap();
assert_eq!(
v["$id"].as_str().unwrap(),
crate::schema::PASSPORT_CAPSULE_SCHEMA_ID
);
}
use crate::audit::{AuditEvent, SignedAuditEvent, ZERO_HASH};
use crate::audit_bundle;
use crate::receipt::{Decision, UnsignedReceipt};
use crate::signer::DevSigner;
fn strict_fixture() -> (
Value,
DevSigner, // receipt signer
DevSigner, // audit signer
AuditBundle,
Value, // canonical policy snapshot
) {
let receipt_signer = DevSigner::from_seed("decision-signer-v1", [7u8; 32]);
let audit_signer = DevSigner::from_seed("audit-signer-v1", [11u8; 32]);
let policy_json: Value = serde_json::json!({
"policy_id": "reference_low_risk_v1",
"version": 1,
"rules": [
{ "id": "allow-low-risk-x402", "decision": "allow" }
]
});
let policy_bytes = hashing::canonical_json(&policy_json).unwrap();
let policy_hash = hashing::sha256_hex(&policy_bytes);
let aprp: Value = serde_json::json!({
"agent_id": "research-agent-01",
"task_id": "demo-task-1",
"intent": "purchase_api_call",
"amount": { "value": "0.05", "currency": "USD" },
"token": "USDC",
"destination": {
"type": "x402_endpoint",
"url": "https://api.example.com/v1/inference",
"method": "POST",
"expected_recipient": "0x1111111111111111111111111111111111111111"
},
"payment_protocol": "x402",
"chain": "base",
"provider_url": "https://api.example.com",
"x402_payload": null,
"expiry": "2026-05-01T10:31:00Z",
"nonce": "01HTAWX5K3R8YV9NQB7C6P2DGM",
"expected_result": null,
"risk_class": "low"
});
let request_hash_hex = hashing::request_hash(&aprp).unwrap();
let e1_event = AuditEvent {
version: 1,
seq: 1,
id: "evt-01HTAWX5K3R8YV9NQB7C6P2DGQ".into(),
ts: chrono::DateTime::parse_from_rfc3339("2026-04-29T12:00:00Z")
.unwrap()
.into(),
event_type: "runtime_started".into(),
actor: "sbo3l-server".into(),
subject_id: "runtime".into(),
payload_hash: ZERO_HASH.into(),
metadata: serde_json::Map::new(),
policy_version: None,
policy_hash: None,
attestation_ref: None,
prev_event_hash: ZERO_HASH.into(),
};
let e1 = SignedAuditEvent::sign(e1_event, &audit_signer).unwrap();
let e2_event = AuditEvent {
version: 1,
seq: 2,
id: "evt-01HTAWX5K3R8YV9NQB7C6P2DGR".into(),
ts: chrono::DateTime::parse_from_rfc3339("2026-04-29T12:00:01Z")
.unwrap()
.into(),
event_type: "policy_decided".into(),
actor: "policy_engine".into(),
subject_id: "pr-strict-001".into(),
payload_hash: request_hash_hex.clone(),
metadata: serde_json::Map::new(),
policy_version: Some(1),
policy_hash: Some(policy_hash.clone()),
attestation_ref: None,
prev_event_hash: e1.event_hash.clone(),
};
let e2 = SignedAuditEvent::sign(e2_event, &audit_signer).unwrap();
let e3_event = AuditEvent {
version: 1,
seq: 3,
id: "evt-01HTAWX5K3R8YV9NQB7C6P2DGS".into(),
ts: chrono::DateTime::parse_from_rfc3339("2026-04-29T12:00:02Z")
.unwrap()
.into(),
event_type: "policy_decided".into(),
actor: "policy_engine".into(),
subject_id: "pr-strict-002".into(),
payload_hash: ZERO_HASH.into(),
metadata: serde_json::Map::new(),
policy_version: Some(1),
policy_hash: Some(policy_hash.clone()),
attestation_ref: None,
prev_event_hash: e2.event_hash.clone(),
};
let e3 = SignedAuditEvent::sign(e3_event, &audit_signer).unwrap();
let unsigned = UnsignedReceipt {
agent_id: "research-agent-01".into(),
decision: Decision::Allow,
deny_code: None,
request_hash: request_hash_hex.clone(),
policy_hash: policy_hash.clone(),
policy_version: Some(1),
audit_event_id: e2.event.id.clone(),
execution_ref: None,
issued_at: chrono::DateTime::parse_from_rfc3339("2026-04-29T12:00:01.500Z")
.unwrap()
.into(),
expires_at: None,
};
let receipt = unsigned.sign(&receipt_signer).unwrap();
let bundle = audit_bundle::build(
receipt.clone(),
vec![e1, e2.clone(), e3],
receipt_signer.verifying_key_hex(),
audit_signer.verifying_key_hex(),
chrono::DateTime::parse_from_rfc3339("2026-04-29T13:00:00Z")
.unwrap()
.into(),
)
.unwrap();
let capsule = serde_json::json!({
"schema": "sbo3l.passport_capsule.v1",
"generated_at": "2026-04-29T12:30:00Z",
"agent": {
"agent_id": "research-agent-01",
"ens_name": "research-agent.team.eth",
"resolver": "offline-fixture",
"records": {
"sbo3l:policy_hash": policy_hash,
"sbo3l:audit_root": "local-mock-anchor-0123456789abcdef",
"sbo3l:passport_schema": "sbo3l.passport_capsule.v1"
}
},
"request": {
"aprp": aprp,
"request_hash": request_hash_hex,
"idempotency_key": "strict-fixture-1",
"nonce": "01HTAWX5K3R8YV9NQB7C6P2DGM"
},
"policy": {
"policy_hash": policy_hash,
"policy_version": 1,
"activated_at": "2026-04-28T10:00:00Z",
"source": "operator-cli"
},
"decision": {
"result": "allow",
"matched_rule": "allow-low-risk-x402",
"deny_code": null,
"receipt": serde_json::to_value(&receipt).unwrap(),
"receipt_signature": receipt.signature.signature_hex.clone()
},
"execution": {
"executor": "keeperhub",
"mode": "mock",
"execution_ref": "kh-strict-001",
"status": "submitted",
"sponsor_payload_hash": ZERO_HASH,
"live_evidence": null
},
"audit": {
"audit_event_id": e2.event.id,
"prev_event_hash": e2.event.prev_event_hash,
"event_hash": e2.event_hash,
"bundle_ref": "sbo3l.audit_bundle.v1",
"checkpoint": {
"schema": "sbo3l.audit_checkpoint.v1",
"sequence": 1,
"latest_event_id": e2.event.id,
"latest_event_hash": e2.event_hash,
"chain_digest": ZERO_HASH,
"mock_anchor": true,
"mock_anchor_ref": "local-mock-anchor-0123456789abcdef",
"created_at": "2026-04-29T12:00:30Z"
}
},
"verification": {
"doctor_status": "ok",
"offline_verifiable": true,
"live_claims": []
}
});
(capsule, receipt_signer, audit_signer, bundle, policy_json)
}
#[test]
fn strict_verify_happy_path_passes_every_check() {
let (capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.is_fully_ok(),
"expected fully-ok; report = {report:?}"
);
assert!(report.structural.is_passed());
assert!(report.request_hash_recompute.is_passed());
assert!(report.policy_hash_recompute.is_passed());
assert!(report.receipt_signature.is_passed());
assert!(report.audit_chain.is_passed());
assert!(report.audit_event_link.is_passed());
}
#[test]
fn strict_verify_tampered_request_body_fails_request_hash_recompute() {
let (mut capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
capsule["request"]["aprp"]["amount"]["value"] = serde_json::Value::String("999.00".into());
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.structural.is_passed(),
"structural should still pass"
);
assert!(
report.request_hash_recompute.is_failed(),
"request_hash_recompute should fail on mutated APRP body"
);
}
#[test]
fn strict_verify_tampered_policy_snapshot_fails_policy_hash_recompute() {
let (capsule, receipt_signer, _audit_signer, bundle, _policy) = strict_fixture();
let bad_policy = serde_json::json!({
"policy_id": "different-policy",
"rules": []
});
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&bad_policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.policy_hash_recompute.is_failed(),
"policy_hash_recompute should fail when supplied policy ≠ capsule.policy.policy_hash"
);
}
#[test]
fn strict_verify_tampered_receipt_signature_fails_receipt_signature() {
let (mut capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
let sig = capsule["decision"]["receipt"]["signature"]["signature_hex"]
.as_str()
.unwrap()
.to_string();
let mut chars: Vec<char> = sig.chars().collect();
chars[0] = if chars[0] == '0' { '1' } else { '0' };
let mutated: String = chars.into_iter().collect();
capsule["decision"]["receipt"]["signature"]["signature_hex"] =
serde_json::Value::String(mutated);
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.receipt_signature.is_failed(),
"receipt_signature must fail on a flipped signature byte"
);
}
#[test]
fn strict_verify_tampered_audit_prev_hash_fails_audit_chain() {
let (capsule, receipt_signer, _audit_signer, mut bundle, policy) = strict_fixture();
let original = bundle.audit_chain_segment[1].event.prev_event_hash.clone();
let mut chars: Vec<char> = original.chars().collect();
chars[0] = if chars[0] == '0' { '1' } else { '0' };
bundle.audit_chain_segment[1].event.prev_event_hash = chars.into_iter().collect();
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.audit_chain.is_failed(),
"audit_chain must fail when prev_event_hash linkage is broken"
);
}
#[test]
fn strict_verify_wrong_audit_bundle_fails_audit_event_link() {
let (mut capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
let bogus = "evt-01ZZZZZZZZZZZZZZZZZZZZZZZZ";
capsule["audit"]["audit_event_id"] = serde_json::Value::String(bogus.into());
capsule["decision"]["receipt"]["audit_event_id"] = serde_json::Value::String(bogus.into());
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.audit_event_link.is_failed(),
"audit_event_link must fail when capsule.audit.audit_event_id is not in the bundle's chain"
);
}
#[test]
fn aux_bundle_overrides_malformed_embedded_segment() {
let (mut capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
capsule["schema"] = serde_json::Value::String("sbo3l.passport_capsule.v2".into());
capsule["audit"]["audit_segment"] = serde_json::json!({
"this_is_not": "a valid sbo3l.audit_bundle.v1",
"garbage": [1, 2, 3]
});
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle), policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(
report.is_fully_ok(),
"caller-supplied --audit-bundle must override garbled embedded segment; \
report = {report:?}"
);
assert!(report.audit_chain.is_passed());
assert!(report.audit_event_link.is_passed());
assert!(report.receipt_signature.is_passed());
}
#[test]
fn embedded_malformed_segment_fails_when_no_aux_bundle_supplied() {
let (mut capsule, _receipt_signer, _audit_signer, _bundle, _policy) = strict_fixture();
capsule["schema"] = serde_json::Value::String("sbo3l.passport_capsule.v2".into());
capsule["audit"]["audit_segment"] = serde_json::json!({
"this_is_not": "a valid sbo3l.audit_bundle.v1"
});
let report = verify_capsule_strict(&capsule, &StrictVerifyOpts::default());
assert!(report.audit_chain.is_failed());
assert!(report.audit_event_link.is_failed());
assert!(report.receipt_signature.is_failed());
}
#[test]
fn strict_verify_no_aux_inputs_skips_aux_dependent_checks() {
let (capsule, _receipt_signer, _audit_signer, _bundle, _policy) = strict_fixture();
let report = verify_capsule_strict(&capsule, &StrictVerifyOpts::default());
assert!(report.structural.is_passed());
assert!(report.request_hash_recompute.is_passed());
assert!(report.policy_hash_recompute.is_skipped());
assert!(report.receipt_signature.is_skipped());
assert!(report.audit_chain.is_skipped());
assert!(report.audit_event_link.is_skipped());
assert!(report.is_ok(), "no failures means is_ok() = true");
assert!(!report.is_fully_ok(), "skips mean is_fully_ok() = false");
}
#[test]
fn strict_verify_structural_failure_short_circuits_crypto_checks() {
let (mut capsule, receipt_signer, _audit_signer, bundle, policy) = strict_fixture();
capsule["request"]["request_hash"] = serde_json::Value::String(
"0000000000000000000000000000000000000000000000000000000000000000".into(),
);
let pk = receipt_signer.verifying_key_hex();
let opts = StrictVerifyOpts {
receipt_pubkey_hex: Some(&pk),
audit_bundle: Some(&bundle),
policy_json: Some(&policy),
};
let report = verify_capsule_strict(&capsule, &opts);
assert!(report.structural.is_failed());
assert!(report.request_hash_recompute.is_skipped());
assert!(report.policy_hash_recompute.is_skipped());
assert!(report.receipt_signature.is_skipped());
assert!(report.audit_chain.is_skipped());
assert!(report.audit_event_link.is_skipped());
}
}