1use zerocopy::AsBytes;
48
49use crate::crypto::{hash, SigningKey, VerifyingKey};
50use crate::serializer::SignatureEntry;
51use crate::types::AuthorId;
52use crate::{AionError, Result};
53
54pub const ARTIFACT_ENTRY_SIZE: usize = 128;
56
57const MANIFEST_DOMAIN: &[u8] = b"AION_V2_MANIFEST_V1";
63
64#[repr(u16)]
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum HashAlgorithm {
71 Blake3_256 = 1,
73}
74
75impl HashAlgorithm {
76 pub fn from_u16(value: u16) -> Result<Self> {
82 match value {
83 1 => Ok(Self::Blake3_256),
84 other => Err(AionError::InvalidFormat {
85 reason: format!("Unknown manifest hash algorithm: {other}"),
86 }),
87 }
88 }
89}
90
91#[repr(C)]
95#[derive(Debug, Clone, Copy, AsBytes)]
96pub struct ArtifactEntry {
97 pub name_offset: u64,
99 pub name_length: u32,
101 pub hash_algorithm: u16,
103 pub reserved1: [u8; 2],
105 pub size: u64,
107 pub hash: [u8; 32],
109 pub reserved2: [u8; 72],
112}
113
114const _: () = assert!(std::mem::size_of::<ArtifactEntry>() == ARTIFACT_ENTRY_SIZE);
115
116impl ArtifactEntry {
117 #[must_use]
120 pub const fn new(name_offset: u64, name_length: u32, size: u64, hash: [u8; 32]) -> Self {
121 Self {
122 name_offset,
123 name_length,
124 hash_algorithm: HashAlgorithm::Blake3_256 as u16,
125 reserved1: [0; 2],
126 size,
127 hash,
128 reserved2: [0; 72],
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct ArtifactHandle {
138 index: usize,
139}
140
141impl ArtifactHandle {
142 #[must_use]
144 pub const fn index(self) -> usize {
145 self.index
146 }
147}
148
149#[derive(Debug, Default)]
151pub struct ArtifactManifestBuilder {
152 entries: Vec<ArtifactEntry>,
153 name_table: Vec<u8>,
154}
155
156impl ArtifactManifestBuilder {
157 #[must_use]
159 #[allow(clippy::missing_const_for_fn)] pub fn new() -> Self {
161 Self {
162 entries: Vec::new(),
163 name_table: Vec::new(),
164 }
165 }
166
167 #[must_use = "the returned ArtifactHandle is the only way to refer to this artifact by index later"]
171 #[allow(clippy::cast_possible_truncation)] pub fn add(&mut self, name: &str, bytes: &[u8]) -> ArtifactHandle {
173 let name_offset = self.name_table.len() as u64;
174 let name_length = name.len() as u32;
175 self.name_table.extend_from_slice(name.as_bytes());
176 self.name_table.push(0);
177
178 let digest = hash(bytes);
179 let entry = ArtifactEntry::new(name_offset, name_length, bytes.len() as u64, digest);
180 let index = self.entries.len();
181 self.entries.push(entry);
182 ArtifactHandle { index }
183 }
184
185 #[must_use]
188 pub fn build(self) -> ArtifactManifest {
189 let canonical = canonical_manifest_bytes(&self.entries, &self.name_table);
190 let manifest_id = hash(&canonical);
191 ArtifactManifest {
192 manifest_id,
193 entries: self.entries,
194 name_table: self.name_table,
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct ArtifactManifest {
202 manifest_id: [u8; 32],
204 entries: Vec<ArtifactEntry>,
205 name_table: Vec<u8>,
206}
207
208impl ArtifactManifest {
209 #[must_use]
211 pub const fn manifest_id(&self) -> &[u8; 32] {
212 &self.manifest_id
213 }
214
215 #[must_use]
217 pub fn entries(&self) -> &[ArtifactEntry] {
218 &self.entries
219 }
220
221 #[must_use]
223 pub fn name_table(&self) -> &[u8] {
224 &self.name_table
225 }
226
227 #[must_use]
229 pub fn canonical_bytes(&self) -> Vec<u8> {
230 canonical_manifest_bytes(&self.entries, &self.name_table)
231 }
232
233 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self> {
249 let manifest_id = crate::crypto::hash(bytes);
250 let body = bytes
251 .strip_prefix(MANIFEST_DOMAIN)
252 .ok_or_else(|| AionError::InvalidFormat {
253 reason: "manifest canonical bytes missing domain prefix".to_string(),
254 })?;
255 if body.len() < 8 {
256 return Err(AionError::InvalidFormat {
257 reason: "manifest canonical bytes truncated before entry count".to_string(),
258 });
259 }
260 let (count_bytes, rest) = body.split_at(8);
261 let mut count_arr = [0u8; 8];
262 count_arr.copy_from_slice(count_bytes);
263 let entry_count = u64::from_le_bytes(count_arr) as usize;
264
265 let entries_len = entry_count
266 .checked_mul(ARTIFACT_ENTRY_SIZE)
267 .ok_or_else(|| AionError::InvalidFormat {
268 reason: "manifest entry count overflows usize".to_string(),
269 })?;
270 if rest.len() < entries_len {
271 return Err(AionError::InvalidFormat {
272 reason: format!(
273 "manifest entries truncated: need {} bytes, have {}",
274 entries_len,
275 rest.len()
276 ),
277 });
278 }
279 let (entries_slice, name_table_slice) = rest.split_at(entries_len);
280 let mut entries = Vec::with_capacity(entry_count);
281 for i in 0..entry_count {
282 let start =
283 i.checked_mul(ARTIFACT_ENTRY_SIZE)
284 .ok_or_else(|| AionError::InvalidFormat {
285 reason: "entry index overflow".to_string(),
286 })?;
287 let end =
288 start
289 .checked_add(ARTIFACT_ENTRY_SIZE)
290 .ok_or_else(|| AionError::InvalidFormat {
291 reason: "entry end overflow".to_string(),
292 })?;
293 let slice = entries_slice
294 .get(start..end)
295 .ok_or_else(|| AionError::InvalidFormat {
296 reason: "entry slice out of bounds".to_string(),
297 })?;
298 entries.push(parse_artifact_entry(slice)?);
299 }
300
301 Ok(Self {
302 manifest_id,
303 entries,
304 name_table: name_table_slice.to_vec(),
305 })
306 }
307
308 pub fn name_of(&self, entry: &ArtifactEntry) -> Result<&str> {
315 slice_name(&self.name_table, entry.name_offset, entry.name_length)
316 }
317
318 pub fn verify_artifact(&self, name: &str, bytes: &[u8]) -> Result<()> {
327 for entry in &self.entries {
328 let candidate = self.name_of(entry)?;
329 if candidate != name {
330 continue;
331 }
332 if bytes.len() as u64 != entry.size {
333 return Err(AionError::InvalidFormat {
334 reason: format!(
335 "artifact '{name}': size mismatch (expected {}, got {})",
336 entry.size,
337 bytes.len()
338 ),
339 });
340 }
341 let digest = hash(bytes);
342 if digest != entry.hash {
343 return Err(AionError::InvalidFormat {
344 reason: format!("artifact '{name}': hash mismatch"),
345 });
346 }
347 return Ok(());
348 }
349 Err(AionError::InvalidFormat {
350 reason: format!("artifact '{name}' not found in manifest"),
351 })
352 }
353}
354
355fn slice_name(table: &[u8], offset: u64, length: u32) -> Result<&str> {
357 let start = usize::try_from(offset).map_err(|_| AionError::InvalidFormat {
358 reason: "manifest name_offset exceeds usize".to_string(),
359 })?;
360 let len = length as usize;
361 let end = start
362 .checked_add(len)
363 .ok_or_else(|| AionError::InvalidFormat {
364 reason: "manifest name_offset + name_length overflows".to_string(),
365 })?;
366 let slice = table
367 .get(start..end)
368 .ok_or_else(|| AionError::InvalidFormat {
369 reason: "manifest name slice out of bounds".to_string(),
370 })?;
371 std::str::from_utf8(slice).map_err(|e| AionError::InvalidFormat {
372 reason: format!("manifest name is not valid UTF-8: {e}"),
373 })
374}
375
376fn parse_artifact_entry(slice: &[u8]) -> Result<ArtifactEntry> {
380 if slice.len() != ARTIFACT_ENTRY_SIZE {
381 return Err(AionError::InvalidFormat {
382 reason: format!(
383 "artifact entry slice must be {ARTIFACT_ENTRY_SIZE} bytes, got {}",
384 slice.len()
385 ),
386 });
387 }
388 let read_u64 = |from: usize| -> Result<u64> {
389 let end = from
390 .checked_add(8)
391 .ok_or_else(|| AionError::InvalidFormat {
392 reason: "u64 offset overflow".to_string(),
393 })?;
394 let bytes = slice
395 .get(from..end)
396 .ok_or_else(|| AionError::InvalidFormat {
397 reason: "u64 read out of bounds".to_string(),
398 })?;
399 let mut a = [0u8; 8];
400 a.copy_from_slice(bytes);
401 Ok(u64::from_le_bytes(a))
402 };
403 let name_offset = read_u64(0)?;
404 let size = read_u64(16)?;
405 let name_length = {
406 let bytes = slice.get(8..12).ok_or_else(|| AionError::InvalidFormat {
407 reason: "name_length out of bounds".to_string(),
408 })?;
409 let mut a = [0u8; 4];
410 a.copy_from_slice(bytes);
411 u32::from_le_bytes(a)
412 };
413 let hash_algorithm = {
414 let bytes = slice.get(12..14).ok_or_else(|| AionError::InvalidFormat {
415 reason: "hash_algorithm out of bounds".to_string(),
416 })?;
417 let mut a = [0u8; 2];
418 a.copy_from_slice(bytes);
419 u16::from_le_bytes(a)
420 };
421 let mut hash = [0u8; 32];
422 let hash_bytes = slice.get(24..56).ok_or_else(|| AionError::InvalidFormat {
423 reason: "hash bytes out of bounds".to_string(),
424 })?;
425 hash.copy_from_slice(hash_bytes);
426
427 let reserved1 = slice.get(14..16).ok_or_else(|| AionError::InvalidFormat {
433 reason: "reserved1 out of bounds".to_string(),
434 })?;
435 if reserved1.iter().any(|b| *b != 0) {
436 return Err(AionError::InvalidFormat {
437 reason: "ArtifactEntry reserved1 must be all zero".to_string(),
438 });
439 }
440 let reserved2 = slice.get(56..128).ok_or_else(|| AionError::InvalidFormat {
441 reason: "reserved2 out of bounds".to_string(),
442 })?;
443 if reserved2.iter().any(|b| *b != 0) {
444 return Err(AionError::InvalidFormat {
445 reason: "ArtifactEntry reserved2 must be all zero".to_string(),
446 });
447 }
448
449 Ok(ArtifactEntry {
450 name_offset,
451 name_length,
452 hash_algorithm,
453 reserved1: [0u8; 2],
454 size,
455 hash,
456 reserved2: [0u8; 72],
457 })
458}
459
460fn canonical_manifest_bytes(entries: &[ArtifactEntry], name_table: &[u8]) -> Vec<u8> {
463 let entries_len = entries
464 .len()
465 .checked_mul(ARTIFACT_ENTRY_SIZE)
466 .unwrap_or_else(|| std::process::abort());
467 let capacity = MANIFEST_DOMAIN
468 .len()
469 .saturating_add(8)
470 .saturating_add(entries_len)
471 .saturating_add(name_table.len());
472 let mut out = Vec::with_capacity(capacity);
473 out.extend_from_slice(MANIFEST_DOMAIN);
474 out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
475 for entry in entries {
476 out.extend_from_slice(entry.as_bytes());
477 }
478 out.extend_from_slice(name_table);
479 out
480}
481
482pub const MANIFEST_SIGNATURE_DOMAIN: &[u8] = b"AION_V2_MANIFEST_SIG_V1\0";
490
491#[must_use]
495pub fn canonical_manifest_signature_message(
496 manifest: &ArtifactManifest,
497 signer: AuthorId,
498) -> Vec<u8> {
499 let capacity = MANIFEST_SIGNATURE_DOMAIN
500 .len()
501 .saturating_add(32)
502 .saturating_add(8);
503 let mut msg = Vec::with_capacity(capacity);
504 msg.extend_from_slice(MANIFEST_SIGNATURE_DOMAIN);
505 msg.extend_from_slice(manifest.manifest_id());
506 msg.extend_from_slice(&signer.as_u64().to_le_bytes());
507 msg
508}
509
510#[must_use]
514pub fn sign_manifest(
515 manifest: &ArtifactManifest,
516 signer: AuthorId,
517 signing_key: &SigningKey,
518) -> SignatureEntry {
519 let message = canonical_manifest_signature_message(manifest, signer);
520 let signature = signing_key.sign(&message);
521 let public_key = signing_key.verifying_key().to_bytes();
522 SignatureEntry::new(signer, public_key, signature)
523}
524
525pub fn verify_manifest_signature(
544 manifest: &ArtifactManifest,
545 signature: &SignatureEntry,
546 registry: &crate::key_registry::KeyRegistry,
547 at_version: u64,
548) -> Result<()> {
549 let signer = AuthorId::new(signature.author_id);
550 let epoch = registry.active_epoch_at(signer, at_version).ok_or(
551 crate::AionError::SignatureVerificationFailed {
552 version: at_version,
553 author: signer,
554 },
555 )?;
556 if signature.public_key != epoch.public_key {
557 return Err(crate::AionError::SignatureVerificationFailed {
558 version: at_version,
559 author: signer,
560 });
561 }
562 let message = canonical_manifest_signature_message(manifest, signer);
563 let verifying_key = VerifyingKey::from_bytes(&signature.public_key)?;
564 verifying_key.verify(&message, &signature.signature)
565}
566
567#[cfg(test)]
568#[allow(clippy::unwrap_used)]
569#[allow(deprecated)] mod tests {
571 use super::*;
572
573 #[test]
574 fn should_build_and_verify_single_artifact() {
575 let bytes = b"payload bytes";
576 let mut b = ArtifactManifestBuilder::new();
577 let _h = b.add("payload.bin", bytes);
578 let m = b.build();
579 assert_eq!(m.entries().len(), 1);
580 assert!(m.verify_artifact("payload.bin", bytes).is_ok());
581 }
582
583 #[test]
584 fn should_reject_size_mismatch() {
585 let mut b = ArtifactManifestBuilder::new();
586 let _ = b.add("x", &[1, 2, 3]);
587 let m = b.build();
588 assert!(m.verify_artifact("x", &[1, 2, 3, 4]).is_err());
589 }
590
591 #[test]
592 fn should_reject_hash_mismatch() {
593 let mut b = ArtifactManifestBuilder::new();
594 let _ = b.add("x", &[1, 2, 3]);
595 let m = b.build();
596 assert!(m.verify_artifact("x", &[3, 2, 1]).is_err());
597 }
598
599 #[test]
600 fn should_reject_unknown_name() {
601 let mut b = ArtifactManifestBuilder::new();
602 let _ = b.add("x", &[1, 2, 3]);
603 let m = b.build();
604 assert!(m.verify_artifact("y", &[1, 2, 3]).is_err());
605 }
606
607 #[test]
608 fn should_handle_empty_artifact() {
609 let mut b = ArtifactManifestBuilder::new();
610 let _ = b.add("empty", &[]);
611 let m = b.build();
612 assert!(m.verify_artifact("empty", &[]).is_ok());
613 }
614
615 use crate::key_registry::KeyRegistry;
616
617 fn reg_pinning(author: AuthorId, key: &SigningKey) -> KeyRegistry {
619 let mut reg = KeyRegistry::new();
620 let master = SigningKey::generate();
621 reg.register_author(author, master.verifying_key(), key.verifying_key(), 0)
622 .unwrap_or_else(|_| std::process::abort());
623 reg
624 }
625
626 #[test]
627 fn should_sign_and_verify_manifest() {
628 let mut b = ArtifactManifestBuilder::new();
629 let _ = b.add("a", b"alpha");
630 let _ = b.add("b", b"beta");
631 let m = b.build();
632 let signer = AuthorId::new(42);
633 let key = SigningKey::generate();
634 let sig = sign_manifest(&m, signer, &key);
635 let reg = reg_pinning(signer, &key);
636 assert!(verify_manifest_signature(&m, &sig, ®, 1).is_ok());
637 }
638
639 #[test]
640 fn should_reject_signature_for_different_manifest() {
641 let key = SigningKey::generate();
642 let signer = AuthorId::new(7);
643
644 let mut b1 = ArtifactManifestBuilder::new();
645 let _ = b1.add("a", b"alpha");
646 let m1 = b1.build();
647
648 let mut b2 = ArtifactManifestBuilder::new();
649 let _ = b2.add("a", b"alpha-different");
650 let m2 = b2.build();
651
652 let sig = sign_manifest(&m1, signer, &key);
653 let reg = reg_pinning(signer, &key);
654 assert!(verify_manifest_signature(&m2, &sig, ®, 1).is_err());
655 }
656
657 mod reserved_validation {
663 use super::*;
664
665 fn one_entry_canonical_bytes() -> Vec<u8> {
669 let mut b = ArtifactManifestBuilder::new();
670 let _ = b.add("a", &[1, 2, 3]);
671 b.build().canonical_bytes()
672 }
673
674 #[allow(clippy::arithmetic_side_effects)] const fn first_entry_offset() -> usize {
678 MANIFEST_DOMAIN.len() + 8
679 }
680
681 #[test]
682 #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
683 fn rejects_nonzero_reserved1() {
684 let mut bytes = one_entry_canonical_bytes();
685 let target = first_entry_offset() + 14;
687 bytes[target] = 0x01;
688 let result = ArtifactManifest::from_canonical_bytes(&bytes);
689 assert!(
690 result.is_err(),
691 "non-zero reserved1 must be rejected, got Ok"
692 );
693 }
694
695 #[test]
696 #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
697 fn rejects_nonzero_reserved2() {
698 let mut bytes = one_entry_canonical_bytes();
699 let target = first_entry_offset() + 100;
702 bytes[target] = 0x42;
703 let result = ArtifactManifest::from_canonical_bytes(&bytes);
704 assert!(
705 result.is_err(),
706 "non-zero reserved2 must be rejected, got Ok"
707 );
708 }
709
710 #[test]
711 fn well_formed_input_still_round_trips() {
712 let bytes = one_entry_canonical_bytes();
713 let manifest = ArtifactManifest::from_canonical_bytes(&bytes).unwrap();
714 assert_eq!(manifest.canonical_bytes(), bytes);
715 }
716 }
717
718 mod properties {
719 use super::*;
720 use hegel::generators as gs;
721
722 fn draw_artifacts(tc: &hegel::TestCase) -> Vec<(String, Vec<u8>)> {
725 let n = tc.draw(gs::integers::<usize>().min_value(1).max_value(6));
726 let mut out: Vec<(String, Vec<u8>)> = Vec::with_capacity(n);
727 let mut counter: u64 = 0;
728 while out.len() < n {
729 let bytes = tc.draw(gs::binary().max_size(512));
730 let name = format!("artifact_{counter}");
732 counter = counter.saturating_add(1);
733 out.push((name, bytes));
734 }
735 out
736 }
737
738 fn build_manifest(pairs: &[(String, Vec<u8>)]) -> ArtifactManifest {
739 let mut b = ArtifactManifestBuilder::new();
740 for (name, bytes) in pairs {
741 let _ = b.add(name, bytes);
742 }
743 b.build()
744 }
745
746 #[hegel::test]
747 fn prop_manifest_build_verify_roundtrip(tc: hegel::TestCase) {
748 let pairs = draw_artifacts(&tc);
749 let manifest = build_manifest(&pairs);
750 for (name, bytes) in &pairs {
751 manifest
752 .verify_artifact(name, bytes)
753 .unwrap_or_else(|_| std::process::abort());
754 }
755 }
756
757 #[hegel::test]
758 fn prop_manifest_byte_flip_rejects(tc: hegel::TestCase) {
759 let pairs = draw_artifacts(&tc);
760 let manifest = build_manifest(&pairs);
761 let candidate = pairs.iter().find(|(_, b)| !b.is_empty());
763 if let Some((name, bytes)) = candidate {
764 let mut tampered = bytes.clone();
765 let max_idx = tampered.len().saturating_sub(1);
766 let idx = tc.draw(gs::integers::<usize>().max_value(max_idx));
767 if let Some(b) = tampered.get_mut(idx) {
768 *b ^= 0x01;
769 }
770 assert!(manifest.verify_artifact(name, &tampered).is_err());
771 }
772 }
773
774 #[hegel::test]
775 fn prop_manifest_size_mismatch_rejects(tc: hegel::TestCase) {
776 let pairs = draw_artifacts(&tc);
777 let manifest = build_manifest(&pairs);
778 for (name, bytes) in &pairs {
779 let mut truncated = bytes.clone();
780 let extra = tc.draw(gs::integers::<u8>().min_value(1).max_value(16));
781 truncated.extend(std::iter::repeat(0u8).take(usize::from(extra)));
782 assert!(manifest.verify_artifact(name, &truncated).is_err());
783 }
784 }
785
786 #[hegel::test]
787 fn prop_manifest_sign_verify_roundtrip(tc: hegel::TestCase) {
788 let pairs = draw_artifacts(&tc);
789 let manifest = build_manifest(&pairs);
790 let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
791 let key = SigningKey::generate();
792 let sig = sign_manifest(&manifest, signer, &key);
793 let reg = reg_pinning(signer, &key);
794 assert!(verify_manifest_signature(&manifest, &sig, ®, 1).is_ok());
795 }
796
797 #[hegel::test]
798 fn prop_manifest_signature_rebinds_after_mutation(tc: hegel::TestCase) {
799 let pairs = draw_artifacts(&tc);
802 let m1 = build_manifest(&pairs);
803 let extra_bytes = tc.draw(gs::binary().min_size(1).max_size(32));
805 let mut b2 = ArtifactManifestBuilder::new();
806 for (name, bytes) in &pairs {
807 let _ = b2.add(name, bytes);
808 }
809 let _ = b2.add("__tamper__", &extra_bytes);
810 let m2 = b2.build();
811 let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
812 let key = SigningKey::generate();
813 let sig = sign_manifest(&m1, signer, &key);
814 let reg = reg_pinning(signer, &key);
815 assert!(verify_manifest_signature(&m2, &sig, ®, 1).is_err());
816 }
817
818 #[hegel::test]
819 fn prop_manifest_signature_rejects_wrong_signer(tc: hegel::TestCase) {
820 let pairs = draw_artifacts(&tc);
821 let m = build_manifest(&pairs);
822 let real_signer =
823 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX / 2)));
824 let fake_signer = AuthorId::new(real_signer.as_u64().saturating_add(1));
825 let key = SigningKey::generate();
826 let mut sig = sign_manifest(&m, real_signer, &key);
827 sig.author_id = fake_signer.as_u64();
828 let reg = reg_pinning(real_signer, &key);
830 assert!(verify_manifest_signature(&m, &sig, ®, 1).is_err());
831 }
832
833 #[hegel::test]
834 fn prop_manifest_signature_domain_is_separated(tc: hegel::TestCase) {
835 let pairs = draw_artifacts(&tc);
841 let m = build_manifest(&pairs);
842 let signer =
843 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX / 2)));
844 let key = SigningKey::generate();
845 let raw_signature = key.sign(m.manifest_id());
846 let entry = SignatureEntry::new(signer, key.verifying_key().to_bytes(), raw_signature);
847 let reg = reg_pinning(signer, &key);
848 assert!(verify_manifest_signature(&m, &entry, ®, 1).is_err());
849 }
850
851 #[hegel::test]
852 fn prop_manifest_registry_verify_accepts_active_epoch(tc: hegel::TestCase) {
853 use crate::key_registry::{sign_rotation_record, KeyRegistry};
854 let pairs = draw_artifacts(&tc);
855 let m = build_manifest(&pairs);
856 let signer =
857 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
858 let master = SigningKey::generate();
859 let op = SigningKey::generate();
860 let mut reg = KeyRegistry::new();
861 reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
862 .unwrap_or_else(|_| std::process::abort());
863 let sig = sign_manifest(&m, signer, &op);
864 let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
865 assert!(verify_manifest_signature(&m, &sig, ®, at).is_ok());
866 let _ = sign_rotation_record; }
868
869 #[hegel::test]
870 fn prop_manifest_registry_verify_rejects_rotated_out_key(tc: hegel::TestCase) {
871 use crate::key_registry::{sign_rotation_record, KeyRegistry};
872 let pairs = draw_artifacts(&tc);
873 let m = build_manifest(&pairs);
874 let signer =
875 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
876 let master = SigningKey::generate();
877 let op0 = SigningKey::generate();
878 let op1 = SigningKey::generate();
879 let mut reg = KeyRegistry::new();
880 reg.register_author(signer, master.verifying_key(), op0.verifying_key(), 0)
881 .unwrap_or_else(|_| std::process::abort());
882 let effective = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
883 let rotation = sign_rotation_record(
884 signer,
885 0,
886 1,
887 op1.verifying_key().to_bytes(),
888 effective,
889 &master,
890 );
891 reg.apply_rotation(&rotation)
892 .unwrap_or_else(|_| std::process::abort());
893 let sig = sign_manifest(&m, signer, &op0);
895 let v_after = effective.saturating_add(1);
896 assert!(verify_manifest_signature(&m, &sig, ®, v_after).is_err());
897 }
898
899 #[hegel::test]
900 fn prop_manifest_registry_verify_rejects_pubkey_substitution(tc: hegel::TestCase) {
901 use crate::key_registry::KeyRegistry;
902 let pairs = draw_artifacts(&tc);
903 let m = build_manifest(&pairs);
904 let signer =
905 AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
906 let master = SigningKey::generate();
907 let op = SigningKey::generate();
908 let mut reg = KeyRegistry::new();
909 reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
910 .unwrap_or_else(|_| std::process::abort());
911 let attacker = SigningKey::generate();
913 let sig = sign_manifest(&m, signer, &attacker);
914 let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
915 assert!(verify_manifest_signature(&m, &sig, ®, at).is_err());
919 }
920
921 #[hegel::test]
925 fn prop_canonical_round_trip_byte_identical(tc: hegel::TestCase) {
926 let pairs = draw_artifacts(&tc);
927 let m = build_manifest(&pairs);
928 let bytes = m.canonical_bytes();
929 let reparsed = ArtifactManifest::from_canonical_bytes(&bytes)
930 .unwrap_or_else(|_| std::process::abort());
931 if reparsed.canonical_bytes() != bytes {
932 std::process::abort();
933 }
934 if reparsed.manifest_id() != m.manifest_id() {
935 std::process::abort();
936 }
937 }
938 }
939}