1use std::{
10 collections::{BTreeMap, BTreeSet},
11 io::{Cursor, Read, Seek, Write},
12};
13
14use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
15use rill_runtime_protocol::{
16 ReleaseIndexPayload, SignedReleaseIndexWithGenerationV1, TrustMetadataV1,
17};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use sha2::{Digest, Sha256};
21use thiserror::Error;
22use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
23
24const MANIFEST_PATH: &str = "manifest.json";
25const CHECKSUMS_PATH: &str = "checksums.json";
26const SIGNATURE_PATH: &str = "META-INF/signature.ed25519";
27
28#[derive(Debug, Default, Clone)]
29pub struct TrustStore(pub BTreeMap<String, VerifyingKey>);
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase", deny_unknown_fields)]
33pub(crate) struct Checksums {
34 schema_version: u32,
35 files: BTreeMap<String, String>,
36}
37
38#[derive(Debug, Error)]
39#[non_exhaustive]
40pub enum ArchiveError {
41 #[error("zip error: {0}")]
42 Zip(#[from] zip::result::ZipError),
43 #[error("I/O error: {0}")]
44 Io(#[from] std::io::Error),
45 #[error("JSON error: {0}")]
46 Json(#[from] serde_json::Error),
47 #[error("unsafe package path {0}")]
48 UnsafePath(String),
49 #[error("forbidden package file {0}")]
50 Forbidden(String),
51 #[error("duplicate package file {0}")]
52 Duplicate(String),
53 #[error("package exceeded {0} limit")]
54 Limit(&'static str),
55 #[error("missing package file {0}")]
56 Missing(&'static str),
57 #[error("missing package file {0}")]
58 MissingOwned(String),
59 #[error("checksum coverage does not exactly match the payload")]
60 ChecksumCoverage,
61 #[error("checksum mismatch for {0}")]
62 Digest(String),
63 #[error("unknown publisher key")]
64 UnknownKey,
65 #[error("signature verification failed")]
66 Signature,
67}
68
69#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum ReleaseIndexError {
72 #[error("JSON error: {0}")]
73 Json(#[from] serde_json::Error),
74 #[error("invalid release index: {0}")]
75 Manifest(String),
76 #[error("unknown release-index publisher key")]
77 UnknownKey,
78 #[error("release-index signature verification failed")]
79 Signature,
80 #[error("canonical JSON error: {0}")]
81 Canonical(ArchiveError),
82 #[error("invalid trust metadata: {0}")]
83 TrustMetadata(String),
84 #[error("trust metadata generation is older than the consumer metadata floor")]
85 MetadataDowngrade,
86 #[error("trust metadata content conflicts with the consumer metadata floor")]
87 MetadataConflict,
88 #[error("release index generation is older than the consumer rollback floor")]
89 Downgrade,
90}
91
92#[derive(Debug, Clone, Copy)]
94pub(crate) struct ArchiveLimits {
95 pub max_files: usize,
96 pub max_file_bytes: u64,
97 pub max_total_bytes: u64,
98 pub max_compressed_total_bytes: u64,
99 pub max_compression_ratio: u64,
100}
101
102pub(crate) struct PackPaths {
104 pub manifest: &'static str,
105 pub checksums: &'static str,
106 pub signature: &'static str,
107}
108
109pub(crate) const DEFAULT_PATHS: PackPaths = PackPaths {
110 manifest: MANIFEST_PATH,
111 checksums: CHECKSUMS_PATH,
112 signature: SIGNATURE_PATH,
113};
114
115pub fn canonical_json(bytes: &[u8]) -> Result<Vec<u8>, ArchiveError> {
116 fn canonical(value: Value) -> Value {
117 match value {
118 Value::Object(map) => {
119 let sorted: BTreeMap<String, Value> = map
122 .into_iter()
123 .map(|(key, value)| (key, canonical(value)))
124 .collect();
125 Value::Object(sorted.into_iter().collect())
126 }
127 Value::Array(items) => Value::Array(items.into_iter().map(canonical).collect()),
128 other => other,
129 }
130 }
131 let value: Value = serde_json::from_slice(bytes)?;
132 Ok(serde_json::to_vec(&canonical(value))?)
133}
134
135pub fn sign_release_index(
136 payload: ReleaseIndexPayload,
137 signing_key: &SigningKey,
138) -> Result<rill_runtime_protocol::SignedReleaseIndex, ReleaseIndexError> {
139 validate_release_payload(&payload)?;
140 let serialized = serde_json::to_vec(&payload)?;
141 let canonical = canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)?;
142 let signature = hex::encode(signing_key.sign(&canonical).to_bytes());
143 Ok(rill_runtime_protocol::SignedReleaseIndex { payload, signature })
144}
145
146pub fn verify_release_index(
147 index: &rill_runtime_protocol::SignedReleaseIndex,
148 trust: &TrustStore,
149) -> Result<(), ReleaseIndexError> {
150 validate_release_payload(&index.payload)?;
151 let signature_bytes =
152 hex::decode(&index.signature).map_err(|_| ReleaseIndexError::Signature)?;
153 let signature =
154 Signature::from_slice(&signature_bytes).map_err(|_| ReleaseIndexError::Signature)?;
155 let key = trust
156 .0
157 .get(&index.payload.publisher_key_id)
158 .ok_or(ReleaseIndexError::UnknownKey)?;
159 let serialized = serde_json::to_vec(&index.payload)?;
160 let canonical = canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)?;
161 key.verify(&canonical, &signature)
162 .map_err(|_| ReleaseIndexError::Signature)
163}
164
165pub fn sign_release_index_with_generation(
170 payload: ReleaseIndexPayload,
171 release_generation: u64,
172 signing_key: &SigningKey,
173) -> Result<SignedReleaseIndexWithGenerationV1, ReleaseIndexError> {
174 let index = sign_release_index(payload, signing_key)?;
175 let mut envelope = SignedReleaseIndexWithGenerationV1 {
176 schema_version: rill_runtime_protocol::RELEASE_INDEX_LIFECYCLE_SCHEMA_VERSION,
177 release_generation,
178 index,
179 lifecycle_signature: String::new(),
180 };
181 let canonical = lifecycle_signing_bytes(&envelope)?;
182 envelope.lifecycle_signature = hex::encode(signing_key.sign(&canonical).to_bytes());
183 Ok(envelope)
184}
185
186pub fn verify_release_index_with_trust_metadata(
193 envelope: &SignedReleaseIndexWithGenerationV1,
194 metadata: &TrustMetadataV1,
195 floor: &rill_runtime_protocol::TrustVerificationFloorV1,
196 now_unix_ms: u64,
197) -> Result<(), ReleaseIndexError> {
198 envelope
199 .validate_shape()
200 .map_err(|error| ReleaseIndexError::TrustMetadata(error.into()))?;
201 metadata
202 .validate_shape()
203 .map_err(|error| ReleaseIndexError::TrustMetadata(error.into()))?;
204 if metadata.metadata_generation < floor.minimum_metadata_generation {
205 return Err(ReleaseIndexError::MetadataDowngrade);
206 }
207 if metadata.metadata_generation == floor.minimum_metadata_generation {
208 let expected = floor
209 .metadata_digest
210 .as_deref()
211 .ok_or(ReleaseIndexError::MetadataConflict)?;
212 if !is_sha256_hex(expected) || trust_metadata_digest(metadata)? != expected {
213 return Err(ReleaseIndexError::MetadataConflict);
214 }
215 }
216 if envelope.release_generation < floor.minimum_release_generation
217 || envelope.release_generation < metadata.minimum_release_generation
218 {
219 return Err(ReleaseIndexError::Downgrade);
220 }
221 let active = metadata
222 .active_keys_at(now_unix_ms)
223 .map_err(|error| ReleaseIndexError::TrustMetadata(error.into()))?;
224 let mut keys = BTreeMap::new();
225 for key in active {
226 let bytes = hex::decode(&key.public_key_hex)
227 .map_err(|_| ReleaseIndexError::TrustMetadata("invalid trust public key".into()))?;
228 let bytes: [u8; 32] = bytes
229 .try_into()
230 .map_err(|_| ReleaseIndexError::TrustMetadata("invalid trust public key".into()))?;
231 let public_key = VerifyingKey::from_bytes(&bytes)
232 .map_err(|_| ReleaseIndexError::TrustMetadata("invalid trust public key".into()))?;
233 keys.insert(key.key_id.clone(), public_key);
234 }
235 let publisher_key = keys
236 .get(&envelope.index.payload.publisher_key_id)
237 .ok_or(ReleaseIndexError::UnknownKey)?;
238 let lifecycle_signature = hex::decode(&envelope.lifecycle_signature)
239 .map_err(|_| ReleaseIndexError::TrustMetadata("invalid lifecycle signature".into()))?;
240 let lifecycle_signature = Signature::from_slice(&lifecycle_signature)
241 .map_err(|_| ReleaseIndexError::TrustMetadata("invalid lifecycle signature".into()))?;
242 let canonical = lifecycle_signing_bytes(envelope)?;
243 publisher_key
244 .verify(&canonical, &lifecycle_signature)
245 .map_err(|_| ReleaseIndexError::Signature)?;
246 verify_release_index(&envelope.index, &TrustStore(keys))
247}
248
249pub fn trust_metadata_digest(metadata: &TrustMetadataV1) -> Result<String, ReleaseIndexError> {
252 metadata
253 .validate_shape()
254 .map_err(|error| ReleaseIndexError::TrustMetadata(error.into()))?;
255 let serialized = serde_json::to_vec(metadata)?;
256 let canonical = canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)?;
257 Ok(hex::encode(Sha256::digest(canonical)))
258}
259
260fn is_sha256_hex(value: &str) -> bool {
261 value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
262}
263
264#[derive(Serialize)]
265#[serde(rename_all = "camelCase")]
266struct LifecycleSigningView<'a> {
267 schema_version: u32,
268 release_generation: u64,
269 index: &'a rill_runtime_protocol::SignedReleaseIndex,
270}
271
272fn lifecycle_signing_bytes(
273 envelope: &SignedReleaseIndexWithGenerationV1,
274) -> Result<Vec<u8>, ReleaseIndexError> {
275 let view = LifecycleSigningView {
276 schema_version: envelope.schema_version,
277 release_generation: envelope.release_generation,
278 index: &envelope.index,
279 };
280 let serialized = serde_json::to_vec(&view)?;
281 canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)
282}
283
284fn validate_release_payload(payload: &ReleaseIndexPayload) -> Result<(), ReleaseIndexError> {
285 payload
286 .validate_shape()
287 .map_err(|message| ReleaseIndexError::Manifest(message.into()))?;
288 let mut identities = BTreeSet::new();
289 for artifact in &payload.artifacts {
290 semver::Version::parse(&artifact.version).map_err(|error| {
291 ReleaseIndexError::Manifest(format!("invalid artifact version: {error}"))
292 })?;
293 let identity = (
294 artifact.kind.clone(),
295 artifact.id.clone(),
296 artifact.target_os.clone(),
297 artifact.target_arch.clone(),
298 artifact.handler_api_version,
299 );
300 if !identities.insert(identity) {
301 return Err(ReleaseIndexError::Manifest(
302 "duplicate release artifact identity".into(),
303 ));
304 }
305 }
306 Ok(())
307}
308
309pub(crate) fn read_archive<R: Read + Seek>(
312 reader: R,
313 allowed: &[&str],
314 limits: ArchiveLimits,
315) -> Result<BTreeMap<String, Vec<u8>>, ArchiveError> {
316 let mut archive = ZipArchive::new(reader)?;
317 if archive.len() > limits.max_files {
318 return Err(ArchiveError::Limit("file count"));
319 }
320 let mut total = 0u64;
321 let mut compressed_total = 0u64;
322 let mut files = BTreeMap::new();
323 for index in 0..archive.len() {
324 let mut entry = archive.by_index(index)?;
325 if entry.is_dir() {
326 continue;
327 }
328 let name = entry.name().to_string();
329 validate_path(&name)?;
330 if !allowed.iter().any(|allowed| *allowed == name) {
331 return Err(ArchiveError::Forbidden(name));
332 }
333 if entry.size() > limits.max_file_bytes {
334 return Err(ArchiveError::Limit("file size"));
335 }
336 let compressed = entry.compressed_size();
337 if compressed > 0 {
345 let cap = compressed
346 .checked_mul(limits.max_compression_ratio)
347 .ok_or(ArchiveError::Limit("compression ratio"))?;
348 if entry.size() > cap {
349 return Err(ArchiveError::Limit("compression ratio"));
350 }
351 }
352 total = total
353 .checked_add(entry.size())
354 .ok_or(ArchiveError::Limit("total size"))?;
355 if total > limits.max_total_bytes {
356 return Err(ArchiveError::Limit("total size"));
357 }
358 compressed_total = compressed_total
359 .checked_add(compressed)
360 .ok_or(ArchiveError::Limit("compressed total size"))?;
361 if compressed_total > limits.max_compressed_total_bytes {
362 return Err(ArchiveError::Limit("compressed total size"));
363 }
364 let mut bytes = Vec::with_capacity(entry.size() as usize);
365 entry.read_to_end(&mut bytes)?;
366 if files.insert(name.clone(), bytes).is_some() {
367 return Err(ArchiveError::Duplicate(name));
368 }
369 }
370 Ok(files)
371}
372
373pub(crate) fn verify_checksums_and_signature(
378 files: &BTreeMap<String, Vec<u8>>,
379 paths: &PackPaths,
380 checksum_payload_names: &[&str],
381 publisher_key_id: &str,
382 trust: &TrustStore,
383) -> Result<(), ArchiveError> {
384 let checksum_bytes = files
385 .get(paths.checksums)
386 .ok_or(ArchiveError::Missing(paths.checksums))?;
387 let checksums: Checksums = serde_json::from_slice(checksum_bytes)?;
388 if checksums.schema_version != 1 {
389 return Err(ArchiveError::Missing("checksum schema version"));
390 }
391 let mut expected_names: Vec<String> = checksum_payload_names
392 .iter()
393 .map(|s| s.to_string())
394 .collect();
395 expected_names.sort();
396 let actual_names: Vec<String> = checksums.files.keys().cloned().collect();
397 if actual_names != expected_names {
398 return Err(ArchiveError::ChecksumCoverage);
399 }
400 for (name, expected) in &checksums.files {
401 let bytes = files
402 .get(name)
403 .ok_or_else(|| ArchiveError::MissingOwned(name.clone()))?;
404 let actual = hex::encode(Sha256::digest(bytes));
405 if &actual != expected {
406 return Err(ArchiveError::Digest(name.clone()));
407 }
408 }
409 let raw_signature = files
410 .get(paths.signature)
411 .ok_or(ArchiveError::Missing(paths.signature))?;
412 let signature = Signature::from_slice(raw_signature).map_err(|_| ArchiveError::Signature)?;
413 let key = trust
414 .0
415 .get(publisher_key_id)
416 .ok_or(ArchiveError::UnknownKey)?;
417 let manifest_bytes = files
418 .get(paths.manifest)
419 .ok_or(ArchiveError::Missing(paths.manifest))?;
420 let mut message = canonical_json(manifest_bytes)?;
421 message.push(b'\n');
422 message.extend(canonical_json(checksum_bytes)?);
423 key.verify(&message, &signature)
424 .map_err(|_| ArchiveError::Signature)
425}
426
427pub(crate) fn build_signed_archive(
430 manifest_bytes: &[u8],
431 payload_name: &str,
432 payload_bytes: &[u8],
433 signing_key: &SigningKey,
434) -> Result<Vec<u8>, ArchiveError> {
435 let checksums = Checksums {
436 schema_version: 1,
437 files: BTreeMap::from([
438 (
439 MANIFEST_PATH.into(),
440 hex::encode(Sha256::digest(manifest_bytes)),
441 ),
442 (
443 payload_name.into(),
444 hex::encode(Sha256::digest(payload_bytes)),
445 ),
446 ]),
447 };
448 let checksum_bytes = serde_json::to_vec_pretty(&checksums)?;
449 let mut message = canonical_json(manifest_bytes)?;
450 message.push(b'\n');
451 message.extend(canonical_json(&checksum_bytes)?);
452 let signature = signing_key.sign(&message).to_bytes();
453
454 let mut output = Cursor::new(Vec::new());
455 {
456 let mut archive = ZipWriter::new(&mut output);
457 let options = SimpleFileOptions::default()
458 .compression_method(zip::CompressionMethod::Deflated)
459 .unix_permissions(0o644);
460 for (name, bytes) in [
461 (MANIFEST_PATH, manifest_bytes),
462 (payload_name, payload_bytes),
463 (CHECKSUMS_PATH, checksum_bytes.as_slice()),
464 (SIGNATURE_PATH, signature.as_slice()),
465 ] {
466 archive.start_file(name, options)?;
467 archive.write_all(bytes)?;
468 }
469 archive.finish()?;
470 }
471 Ok(output.into_inner())
472}
473
474fn validate_path(name: &str) -> Result<(), ArchiveError> {
475 if name.starts_with('/')
476 || name.contains('\\')
477 || name
478 .split('/')
479 .any(|part| part.is_empty() || part == "." || part == "..")
480 {
481 return Err(ArchiveError::UnsafePath(name.into()));
482 }
483 Ok(())
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 fn crc32(data: &[u8]) -> u32 {
493 let mut crc: u32 = 0xFFFFFFFF;
494 for &byte in data {
495 crc ^= byte as u32;
496 for _ in 0..8 {
497 crc = (crc >> 1) ^ (0xEDB88320 & (0u32.wrapping_sub(crc & 1)));
498 }
499 }
500 !crc
501 }
502
503 fn build_zip_with_sizes(
512 name: &str,
513 data: &[u8],
514 uncompressed_size: u32,
515 compressed_size: u32,
516 ) -> Vec<u8> {
517 let crc = crc32(data);
518 let mut buf = Vec::new();
519 let local_offset = 0u32;
520
521 buf.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
523 buf.extend_from_slice(&20u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&crc.to_le_bytes());
529 buf.extend_from_slice(&compressed_size.to_le_bytes());
530 buf.extend_from_slice(&uncompressed_size.to_le_bytes());
531 buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
532 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(name.as_bytes());
534 buf.extend_from_slice(data);
535
536 let cd_start = buf.len() as u32;
537
538 buf.extend_from_slice(&[0x50, 0x4b, 0x01, 0x02]);
540 buf.extend_from_slice(&20u16.to_le_bytes()); buf.extend_from_slice(&20u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&crc.to_le_bytes());
547 buf.extend_from_slice(&compressed_size.to_le_bytes());
548 buf.extend_from_slice(&uncompressed_size.to_le_bytes());
549 buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
550 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&local_offset.to_le_bytes());
556 buf.extend_from_slice(name.as_bytes());
557
558 let cd_size = buf.len() as u32 - cd_start;
559
560 buf.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
562 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&cd_size.to_le_bytes());
567 buf.extend_from_slice(&cd_start.to_le_bytes());
568 buf.extend_from_slice(&0u16.to_le_bytes()); buf
571 }
572
573 fn limits_with_ratio(ratio: u64) -> ArchiveLimits {
574 ArchiveLimits {
575 max_files: 10,
576 max_file_bytes: 1024 * 1024,
577 max_total_bytes: 1024 * 1024,
578 max_compressed_total_bytes: 1024 * 1024,
579 max_compression_ratio: ratio,
580 }
581 }
582
583 #[test]
584 fn compression_ratio_accepts_exact_boundary() {
585 let data = b"0123456789"; let zip = build_zip_with_sizes("payload.bin", data, 1000, 10);
596 let files = read_archive(
597 std::io::Cursor::new(&zip),
598 &["payload.bin"],
599 limits_with_ratio(100),
600 )
601 .expect("exact boundary must be accepted");
602 assert_eq!(files.get("payload.bin").map(Vec::as_slice), Some(&data[..]));
603 }
604
605 #[test]
606 fn compression_ratio_rejects_one_byte_over_boundary() {
607 let data = b"0123456789"; let zip = build_zip_with_sizes("payload.bin", data, 1001, 10);
613 let result = read_archive(
614 std::io::Cursor::new(&zip),
615 &["payload.bin"],
616 limits_with_ratio(100),
617 );
618 assert!(
619 matches!(result, Err(ArchiveError::Limit("compression ratio"))),
620 "expected compression-ratio rejection, got: {result:?}"
621 );
622 }
623
624 #[test]
625 fn compression_ratio_skips_zero_compressed_size() {
626 let zip = build_zip_with_sizes("payload.bin", b"", 0, 0);
629 let files = read_archive(
630 std::io::Cursor::new(&zip),
631 &["payload.bin"],
632 limits_with_ratio(100),
633 )
634 .expect("zero-size entry must be accepted");
635 assert!(files.get("payload.bin").map(Vec::is_empty).unwrap_or(false));
636 }
637
638 #[test]
639 fn compression_ratio_rejects_overflowing_product() {
640 let data = b"xy"; let zip = build_zip_with_sizes("payload.bin", data, 2, 2);
650 let limits = ArchiveLimits {
651 max_files: 10,
652 max_file_bytes: 1024 * 1024,
653 max_total_bytes: 1024 * 1024,
654 max_compressed_total_bytes: 1024 * 1024,
655 max_compression_ratio: u64::MAX,
656 };
657 let result = read_archive(std::io::Cursor::new(&zip), &["payload.bin"], limits);
658 assert!(
659 matches!(result, Err(ArchiveError::Limit("compression ratio"))),
660 "expected overflow rejection, got: {result:?}"
661 );
662 }
663
664 fn lifecycle_payload() -> ReleaseIndexPayload {
665 use rill_runtime_protocol::{
666 RELEASE_INDEX_SCHEMA_VERSION, RUNTIME_API_VERSION, RUNTIME_ARTIFACT_ID,
667 ReleaseArtifact, ReleaseArtifactKind,
668 };
669 ReleaseIndexPayload {
670 schema_version: RELEASE_INDEX_SCHEMA_VERSION,
671 channel: "stable".into(),
672 generated_at: "2026-08-22T00:00:00Z".into(),
673 publisher_key_id: "current".into(),
674 artifacts: vec![ReleaseArtifact {
675 kind: ReleaseArtifactKind::Runtime,
676 id: RUNTIME_ARTIFACT_ID.into(),
677 version: "1.3.0".into(),
678 runtime_api_version: RUNTIME_API_VERSION,
679 target_os: Some("linux".into()),
680 target_arch: Some("x86_64".into()),
681 target_libc: Some("gnu".into()),
682 handler_api_version: None,
683 min_runtime_version: None,
684 pm_adapter_protocol_version: None,
685 url: "https://example.invalid/runtime".into(),
686 sha256: "ab".repeat(32),
687 size: 1024,
688 }],
689 }
690 }
691
692 fn metadata(signing: &SigningKey) -> rill_runtime_protocol::TrustMetadataV1 {
693 use rill_runtime_protocol::{
694 TRUST_METADATA_SCHEMA_VERSION, TrustKeyMetadataV1, TrustKeyRole,
695 };
696 rill_runtime_protocol::TrustMetadataV1 {
697 schema_version: TRUST_METADATA_SCHEMA_VERSION,
698 metadata_generation: 1,
699 minimum_release_generation: 1,
700 keys: vec![TrustKeyMetadataV1 {
701 key_id: "current".into(),
702 public_key_hex: hex::encode(signing.verifying_key().to_bytes()),
703 role: TrustKeyRole::Current,
704 not_before_unix_ms: 0,
705 not_after_unix_ms: None,
706 revoked_at_unix_ms: None,
707 emergency_revoked: false,
708 }],
709 }
710 }
711
712 fn floor(
713 metadata: &rill_runtime_protocol::TrustMetadataV1,
714 ) -> rill_runtime_protocol::TrustVerificationFloorV1 {
715 rill_runtime_protocol::TrustVerificationFloorV1 {
716 minimum_metadata_generation: metadata.metadata_generation,
717 minimum_release_generation: metadata.minimum_release_generation,
718 metadata_digest: Some(trust_metadata_digest(metadata).unwrap()),
719 }
720 }
721
722 #[test]
723 fn trust_metadata_current_key_passes() {
724 let signing = SigningKey::from_bytes(&[71; 32]);
725 let envelope =
726 sign_release_index_with_generation(lifecycle_payload(), 1, &signing).unwrap();
727 let trust = metadata(&signing);
728 verify_release_index_with_trust_metadata(&envelope, &trust, &floor(&trust), 100).unwrap();
729 }
730
731 #[test]
732 fn trust_metadata_overlap_next_key_passes() {
733 use rill_runtime_protocol::{TrustKeyMetadataV1, TrustKeyRole};
734 let current = SigningKey::from_bytes(&[72; 32]);
735 let next = SigningKey::from_bytes(&[73; 32]);
736 let mut trust = metadata(¤t);
737 trust.keys.push(TrustKeyMetadataV1 {
738 key_id: "next".into(),
739 public_key_hex: hex::encode(next.verifying_key().to_bytes()),
740 role: TrustKeyRole::Next,
741 not_before_unix_ms: 50,
742 not_after_unix_ms: None,
743 revoked_at_unix_ms: None,
744 emergency_revoked: false,
745 });
746 let mut payload = lifecycle_payload();
747 payload.publisher_key_id = "next".into();
748 let envelope = sign_release_index_with_generation(payload, 2, &next).unwrap();
749 verify_release_index_with_trust_metadata(&envelope, &trust, &floor(&trust), 100).unwrap();
750 }
751
752 #[test]
753 fn trust_metadata_rejects_future_expired_revoked_and_emergency_keys() {
754 let signing = SigningKey::from_bytes(&[74; 32]);
755 let envelope =
756 sign_release_index_with_generation(lifecycle_payload(), 1, &signing).unwrap();
757 let mut future = metadata(&signing);
758 future.keys[0].not_before_unix_ms = 101;
759 let future_floor = floor(&future);
760 assert!(
761 verify_release_index_with_trust_metadata(&envelope, &future, &future_floor, 100)
762 .is_err()
763 );
764 let mut expired = metadata(&signing);
765 expired.keys[0].not_after_unix_ms = Some(100);
766 let expired_floor = floor(&expired);
767 assert!(
768 verify_release_index_with_trust_metadata(&envelope, &expired, &expired_floor, 100)
769 .is_err()
770 );
771 let mut revoked = metadata(&signing);
772 revoked.keys[0].revoked_at_unix_ms = Some(100);
773 let revoked_floor = floor(&revoked);
774 assert!(
775 verify_release_index_with_trust_metadata(&envelope, &revoked, &revoked_floor, 100)
776 .is_err()
777 );
778 let mut emergency = metadata(&signing);
779 emergency.keys[0].emergency_revoked = true;
780 let emergency_floor = floor(&emergency);
781 assert!(
782 verify_release_index_with_trust_metadata(&envelope, &emergency, &emergency_floor, 100)
783 .is_err()
784 );
785 }
786
787 #[test]
788 fn trust_metadata_rejects_unknown_damaged_and_downgraded() {
789 use rill_runtime_protocol::TrustKeyMetadataV1;
790 let signing = SigningKey::from_bytes(&[75; 32]);
791 let envelope =
792 sign_release_index_with_generation(lifecycle_payload(), 1, &signing).unwrap();
793 let other = SigningKey::from_bytes(&[76; 32]);
794 let mut unknown = metadata(&other);
795 unknown.keys[0].key_id = "other".into();
796 assert!(matches!(
797 verify_release_index_with_trust_metadata(&envelope, &unknown, &floor(&unknown), 100),
798 Err(ReleaseIndexError::UnknownKey)
799 ));
800 let accepted = metadata(&signing);
801 let accepted_floor = floor(&accepted);
802 let mut damaged = metadata(&signing);
803 damaged.keys.push(TrustKeyMetadataV1 {
804 key_id: "current".into(),
805 public_key_hex: hex::encode(signing.verifying_key().to_bytes()),
806 role: rill_runtime_protocol::TrustKeyRole::Next,
807 not_before_unix_ms: 0,
808 not_after_unix_ms: None,
809 revoked_at_unix_ms: None,
810 emergency_revoked: false,
811 });
812 assert!(matches!(
813 verify_release_index_with_trust_metadata(&envelope, &damaged, &accepted_floor, 100),
814 Err(ReleaseIndexError::TrustMetadata(_))
815 ));
816 let mut downgrade = metadata(&signing);
817 downgrade.minimum_release_generation = 2;
818 assert!(matches!(
819 verify_release_index_with_trust_metadata(
820 &envelope,
821 &downgrade,
822 &floor(&downgrade),
823 100
824 ),
825 Err(ReleaseIndexError::Downgrade)
826 ));
827 }
828
829 #[test]
830 fn trust_metadata_rejects_generation_tampering() {
831 let signing = SigningKey::from_bytes(&[77; 32]);
832 let mut envelope =
833 sign_release_index_with_generation(lifecycle_payload(), 1, &signing).unwrap();
834 envelope.release_generation = 2;
835 let trust = metadata(&signing);
836 assert!(matches!(
837 verify_release_index_with_trust_metadata(&envelope, &trust, &floor(&trust), 100),
838 Err(ReleaseIndexError::Signature)
839 ));
840 }
841
842 #[test]
843 fn trust_metadata_rejects_old_authenticated_document_after_revocation() {
844 use rill_runtime_protocol::{TrustKeyMetadataV1, TrustKeyRole};
845 let current = SigningKey::from_bytes(&[78; 32]);
846 let revoked = SigningKey::from_bytes(&[79; 32]);
847 let mut accepted = metadata(¤t);
848 accepted.metadata_generation = 10;
849 accepted.keys.push(TrustKeyMetadataV1 {
850 key_id: "revoked".into(),
851 public_key_hex: hex::encode(revoked.verifying_key().to_bytes()),
852 role: TrustKeyRole::Next,
853 not_before_unix_ms: 0,
854 not_after_unix_ms: None,
855 revoked_at_unix_ms: Some(100),
856 emergency_revoked: false,
857 });
858 let consumer_floor = floor(&accepted);
859
860 let mut old = accepted.clone();
861 old.metadata_generation = 9;
862 old.keys[1].revoked_at_unix_ms = None;
863 let mut payload = lifecycle_payload();
864 payload.publisher_key_id = "revoked".into();
865 let envelope = sign_release_index_with_generation(payload, 2, &revoked).unwrap();
866
867 assert!(matches!(
868 verify_release_index_with_trust_metadata(&envelope, &old, &consumer_floor, 100),
869 Err(ReleaseIndexError::MetadataDowngrade)
870 ));
871 }
872
873 #[test]
874 fn trust_metadata_rejects_same_generation_content_conflict() {
875 let signing = SigningKey::from_bytes(&[80; 32]);
876 let accepted = metadata(&signing);
877 let floor = floor(&accepted);
878 let mut conflicting = accepted.clone();
879 conflicting.keys[0].emergency_revoked = true;
880 let envelope =
881 sign_release_index_with_generation(lifecycle_payload(), 1, &signing).unwrap();
882
883 assert!(matches!(
884 verify_release_index_with_trust_metadata(&envelope, &conflicting, &floor, 100),
885 Err(ReleaseIndexError::MetadataConflict)
886 ));
887 }
888
889 #[test]
890 fn trust_metadata_accepts_future_generation_for_consumer_promotion() {
891 let signing = SigningKey::from_bytes(&[81; 32]);
892 let accepted = metadata(&signing);
893 let floor = floor(&accepted);
894 let mut future = accepted.clone();
895 future.metadata_generation = accepted.metadata_generation + 1;
896 let envelope =
897 sign_release_index_with_generation(lifecycle_payload(), 2, &signing).unwrap();
898
899 verify_release_index_with_trust_metadata(&envelope, &future, &floor, 100).unwrap();
900 }
901}