use std::collections::BTreeSet;
use runx_contracts::{
AuthorityAttenuation, AuthoritySubsetResult, Receipt, ReceiptVerificationSummary,
SignatureAlgorithm,
};
use crate::{canonical_receipt_body_digest, content_addressed_receipt_id};
use super::{ReceiptFinding, ReceiptFindingCode, ReceiptVerification, verify_receipt};
mod signature;
pub use signature::{SignatureVerificationFailure, SignatureVerifier};
use signature::{signature_failure_code, signature_failure_message, signature_failure_path};
pub fn validate_receipt_proof(
receipt: &Receipt,
context: &ReceiptProofContext<'_>,
) -> Result<(), ReceiptVerification> {
let verification = verify_receipt_proof(receipt, context);
if verification.valid {
Ok(())
} else {
Err(verification)
}
}
#[must_use]
pub fn verify_receipt_proof(
receipt: &Receipt,
context: &ReceiptProofContext<'_>,
) -> ReceiptVerification {
let mut findings = verify_receipt(receipt).findings;
let mut verifier = ProofVerifier {
context,
findings: Vec::new(),
};
verifier.check_body_proof(receipt);
findings.extend(verifier.findings);
ReceiptVerification::from_findings(findings)
}
#[derive(Default)]
pub struct ReceiptProofContext<'a> {
pub signature_verifier: Option<&'a dyn SignatureVerifier>,
pub authority_verified: bool,
pub external_attestations_verified: bool,
pub verified_redaction_refs: BTreeSet<String>,
pub verified_hash_commitments: BTreeSet<String>,
}
struct ProofVerifier<'a> {
context: &'a ReceiptProofContext<'a>,
findings: Vec<ReceiptFinding>,
}
#[must_use]
pub fn receipt_id_is_content_addressed(receipt: &Receipt) -> bool {
content_addressed_receipt_id(receipt).is_ok_and(|content_id| receipt.id == content_id)
}
impl ProofVerifier<'_> {
fn check_body_proof(&mut self, receipt: &Receipt) {
let Ok(body_digest) = canonical_receipt_body_digest(receipt) else {
self.push(
ReceiptFindingCode::SealDigestMismatch,
"digest",
"receipt body digest could not be recomputed",
);
return;
};
self.check_body_digest(receipt, &body_digest);
self.check_signature(receipt, &body_digest);
}
fn check_body_digest(&mut self, receipt: &Receipt, body_digest: &str) {
if receipt.digest != body_digest {
self.push(
ReceiptFindingCode::SealDigestMismatch,
"digest",
"receipt digest must match the canonical receipt body commitment",
);
}
}
fn check_signature(&mut self, receipt: &Receipt, body_digest: &str) {
match self.context.signature_verifier {
Some(verifier) => {
if receipt.signature.alg != SignatureAlgorithm::Ed25519 {
self.push(
ReceiptFindingCode::SignatureUnsupportedAlgorithm,
"signature.alg",
"unsupported receipt signature algorithm",
);
return;
}
if let Err(error) =
verifier.verify(&receipt.issuer, &receipt.signature, body_digest)
{
self.push(
signature_failure_code(&error),
signature_failure_path(&error),
signature_failure_message(&error),
);
}
}
None => self.push(
ReceiptFindingCode::SignatureVerifierMissing,
"signature",
"strict receipt proof verification requires an injected signature verifier",
),
}
}
fn push(
&mut self,
code: ReceiptFindingCode,
path: impl Into<String>,
message: impl Into<String>,
) {
self.findings.push(ReceiptFinding {
code,
path: path.into(),
message: message.into(),
});
}
}
#[must_use]
pub fn compute_verification_summary(
receipt: &Receipt,
context: &ReceiptProofContext<'_>,
) -> ReceiptVerificationSummary {
let body_digest = canonical_receipt_body_digest(receipt).ok();
let signature_valid = context.signature_verifier.is_some_and(|verifier| {
body_digest.as_deref().is_some_and(|body_digest| {
receipt.signature.alg == SignatureAlgorithm::Ed25519
&& verifier
.verify(&receipt.issuer, &receipt.signature, body_digest)
.is_ok()
})
});
let authority_attenuation_valid = context.authority_verified
&& has_verified_attenuation_shape(&receipt.authority.attenuation);
let structural_verification = verify_receipt(receipt);
let criteria_bound = structural_verification.findings.iter().all(|finding| {
!matches!(
finding.code,
ReceiptFindingCode::SealCriterionActMissing | ReceiptFindingCode::SealCriterionUnbound
)
});
let redaction_valid = receipt
.authority
.enforcement
.redaction_refs
.iter()
.all(|reference| {
context
.verified_redaction_refs
.contains(reference.uri.as_str())
});
let hash_commitments_valid = receipt.subject.commitments.iter().all(|commitment| {
context
.verified_hash_commitments
.contains(commitment.value.as_str())
});
ReceiptVerificationSummary {
signature_valid,
content_address_valid: receipt_id_is_content_addressed(receipt),
hash_commitments_valid,
authority_attenuation_valid,
criteria_bound,
redaction_valid,
external_attestations_present: context.external_attestations_verified,
}
}
fn has_verified_attenuation_shape(attenuation: &AuthorityAttenuation) -> bool {
let (Some(parent), Some(proof)) = (
attenuation.parent_authority_ref.as_ref(),
attenuation.subset_proof.as_ref(),
) else {
return false;
};
proof.parent_authority_ref == *parent && matches!(proof.result, AuthoritySubsetResult::Subset)
}