use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AttestationKind {
Sbom,
Provenance,
Audit,
Vex,
Qualification,
}
impl std::fmt::Display for AttestationKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Sbom => "sbom",
Self::Provenance => "provenance",
Self::Audit => "audit",
Self::Vex => "vex",
Self::Qualification => "qualification",
};
f.write_str(s)
}
}
impl std::str::FromStr for AttestationKind {
type Err = AttestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sbom" => Ok(Self::Sbom),
"provenance" => Ok(Self::Provenance),
"audit" => Ok(Self::Audit),
"vex" => Ok(Self::Vex),
"qualification" => Ok(Self::Qualification),
other => Err(AttestError::UnknownKind(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttestationStatement {
pub layer: String,
pub layer_manifest_digest: String,
pub kind: AttestationKind,
pub digest: String,
pub producer: String,
}
#[derive(Debug, thiserror::Error)]
pub enum AttestError {
#[error(
"unknown attestation kind '{0}' (expected: sbom, provenance, audit, vex, qualification)"
)]
UnknownKind(String),
#[error("attestation statement is not valid JSON: {0}")]
Payload(String),
#[error(
"attestation bytes do not match the signed statement: statement names {expected}, \
the carried bytes hash to {got} — refusing"
)]
DigestMismatch { expected: String, got: String },
#[error(
"this attestation belongs to layer manifest {expected}, but it was presented for \
{got} — refusing to associate it"
)]
LayerMismatch { expected: String, got: String },
#[error(
"attestation statement names layer '{named}' but the digest it carries is that of \
'{found}' — refusing a statement whose own two identities disagree"
)]
NameDigestMismatch { named: String, found: String },
#[error("signature: {0}")]
Signature(String),
}
pub fn statement(
layer: &str,
layer_manifest_digest: &str,
kind: AttestationKind,
bytes: &[u8],
producer: &str,
) -> AttestationStatement {
AttestationStatement {
layer: layer.to_string(),
layer_manifest_digest: layer_manifest_digest.to_string(),
kind,
digest: crate::store::manifest_digest(bytes),
producer: producer.to_string(),
}
}
pub fn check(
st: &AttestationStatement,
bytes: &[u8],
layer_manifest_digest: &str,
layer_name: &str,
) -> Result<(), AttestError> {
let got = crate::store::manifest_digest(bytes);
if got != st.digest {
return Err(AttestError::DigestMismatch {
expected: st.digest.clone(),
got,
});
}
if st.layer_manifest_digest != layer_manifest_digest {
return Err(AttestError::LayerMismatch {
expected: st.layer_manifest_digest.clone(),
got: layer_manifest_digest.to_string(),
});
}
if st.layer != layer_name {
return Err(AttestError::NameDigestMismatch {
named: st.layer.clone(),
found: layer_name.to_string(),
});
}
Ok(())
}
pub fn sign(
st: &AttestationStatement,
secret_key: &[u8],
key_id: &str,
) -> Result<String, AttestError> {
let payload = serde_json::to_vec(st).map_err(|e| AttestError::Payload(e.to_string()))?;
crate::verify::dsse_sign_typed(&payload, PAYLOAD_TYPE, secret_key, key_id)
.map_err(|e| AttestError::Signature(e.to_string()))
}
pub fn verify_statement(
envelope: &[u8],
root_pk: &[u8],
) -> Result<AttestationStatement, AttestError> {
let payload = crate::verify::dsse_verify_typed(envelope, PAYLOAD_TYPE, root_pk)
.map_err(|e| AttestError::Signature(e.to_string()))?;
serde_json::from_slice(&payload).map_err(|e| AttestError::Payload(e.to_string()))
}
pub const PAYLOAD_TYPE: &str = "application/vnd.pulseengine.varve.attestation-statement.v1+json";
#[cfg(test)]
mod tests {
use super::*;
const BYTES: &[u8] = b"{\"bomFormat\":\"CycloneDX\"}";
const LAYER_DIGEST: &str = "sha256:1111";
fn st() -> AttestationStatement {
statement(
"2026.08.0",
LAYER_DIGEST,
AttestationKind::Sbom,
BYTES,
"varve",
)
}
#[test]
fn a_statement_names_the_bytes_and_the_layer() {
let s = st();
assert_eq!(s.digest, crate::store::manifest_digest(BYTES));
assert_eq!(s.layer_manifest_digest, LAYER_DIGEST);
assert_eq!(s.kind, AttestationKind::Sbom);
assert!(check(&s, BYTES, LAYER_DIGEST, "2026.08.0").is_ok());
}
#[test]
fn swapped_bytes_are_refused() {
let s = st();
match check(
&s,
b"different attestation entirely",
LAYER_DIGEST,
"2026.08.0",
) {
Err(AttestError::DigestMismatch { .. }) => {}
other => panic!("expected DigestMismatch, got {other:?}"),
}
}
#[test]
fn an_attestation_for_another_layer_is_refused() {
let s = st();
match check(&s, BYTES, "sha256:2222", "2026.08.0") {
Err(AttestError::LayerMismatch { .. }) => {}
other => panic!("expected LayerMismatch, got {other:?}"),
}
}
#[test]
fn a_statement_whose_own_two_identities_disagree_is_refused() {
let s = st();
match check(&s, BYTES, LAYER_DIGEST, "2026.01.0") {
Err(AttestError::NameDigestMismatch { .. }) => {}
other => panic!("expected NameDigestMismatch, got {other:?}"),
}
}
#[test]
fn an_unknown_attestation_kind_is_refused_not_guessed() {
assert!("sbom".parse::<AttestationKind>().is_ok());
assert!("qualification".parse::<AttestationKind>().is_ok());
assert!("vibes".parse::<AttestationKind>().is_err());
assert!("".parse::<AttestationKind>().is_err());
}
#[test]
fn a_statement_round_trips_through_a_signature() {
let (sk, pk) = crate::generate_root_keypair();
let s = st();
let env = sign(&s, &sk, "test-root").unwrap();
let back = verify_statement(env.as_bytes(), &pk).unwrap();
assert_eq!(back, s);
}
#[test]
fn the_wrong_root_cannot_vouch_for_an_association() {
let (sk, _) = crate::generate_root_keypair();
let (_, other_pk) = crate::generate_root_keypair();
let env = sign(&st(), &sk, "test-root").unwrap();
assert!(verify_statement(env.as_bytes(), &other_pk).is_err());
}
}