1use std::collections::BTreeMap;
56
57use crate::aibom::{
58 AiBom, DatasetRef, ExportControl, ExternalReference, FrameworkRef, License, ModelRef,
59 SafetyAttestation,
60};
61use crate::crypto::SigningKey;
62use crate::dsse::{self, DsseEnvelope, AION_MANIFEST_TYPE};
63use crate::key_registry::KeyRegistry;
64use crate::manifest::{
65 sign_manifest, verify_manifest_signature, ArtifactEntry, ArtifactManifest,
66 ArtifactManifestBuilder,
67};
68use crate::oci::{
69 build_aion_manifest, build_attestation_manifest, AionConfig, OciArtifactManifest,
70 AION_CONFIG_MEDIA_TYPE,
71};
72use crate::serializer::SignatureEntry;
73use crate::slsa::{
74 wrap_statement_dsse, InTotoStatement, SlsaStatementBuilder, IN_TOTO_PAYLOAD_TYPE,
75};
76use crate::transparency_log::{LogEntryKind, TransparencyLog};
77use crate::types::AuthorId;
78use crate::{AionError, Result};
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[non_exhaustive]
86pub struct LogSeq {
87 pub kind: LogEntryKind,
89 pub seq: u64,
91}
92
93#[derive(Debug)]
95pub struct ReleaseBuilder {
96 model_name: String,
97 model_version: String,
98 model_format: String,
99 primary_artifact: Option<(String, Vec<u8>)>,
100 auxiliary_artifacts: Vec<(String, Vec<u8>)>,
101 frameworks: Vec<FrameworkRef>,
102 datasets: Vec<DatasetRef>,
103 licenses: Vec<License>,
104 hyperparameters: BTreeMap<String, serde_json::Value>,
105 safety_attestations: Vec<SafetyAttestation>,
106 export_controls: Vec<ExportControl>,
107 references: Vec<ExternalReference>,
108 builder_id: String,
109 external_parameters: serde_json::Value,
110 current_aion_version: u64,
111}
112
113impl ReleaseBuilder {
114 #[must_use]
116 pub fn new(
117 name: impl Into<String>,
118 version: impl Into<String>,
119 format: impl Into<String>,
120 ) -> Self {
121 Self {
122 model_name: name.into(),
123 model_version: version.into(),
124 model_format: format.into(),
125 primary_artifact: None,
126 auxiliary_artifacts: Vec::new(),
127 frameworks: Vec::new(),
128 datasets: Vec::new(),
129 licenses: Vec::new(),
130 hyperparameters: BTreeMap::new(),
131 safety_attestations: Vec::new(),
132 export_controls: Vec::new(),
133 references: Vec::new(),
134 builder_id: String::new(),
135 external_parameters: serde_json::json!({}),
136 current_aion_version: 0,
137 }
138 }
139
140 pub fn primary_artifact(&mut self, name: impl Into<String>, bytes: Vec<u8>) -> &mut Self {
142 self.primary_artifact = Some((name.into(), bytes));
143 self
144 }
145
146 pub fn add_auxiliary(&mut self, name: impl Into<String>, bytes: Vec<u8>) -> &mut Self {
148 self.auxiliary_artifacts.push((name.into(), bytes));
149 self
150 }
151
152 pub fn add_framework(&mut self, f: FrameworkRef) -> &mut Self {
154 self.frameworks.push(f);
155 self
156 }
157
158 pub fn add_dataset(&mut self, d: DatasetRef) -> &mut Self {
160 self.datasets.push(d);
161 self
162 }
163
164 pub fn add_license(&mut self, l: License) -> &mut Self {
166 self.licenses.push(l);
167 self
168 }
169
170 pub fn hyperparameter(&mut self, k: impl Into<String>, v: serde_json::Value) -> &mut Self {
172 self.hyperparameters.insert(k.into(), v);
173 self
174 }
175
176 pub fn add_safety_attestation(&mut self, s: SafetyAttestation) -> &mut Self {
178 self.safety_attestations.push(s);
179 self
180 }
181
182 pub fn add_export_control(&mut self, e: ExportControl) -> &mut Self {
184 self.export_controls.push(e);
185 self
186 }
187
188 pub fn add_reference(&mut self, r: ExternalReference) -> &mut Self {
190 self.references.push(r);
191 self
192 }
193
194 pub fn builder_id(&mut self, id: impl Into<String>) -> &mut Self {
196 self.builder_id = id.into();
197 self
198 }
199
200 pub fn external_parameters(&mut self, v: serde_json::Value) -> &mut Self {
202 self.external_parameters = v;
203 self
204 }
205
206 pub fn current_aion_version(&mut self, v: u64) -> &mut Self {
208 self.current_aion_version = v;
209 self
210 }
211
212 pub fn seal(
221 self,
222 signer: AuthorId,
223 signing_key: &SigningKey,
224 log: &mut TransparencyLog,
225 ) -> Result<SignedRelease> {
226 let core = self.build_core()?;
227 let manifest_signature = sign_manifest(&core.manifest, signer, signing_key);
228 let manifest_dsse = dsse::wrap_manifest(&core.manifest, signer, signing_key);
229 let aibom_dsse = crate::aibom::wrap_aibom_dsse(&core.aibom, signer, signing_key)?;
230 let slsa_dsse = wrap_statement_dsse(&core.slsa_statement, signer, signing_key)?;
231 let log_entries = append_release_log_entries(
232 log,
233 &manifest_signature.signature,
234 &aibom_dsse,
235 &slsa_dsse,
236 core.current_aion_version,
237 )?;
238 let (oci_primary, oci_aibom_referrer, oci_slsa_referrer) = build_oci_graph(
239 &core.manifest,
240 &core.model_ref.name,
241 core.current_aion_version,
242 &aibom_dsse,
243 &slsa_dsse,
244 )?;
245 Ok(SignedRelease {
246 signer,
247 model_ref: core.model_ref,
248 manifest: core.manifest,
249 manifest_signature,
250 manifest_dsse,
251 aibom: core.aibom,
252 aibom_dsse,
253 slsa_statement: core.slsa_statement,
254 slsa_dsse,
255 oci_primary,
256 oci_aibom_referrer,
257 oci_slsa_referrer,
258 log_entries,
259 })
260 }
261
262 fn build_core(self) -> Result<SealedCore> {
266 let Self {
267 model_name,
268 model_version,
269 model_format,
270 primary_artifact,
271 auxiliary_artifacts,
272 frameworks,
273 datasets,
274 licenses,
275 hyperparameters,
276 safety_attestations,
277 export_controls,
278 references,
279 builder_id,
280 external_parameters,
281 current_aion_version,
282 } = self;
283 let (primary_name, primary_bytes) =
284 primary_artifact.ok_or_else(|| AionError::InvalidFormat {
285 reason: "ReleaseBuilder requires a primary_artifact".to_string(),
286 })?;
287 if builder_id.is_empty() {
288 return Err(AionError::InvalidFormat {
289 reason: "ReleaseBuilder requires a non-empty builder_id".to_string(),
290 });
291 }
292 let manifest =
293 construct_artifact_manifest(&primary_name, &primary_bytes, &auxiliary_artifacts);
294 let model_ref =
295 model_ref_from_manifest(&manifest, model_name, model_version, model_format)?;
296 let aibom = assemble_aibom(
297 model_ref.clone(),
298 current_aion_version,
299 AibomFields {
300 frameworks,
301 datasets,
302 licenses,
303 hyperparameters,
304 safety_attestations,
305 export_controls,
306 references,
307 },
308 );
309 let slsa_statement = assemble_slsa_statement(&manifest, builder_id, external_parameters)?;
310 Ok(SealedCore {
311 manifest,
312 model_ref,
313 aibom,
314 slsa_statement,
315 current_aion_version,
316 })
317 }
318}
319
320struct SealedCore {
323 manifest: ArtifactManifest,
324 model_ref: ModelRef,
325 aibom: AiBom,
326 slsa_statement: InTotoStatement,
327 current_aion_version: u64,
328}
329
330struct AibomFields {
335 frameworks: Vec<FrameworkRef>,
336 datasets: Vec<DatasetRef>,
337 licenses: Vec<License>,
338 hyperparameters: BTreeMap<String, serde_json::Value>,
339 safety_attestations: Vec<SafetyAttestation>,
340 export_controls: Vec<ExportControl>,
341 references: Vec<ExternalReference>,
342}
343
344fn construct_artifact_manifest(
347 primary_name: &str,
348 primary_bytes: &[u8],
349 auxiliaries: &[(String, Vec<u8>)],
350) -> ArtifactManifest {
351 let mut mb = ArtifactManifestBuilder::new();
352 let _ = mb.add(primary_name, primary_bytes);
353 for (name, bytes) in auxiliaries {
354 let _ = mb.add(name, bytes);
355 }
356 mb.build()
357}
358
359fn model_ref_from_manifest(
361 manifest: &ArtifactManifest,
362 name: String,
363 version: String,
364 format: String,
365) -> Result<ModelRef> {
366 let primary = manifest
367 .entries()
368 .first()
369 .ok_or_else(|| AionError::InvalidFormat {
370 reason: "manifest is unexpectedly empty".to_string(),
371 })?;
372 Ok(ModelRef {
373 name,
374 version,
375 hash_algorithm: "BLAKE3-256".to_string(),
376 hash: primary.hash,
377 size: primary.size,
378 format,
379 })
380}
381
382fn assemble_aibom(model_ref: ModelRef, current_aion_version: u64, fields: AibomFields) -> AiBom {
384 let mut ab = AiBom::builder(model_ref, current_aion_version);
385 for f in fields.frameworks {
386 ab.add_framework(f);
387 }
388 for d in fields.datasets {
389 ab.add_dataset(d);
390 }
391 for l in fields.licenses {
392 ab.add_license(l);
393 }
394 for (k, v) in fields.hyperparameters {
395 ab.hyperparameter(k, v);
396 }
397 for s in fields.safety_attestations {
398 ab.add_safety_attestation(s);
399 }
400 for e in fields.export_controls {
401 ab.add_export_control(e);
402 }
403 for r in fields.references {
404 ab.add_reference(r);
405 }
406 ab.build()
407}
408
409fn assemble_slsa_statement(
411 manifest: &ArtifactManifest,
412 builder_id: String,
413 external_parameters: serde_json::Value,
414) -> Result<InTotoStatement> {
415 let mut sb = SlsaStatementBuilder::new(builder_id);
416 sb.add_all_subjects_from_manifest(manifest)?;
417 sb.external_parameters(external_parameters);
418 sb.build()
419}
420
421fn append_release_log_entries(
424 log: &mut TransparencyLog,
425 manifest_sig_bytes: &[u8; 64],
426 aibom_dsse: &DsseEnvelope,
427 slsa_dsse: &DsseEnvelope,
428 current_aion_version: u64,
429) -> Result<Vec<LogSeq>> {
430 let seq_manifest = log.append(
431 LogEntryKind::ManifestSignature,
432 manifest_sig_bytes,
433 current_aion_version,
434 )?;
435 let seq_aibom = log.append(
436 LogEntryKind::DsseEnvelope,
437 aibom_dsse.to_json()?.as_bytes(),
438 current_aion_version,
439 )?;
440 let seq_slsa = log.append(
441 LogEntryKind::SlsaStatement,
442 slsa_dsse.to_json()?.as_bytes(),
443 current_aion_version,
444 )?;
445 Ok(vec![
446 LogSeq {
447 kind: LogEntryKind::ManifestSignature,
448 seq: seq_manifest,
449 },
450 LogSeq {
451 kind: LogEntryKind::DsseEnvelope,
452 seq: seq_aibom,
453 },
454 LogSeq {
455 kind: LogEntryKind::SlsaStatement,
456 seq: seq_slsa,
457 },
458 ])
459}
460
461fn build_oci_graph(
468 manifest: &ArtifactManifest,
469 model_name: &str,
470 current_aion_version: u64,
471 aibom_dsse: &DsseEnvelope,
472 slsa_dsse: &DsseEnvelope,
473) -> Result<(
474 OciArtifactManifest,
475 OciArtifactManifest,
476 OciArtifactManifest,
477)> {
478 let _ = AION_MANIFEST_TYPE;
481 let _ = AION_CONFIG_MEDIA_TYPE;
482 let oci_config = AionConfig {
483 schema_version: "aion.oci.config.v1".to_string(),
484 format_version: 2,
485 file_id: 0,
486 created_at_version: current_aion_version,
487 created_at: "release-orchestration-phase-b".to_string(),
488 };
489 let oci_layer_payload = manifest.canonical_bytes();
490 let oci_primary = build_aion_manifest(&oci_layer_payload, model_name, &oci_config)?;
491 let oci_aibom_referrer = build_attestation_manifest(
492 aibom_dsse.to_json()?.as_bytes(),
493 crate::aibom::AIBOM_PAYLOAD_TYPE,
494 &oci_primary,
495 )?;
496 let oci_slsa_referrer = build_attestation_manifest(
497 slsa_dsse.to_json()?.as_bytes(),
498 IN_TOTO_PAYLOAD_TYPE,
499 &oci_primary,
500 )?;
501 Ok((oci_primary, oci_aibom_referrer, oci_slsa_referrer))
502}
503
504#[derive(Debug, Clone)]
511#[non_exhaustive]
512pub struct SignedRelease {
513 pub signer: AuthorId,
517 pub model_ref: ModelRef,
519 pub manifest: ArtifactManifest,
521 pub manifest_signature: SignatureEntry,
523 pub manifest_dsse: DsseEnvelope,
525 pub aibom: AiBom,
527 pub aibom_dsse: DsseEnvelope,
529 pub slsa_statement: InTotoStatement,
531 pub slsa_dsse: DsseEnvelope,
533 pub oci_primary: OciArtifactManifest,
535 pub oci_aibom_referrer: OciArtifactManifest,
537 pub oci_slsa_referrer: OciArtifactManifest,
539 pub log_entries: Vec<LogSeq>,
541}
542
543#[derive(Debug, Clone)]
562pub struct SignedReleaseComponents {
563 pub signer: AuthorId,
565 pub model_ref: ModelRef,
567 pub manifest: ArtifactManifest,
569 pub manifest_signature: SignatureEntry,
571 pub manifest_dsse: DsseEnvelope,
573 pub aibom: AiBom,
575 pub aibom_dsse: DsseEnvelope,
577 pub slsa_statement: InTotoStatement,
579 pub slsa_dsse: DsseEnvelope,
581 pub oci_primary: OciArtifactManifest,
583 pub oci_aibom_referrer: OciArtifactManifest,
585 pub oci_slsa_referrer: OciArtifactManifest,
587 pub log_entries: Vec<(LogEntryKind, u64)>,
591}
592
593impl SignedRelease {
594 #[must_use]
606 pub fn from_components(parts: SignedReleaseComponents) -> Self {
607 let log_seqs = parts
608 .log_entries
609 .into_iter()
610 .map(|(kind, seq)| LogSeq { kind, seq })
611 .collect();
612 Self {
613 signer: parts.signer,
614 model_ref: parts.model_ref,
615 manifest: parts.manifest,
616 manifest_signature: parts.manifest_signature,
617 manifest_dsse: parts.manifest_dsse,
618 aibom: parts.aibom,
619 aibom_dsse: parts.aibom_dsse,
620 slsa_statement: parts.slsa_statement,
621 slsa_dsse: parts.slsa_dsse,
622 oci_primary: parts.oci_primary,
623 oci_aibom_referrer: parts.oci_aibom_referrer,
624 oci_slsa_referrer: parts.oci_slsa_referrer,
625 log_entries: log_seqs,
626 }
627 }
628
629 pub fn verify(&self, registry: &KeyRegistry, at_version: u64) -> Result<()> {
638 verify_manifest_signature(
639 &self.manifest,
640 &self.manifest_signature,
641 registry,
642 at_version,
643 )?;
644 let _ = dsse::verify_envelope(&self.manifest_dsse, registry, at_version)?;
645 let _ = dsse::verify_envelope(&self.aibom_dsse, registry, at_version)?;
646 let _ = dsse::verify_envelope(&self.slsa_dsse, registry, at_version)?;
647 self.verify_aibom_manifest_linkage()?;
648 verify_slsa_subjects_against_manifest(&self.slsa_statement, &self.manifest)?;
649 self.verify_oci_linkage()?;
650 self.verify_log_entry_kinds()?;
651 Ok(())
652 }
653
654 fn verify_aibom_manifest_linkage(&self) -> Result<()> {
657 let primary = self
658 .manifest
659 .entries()
660 .first()
661 .ok_or_else(|| AionError::InvalidFormat {
662 reason: "manifest has no primary entry".to_string(),
663 })?;
664 if primary.hash != self.aibom.model.hash {
665 return Err(AionError::InvalidFormat {
666 reason: "AIBOM model hash does not match manifest primary entry".to_string(),
667 });
668 }
669 if primary.size != self.aibom.model.size {
670 return Err(AionError::InvalidFormat {
671 reason: "AIBOM model size does not match manifest primary entry".to_string(),
672 });
673 }
674 Ok(())
675 }
676
677 fn verify_oci_linkage(&self) -> Result<()> {
680 let primary_digest = self.oci_primary.digest()?;
681 check_referrer_subject(&self.oci_aibom_referrer, &primary_digest, "AIBOM")?;
682 check_referrer_subject(&self.oci_slsa_referrer, &primary_digest, "SLSA")
683 }
684
685 fn verify_log_entry_kinds(&self) -> Result<()> {
688 let expected = [
689 LogEntryKind::ManifestSignature,
690 LogEntryKind::DsseEnvelope,
691 LogEntryKind::SlsaStatement,
692 ];
693 if self.log_entries.len() != expected.len() {
694 return Err(AionError::InvalidFormat {
695 reason: format!(
696 "expected {} log entries, got {}",
697 expected.len(),
698 self.log_entries.len()
699 ),
700 });
701 }
702 for (entry, want) in self.log_entries.iter().zip(expected.iter()) {
703 if entry.kind != *want {
704 return Err(AionError::InvalidFormat {
705 reason: format!(
706 "log entry kind mismatch: got {:?}, expected {want:?}",
707 entry.kind
708 ),
709 });
710 }
711 }
712 Ok(())
713 }
714}
715
716fn check_referrer_subject(
720 referrer: &OciArtifactManifest,
721 expected_digest: &str,
722 label: &str,
723) -> Result<()> {
724 let subject = referrer
725 .subject
726 .as_ref()
727 .ok_or_else(|| AionError::InvalidFormat {
728 reason: format!("{label} OCI referrer missing subject"),
729 })?;
730 if subject.digest != expected_digest {
731 return Err(AionError::InvalidFormat {
732 reason: format!("{label} OCI referrer subject.digest != primary digest"),
733 });
734 }
735 Ok(())
736}
737
738fn verify_slsa_subjects_against_manifest(
741 statement: &InTotoStatement,
742 manifest: &ArtifactManifest,
743) -> Result<()> {
744 for subject in &statement.subject {
745 let want = subject
746 .digest
747 .get("blake3-256")
748 .ok_or_else(|| AionError::InvalidFormat {
749 reason: format!("SLSA subject '{}' missing blake3-256 digest", subject.name),
750 })?;
751 let matched = manifest.entries().iter().any(|entry: &ArtifactEntry| {
752 let observed = hex::encode(entry.hash);
753 observed == *want
754 });
755 if !matched {
756 return Err(AionError::InvalidFormat {
757 reason: format!(
758 "SLSA subject digest for '{}' not found in manifest",
759 subject.name
760 ),
761 });
762 }
763 }
764 Ok(())
765}
766
767#[cfg(test)]
768#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
769mod tests {
770 use super::*;
771
772 fn reg_pinning(signer: AuthorId, key: &SigningKey) -> KeyRegistry {
774 let mut reg = KeyRegistry::new();
775 let master = SigningKey::generate();
776 reg.register_author(signer, master.verifying_key(), key.verifying_key(), 0)
777 .unwrap();
778 reg
779 }
780
781 fn sample_builder() -> ReleaseBuilder {
782 let mut b = ReleaseBuilder::new("acme-7b-chat", "0.3.1", "safetensors");
783 b.primary_artifact("model.safetensors", vec![0xAAu8; 256])
784 .add_auxiliary("tokenizer.json", b"{}".to_vec())
785 .add_framework(FrameworkRef {
786 name: "pytorch".into(),
787 version: "2.3.1".into(),
788 cpe: None,
789 })
790 .add_license(License {
791 spdx_id: "Apache-2.0".into(),
792 scope: crate::aibom::LicenseScope::Weights,
793 text_uri: None,
794 })
795 .builder_id("https://example.com/ci/run/1")
796 .current_aion_version(1);
797 b
798 }
799
800 #[test]
801 fn seal_requires_primary_artifact() {
802 let mut log = TransparencyLog::new();
803 let key = SigningKey::generate();
804 let mut b = ReleaseBuilder::new("m", "1", "safetensors");
805 b.builder_id("https://ci/1");
806 assert!(b.seal(AuthorId::new(1), &key, &mut log).is_err());
807 }
808
809 #[test]
810 fn seal_requires_builder_id() {
811 let mut log = TransparencyLog::new();
812 let key = SigningKey::generate();
813 let mut b = ReleaseBuilder::new("m", "1", "safetensors");
814 b.primary_artifact("x", vec![0u8; 32]);
815 assert!(b.seal(AuthorId::new(1), &key, &mut log).is_err());
816 }
817
818 #[test]
819 fn seal_and_verify_round_trip() {
820 let mut log = TransparencyLog::new();
821 let key = SigningKey::generate();
822 let signed = sample_builder()
823 .seal(AuthorId::new(50_001), &key, &mut log)
824 .unwrap();
825 signed
826 .verify(®_pinning(AuthorId::new(50_001), &key), 1)
827 .unwrap();
828 }
829
830 #[test]
831 fn log_has_three_entries_in_kind_order() {
832 let mut log = TransparencyLog::new();
833 let key = SigningKey::generate();
834 let signed = sample_builder()
835 .seal(AuthorId::new(50_001), &key, &mut log)
836 .unwrap();
837 assert_eq!(signed.log_entries.len(), 3);
838 assert_eq!(signed.log_entries[0].kind, LogEntryKind::ManifestSignature);
839 assert_eq!(signed.log_entries[1].kind, LogEntryKind::DsseEnvelope);
840 assert_eq!(signed.log_entries[2].kind, LogEntryKind::SlsaStatement);
841 }
842
843 #[test]
844 fn oci_referrers_link_to_primary() {
845 let mut log = TransparencyLog::new();
846 let key = SigningKey::generate();
847 let signed = sample_builder()
848 .seal(AuthorId::new(50_001), &key, &mut log)
849 .unwrap();
850 let primary_digest = signed.oci_primary.digest().unwrap();
851 assert_eq!(
852 signed.oci_aibom_referrer.subject.as_ref().unwrap().digest,
853 primary_digest
854 );
855 assert_eq!(
856 signed.oci_slsa_referrer.subject.as_ref().unwrap().digest,
857 primary_digest
858 );
859 }
860
861 #[test]
862 fn aibom_model_hash_equals_manifest_primary() {
863 let mut log = TransparencyLog::new();
864 let key = SigningKey::generate();
865 let signed = sample_builder()
866 .seal(AuthorId::new(50_001), &key, &mut log)
867 .unwrap();
868 assert_eq!(
869 signed.aibom.model.hash,
870 signed.manifest.entries().first().unwrap().hash
871 );
872 }
873
874 #[test]
875 fn tampered_aibom_envelope_rejects() {
876 let mut log = TransparencyLog::new();
877 let key = SigningKey::generate();
878 let mut signed = sample_builder()
879 .seal(AuthorId::new(50_001), &key, &mut log)
880 .unwrap();
881 signed.aibom_dsse.payload[0] ^= 0x01;
882 assert!(signed
883 .verify(®_pinning(AuthorId::new(50_001), &key), 1)
884 .is_err());
885 }
886
887 #[test]
888 fn wrong_key_rejects() {
889 let mut log = TransparencyLog::new();
890 let key = SigningKey::generate();
891 let other = SigningKey::generate();
892 let signed = sample_builder()
893 .seal(AuthorId::new(50_001), &key, &mut log)
894 .unwrap();
895 assert!(signed
897 .verify(®_pinning(AuthorId::new(50_001), &other), 1)
898 .is_err());
899 }
900
901 mod properties {
902 use super::*;
903 use hegel::generators as gs;
904
905 fn build_and_seal(
906 tc: &hegel::TestCase,
907 log: &mut TransparencyLog,
908 signer: AuthorId,
909 key: &SigningKey,
910 ) -> SignedRelease {
911 let primary_bytes = tc.draw(gs::binary().min_size(1).max_size(1024));
912 let n_aux = tc.draw(gs::integers::<usize>().max_value(3));
913 let mut b = ReleaseBuilder::new("model", "0.1.0", "safetensors");
914 b.primary_artifact("model.bin", primary_bytes)
915 .builder_id("https://ci/run/42")
916 .current_aion_version(tc.draw(gs::integers::<u64>().max_value(1 << 40)));
917 for i in 0..n_aux {
918 let bytes = tc.draw(gs::binary().max_size(256));
919 b.add_auxiliary(format!("aux_{i}"), bytes);
920 }
921 b.seal(signer, key, log)
922 .unwrap_or_else(|_| std::process::abort())
923 }
924
925 #[hegel::test]
926 fn prop_release_seal_verify_roundtrip(tc: hegel::TestCase) {
927 let mut log = TransparencyLog::new();
928 let key = SigningKey::generate();
929 let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
930 let signed = build_and_seal(&tc, &mut log, signer, &key);
931 let reg = reg_pinning(signer, &key);
932 signed
933 .verify(®, 1)
934 .unwrap_or_else(|_| std::process::abort());
935 }
936
937 #[hegel::test]
938 fn prop_release_tampered_manifest_detected(tc: hegel::TestCase) {
939 let mut log = TransparencyLog::new();
940 let key = SigningKey::generate();
941 let mut signed = build_and_seal(
942 &tc,
943 &mut log,
944 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
945 &key,
946 );
947 let idx = tc.draw(
948 gs::integers::<usize>()
949 .max_value(signed.manifest_dsse.payload.len().saturating_sub(1)),
950 );
951 if let Some(b) = signed.manifest_dsse.payload.get_mut(idx) {
952 *b ^= 0x01;
953 }
954 assert!(signed
955 .verify(®_pinning(AuthorId::new(50_001), &key), 1)
956 .is_err());
957 }
958
959 #[hegel::test]
960 fn prop_release_oci_referrers_link_to_primary(tc: hegel::TestCase) {
961 let mut log = TransparencyLog::new();
962 let key = SigningKey::generate();
963 let signed = build_and_seal(
964 &tc,
965 &mut log,
966 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
967 &key,
968 );
969 let primary_digest = signed
970 .oci_primary
971 .digest()
972 .unwrap_or_else(|_| std::process::abort());
973 let aibom_subject = signed
974 .oci_aibom_referrer
975 .subject
976 .as_ref()
977 .unwrap_or_else(|| std::process::abort());
978 let slsa_subject = signed
979 .oci_slsa_referrer
980 .subject
981 .as_ref()
982 .unwrap_or_else(|| std::process::abort());
983 assert_eq!(aibom_subject.digest, primary_digest);
984 assert_eq!(slsa_subject.digest, primary_digest);
985 }
986
987 #[hegel::test]
988 fn prop_release_aibom_model_ref_matches_manifest(tc: hegel::TestCase) {
989 let mut log = TransparencyLog::new();
990 let key = SigningKey::generate();
991 let signed = build_and_seal(
992 &tc,
993 &mut log,
994 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
995 &key,
996 );
997 let primary = signed
998 .manifest
999 .entries()
1000 .first()
1001 .unwrap_or_else(|| std::process::abort());
1002 assert_eq!(signed.aibom.model.hash, primary.hash);
1003 assert_eq!(signed.aibom.model.size, primary.size);
1004 }
1005
1006 #[hegel::test]
1007 fn prop_release_log_has_expected_kinds(tc: hegel::TestCase) {
1008 let mut log = TransparencyLog::new();
1009 let key = SigningKey::generate();
1010 let signed = build_and_seal(
1011 &tc,
1012 &mut log,
1013 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1))),
1014 &key,
1015 );
1016 assert_eq!(signed.log_entries.len(), 3);
1017 assert_eq!(signed.log_entries[0].kind, LogEntryKind::ManifestSignature);
1018 assert_eq!(signed.log_entries[1].kind, LogEntryKind::DsseEnvelope);
1019 assert_eq!(signed.log_entries[2].kind, LogEntryKind::SlsaStatement);
1020 }
1021
1022 #[hegel::test]
1023 fn prop_release_registry_verify_accepts_pinned_release(tc: hegel::TestCase) {
1024 use crate::key_registry::KeyRegistry;
1025 let mut log = TransparencyLog::new();
1026 let master = SigningKey::generate();
1027 let op = SigningKey::generate();
1028 let signer =
1029 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
1030 let mut reg = KeyRegistry::new();
1031 reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
1032 .unwrap_or_else(|_| std::process::abort());
1033 let signed = build_and_seal(&tc, &mut log, signer, &op);
1034 let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
1035 signed
1036 .verify(®, at)
1037 .unwrap_or_else(|_| std::process::abort());
1038 }
1039
1040 #[hegel::test]
1041 fn prop_release_registry_verify_rejects_rotated_out_signer(tc: hegel::TestCase) {
1042 use crate::key_registry::{sign_rotation_record, KeyRegistry};
1043 let mut log = TransparencyLog::new();
1044 let master = SigningKey::generate();
1045 let op0 = SigningKey::generate();
1046 let op1 = SigningKey::generate();
1047 let signer =
1048 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
1049 let mut reg = KeyRegistry::new();
1050 reg.register_author(signer, master.verifying_key(), op0.verifying_key(), 0)
1051 .unwrap_or_else(|_| std::process::abort());
1052 let effective = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
1053 let rotation = sign_rotation_record(
1054 signer,
1055 0,
1056 1,
1057 op1.verifying_key().to_bytes(),
1058 effective,
1059 &master,
1060 );
1061 reg.apply_rotation(&rotation)
1062 .unwrap_or_else(|_| std::process::abort());
1063 let signed = build_and_seal(&tc, &mut log, signer, &op0);
1065 let v_after = effective.saturating_add(1);
1066 assert!(signed.verify(®, v_after).is_err());
1067 }
1068 }
1069}