use serde::{Deserialize, Serialize};
use crate::frame::Provenance;
use crate::identity::FrameId;
pub const ALGORITHM_ED25519: &str = "ed25519";
#[cfg(feature = "attestation")]
mod domain {
pub(super) const GENESIS: &[u8] = b"contextgraph/attest/1/genesis";
pub(super) const LINK: &[u8] = b"contextgraph/attest/1/link";
pub(super) const FRAME: &[u8] = b"contextgraph/attest/1/frame";
pub(super) const MERKLE_EMPTY: &[u8] = b"contextgraph/attest/1/merkle-empty";
pub(super) const MERKLE_LEAF: &[u8] = &[0x00];
pub(super) const MERKLE_NODE: &[u8] = &[0x01];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceAttestation {
pub signed_commitment: String,
pub key_id: String,
pub algorithm: String,
pub attester_id: String,
pub signature: String,
pub issued_at: String,
}
impl ProvenanceAttestation {
pub fn new(
signed_commitment: impl Into<String>,
key_id: impl Into<String>,
algorithm: impl Into<String>,
attester_id: impl Into<String>,
signature: impl Into<String>,
issued_at: impl Into<String>,
) -> Self {
Self {
signed_commitment: signed_commitment.into(),
key_id: key_id.into(),
algorithm: algorithm.into(),
attester_id: attester_id.into(),
signature: signature.into(),
issued_at: issued_at.into(),
}
}
pub fn uses_known_algorithm(&self) -> bool {
self.algorithm == ALGORITHM_ED25519
}
pub fn has_well_formed_issued_at(&self) -> bool {
crate::validate::is_protocol_timestamp(&self.issued_at)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InclusionStep {
pub sibling: String,
pub sibling_is_left: bool,
}
pub const MAX_INCLUSION_PATH_STEPS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InclusionProof {
pub leaf_index: usize,
pub leaf_count: usize,
pub path: Vec<InclusionStep>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrameAttestation {
pub frame: FrameId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attestation: Option<ProvenanceAttestation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inclusion_proof: Option<InclusionProof>,
}
impl FrameAttestation {
pub fn signed(frame: FrameId, attestation: ProvenanceAttestation) -> Self {
Self {
frame,
attestation: Some(attestation),
inclusion_proof: None,
}
}
pub fn proven(frame: FrameId, inclusion_proof: InclusionProof) -> Self {
Self {
frame,
attestation: None,
inclusion_proof: Some(inclusion_proof),
}
}
pub fn carries_evidence(&self) -> bool {
self.attestation.is_some() || self.inclusion_proof.is_some()
}
pub fn with_inclusion_proof(mut self, proof: InclusionProof) -> Self {
self.inclusion_proof = Some(proof);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttestationVerdict {
Valid,
ValidIdentityOnly,
CommitmentMismatch {
expected: String,
signed: String,
},
BadSignature,
UnknownAlgorithm(String),
MalformedKey,
MalformedSignature,
MalformedCommitment,
}
impl AttestationVerdict {
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
pub fn signature_verifies(&self) -> bool {
matches!(self, Self::Valid | Self::ValidIdentityOnly)
}
pub fn binds_content(&self) -> bool {
matches!(self, Self::Valid)
}
}
fn enc_str(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u32).to_be_bytes());
out.extend_from_slice(s.as_bytes());
}
fn enc_opt(out: &mut Vec<u8>, s: Option<&str>) {
match s {
None => out.push(0x00),
Some(s) => {
out.push(0x01);
enc_str(out, s);
}
}
}
pub fn encode_provenance_link(link: &Provenance) -> Vec<u8> {
let mut out = Vec::new();
enc_str(&mut out, &link.kind);
enc_opt(&mut out, link.uri.as_deref());
enc_opt(&mut out, link.range.as_deref());
enc_opt(&mut out, link.digest.as_deref());
enc_opt(&mut out, link.method.as_deref());
enc_opt(&mut out, link.by.as_deref());
out
}
pub fn digest_string(bytes: &[u8; 32]) -> String {
let mut s = String::with_capacity(7 + 64);
s.push_str("sha256:");
for b in bytes {
s.push(char::from_digit((b >> 4) as u32, 16).expect("nibble is < 16"));
s.push(char::from_digit((b & 0x0f) as u32, 16).expect("nibble is < 16"));
}
s
}
#[cfg(feature = "attestation")]
fn from_hex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
let (pairs, _) = s.as_bytes().as_chunks::<2>();
for pair in pairs {
let hi = lowercase_hex_digit(pair[0])?;
let lo = lowercase_hex_digit(pair[1])?;
out.push(hi * 16 + lo);
}
Some(out)
}
#[cfg(feature = "attestation")]
fn lowercase_hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}
#[cfg(feature = "attestation")]
fn parse_digest(digest: &str) -> Option<[u8; 32]> {
let hex = digest.strip_prefix("sha256:")?;
let bytes = from_hex(hex)?;
bytes.try_into().ok()
}
#[cfg(feature = "attestation")]
mod crypto {
use super::*;
use crate::frame::ContextFrame;
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use sha2::{Digest, Sha256};
fn sha256(parts: &[&[u8]]) -> [u8; 32] {
let mut hasher = Sha256::new();
for part in parts {
hasher.update(part);
}
hasher.finalize().into()
}
pub fn provenance_chain_head(links: &[Provenance]) -> [u8; 32] {
let mut head = sha256(&[domain::GENESIS]);
for link in links {
let encoded = encode_provenance_link(link);
head = sha256(&[domain::LINK, &head, &encoded]);
}
head
}
pub fn frame_commitment(provider_id: &str, frame: &ContextFrame) -> [u8; 32] {
let chain_head = provenance_chain_head(&frame.provenance);
let mut preimage = Vec::new();
enc_str(&mut preimage, provider_id);
enc_str(&mut preimage, &frame.id);
enc_opt(&mut preimage, frame.content_digest.as_deref());
sha256(&[domain::FRAME, &preimage, &chain_head])
}
pub fn result_set_commitments(
provider_id: &str,
frames: &[ContextFrame],
) -> Vec<(FrameId, [u8; 32])> {
let mut ordered: Vec<(FrameId, &ContextFrame)> = frames
.iter()
.map(|frame| (frame.identity(provider_id), frame))
.collect();
ordered.sort_by(|(a, _), (b, _)| a.cmp(b));
ordered
.into_iter()
.map(|(id, frame)| {
let commitment = frame_commitment(provider_id, frame);
(id, commitment)
})
.collect()
}
pub fn result_set_root(provider_id: &str, frames: &[ContextFrame]) -> [u8; 32] {
let commitments: Vec<[u8; 32]> = result_set_commitments(provider_id, frames)
.into_iter()
.map(|(_, commitment)| commitment)
.collect();
merkle_root(&commitments)
}
fn leaf_hash(commitment: &[u8; 32]) -> [u8; 32] {
sha256(&[domain::MERKLE_LEAF, commitment])
}
fn node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
sha256(&[domain::MERKLE_NODE, left, right])
}
fn split_point(n: usize) -> usize {
let mut k = 1;
while k * 2 < n {
k *= 2;
}
k
}
pub fn merkle_root(commitments: &[[u8; 32]]) -> [u8; 32] {
match commitments.len() {
0 => sha256(&[domain::MERKLE_EMPTY]),
1 => leaf_hash(&commitments[0]),
n => {
let k = split_point(n);
node_hash(
&merkle_root(&commitments[..k]),
&merkle_root(&commitments[k..]),
)
}
}
}
pub fn inclusion_proof(commitments: &[[u8; 32]], leaf_index: usize) -> Option<InclusionProof> {
if leaf_index >= commitments.len() {
return None;
}
let mut path = Vec::new();
collect_path(commitments, leaf_index, &mut path);
Some(InclusionProof {
leaf_index,
leaf_count: commitments.len(),
path,
})
}
fn collect_path(commitments: &[[u8; 32]], index: usize, path: &mut Vec<InclusionStep>) {
if commitments.len() <= 1 {
return;
}
let k = split_point(commitments.len());
if index < k {
collect_path(&commitments[..k], index, path);
path.push(InclusionStep {
sibling: digest_string(&merkle_root(&commitments[k..])),
sibling_is_left: false,
});
} else {
collect_path(&commitments[k..], index - k, path);
path.push(InclusionStep {
sibling: digest_string(&merkle_root(&commitments[..k])),
sibling_is_left: true,
});
}
}
pub fn root_from_proof(commitment: &[u8; 32], proof: &InclusionProof) -> Option<[u8; 32]> {
if proof.leaf_index >= proof.leaf_count {
return None;
}
let mut acc = leaf_hash(commitment);
for step in &proof.path {
let sibling = parse_digest(&step.sibling)?;
acc = if step.sibling_is_left {
node_hash(&sibling, &acc)
} else {
node_hash(&acc, &sibling)
};
}
Some(acc)
}
pub fn verify_frame_attestation(
provider_id: &str,
frame: &ContextFrame,
attestation: &ProvenanceAttestation,
public_key: &[u8],
) -> AttestationVerdict {
let expected = frame_commitment(provider_id, frame);
let verdict = verify_commitment(&expected, attestation, public_key);
match verdict {
AttestationVerdict::Valid if frame.content_digest.is_none() => {
AttestationVerdict::ValidIdentityOnly
}
other => other,
}
}
pub fn verify_frame_inclusion(
provider_id: &str,
frame: &ContextFrame,
proof: &InclusionProof,
result_attestation: &ProvenanceAttestation,
public_key: &[u8],
) -> AttestationVerdict {
if proof.path.len() > MAX_INCLUSION_PATH_STEPS {
return AttestationVerdict::MalformedCommitment;
}
let commitment = frame_commitment(provider_id, frame);
let Some(root) = root_from_proof(&commitment, proof) else {
return AttestationVerdict::MalformedCommitment;
};
match verify_commitment(&root, result_attestation, public_key) {
AttestationVerdict::Valid if frame.content_digest.is_none() => {
AttestationVerdict::ValidIdentityOnly
}
other => other,
}
}
pub fn verify_commitment(
expected: &[u8; 32],
attestation: &ProvenanceAttestation,
public_key: &[u8],
) -> AttestationVerdict {
if attestation.algorithm != ALGORITHM_ED25519 {
return AttestationVerdict::UnknownAlgorithm(attestation.algorithm.clone());
}
let Some(signed) = parse_digest(&attestation.signed_commitment) else {
return AttestationVerdict::MalformedCommitment;
};
if signed != *expected {
return AttestationVerdict::CommitmentMismatch {
expected: digest_string(expected),
signed: attestation.signed_commitment.clone(),
};
}
let Ok(key_bytes) = <[u8; 32]>::try_from(public_key) else {
return AttestationVerdict::MalformedKey;
};
let Ok(verifying_key) = VerifyingKey::from_bytes(&key_bytes) else {
return AttestationVerdict::MalformedKey;
};
let Some(sig_bytes) = from_hex(&attestation.signature) else {
return AttestationVerdict::MalformedSignature;
};
let Ok(sig_bytes) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
return AttestationVerdict::MalformedSignature;
};
let signature = Signature::from_bytes(&sig_bytes);
match verifying_key.verify_strict(&signed, &signature) {
Ok(()) => AttestationVerdict::Valid,
Err(_) => AttestationVerdict::BadSignature,
}
}
pub fn sign_frame_attestation(
provider_id: &str,
frame: &ContextFrame,
signing_key_seed: &[u8; 32],
key_id: impl Into<String>,
attester_id: impl Into<String>,
issued_at: impl Into<String>,
) -> ProvenanceAttestation {
let commitment = frame_commitment(provider_id, frame);
sign_commitment(
&commitment,
signing_key_seed,
key_id,
attester_id,
issued_at,
)
}
pub fn sign_commitment(
commitment: &[u8; 32],
signing_key_seed: &[u8; 32],
key_id: impl Into<String>,
attester_id: impl Into<String>,
issued_at: impl Into<String>,
) -> ProvenanceAttestation {
let signing_key = SigningKey::from_bytes(signing_key_seed);
let signature = signing_key.sign(commitment);
let mut hex = String::with_capacity(128);
for b in signature.to_bytes() {
hex.push(char::from_digit((b >> 4) as u32, 16).expect("nibble is < 16"));
hex.push(char::from_digit((b & 0x0f) as u32, 16).expect("nibble is < 16"));
}
ProvenanceAttestation::new(
digest_string(commitment),
key_id,
ALGORITHM_ED25519,
attester_id,
hex,
issued_at,
)
}
pub fn public_key_for(signing_key_seed: &[u8; 32]) -> [u8; 32] {
SigningKey::from_bytes(signing_key_seed)
.verifying_key()
.to_bytes()
}
}
#[cfg(feature = "attestation")]
pub use crypto::{
frame_commitment, inclusion_proof, merkle_root, provenance_chain_head, public_key_for,
result_set_commitments, result_set_root, root_from_proof, sign_commitment,
sign_frame_attestation, verify_commitment, verify_frame_attestation, verify_frame_inclusion,
};
#[cfg(all(test, feature = "attestation"))]
mod tests {
use super::*;
use crate::frame::{ContextFrame, FrameKind};
const SEED: [u8; 32] = [7u8; 32];
fn link(kind: &str, uri: Option<&str>, digest: Option<&str>) -> Provenance {
Provenance {
kind: kind.into(),
uri: uri.map(Into::into),
range: None,
digest: digest.map(Into::into),
method: None,
by: None,
}
}
fn frame_with(id: &str, provenance: Vec<Provenance>) -> ContextFrame {
let mut frame = ContextFrame::full(id, FrameKind::Doc, "Retry policy", "body", 0.9, 1);
frame.content_digest = Some("sha256:abcd".into());
frame.provenance = provenance;
frame
}
#[test]
fn the_encoding_is_injective_across_field_boundaries() {
let a = link("file", Some("ab"), Some("c"));
let b = link("file", Some("a"), Some("bc"));
assert_ne!(encode_provenance_link(&a), encode_provenance_link(&b));
}
#[test]
fn an_absent_field_never_encodes_like_an_empty_one() {
let absent = link("file", None, None);
let empty = link("file", Some(""), None);
assert_ne!(
encode_provenance_link(&absent),
encode_provenance_link(&empty),
"the presence byte must keep None distinct from Some(\"\")"
);
}
#[test]
fn an_empty_chain_has_a_stated_head_not_a_zero() {
let head = provenance_chain_head(&[]);
assert_ne!(head, [0u8; 32], "\"no provenance\" is a claim, not a gap");
assert_eq!(head, provenance_chain_head(&[]));
}
#[test]
fn reordering_the_chain_changes_the_head() {
let a = link("file", Some("src/a.rs"), Some("sha256:aa"));
let b = link("derivation", Some("summary"), Some("sha256:bb"));
let forward = provenance_chain_head(&[a.clone(), b.clone()]);
let reversed = provenance_chain_head(&[b, a]);
assert_ne!(
forward, reversed,
"a hash chain must bind order; per-link digests never did"
);
}
#[test]
fn dropping_a_link_changes_the_head() {
let a = link("file", Some("src/a.rs"), Some("sha256:aa"));
let b = link("derivation", None, None);
assert_ne!(
provenance_chain_head(&[a.clone(), b]),
provenance_chain_head(&[a]),
"truncating provenance must be detectable"
);
}
#[test]
fn a_signed_frame_verifies_against_its_own_key() {
let frame = frame_with(
"f1",
vec![link("file", Some("src/a.rs"), Some("sha256:aa"))],
);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let key = public_key_for(&SEED);
assert_eq!(
verify_frame_attestation("repo-graph", &frame, &attestation, &key),
AttestationVerdict::Valid
);
assert!(attestation.uses_known_algorithm());
assert!(attestation.has_well_formed_issued_at());
}
#[test]
fn editing_provenance_after_signing_is_caught_as_a_mismatch() {
let frame = frame_with(
"f1",
vec![link("file", Some("src/a.rs"), Some("sha256:aa"))],
);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let mut tampered = frame.clone();
tampered.provenance[0].uri = Some("src/evil.rs".into());
tampered.provenance[0].digest = Some("sha256:ff".into());
let key = public_key_for(&SEED);
let verdict = verify_frame_attestation("repo-graph", &tampered, &attestation, &key);
assert!(
matches!(verdict, AttestationVerdict::CommitmentMismatch { .. }),
"expected a commitment mismatch, got {verdict:?}"
);
assert!(!verdict.is_valid());
}
#[test]
fn a_signature_cannot_be_lifted_onto_another_frame() {
let shared = vec![link("file", Some("src/a.rs"), Some("sha256:aa"))];
let honest = frame_with("f1", shared.clone());
let forged = frame_with("f2", shared);
assert_eq!(
provenance_chain_head(&honest.provenance),
provenance_chain_head(&forged.provenance),
"precondition: identical provenance means an identical chain head"
);
let attestation = sign_frame_attestation(
"repo-graph",
&honest,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let key = public_key_for(&SEED);
assert!(
matches!(
verify_frame_attestation("repo-graph", &forged, &attestation, &key),
AttestationVerdict::CommitmentMismatch { .. }
),
"a stolen signature must not validate a different frame"
);
}
#[test]
fn the_same_frame_from_another_provider_does_not_verify() {
let frame = frame_with("f1", vec![link("file", Some("src/a.rs"), None)]);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let key = public_key_for(&SEED);
assert!(
matches!(
verify_frame_attestation("impostor", &frame, &attestation, &key),
AttestationVerdict::CommitmentMismatch { .. }
),
"the provider id is part of the signed identity"
);
}
#[test]
fn re_serving_different_bytes_under_the_same_id_is_caught() {
let frame = frame_with("f1", vec![link("file", Some("src/a.rs"), None)]);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let mut swapped = frame.clone();
swapped.content_digest = Some("sha256:0000".into());
let key = public_key_for(&SEED);
assert!(
matches!(
verify_frame_attestation("repo-graph", &swapped, &attestation, &key),
AttestationVerdict::CommitmentMismatch { .. }
),
"the signature covers the frame's bytes, not just its name"
);
}
#[test]
fn a_wrong_key_is_a_bad_signature_not_a_mismatch() {
let frame = frame_with("f1", vec![]);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let other = public_key_for(&[9u8; 32]);
assert_eq!(
verify_frame_attestation("repo-graph", &frame, &attestation, &other),
AttestationVerdict::BadSignature,
"the commitment is intact; only the key is wrong"
);
}
#[test]
fn an_unknown_algorithm_is_declined_rather_than_failed() {
let frame = frame_with("f1", vec![]);
let mut attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
attestation.algorithm = "dilithium3".into();
let key = public_key_for(&SEED);
let verdict = verify_frame_attestation("repo-graph", &frame, &attestation, &key);
assert_eq!(
verdict,
AttestationVerdict::UnknownAlgorithm("dilithium3".into())
);
assert!(!verdict.is_valid(), "declining is still not accepting");
assert!(!attestation.uses_known_algorithm());
}
#[test]
fn malformed_keys_and_signatures_are_named_distinctly() {
let frame = frame_with("f1", vec![]);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
assert_eq!(
verify_frame_attestation("repo-graph", &frame, &attestation, &[0u8; 5]),
AttestationVerdict::MalformedKey
);
let mut truncated = attestation.clone();
truncated.signature = "abcd".into();
assert_eq!(
verify_frame_attestation("repo-graph", &frame, &truncated, &public_key_for(&SEED)),
AttestationVerdict::MalformedSignature
);
let mut bad_commitment = attestation;
bad_commitment.signed_commitment = "not-a-digest".into();
assert_eq!(
verify_frame_attestation(
"repo-graph",
&frame,
&bad_commitment,
&public_key_for(&SEED)
),
AttestationVerdict::MalformedCommitment
);
}
#[test]
fn an_attestation_round_trips_through_json() {
let frame = frame_with("f1", vec![link("file", Some("a"), None)]);
let attestation = sign_frame_attestation(
"repo-graph",
&frame,
&SEED,
"key-1",
"oxagen",
"2026-08-27T00:00:00Z",
);
let json = serde_json::to_string(&attestation).unwrap();
let back: ProvenanceAttestation = serde_json::from_str(&json).unwrap();
assert_eq!(back, attestation);
}
#[test]
fn every_leaf_of_a_signed_set_proves_its_own_membership() {
let commitments: Vec<[u8; 32]> = (0..7)
.map(|i| frame_commitment("repo-graph", &frame_with(&format!("f{i}"), vec![])))
.collect();
let root = merkle_root(&commitments);
for (index, commitment) in commitments.iter().enumerate() {
let proof = inclusion_proof(&commitments, index).expect("index is in range");
assert_eq!(proof.leaf_index, index);
assert_eq!(proof.leaf_count, 7);
assert_eq!(
root_from_proof(commitment, &proof),
Some(root),
"leaf {index} must recompute the signed root"
);
}
}
#[test]
fn a_proof_does_not_validate_a_commitment_that_was_not_in_the_set() {
let commitments: Vec<[u8; 32]> = (0..4)
.map(|i| frame_commitment("repo-graph", &frame_with(&format!("f{i}"), vec![])))
.collect();
let root = merkle_root(&commitments);
let proof = inclusion_proof(&commitments, 1).unwrap();
let outsider = frame_commitment("repo-graph", &frame_with("intruder", vec![]));
assert_ne!(
root_from_proof(&outsider, &proof),
Some(root),
"an unsigned frame must not ride someone else's proof"
);
}
#[test]
fn a_single_frame_set_still_produces_a_usable_proof() {
let commitments = vec![frame_commitment("repo-graph", &frame_with("only", vec![]))];
let root = merkle_root(&commitments);
let proof = inclusion_proof(&commitments, 0).unwrap();
assert!(proof.path.is_empty(), "a lone leaf needs no siblings");
assert_eq!(root_from_proof(&commitments[0], &proof), Some(root));
}
#[test]
fn an_empty_set_has_a_distinct_root() {
let empty = merkle_root(&[]);
let lone = merkle_root(&[frame_commitment("repo-graph", &frame_with("only", vec![]))]);
assert_ne!(empty, lone);
assert!(inclusion_proof(&[], 0).is_none());
}
fn root_signed(
provider_id: &str,
frames: &[ContextFrame],
) -> (ProvenanceAttestation, Vec<InclusionProof>) {
let commitments: Vec<[u8; 32]> = result_set_commitments(provider_id, frames)
.into_iter()
.map(|(_, commitment)| commitment)
.collect();
let root = merkle_root(&commitments);
let proofs = (0..commitments.len())
.map(|index| inclusion_proof(&commitments, index).expect("index is in range"))
.collect();
let attestation = sign_commitment(
&root,
&SEED,
"repo-graph-2026-08",
"repo-graph",
"2026-08-29T00:00:00Z",
);
(attestation, proofs)
}
#[test]
fn one_root_signature_attests_every_frame_it_covers() {
let frames = vec![
frame_with("a", vec![link("file", Some("src/a.rs"), Some("sha256:aa"))]),
frame_with("b", vec![]),
frame_with("c", vec![]),
];
let ordered: Vec<ContextFrame> = {
let mut sorted = frames.clone();
sorted.sort_by_key(|frame| frame.identity("repo-graph"));
sorted
};
let (root, proofs) = root_signed("repo-graph", &frames);
let public_key = public_key_for(&SEED);
for (frame, proof) in ordered.iter().zip(&proofs) {
assert_eq!(
verify_frame_inclusion("repo-graph", frame, proof, &root, &public_key),
AttestationVerdict::Valid,
"frame `{}` is a leaf of the signed root",
frame.id
);
}
}
#[test]
fn a_frame_edited_after_the_root_was_signed_recomputes_a_different_root() {
let frames = vec![frame_with("a", vec![]), frame_with("b", vec![])];
let (root, proofs) = root_signed("repo-graph", &frames);
let mut ordered = frames.clone();
ordered.sort_by_key(|frame| frame.identity("repo-graph"));
ordered[0].provenance.push(link("derivation", None, None));
assert!(
matches!(
verify_frame_inclusion(
"repo-graph",
&ordered[0],
&proofs[0],
&root,
&public_key_for(&SEED),
),
AttestationVerdict::CommitmentMismatch { .. }
),
"a proof must not launder an edit the root never covered"
);
}
#[test]
fn a_digest_less_frame_proven_through_a_root_binds_no_content() {
let mut frame = frame_with("a", vec![]);
frame.content_digest = None;
let frames = vec![frame.clone()];
let (root, proofs) = root_signed("repo-graph", &frames);
let verdict = verify_frame_inclusion(
"repo-graph",
&frame,
&proofs[0],
&root,
&public_key_for(&SEED),
);
assert_eq!(verdict, AttestationVerdict::ValidIdentityOnly);
assert!(verdict.signature_verifies());
assert!(!verdict.binds_content());
}
#[test]
fn a_root_signed_by_the_wrong_key_is_a_bad_signature_not_a_mismatch() {
let frames = vec![frame_with("a", vec![])];
let (root, proofs) = root_signed("repo-graph", &frames);
let impostor = public_key_for(&[8u8; 32]);
assert_eq!(
verify_frame_inclusion("repo-graph", &frames[0], &proofs[0], &root, &impostor),
AttestationVerdict::BadSignature
);
}
#[test]
fn an_inclusion_path_longer_than_the_cap_is_rejected_before_it_is_walked() {
let frames = vec![frame_with("a", vec![])];
let (root, _) = root_signed("repo-graph", &frames);
let oversized = InclusionProof {
leaf_index: 0,
leaf_count: usize::MAX,
path: vec![
InclusionStep {
sibling: digest_string(&[1u8; 32]),
sibling_is_left: false,
};
MAX_INCLUSION_PATH_STEPS + 1
],
};
assert_eq!(
verify_frame_inclusion(
"repo-graph",
&frames[0],
&oversized,
&root,
&public_key_for(&SEED)
),
AttestationVerdict::MalformedCommitment
);
}
#[test]
fn leaf_and_node_hashing_are_domain_separated() {
let a = frame_commitment("repo-graph", &frame_with("a", vec![]));
let b = frame_commitment("repo-graph", &frame_with("b", vec![]));
let pair_root = merkle_root(&[a, b]);
assert_ne!(pair_root, merkle_root(&[a]));
assert_ne!(pair_root, merkle_root(&[b]));
}
#[test]
fn a_signed_merkle_root_verifies_for_the_whole_result_set() {
let commitments: Vec<[u8; 32]> = (0..3)
.map(|i| frame_commitment("repo-graph", &frame_with(&format!("f{i}"), vec![])))
.collect();
let root = merkle_root(&commitments);
let attestation = sign_commitment(&root, &SEED, "key-1", "oxagen", "2026-08-27T00:00:00Z");
let key = public_key_for(&SEED);
assert_eq!(
verify_commitment(&root, &attestation, &key),
AttestationVerdict::Valid
);
}
#[test]
fn digest_strings_are_well_formed_protocol_digests() {
let head = provenance_chain_head(&[link("file", Some("a"), None)]);
let rendered = digest_string(&head);
assert!(
crate::validate::is_well_formed_digest(&rendered),
"{rendered} must satisfy the protocol digest grammar"
);
}
}
#[cfg(all(test, feature = "attestation"))]
mod content_binding_tests {
use super::*;
use crate::frame::{ContextFrame, FrameKind};
const SEED: [u8; 32] = [7u8; 32];
const PROVIDER: &str = "acme.docs";
fn frame(id: &str, content: &str, digest: Option<&str>) -> ContextFrame {
let mut f = ContextFrame::full(id, FrameKind::Doc, "Retry policy", content, 0.9, 1);
f.content_digest = digest.map(Into::into);
f
}
fn attest(frame: &ContextFrame) -> ProvenanceAttestation {
sign_frame_attestation(PROVIDER, frame, &SEED, "k1", "acme", "2026-09-10T00:00:00Z")
}
#[test]
fn a_signed_frame_with_no_content_digest_is_attested_over_nothing_it_says() {
let signed = frame("f1", "retry three times", None);
let attestation = attest(&signed);
let key = public_key_for(&SEED);
let before = verify_frame_attestation(PROVIDER, &signed, &attestation, &key);
assert_eq!(before, AttestationVerdict::ValidIdentityOnly);
let mut rewritten = signed.clone();
rewritten.content = Some("retry zero times, drop the request".into());
let after = verify_frame_attestation(PROVIDER, &rewritten, &attestation, &key);
assert_eq!(
after,
AttestationVerdict::ValidIdentityOnly,
"rewriting the content of a digest-less frame does not disturb the signature"
);
assert!(
!after.is_valid(),
"an identity-only attestation is not `is_valid`"
);
assert!(!after.binds_content(), "it binds nothing about the content");
assert!(
after.signature_verifies(),
"the signature itself is genuine — that is why this is subtle"
);
}
#[test]
fn a_frame_that_declares_a_digest_is_bound_to_it() {
let signed = frame("f2", "retry three times", Some("sha256:aaaa"));
let attestation = attest(&signed);
let key = public_key_for(&SEED);
let verdict = verify_frame_attestation(PROVIDER, &signed, &attestation, &key);
assert_eq!(verdict, AttestationVerdict::Valid);
assert!(verdict.is_valid() && verdict.binds_content());
let mut altered = signed.clone();
altered.content_digest = Some("sha256:bbbb".into());
let verdict = verify_frame_attestation(PROVIDER, &altered, &attestation, &key);
assert!(
matches!(verdict, AttestationVerdict::CommitmentMismatch { .. }),
"got {verdict:?}"
);
}
#[test]
fn stripping_a_digest_after_signing_is_a_mismatch_not_a_downgrade() {
let signed = frame("f3", "retry three times", Some("sha256:aaaa"));
let attestation = attest(&signed);
let key = public_key_for(&SEED);
let mut stripped = signed.clone();
stripped.content_digest = None;
let verdict = verify_frame_attestation(PROVIDER, &stripped, &attestation, &key);
assert!(
matches!(verdict, AttestationVerdict::CommitmentMismatch { .. }),
"stripping the digest must not downgrade to ValidIdentityOnly; got {verdict:?}"
);
}
}
#[cfg(all(test, feature = "attestation"))]
mod lowercase_hex_tests {
use super::*;
const SEED: [u8; 32] = [9u8; 32];
const PROVIDER: &str = "acme.docs";
fn frame() -> crate::frame::ContextFrame {
let mut f = crate::frame::ContextFrame::full(
"f1",
crate::frame::FrameKind::Doc,
"Retry policy",
"body",
0.9,
1,
);
f.content_digest = Some("sha256:aaaa".into());
f
}
#[test]
fn an_uppercase_commitment_is_malformed_not_valid() {
let f = frame();
let mut att =
sign_frame_attestation(PROVIDER, &f, &SEED, "k1", "acme", "2026-09-10T00:00:00Z");
let key = public_key_for(&SEED);
assert_eq!(
verify_frame_attestation(PROVIDER, &f, &att, &key),
AttestationVerdict::Valid
);
let (scheme, hex) = att.signed_commitment.split_once(':').expect("scheme");
att.signed_commitment = format!("{scheme}:{}", hex.to_uppercase());
assert_eq!(
verify_frame_attestation(PROVIDER, &f, &att, &key),
AttestationVerdict::MalformedCommitment,
"uppercase hex is outside SPEC.md's digest grammar and every SDK rejects it"
);
}
#[test]
fn an_uppercase_signature_is_malformed_not_valid() {
let f = frame();
let mut att =
sign_frame_attestation(PROVIDER, &f, &SEED, "k1", "acme", "2026-09-10T00:00:00Z");
let key = public_key_for(&SEED);
att.signature = att.signature.to_uppercase();
assert_eq!(
verify_frame_attestation(PROVIDER, &f, &att, &key),
AttestationVerdict::MalformedSignature
);
}
#[test]
fn everything_this_module_emits_is_still_lowercase() {
let f = frame();
let att = sign_frame_attestation(PROVIDER, &f, &SEED, "k1", "acme", "2026-09-10T00:00:00Z");
assert_eq!(att.signed_commitment, att.signed_commitment.to_lowercase());
assert_eq!(att.signature, att.signature.to_lowercase());
assert_eq!(
digest_string(&frame_commitment(PROVIDER, &f)),
digest_string(&frame_commitment(PROVIDER, &f)).to_lowercase()
);
}
#[test]
fn lowercase_digits_decode_and_uppercase_ones_do_not() {
assert_eq!(lowercase_hex_digit(b'0'), Some(0));
assert_eq!(lowercase_hex_digit(b'9'), Some(9));
assert_eq!(lowercase_hex_digit(b'a'), Some(10));
assert_eq!(lowercase_hex_digit(b'f'), Some(15));
for byte in *b"AFgG :" {
assert_eq!(
lowercase_hex_digit(byte),
None,
"byte {byte:?} must not decode"
);
}
}
}