Skip to main content

rill_runtime/
handler_package.rs

1//! Signed handler-pack (`.rillhandler`) construction and verification.
2
3use 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    // Verify the manifest's moduleSha256 matches the actual module bytes.
104    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    // Verify moduleSha256 and moduleSize match the actual module.
135    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        // Set the digest to the real module's digest, but provide the fake module.
195        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        // A handler signed with one key must not verify against a different key.
206        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        // Model trust store uses a different key under the same key id.
213        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}