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