use std::collections::BTreeMap;
use crate::aibom::{
AiBom, DatasetRef, ExportControl, ExternalReference, FrameworkRef, License, ModelRef,
SafetyAttestation,
};
use crate::crypto::SigningKey;
use crate::dsse::{self, DsseEnvelope, AION_MANIFEST_TYPE};
use crate::key_registry::KeyRegistry;
use crate::manifest::{
sign_manifest, verify_manifest_signature, ArtifactEntry, ArtifactManifest,
ArtifactManifestBuilder,
};
use crate::oci::{
build_aion_manifest, build_attestation_manifest, AionConfig, OciArtifactManifest,
AION_CONFIG_MEDIA_TYPE,
};
use crate::serializer::SignatureEntry;
use crate::slsa::{
wrap_statement_dsse, InTotoStatement, SlsaStatementBuilder, IN_TOTO_PAYLOAD_TYPE,
};
use crate::transparency_log::{LogEntryKind, TransparencyLog};
use crate::types::AuthorId;
use crate::{AionError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LogSeq {
pub kind: LogEntryKind,
pub seq: u64,
}
#[derive(Debug)]
pub struct ReleaseBuilder {
model_name: String,
model_version: String,
model_format: String,
primary_artifact: Option<(String, Vec<u8>)>,
auxiliary_artifacts: Vec<(String, Vec<u8>)>,
frameworks: Vec<FrameworkRef>,
datasets: Vec<DatasetRef>,
licenses: Vec<License>,
hyperparameters: BTreeMap<String, serde_json::Value>,
safety_attestations: Vec<SafetyAttestation>,
export_controls: Vec<ExportControl>,
references: Vec<ExternalReference>,
builder_id: String,
external_parameters: serde_json::Value,
current_aion_version: u64,
}
impl ReleaseBuilder {
#[must_use]
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
format: impl Into<String>,
) -> Self {
Self {
model_name: name.into(),
model_version: version.into(),
model_format: format.into(),
primary_artifact: None,
auxiliary_artifacts: Vec::new(),
frameworks: Vec::new(),
datasets: Vec::new(),
licenses: Vec::new(),
hyperparameters: BTreeMap::new(),
safety_attestations: Vec::new(),
export_controls: Vec::new(),
references: Vec::new(),
builder_id: String::new(),
external_parameters: serde_json::json!({}),
current_aion_version: 0,
}
}
pub fn primary_artifact(&mut self, name: impl Into<String>, bytes: Vec<u8>) -> &mut Self {
self.primary_artifact = Some((name.into(), bytes));
self
}
pub fn add_auxiliary(&mut self, name: impl Into<String>, bytes: Vec<u8>) -> &mut Self {
self.auxiliary_artifacts.push((name.into(), bytes));
self
}
pub fn add_framework(&mut self, f: FrameworkRef) -> &mut Self {
self.frameworks.push(f);
self
}
pub fn add_dataset(&mut self, d: DatasetRef) -> &mut Self {
self.datasets.push(d);
self
}
pub fn add_license(&mut self, l: License) -> &mut Self {
self.licenses.push(l);
self
}
pub fn hyperparameter(&mut self, k: impl Into<String>, v: serde_json::Value) -> &mut Self {
self.hyperparameters.insert(k.into(), v);
self
}
pub fn add_safety_attestation(&mut self, s: SafetyAttestation) -> &mut Self {
self.safety_attestations.push(s);
self
}
pub fn add_export_control(&mut self, e: ExportControl) -> &mut Self {
self.export_controls.push(e);
self
}
pub fn add_reference(&mut self, r: ExternalReference) -> &mut Self {
self.references.push(r);
self
}
pub fn builder_id(&mut self, id: impl Into<String>) -> &mut Self {
self.builder_id = id.into();
self
}
pub fn external_parameters(&mut self, v: serde_json::Value) -> &mut Self {
self.external_parameters = v;
self
}
pub fn current_aion_version(&mut self, v: u64) -> &mut Self {
self.current_aion_version = v;
self
}
pub fn seal(
self,
signer: AuthorId,
signing_key: &SigningKey,
log: &mut TransparencyLog,
) -> Result<SignedRelease> {
let core = self.build_core()?;
let manifest_signature = sign_manifest(&core.manifest, signer, signing_key);
let manifest_dsse = dsse::wrap_manifest(&core.manifest, signer, signing_key);
let aibom_dsse = crate::aibom::wrap_aibom_dsse(&core.aibom, signer, signing_key)?;
let slsa_dsse = wrap_statement_dsse(&core.slsa_statement, signer, signing_key)?;
let log_entries = append_release_log_entries(
log,
&manifest_signature.signature,
&aibom_dsse,
&slsa_dsse,
core.current_aion_version,
)?;
let (oci_primary, oci_aibom_referrer, oci_slsa_referrer) = build_oci_graph(
&core.manifest,
&core.model_ref.name,
core.current_aion_version,
&aibom_dsse,
&slsa_dsse,
)?;
Ok(SignedRelease {
signer,
model_ref: core.model_ref,
manifest: core.manifest,
manifest_signature,
manifest_dsse,
aibom: core.aibom,
aibom_dsse,
slsa_statement: core.slsa_statement,
slsa_dsse,
oci_primary,
oci_aibom_referrer,
oci_slsa_referrer,
log_entries,
})
}
fn build_core(self) -> Result<SealedCore> {
let Self {
model_name,
model_version,
model_format,
primary_artifact,
auxiliary_artifacts,
frameworks,
datasets,
licenses,
hyperparameters,
safety_attestations,
export_controls,
references,
builder_id,
external_parameters,
current_aion_version,
} = self;
let (primary_name, primary_bytes) =
primary_artifact.ok_or_else(|| AionError::InvalidFormat {
reason: "ReleaseBuilder requires a primary_artifact".to_string(),
})?;
if builder_id.is_empty() {
return Err(AionError::InvalidFormat {
reason: "ReleaseBuilder requires a non-empty builder_id".to_string(),
});
}
let manifest =
construct_artifact_manifest(&primary_name, &primary_bytes, &auxiliary_artifacts);
let model_ref =
model_ref_from_manifest(&manifest, model_name, model_version, model_format)?;
let aibom = assemble_aibom(
model_ref.clone(),
current_aion_version,
AibomFields {
frameworks,
datasets,
licenses,
hyperparameters,
safety_attestations,
export_controls,
references,
},
);
let slsa_statement = assemble_slsa_statement(&manifest, builder_id, external_parameters)?;
Ok(SealedCore {
manifest,
model_ref,
aibom,
slsa_statement,
current_aion_version,
})
}
}
struct SealedCore {
manifest: ArtifactManifest,
model_ref: ModelRef,
aibom: AiBom,
slsa_statement: InTotoStatement,
current_aion_version: u64,
}
struct AibomFields {
frameworks: Vec<FrameworkRef>,
datasets: Vec<DatasetRef>,
licenses: Vec<License>,
hyperparameters: BTreeMap<String, serde_json::Value>,
safety_attestations: Vec<SafetyAttestation>,
export_controls: Vec<ExportControl>,
references: Vec<ExternalReference>,
}
fn construct_artifact_manifest(
primary_name: &str,
primary_bytes: &[u8],
auxiliaries: &[(String, Vec<u8>)],
) -> ArtifactManifest {
let mut mb = ArtifactManifestBuilder::new();
let _ = mb.add(primary_name, primary_bytes);
for (name, bytes) in auxiliaries {
let _ = mb.add(name, bytes);
}
mb.build()
}
fn model_ref_from_manifest(
manifest: &ArtifactManifest,
name: String,
version: String,
format: String,
) -> Result<ModelRef> {
let primary = manifest
.entries()
.first()
.ok_or_else(|| AionError::InvalidFormat {
reason: "manifest is unexpectedly empty".to_string(),
})?;
Ok(ModelRef {
name,
version,
hash_algorithm: "BLAKE3-256".to_string(),
hash: primary.hash,
size: primary.size,
format,
})
}
fn assemble_aibom(model_ref: ModelRef, current_aion_version: u64, fields: AibomFields) -> AiBom {
let mut ab = AiBom::builder(model_ref, current_aion_version);
for f in fields.frameworks {
ab.add_framework(f);
}
for d in fields.datasets {
ab.add_dataset(d);
}
for l in fields.licenses {
ab.add_license(l);
}
for (k, v) in fields.hyperparameters {
ab.hyperparameter(k, v);
}
for s in fields.safety_attestations {
ab.add_safety_attestation(s);
}
for e in fields.export_controls {
ab.add_export_control(e);
}
for r in fields.references {
ab.add_reference(r);
}
ab.build()
}
fn assemble_slsa_statement(
manifest: &ArtifactManifest,
builder_id: String,
external_parameters: serde_json::Value,
) -> Result<InTotoStatement> {
let mut sb = SlsaStatementBuilder::new(builder_id);
sb.add_all_subjects_from_manifest(manifest)?;
sb.external_parameters(external_parameters);
sb.build()
}
fn append_release_log_entries(
log: &mut TransparencyLog,
manifest_sig_bytes: &[u8; 64],
aibom_dsse: &DsseEnvelope,
slsa_dsse: &DsseEnvelope,
current_aion_version: u64,
) -> Result<Vec<LogSeq>> {
let seq_manifest = log.append(
LogEntryKind::ManifestSignature,
manifest_sig_bytes,
current_aion_version,
)?;
let seq_aibom = log.append(
LogEntryKind::DsseEnvelope,
aibom_dsse.to_json()?.as_bytes(),
current_aion_version,
)?;
let seq_slsa = log.append(
LogEntryKind::SlsaStatement,
slsa_dsse.to_json()?.as_bytes(),
current_aion_version,
)?;
Ok(vec![
LogSeq {
kind: LogEntryKind::ManifestSignature,
seq: seq_manifest,
},
LogSeq {
kind: LogEntryKind::DsseEnvelope,
seq: seq_aibom,
},
LogSeq {
kind: LogEntryKind::SlsaStatement,
seq: seq_slsa,
},
])
}
fn build_oci_graph(
manifest: &ArtifactManifest,
model_name: &str,
current_aion_version: u64,
aibom_dsse: &DsseEnvelope,
slsa_dsse: &DsseEnvelope,
) -> Result<(
OciArtifactManifest,
OciArtifactManifest,
OciArtifactManifest,
)> {
let _ = AION_MANIFEST_TYPE;
let _ = AION_CONFIG_MEDIA_TYPE;
let oci_config = AionConfig {
schema_version: "aion.oci.config.v1".to_string(),
format_version: 2,
file_id: 0,
created_at_version: current_aion_version,
created_at: "release-orchestration-phase-b".to_string(),
};
let oci_layer_payload = manifest.canonical_bytes();
let oci_primary = build_aion_manifest(&oci_layer_payload, model_name, &oci_config)?;
let oci_aibom_referrer = build_attestation_manifest(
aibom_dsse.to_json()?.as_bytes(),
crate::aibom::AIBOM_PAYLOAD_TYPE,
&oci_primary,
)?;
let oci_slsa_referrer = build_attestation_manifest(
slsa_dsse.to_json()?.as_bytes(),
IN_TOTO_PAYLOAD_TYPE,
&oci_primary,
)?;
Ok((oci_primary, oci_aibom_referrer, oci_slsa_referrer))
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SignedRelease {
pub signer: AuthorId,
pub model_ref: ModelRef,
pub manifest: ArtifactManifest,
pub manifest_signature: SignatureEntry,
pub manifest_dsse: DsseEnvelope,
pub aibom: AiBom,
pub aibom_dsse: DsseEnvelope,
pub slsa_statement: InTotoStatement,
pub slsa_dsse: DsseEnvelope,
pub oci_primary: OciArtifactManifest,
pub oci_aibom_referrer: OciArtifactManifest,
pub oci_slsa_referrer: OciArtifactManifest,
pub log_entries: Vec<LogSeq>,
}
#[derive(Debug, Clone)]
pub struct SignedReleaseComponents {
pub signer: AuthorId,
pub model_ref: ModelRef,
pub manifest: ArtifactManifest,
pub manifest_signature: SignatureEntry,
pub manifest_dsse: DsseEnvelope,
pub aibom: AiBom,
pub aibom_dsse: DsseEnvelope,
pub slsa_statement: InTotoStatement,
pub slsa_dsse: DsseEnvelope,
pub oci_primary: OciArtifactManifest,
pub oci_aibom_referrer: OciArtifactManifest,
pub oci_slsa_referrer: OciArtifactManifest,
pub log_entries: Vec<(LogEntryKind, u64)>,
}
impl SignedRelease {
#[must_use]
pub fn from_components(parts: SignedReleaseComponents) -> Self {
let log_seqs = parts
.log_entries
.into_iter()
.map(|(kind, seq)| LogSeq { kind, seq })
.collect();
Self {
signer: parts.signer,
model_ref: parts.model_ref,
manifest: parts.manifest,
manifest_signature: parts.manifest_signature,
manifest_dsse: parts.manifest_dsse,
aibom: parts.aibom,
aibom_dsse: parts.aibom_dsse,
slsa_statement: parts.slsa_statement,
slsa_dsse: parts.slsa_dsse,
oci_primary: parts.oci_primary,
oci_aibom_referrer: parts.oci_aibom_referrer,
oci_slsa_referrer: parts.oci_slsa_referrer,
log_entries: log_seqs,
}
}
pub fn verify(&self, registry: &KeyRegistry, at_version: u64) -> Result<()> {
verify_manifest_signature(
&self.manifest,
&self.manifest_signature,
registry,
at_version,
)?;
let _ = dsse::verify_envelope(&self.manifest_dsse, registry, at_version)?;
let _ = dsse::verify_envelope(&self.aibom_dsse, registry, at_version)?;
let _ = dsse::verify_envelope(&self.slsa_dsse, registry, at_version)?;
self.verify_aibom_manifest_linkage()?;
verify_slsa_subjects_against_manifest(&self.slsa_statement, &self.manifest)?;
self.verify_oci_linkage()?;
self.verify_log_entry_kinds()?;
Ok(())
}
fn verify_aibom_manifest_linkage(&self) -> Result<()> {
let primary = self
.manifest
.entries()
.first()
.ok_or_else(|| AionError::InvalidFormat {
reason: "manifest has no primary entry".to_string(),
})?;
if primary.hash != self.aibom.model.hash {
return Err(AionError::InvalidFormat {
reason: "AIBOM model hash does not match manifest primary entry".to_string(),
});
}
if primary.size != self.aibom.model.size {
return Err(AionError::InvalidFormat {
reason: "AIBOM model size does not match manifest primary entry".to_string(),
});
}
Ok(())
}
fn verify_oci_linkage(&self) -> Result<()> {
let primary_digest = self.oci_primary.digest()?;
check_referrer_subject(&self.oci_aibom_referrer, &primary_digest, "AIBOM")?;
check_referrer_subject(&self.oci_slsa_referrer, &primary_digest, "SLSA")
}
fn verify_log_entry_kinds(&self) -> Result<()> {
let expected = [
LogEntryKind::ManifestSignature,
LogEntryKind::DsseEnvelope,
LogEntryKind::SlsaStatement,
];
if self.log_entries.len() != expected.len() {
return Err(AionError::InvalidFormat {
reason: format!(
"expected {} log entries, got {}",
expected.len(),
self.log_entries.len()
),
});
}
for (entry, want) in self.log_entries.iter().zip(expected.iter()) {
if entry.kind != *want {
return Err(AionError::InvalidFormat {
reason: format!(
"log entry kind mismatch: got {:?}, expected {want:?}",
entry.kind
),
});
}
}
Ok(())
}
}
fn check_referrer_subject(
referrer: &OciArtifactManifest,
expected_digest: &str,
label: &str,
) -> Result<()> {
let subject = referrer
.subject
.as_ref()
.ok_or_else(|| AionError::InvalidFormat {
reason: format!("{label} OCI referrer missing subject"),
})?;
if subject.digest != expected_digest {
return Err(AionError::InvalidFormat {
reason: format!("{label} OCI referrer subject.digest != primary digest"),
});
}
Ok(())
}
fn verify_slsa_subjects_against_manifest(
statement: &InTotoStatement,
manifest: &ArtifactManifest,
) -> Result<()> {
for subject in &statement.subject {
let want = subject
.digest
.get("blake3-256")
.ok_or_else(|| AionError::InvalidFormat {
reason: format!("SLSA subject '{}' missing blake3-256 digest", subject.name),
})?;
let matched = manifest.entries().iter().any(|entry: &ArtifactEntry| {
let observed = hex::encode(entry.hash);
observed == *want
});
if !matched {
return Err(AionError::InvalidFormat {
reason: format!(
"SLSA subject digest for '{}' not found in manifest",
subject.name
),
});
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
fn reg_pinning(signer: AuthorId, key: &SigningKey) -> KeyRegistry {
let mut reg = KeyRegistry::new();
let master = SigningKey::generate();
reg.register_author(signer, master.verifying_key(), key.verifying_key(), 0)
.unwrap();
reg
}
fn sample_builder() -> ReleaseBuilder {
let mut b = ReleaseBuilder::new("acme-7b-chat", "0.3.1", "safetensors");
b.primary_artifact("model.safetensors", vec![0xAAu8; 256])
.add_auxiliary("tokenizer.json", b"{}".to_vec())
.add_framework(FrameworkRef {
name: "pytorch".into(),
version: "2.3.1".into(),
cpe: None,
})
.add_license(License {
spdx_id: "Apache-2.0".into(),
scope: crate::aibom::LicenseScope::Weights,
text_uri: None,
})
.builder_id("https://example.com/ci/run/1")
.current_aion_version(1);
b
}
#[test]
fn seal_requires_primary_artifact() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let mut b = ReleaseBuilder::new("m", "1", "safetensors");
b.builder_id("https://ci/1");
assert!(b.seal(AuthorId::new(1), &key, &mut log).is_err());
}
#[test]
fn seal_requires_builder_id() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let mut b = ReleaseBuilder::new("m", "1", "safetensors");
b.primary_artifact("x", vec![0u8; 32]);
assert!(b.seal(AuthorId::new(1), &key, &mut log).is_err());
}
#[test]
fn seal_and_verify_round_trip() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
signed
.verify(®_pinning(AuthorId::new(50_001), &key), 1)
.unwrap();
}
#[test]
fn log_has_three_entries_in_kind_order() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
assert_eq!(signed.log_entries.len(), 3);
assert_eq!(signed.log_entries[0].kind, LogEntryKind::ManifestSignature);
assert_eq!(signed.log_entries[1].kind, LogEntryKind::DsseEnvelope);
assert_eq!(signed.log_entries[2].kind, LogEntryKind::SlsaStatement);
}
#[test]
fn oci_referrers_link_to_primary() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
let primary_digest = signed.oci_primary.digest().unwrap();
assert_eq!(
signed.oci_aibom_referrer.subject.as_ref().unwrap().digest,
primary_digest
);
assert_eq!(
signed.oci_slsa_referrer.subject.as_ref().unwrap().digest,
primary_digest
);
}
#[test]
fn aibom_model_hash_equals_manifest_primary() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
assert_eq!(
signed.aibom.model.hash,
signed.manifest.entries().first().unwrap().hash
);
}
#[test]
fn tampered_aibom_envelope_rejects() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let mut signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
signed.aibom_dsse.payload[0] ^= 0x01;
assert!(signed
.verify(®_pinning(AuthorId::new(50_001), &key), 1)
.is_err());
}
#[test]
fn wrong_key_rejects() {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let other = SigningKey::generate();
let signed = sample_builder()
.seal(AuthorId::new(50_001), &key, &mut log)
.unwrap();
assert!(signed
.verify(®_pinning(AuthorId::new(50_001), &other), 1)
.is_err());
}
mod properties {
use super::*;
use hegel::generators as gs;
fn build_and_seal(
tc: &hegel::TestCase,
log: &mut TransparencyLog,
signer: AuthorId,
key: &SigningKey,
) -> SignedRelease {
let primary_bytes = tc.draw(gs::binary().min_size(1).max_size(1024));
let n_aux = tc.draw(gs::integers::<usize>().max_value(3));
let mut b = ReleaseBuilder::new("model", "0.1.0", "safetensors");
b.primary_artifact("model.bin", primary_bytes)
.builder_id("https://ci/run/42")
.current_aion_version(tc.draw(gs::integers::<u64>().max_value(1 << 40)));
for i in 0..n_aux {
let bytes = tc.draw(gs::binary().max_size(256));
b.add_auxiliary(format!("aux_{i}"), bytes);
}
b.seal(signer, key, log)
.unwrap_or_else(|_| std::process::abort())
}
#[hegel::test]
fn prop_release_seal_verify_roundtrip(tc: hegel::TestCase) {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
let signed = build_and_seal(&tc, &mut log, signer, &key);
let reg = reg_pinning(signer, &key);
signed
.verify(®, 1)
.unwrap_or_else(|_| std::process::abort());
}
#[hegel::test]
fn prop_release_tampered_manifest_detected(tc: hegel::TestCase) {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let mut signed = build_and_seal(
&tc,
&mut log,
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
&key,
);
let idx = tc.draw(
gs::integers::<usize>()
.max_value(signed.manifest_dsse.payload.len().saturating_sub(1)),
);
if let Some(b) = signed.manifest_dsse.payload.get_mut(idx) {
*b ^= 0x01;
}
assert!(signed
.verify(®_pinning(AuthorId::new(50_001), &key), 1)
.is_err());
}
#[hegel::test]
fn prop_release_oci_referrers_link_to_primary(tc: hegel::TestCase) {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = build_and_seal(
&tc,
&mut log,
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
&key,
);
let primary_digest = signed
.oci_primary
.digest()
.unwrap_or_else(|_| std::process::abort());
let aibom_subject = signed
.oci_aibom_referrer
.subject
.as_ref()
.unwrap_or_else(|| std::process::abort());
let slsa_subject = signed
.oci_slsa_referrer
.subject
.as_ref()
.unwrap_or_else(|| std::process::abort());
assert_eq!(aibom_subject.digest, primary_digest);
assert_eq!(slsa_subject.digest, primary_digest);
}
#[hegel::test]
fn prop_release_aibom_model_ref_matches_manifest(tc: hegel::TestCase) {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = build_and_seal(
&tc,
&mut log,
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
&key,
);
let primary = signed
.manifest
.entries()
.first()
.unwrap_or_else(|| std::process::abort());
assert_eq!(signed.aibom.model.hash, primary.hash);
assert_eq!(signed.aibom.model.size, primary.size);
}
#[hegel::test]
fn prop_release_log_has_expected_kinds(tc: hegel::TestCase) {
let mut log = TransparencyLog::new();
let key = SigningKey::generate();
let signed = build_and_seal(
&tc,
&mut log,
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
&key,
);
assert_eq!(signed.log_entries.len(), 3);
assert_eq!(signed.log_entries[0].kind, LogEntryKind::ManifestSignature);
assert_eq!(signed.log_entries[1].kind, LogEntryKind::DsseEnvelope);
assert_eq!(signed.log_entries[2].kind, LogEntryKind::SlsaStatement);
}
#[hegel::test]
fn prop_release_registry_verify_accepts_pinned_release(tc: hegel::TestCase) {
use crate::key_registry::KeyRegistry;
let mut log = TransparencyLog::new();
let master = SigningKey::generate();
let op = SigningKey::generate();
let signer =
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
let mut reg = KeyRegistry::new();
reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
let signed = build_and_seal(&tc, &mut log, signer, &op);
let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
signed
.verify(®, at)
.unwrap_or_else(|_| std::process::abort());
}
#[hegel::test]
fn prop_release_registry_verify_rejects_rotated_out_signer(tc: hegel::TestCase) {
use crate::key_registry::{sign_rotation_record, KeyRegistry};
let mut log = TransparencyLog::new();
let master = SigningKey::generate();
let op0 = SigningKey::generate();
let op1 = SigningKey::generate();
let signer =
AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
let mut reg = KeyRegistry::new();
reg.register_author(signer, master.verifying_key(), op0.verifying_key(), 0)
.unwrap_or_else(|_| std::process::abort());
let effective = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
let rotation = sign_rotation_record(
signer,
0,
1,
op1.verifying_key().to_bytes(),
effective,
&master,
);
reg.apply_rotation(&rotation)
.unwrap_or_else(|_| std::process::abort());
let signed = build_and_seal(&tc, &mut log, signer, &op0);
let v_after = effective.saturating_add(1);
assert!(signed.verify(®, v_after).is_err());
}
}
}