1use std::io::{Read, Seek};
4
5use ed25519_dalek::SigningKey;
6use rill_runtime_protocol::HandlerPackManifest;
7use serde::Serialize;
8use sha2::{Digest, Sha256};
9use thiserror::Error;
10
11use crate::archive::{
12 ArchiveError, ArchiveLimits, DEFAULT_PATHS, TrustStore, build_signed_archive, read_archive,
13 verify_checksums_and_signature,
14};
15
16const MODULE_PATH: &str = "handler.wasm";
17
18const HANDLER_PACK_LIMITS: ArchiveLimits = ArchiveLimits {
19 max_files: 8,
20 max_file_bytes: 4 * 1024 * 1024,
21 max_total_bytes: 16 * 1024 * 1024,
22 max_compressed_total_bytes: 8 * 1024 * 1024,
23 max_compression_ratio: 100,
24};
25
26const HANDLER_PACK_ALLOWED: &[&str] = &[
27 "manifest.json",
28 MODULE_PATH,
29 "checksums.json",
30 "META-INF/signature.ed25519",
31];
32
33#[derive(Debug, Clone)]
34pub struct LoadedHandlerPack {
35 pub manifest: HandlerPackManifest,
36 pub module: Vec<u8>,
37}
38
39#[derive(Debug, Clone, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct HandlerPackInspection {
42 pub id: String,
43 pub version: String,
44 pub publisher_key_id: String,
45 pub handler_api_version: u32,
46 pub min_runtime_version: String,
47 pub capabilities: Vec<String>,
48 pub module_sha256: String,
49 pub module_size: u64,
50 pub signature_verified: bool,
51}
52
53#[derive(Debug, Error)]
54#[non_exhaustive]
55pub enum HandlerPackError {
56 #[error(transparent)]
57 Archive(#[from] ArchiveError),
58 #[error(transparent)]
59 Json(#[from] serde_json::Error),
60 #[error("invalid handler manifest: {0}")]
61 Manifest(String),
62 #[error("runtime {actual} is older than handler requirement {minimum}")]
63 RuntimeTooOld { minimum: String, actual: String },
64 #[error("handler module SHA-256 mismatch")]
65 ModuleDigestMismatch,
66}
67
68pub fn load_handler_pack<R: Read + Seek>(
69 reader: R,
70 trust: &TrustStore,
71) -> Result<(LoadedHandlerPack, HandlerPackInspection), HandlerPackError> {
72 let files = read_archive(reader, HANDLER_PACK_ALLOWED, HANDLER_PACK_LIMITS)?;
73 let manifest_bytes = files
74 .get(DEFAULT_PATHS.manifest)
75 .ok_or(ArchiveError::Missing(DEFAULT_PATHS.manifest))?;
76 let manifest: HandlerPackManifest = serde_json::from_slice(manifest_bytes)?;
77 manifest
78 .validate_shape()
79 .map_err(|message| HandlerPackError::Manifest(message.into()))?;
80 semver::Version::parse(&manifest.version)
81 .map_err(|error| HandlerPackError::Manifest(format!("invalid handler version: {error}")))?;
82 let minimum = semver::Version::parse(&manifest.min_runtime_version)
83 .map_err(|error| HandlerPackError::Manifest(format!("invalid minimum runtime: {error}")))?;
84 let runtime = semver::Version::parse(env!("CARGO_PKG_VERSION"))
85 .map_err(|error| HandlerPackError::Manifest(format!("invalid runtime version: {error}")))?;
86 if runtime < minimum {
87 return Err(HandlerPackError::RuntimeTooOld {
88 minimum: minimum.to_string(),
89 actual: runtime.to_string(),
90 });
91 }
92 verify_checksums_and_signature(
93 &files,
94 &DEFAULT_PATHS,
95 &[DEFAULT_PATHS.manifest, MODULE_PATH],
96 &manifest.publisher_key_id,
97 trust,
98 )?;
99
100 let module = files
101 .get(MODULE_PATH)
102 .ok_or(ArchiveError::Missing(MODULE_PATH))?
103 .clone();
104
105 let actual_digest = hex::encode(Sha256::digest(&module));
107 if actual_digest != manifest.module_sha256 {
108 return Err(HandlerPackError::ModuleDigestMismatch);
109 }
110 if module.len() as u64 != manifest.module_size {
111 return Err(HandlerPackError::Manifest("module size mismatch".into()));
112 }
113
114 let inspection = HandlerPackInspection {
115 id: manifest.id.clone(),
116 version: manifest.version.clone(),
117 publisher_key_id: manifest.publisher_key_id.clone(),
118 handler_api_version: manifest.handler_api_version,
119 min_runtime_version: manifest.min_runtime_version.clone(),
120 capabilities: manifest.capabilities.clone(),
121 module_sha256: manifest.module_sha256.clone(),
122 module_size: manifest.module_size,
123 signature_verified: true,
124 };
125 Ok((LoadedHandlerPack { manifest, module }, inspection))
126}
127
128pub fn build_signed_handler_pack(
129 manifest: &HandlerPackManifest,
130 module: &[u8],
131 signing_key: &SigningKey,
132) -> Result<Vec<u8>, HandlerPackError> {
133 manifest
134 .validate_shape()
135 .map_err(|message| HandlerPackError::Manifest(message.into()))?;
136 let actual_digest = hex::encode(Sha256::digest(module));
138 if actual_digest != manifest.module_sha256 {
139 return Err(HandlerPackError::ModuleDigestMismatch);
140 }
141 if module.len() as u64 != manifest.module_size {
142 return Err(HandlerPackError::Manifest("module size mismatch".into()));
143 }
144 let manifest_bytes = serde_json::to_vec_pretty(manifest)?;
145 let archive = build_signed_archive(&manifest_bytes, MODULE_PATH, module, signing_key)?;
146 Ok(archive)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use rill_runtime_protocol::{HANDLER_API_VERSION, HANDLER_PACKAGE_FORMAT_VERSION};
153 use std::collections::BTreeMap;
154
155 fn manifest(key_id: &str, module: &[u8]) -> HandlerPackManifest {
156 HandlerPackManifest {
157 format_version: HANDLER_PACKAGE_FORMAT_VERSION,
158 id: "org.example.handler".into(),
159 version: "1.0.0".into(),
160 handler_api_version: HANDLER_API_VERSION,
161 min_runtime_version: "0.7.0".into(),
162 publisher_key_id: key_id.into(),
163 capabilities: vec!["org.example.predict".into()],
164 module_sha256: hex::encode(Sha256::digest(module)),
165 module_size: module.len() as u64,
166 }
167 }
168
169 #[test]
170 fn handler_pack_roundtrip_and_tamper_rejection() {
171 let signing = SigningKey::from_bytes(&[11; 32]);
172 let key_id = "handler-test-key";
173 let module = b"\0asm\x01\x00\x00\x00test module bytes";
174 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
175 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
176 let (loaded, inspection) = load_handler_pack(std::io::Cursor::new(&bytes), &trust).unwrap();
177 assert_eq!(loaded.manifest.id, "org.example.handler");
178 assert_eq!(loaded.module, module);
179 assert!(inspection.signature_verified);
180
181 let wrong = SigningKey::from_bytes(&[12; 32]);
182 let wrong_trust = TrustStore(BTreeMap::from([(key_id.into(), wrong.verifying_key())]));
183 assert!(matches!(
184 load_handler_pack(std::io::Cursor::new(bytes), &wrong_trust),
185 Err(HandlerPackError::Archive(ArchiveError::Signature))
186 ));
187 }
188
189 #[test]
190 fn handler_pack_rejects_wrong_module_digest() {
191 let signing = SigningKey::from_bytes(&[13; 32]);
192 let key_id = "digest-test";
193 let real_module = b"real module";
194 let fake_module = b"fake module";
195 let mut bad_manifest = manifest(key_id, fake_module);
196 bad_manifest.module_sha256 = hex::encode(Sha256::digest(real_module));
198 bad_manifest.module_size = real_module.len() as u64;
199 assert!(matches!(
200 build_signed_handler_pack(&bad_manifest, fake_module, &signing),
201 Err(HandlerPackError::ModuleDigestMismatch)
202 ));
203 }
204
205 #[test]
206 fn handler_pack_rejects_model_trust_key() {
207 let handler_signing = SigningKey::from_bytes(&[14; 32]);
209 let model_signing = SigningKey::from_bytes(&[15; 32]);
210 let key_id = "shared-key-id";
211 let module = b"test module";
212 let bytes =
213 build_signed_handler_pack(&manifest(key_id, module), module, &handler_signing).unwrap();
214 let model_trust = TrustStore(BTreeMap::from([(
216 key_id.into(),
217 model_signing.verifying_key(),
218 )]));
219 assert!(matches!(
220 load_handler_pack(std::io::Cursor::new(&bytes), &model_trust),
221 Err(HandlerPackError::Archive(ArchiveError::Signature))
222 ));
223 }
224
225 use std::io::{Cursor, Write};
226 use zip::{ZipWriter, write::SimpleFileOptions};
227
228 fn build_malicious_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
229 let mut output = Cursor::new(Vec::new());
230 {
231 let mut zip = ZipWriter::new(&mut output);
232 let options = SimpleFileOptions::default();
233 for (name, data) in files {
234 zip.start_file(name, options).unwrap();
235 zip.write_all(data).unwrap();
236 }
237 zip.finish().unwrap();
238 }
239 output.into_inner()
240 }
241
242 #[test]
243 fn handler_pack_rejects_extra_file() {
244 let files: &[(&str, &[u8])] = &[
245 ("manifest.json", b"{}"),
246 ("handler.wasm", b"wasm"),
247 ("checksums.json", b"{}"),
248 ("META-INF/signature.ed25519", b"sig"),
249 ("evil.txt", b"evil"),
250 ];
251 let bytes = build_malicious_zip(files);
252 let trust = TrustStore(BTreeMap::new());
253 assert!(matches!(
254 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
255 Err(HandlerPackError::Archive(ArchiveError::Forbidden(_)))
256 ));
257 }
258
259 #[test]
260 fn handler_pack_rejects_path_traversal() {
261 let bytes = build_malicious_zip(&[("../escape.txt", b"escape")]);
262 let trust = TrustStore(BTreeMap::new());
263 assert!(matches!(
264 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
265 Err(HandlerPackError::Archive(ArchiveError::UnsafePath(_)))
266 ));
267 }
268
269 #[test]
270 fn handler_pack_rejects_absolute_path() {
271 let bytes = build_malicious_zip(&[("/etc/passwd", b"root")]);
272 let trust = TrustStore(BTreeMap::new());
273 assert!(matches!(
274 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
275 Err(HandlerPackError::Archive(ArchiveError::UnsafePath(_)))
276 ));
277 }
278
279 fn crc32(data: &[u8]) -> u32 {
280 let mut crc: u32 = 0xFFFFFFFF;
281 for &byte in data {
282 crc ^= byte as u32;
283 for _ in 0..8 {
284 crc = (crc >> 1) ^ (0xEDB88320 & (0u32.wrapping_sub(crc & 1)));
285 }
286 }
287 !crc
288 }
289
290 fn build_raw_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
293 let mut buf = Vec::new();
294 let mut offsets: Vec<u32> = Vec::new();
295 for (name, data) in files {
296 let crc = crc32(data);
297 offsets.push(buf.len() as u32);
298 buf.extend_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
300 buf.extend_from_slice(&20u16.to_le_bytes());
301 buf.extend_from_slice(&0u16.to_le_bytes());
302 buf.extend_from_slice(&0u16.to_le_bytes());
303 buf.extend_from_slice(&0u16.to_le_bytes());
304 buf.extend_from_slice(&0u16.to_le_bytes());
305 buf.extend_from_slice(&crc.to_le_bytes());
306 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
307 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
308 buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
309 buf.extend_from_slice(&0u16.to_le_bytes());
310 buf.extend_from_slice(name.as_bytes());
311 buf.extend_from_slice(data);
312 }
313 let cd_start = buf.len() as u32;
314 for (i, (name, data)) in files.iter().enumerate() {
315 let crc = crc32(data);
316 buf.extend_from_slice(&[0x50, 0x4b, 0x01, 0x02]);
318 buf.extend_from_slice(&20u16.to_le_bytes());
319 buf.extend_from_slice(&20u16.to_le_bytes());
320 buf.extend_from_slice(&0u16.to_le_bytes());
321 buf.extend_from_slice(&0u16.to_le_bytes());
322 buf.extend_from_slice(&0u16.to_le_bytes());
323 buf.extend_from_slice(&0u16.to_le_bytes());
324 buf.extend_from_slice(&crc.to_le_bytes());
325 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
326 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
327 buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
328 buf.extend_from_slice(&0u16.to_le_bytes());
329 buf.extend_from_slice(&0u16.to_le_bytes());
330 buf.extend_from_slice(&0u16.to_le_bytes());
331 buf.extend_from_slice(&0u16.to_le_bytes());
332 buf.extend_from_slice(&0u32.to_le_bytes());
333 buf.extend_from_slice(&offsets[i].to_le_bytes());
334 buf.extend_from_slice(name.as_bytes());
335 }
336 let cd_size = buf.len() as u32 - cd_start;
337 buf.extend_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
339 buf.extend_from_slice(&0u16.to_le_bytes());
340 buf.extend_from_slice(&0u16.to_le_bytes());
341 buf.extend_from_slice(&(files.len() as u16).to_le_bytes());
342 buf.extend_from_slice(&(files.len() as u16).to_le_bytes());
343 buf.extend_from_slice(&cd_size.to_le_bytes());
344 buf.extend_from_slice(&cd_start.to_le_bytes());
345 buf.extend_from_slice(&0u16.to_le_bytes());
346 buf
347 }
348
349 #[test]
350 fn handler_pack_rejects_duplicate_entry() {
351 let bytes = build_raw_zip(&[("manifest.json", b"first"), ("manifest.json", b"second")]);
355 let trust = TrustStore(BTreeMap::new());
356 assert!(load_handler_pack(std::io::Cursor::new(&bytes), &trust).is_err());
357 }
358
359 #[test]
360 fn handler_pack_rejects_invalid_semver() {
361 let signing = SigningKey::from_bytes(&[20; 32]);
365 let key_id = "semver-test";
366 let module = b"semver module";
367 let mut bad_manifest = manifest(key_id, module);
368 bad_manifest.version = "not-semver".into();
369 let bytes = build_signed_handler_pack(&bad_manifest, module, &signing).unwrap();
370 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
371 assert!(matches!(
372 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
373 Err(HandlerPackError::Manifest(_))
374 ));
375 }
376
377 #[test]
378 fn handler_pack_rejects_duplicate_capabilities() {
379 let module = b"dup cap module";
380 let mut bad_manifest = manifest("dup-cap-test", module);
381 bad_manifest.capabilities = vec!["dup".into(), "dup".into()];
382 assert!(bad_manifest.validate_shape().is_err());
383 }
384
385 #[test]
386 fn handler_pack_rejects_unknown_format_version() {
387 let module = b"format module";
388 let mut bad_manifest = manifest("format-test", module);
389 bad_manifest.format_version = 99;
390 assert!(bad_manifest.validate_shape().is_err());
391 }
392
393 #[test]
394 fn handler_pack_rejects_unknown_handler_api_version() {
395 let module = b"api module";
396 let mut bad_manifest = manifest("api-test", module);
397 bad_manifest.handler_api_version = 99;
398 assert!(bad_manifest.validate_shape().is_err());
399 }
400
401 #[test]
402 fn handler_pack_rejects_empty_capabilities() {
403 let module = b"empty cap module";
404 let mut bad_manifest = manifest("empty-cap-test", module);
405 bad_manifest.capabilities = vec![];
406 assert!(bad_manifest.validate_shape().is_err());
407 }
408
409 #[test]
410 fn handler_pack_rejects_oversized_module() {
411 let module = b"oversized module";
412 let mut bad_manifest = manifest("oversized-test", module);
413 bad_manifest.module_size = 5 * 1024 * 1024;
414 assert!(bad_manifest.validate_shape().is_err());
415 }
416
417 use std::io::Read;
420
421 fn read_zip_entries(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
423 let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes.to_vec())).unwrap();
424 let mut entries = BTreeMap::new();
425 for index in 0..archive.len() {
426 let mut file = archive.by_index(index).unwrap();
427 let name = file.name().to_string();
428 let mut data = Vec::new();
429 file.read_to_end(&mut data).unwrap();
430 entries.insert(name, data);
431 }
432 entries
433 }
434
435 fn rebuild_with_tamper(bytes: &[u8], name_to_tamper: &str) -> Vec<u8> {
437 let mut entries = read_zip_entries(bytes);
438 let data = entries.get_mut(name_to_tamper).unwrap();
439 let idx = data.len() / 2;
440 data[idx] ^= 0xFF;
441 let files: Vec<(&str, &[u8])> = entries
442 .iter()
443 .map(|(k, v)| (k.as_str(), v.as_slice()))
444 .collect();
445 build_malicious_zip(&files)
446 }
447
448 #[test]
449 fn handler_pack_rejects_tampered_manifest() {
450 let signing = SigningKey::from_bytes(&[31; 32]);
451 let key_id = "tamper-manifest";
452 let module = b"tamper manifest module";
453 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
454 let tampered = rebuild_with_tamper(&bytes, "manifest.json");
455 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
456 let result = load_handler_pack(std::io::Cursor::new(&tampered), &trust);
459 assert!(result.is_err(), "tampered manifest must be rejected");
460 }
461
462 #[test]
463 fn handler_pack_rejects_tampered_module() {
464 let signing = SigningKey::from_bytes(&[32; 32]);
465 let key_id = "tamper-module";
466 let module = b"tamper module bytes";
467 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
468 let tampered = rebuild_with_tamper(&bytes, "handler.wasm");
469 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
470 assert!(matches!(
471 load_handler_pack(std::io::Cursor::new(&tampered), &trust),
472 Err(HandlerPackError::Archive(ArchiveError::Digest(_)))
473 ));
474 }
475
476 #[test]
477 fn handler_pack_rejects_tampered_checksums() {
478 let signing = SigningKey::from_bytes(&[33; 32]);
479 let key_id = "tamper-checksums";
480 let module = b"tamper checksums module";
481 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
482 let tampered = rebuild_with_tamper(&bytes, "checksums.json");
483 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
484 let result = load_handler_pack(std::io::Cursor::new(&tampered), &trust);
488 assert!(result.is_err(), "tampered checksums must be rejected");
489 }
490
491 #[test]
492 fn handler_pack_rejects_tampered_signature() {
493 let signing = SigningKey::from_bytes(&[34; 32]);
494 let key_id = "tamper-signature";
495 let module = b"tamper signature module";
496 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
497 let tampered = rebuild_with_tamper(&bytes, "META-INF/signature.ed25519");
498 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
499 assert!(matches!(
500 load_handler_pack(std::io::Cursor::new(&tampered), &trust),
501 Err(HandlerPackError::Archive(ArchiveError::Signature))
502 ));
503 }
504
505 #[test]
506 fn handler_pack_rejects_unknown_publisher_key() {
507 let signing = SigningKey::from_bytes(&[35; 32]);
508 let key_id = "unknown-key-test";
509 let module = b"unknown key module";
510 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
511 let trust = TrustStore(BTreeMap::new());
513 assert!(matches!(
514 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
515 Err(HandlerPackError::Archive(ArchiveError::UnknownKey))
516 ));
517 }
518
519 #[test]
520 fn handler_pack_skips_directory_entries() {
521 let signing = SigningKey::from_bytes(&[36; 32]);
522 let key_id = "dir-entry-test";
523 let module = b"dir entry module";
524 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
525
526 let entries = read_zip_entries(&bytes);
528 let files: Vec<(&str, &[u8])> = entries
529 .iter()
530 .map(|(k, v)| (k.as_str(), v.as_slice()))
531 .collect();
532
533 let mut output = Cursor::new(Vec::new());
537 {
538 let mut zip = ZipWriter::new(&mut output);
539 let options = SimpleFileOptions::default();
540 zip.add_directory("META-INF/", options).unwrap();
541 for (name, data) in &files {
542 zip.start_file(name, options).unwrap();
543 zip.write_all(data).unwrap();
544 }
545 zip.finish().unwrap();
546 }
547 let rebuilt = output.into_inner();
548
549 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
550 let result = load_handler_pack(std::io::Cursor::new(&rebuilt), &trust);
551 assert!(
552 result.is_ok(),
553 "directory entry should be skipped: {:?}",
554 result.err()
555 );
556 }
557
558 #[test]
559 fn handler_pack_rejects_oversized_file_in_zip() {
560 let oversized: Vec<u8> = vec![0u8; (4 * 1024 * 1024) + 1];
564 let files: &[(&str, &[u8])] = &[
565 ("manifest.json", b"{}"),
566 ("handler.wasm", &oversized),
567 ("checksums.json", b"{}"),
568 ("META-INF/signature.ed25519", &[0u8; 64]),
569 ];
570 let bytes = build_malicious_zip(files);
571 let trust = TrustStore(BTreeMap::new());
572 assert!(matches!(
573 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
574 Err(HandlerPackError::Archive(ArchiveError::Limit("file size")))
575 ));
576 }
577
578 #[test]
579 fn handler_pack_rejects_compression_bomb() {
580 let uncompressed_size: usize = 1024 * 1024;
584 let bomb_payload: Vec<u8> = vec![0u8; uncompressed_size];
585 let mut output = Cursor::new(Vec::new());
586 {
587 let mut zip = ZipWriter::new(&mut output);
588 let options =
589 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
590 zip.start_file("handler.wasm", options).unwrap();
591 zip.write_all(&bomb_payload).unwrap();
592 zip.finish().unwrap();
593 }
594 let bytes = output.into_inner();
595 let trust = TrustStore(BTreeMap::new());
596 assert!(matches!(
597 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
598 Err(HandlerPackError::Archive(ArchiveError::Limit(
599 "compression ratio"
600 )))
601 ));
602 }
603
604 #[test]
605 fn handler_pack_rejects_load_time_module_digest_mismatch() {
606 let signing = SigningKey::from_bytes(&[37; 32]);
612 let key_id = "load-digest-test";
613 let real_module = b"real module for load digest test";
614 let fake_module = b"fake module for load digest test";
615
616 let manifest_json = serde_json::json!({
617 "formatVersion": HANDLER_PACKAGE_FORMAT_VERSION,
618 "id": "org.example.handler",
619 "version": "1.0.0",
620 "handlerApiVersion": HANDLER_API_VERSION,
621 "minRuntimeVersion": "0.7.0",
622 "publisherKeyId": key_id,
623 "capabilities": ["org.example.predict"],
624 "moduleSha256": hex::encode(Sha256::digest(real_module)),
625 "moduleSize": real_module.len() as u64,
626 });
627 let manifest_bytes = serde_json::to_vec_pretty(&manifest_json).unwrap();
628 let archive =
629 build_signed_archive(&manifest_bytes, MODULE_PATH, fake_module, &signing).unwrap();
630
631 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
632 assert!(matches!(
633 load_handler_pack(std::io::Cursor::new(&archive), &trust),
634 Err(HandlerPackError::ModuleDigestMismatch)
635 ));
636 }
637
638 #[test]
641 fn handler_pack_rejects_runtime_too_old() {
642 let signing = SigningKey::from_bytes(&[41; 32]);
645 let key_id = "runtime-too-old-test";
646 let module = b"module for runtime too old test";
647 let mut bad_manifest = manifest(key_id, module);
648 bad_manifest.min_runtime_version = "999.0.0".into();
650 let bytes = build_signed_handler_pack(&bad_manifest, module, &signing).unwrap();
651 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
652 assert!(matches!(
653 load_handler_pack(std::io::Cursor::new(&bytes), &trust),
654 Err(HandlerPackError::RuntimeTooOld { .. })
655 ));
656 }
657}