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_compression_ratio: 100,
23};
24
25const HANDLER_PACK_ALLOWED: &[&str] = &[
26 "manifest.json",
27 MODULE_PATH,
28 "checksums.json",
29 "META-INF/signature.ed25519",
30];
31
32#[derive(Debug, Clone)]
33pub struct LoadedHandlerPack {
34 pub manifest: HandlerPackManifest,
35 pub module: Vec<u8>,
36}
37
38#[derive(Debug, Clone, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub struct HandlerPackInspection {
41 pub id: String,
42 pub version: String,
43 pub publisher_key_id: String,
44 pub handler_api_version: u32,
45 pub min_runtime_version: String,
46 pub capabilities: Vec<String>,
47 pub module_sha256: String,
48 pub module_size: u64,
49 pub signature_verified: bool,
50}
51
52#[derive(Debug, Error)]
53pub enum HandlerPackError {
54 #[error(transparent)]
55 Archive(#[from] ArchiveError),
56 #[error(transparent)]
57 Json(#[from] serde_json::Error),
58 #[error("invalid handler manifest: {0}")]
59 Manifest(String),
60 #[error("runtime {actual} is older than handler requirement {minimum}")]
61 RuntimeTooOld { minimum: String, actual: String },
62 #[error("handler module SHA-256 mismatch")]
63 ModuleDigestMismatch,
64}
65
66pub fn load_handler_pack<R: Read + Seek>(
67 reader: R,
68 trust: &TrustStore,
69) -> Result<(LoadedHandlerPack, HandlerPackInspection), HandlerPackError> {
70 let files = read_archive(reader, HANDLER_PACK_ALLOWED, HANDLER_PACK_LIMITS)?;
71 let manifest_bytes = files
72 .get(DEFAULT_PATHS.manifest)
73 .ok_or(ArchiveError::Missing(DEFAULT_PATHS.manifest))?;
74 let manifest: HandlerPackManifest = serde_json::from_slice(manifest_bytes)?;
75 manifest
76 .validate_shape()
77 .map_err(|message| HandlerPackError::Manifest(message.into()))?;
78 semver::Version::parse(&manifest.version)
79 .map_err(|error| HandlerPackError::Manifest(format!("invalid handler version: {error}")))?;
80 let minimum = semver::Version::parse(&manifest.min_runtime_version)
81 .map_err(|error| HandlerPackError::Manifest(format!("invalid minimum runtime: {error}")))?;
82 let runtime = semver::Version::parse(env!("CARGO_PKG_VERSION"))
83 .map_err(|error| HandlerPackError::Manifest(format!("invalid runtime version: {error}")))?;
84 if runtime < minimum {
85 return Err(HandlerPackError::RuntimeTooOld {
86 minimum: minimum.to_string(),
87 actual: runtime.to_string(),
88 });
89 }
90 verify_checksums_and_signature(
91 &files,
92 &DEFAULT_PATHS,
93 &[DEFAULT_PATHS.manifest, MODULE_PATH],
94 &manifest.publisher_key_id,
95 trust,
96 )?;
97
98 let module = files
99 .get(MODULE_PATH)
100 .ok_or(ArchiveError::Missing(MODULE_PATH))?
101 .clone();
102
103 let actual_digest = hex::encode(Sha256::digest(&module));
105 if actual_digest != manifest.module_sha256 {
106 return Err(HandlerPackError::ModuleDigestMismatch);
107 }
108 if module.len() as u64 != manifest.module_size {
109 return Err(HandlerPackError::Manifest("module size mismatch".into()));
110 }
111
112 let inspection = HandlerPackInspection {
113 id: manifest.id.clone(),
114 version: manifest.version.clone(),
115 publisher_key_id: manifest.publisher_key_id.clone(),
116 handler_api_version: manifest.handler_api_version,
117 min_runtime_version: manifest.min_runtime_version.clone(),
118 capabilities: manifest.capabilities.clone(),
119 module_sha256: manifest.module_sha256.clone(),
120 module_size: manifest.module_size,
121 signature_verified: true,
122 };
123 Ok((LoadedHandlerPack { manifest, module }, inspection))
124}
125
126pub fn build_signed_handler_pack(
127 manifest: &HandlerPackManifest,
128 module: &[u8],
129 signing_key: &SigningKey,
130) -> Result<Vec<u8>, HandlerPackError> {
131 manifest
132 .validate_shape()
133 .map_err(|message| HandlerPackError::Manifest(message.into()))?;
134 let actual_digest = hex::encode(Sha256::digest(module));
136 if actual_digest != manifest.module_sha256 {
137 return Err(HandlerPackError::ModuleDigestMismatch);
138 }
139 if module.len() as u64 != manifest.module_size {
140 return Err(HandlerPackError::Manifest("module size mismatch".into()));
141 }
142 let manifest_bytes = serde_json::to_vec_pretty(manifest)?;
143 let archive = build_signed_archive(&manifest_bytes, MODULE_PATH, module, signing_key)?;
144 Ok(archive)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use rill_runtime_protocol::{HANDLER_API_VERSION, HANDLER_PACKAGE_FORMAT_VERSION};
151 use std::collections::BTreeMap;
152
153 fn manifest(key_id: &str, module: &[u8]) -> HandlerPackManifest {
154 HandlerPackManifest {
155 format_version: HANDLER_PACKAGE_FORMAT_VERSION,
156 id: "org.example.handler".into(),
157 version: "1.0.0".into(),
158 handler_api_version: HANDLER_API_VERSION,
159 min_runtime_version: "0.7.0".into(),
160 publisher_key_id: key_id.into(),
161 capabilities: vec!["org.example.predict".into()],
162 module_sha256: hex::encode(Sha256::digest(module)),
163 module_size: module.len() as u64,
164 }
165 }
166
167 #[test]
168 fn handler_pack_roundtrip_and_tamper_rejection() {
169 let signing = SigningKey::from_bytes(&[11; 32]);
170 let key_id = "handler-test-key";
171 let module = b"\0asm\x01\x00\x00\x00test module bytes";
172 let bytes = build_signed_handler_pack(&manifest(key_id, module), module, &signing).unwrap();
173 let trust = TrustStore(BTreeMap::from([(key_id.into(), signing.verifying_key())]));
174 let (loaded, inspection) = load_handler_pack(std::io::Cursor::new(&bytes), &trust).unwrap();
175 assert_eq!(loaded.manifest.id, "org.example.handler");
176 assert_eq!(loaded.module, module);
177 assert!(inspection.signature_verified);
178
179 let wrong = SigningKey::from_bytes(&[12; 32]);
180 let wrong_trust = TrustStore(BTreeMap::from([(key_id.into(), wrong.verifying_key())]));
181 assert!(matches!(
182 load_handler_pack(std::io::Cursor::new(bytes), &wrong_trust),
183 Err(HandlerPackError::Archive(ArchiveError::Signature))
184 ));
185 }
186
187 #[test]
188 fn handler_pack_rejects_wrong_module_digest() {
189 let signing = SigningKey::from_bytes(&[13; 32]);
190 let key_id = "digest-test";
191 let real_module = b"real module";
192 let fake_module = b"fake module";
193 let mut bad_manifest = manifest(key_id, fake_module);
194 bad_manifest.module_sha256 = hex::encode(Sha256::digest(real_module));
196 bad_manifest.module_size = real_module.len() as u64;
197 assert!(matches!(
198 build_signed_handler_pack(&bad_manifest, fake_module, &signing),
199 Err(HandlerPackError::ModuleDigestMismatch)
200 ));
201 }
202
203 #[test]
204 fn handler_pack_rejects_model_trust_key() {
205 let handler_signing = SigningKey::from_bytes(&[14; 32]);
207 let model_signing = SigningKey::from_bytes(&[15; 32]);
208 let key_id = "shared-key-id";
209 let module = b"test module";
210 let bytes =
211 build_signed_handler_pack(&manifest(key_id, module), module, &handler_signing).unwrap();
212 let model_trust = TrustStore(BTreeMap::from([(
214 key_id.into(),
215 model_signing.verifying_key(),
216 )]));
217 assert!(matches!(
218 load_handler_pack(std::io::Cursor::new(&bytes), &model_trust),
219 Err(HandlerPackError::Archive(ArchiveError::Signature))
220 ));
221 }
222}