Skip to main content

shape_runtime/
module_manifest.rs

1//! Module manifest for content-addressed module distribution.
2//!
3//! A `ModuleManifest` describes a distributable module by mapping its exported
4//! functions and type schemas to content-addressed hashes. This allows the
5//! loader to fetch only the blobs it needs from a `BlobStore`.
6
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10
11/// Manifest for a content-addressed module distribution.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ModuleManifest {
14    pub name: String,
15    pub version: String,
16    /// Exported function names mapped to their content hashes.
17    pub exports: HashMap<String, [u8; 32]>,
18    /// Type schema names mapped to their content hashes.
19    pub type_schemas: HashMap<String, [u8; 32]>,
20    /// Permissions required by this module.
21    pub required_permission_bits: u64,
22    /// Transitive dependency closure for each export: export hash → list of dependency hashes.
23    #[serde(default)]
24    pub dependency_closure: HashMap<[u8; 32], Vec<[u8; 32]>>,
25    /// SHA-256 hash of this manifest (excluding this field and signature).
26    pub manifest_hash: [u8; 32],
27    /// Optional cryptographic signature.
28    pub signature: Option<ModuleSignature>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ModuleSignature {
33    pub author_key: [u8; 32],
34    /// Ed25519 signature (64 bytes). Uses `Vec<u8>` because serde does not
35    /// support `[u8; 64]` out of the box.
36    pub signature: Vec<u8>,
37    pub signed_at: u64,
38}
39
40/// Helper struct for deterministic manifest hashing.
41/// We hash only the fields that define the manifest's identity,
42/// excluding `manifest_hash` and `signature`.
43#[derive(Serialize)]
44struct ManifestHashInput<'a> {
45    name: &'a str,
46    version: &'a str,
47    exports: Vec<(&'a String, &'a [u8; 32])>,
48    type_schemas: Vec<(&'a String, &'a [u8; 32])>,
49    required_permission_bits: u64,
50    dependency_closure: Vec<(&'a [u8; 32], &'a Vec<[u8; 32]>)>,
51}
52
53impl ModuleManifest {
54    pub fn new(name: String, version: String) -> Self {
55        Self {
56            name,
57            version,
58            exports: HashMap::new(),
59            type_schemas: HashMap::new(),
60            required_permission_bits: 0,
61            dependency_closure: HashMap::new(),
62            manifest_hash: [0u8; 32],
63            signature: None,
64        }
65    }
66
67    pub fn add_export(&mut self, name: String, hash: [u8; 32]) {
68        self.exports.insert(name, hash);
69    }
70
71    pub fn add_type_schema(&mut self, name: String, hash: [u8; 32]) {
72        self.type_schemas.insert(name, hash);
73    }
74
75    /// Compute `manifest_hash` from the identity fields.
76    ///
77    /// Exports and type schemas are sorted by key for deterministic hashing.
78    pub fn finalize(&mut self) {
79        let mut exports: Vec<_> = self.exports.iter().collect();
80        exports.sort_by_key(|(k, _)| *k);
81
82        let mut type_schemas: Vec<_> = self.type_schemas.iter().collect();
83        type_schemas.sort_by_key(|(k, _)| *k);
84
85        let mut dep_closure: Vec<_> = self.dependency_closure.iter().collect();
86        dep_closure.sort_by_key(|(k, _)| *k);
87
88        let input = ManifestHashInput {
89            name: &self.name,
90            version: &self.version,
91            exports,
92            type_schemas,
93            required_permission_bits: self.required_permission_bits,
94            dependency_closure: dep_closure,
95        };
96
97        let bytes = rmp_serde::encode::to_vec(&input)
98            .expect("ManifestHashInput serialization should not fail");
99        let digest = Sha256::digest(&bytes);
100        self.manifest_hash.copy_from_slice(&digest);
101    }
102
103    /// Verify that `manifest_hash` matches the current content.
104    pub fn verify_integrity(&self) -> bool {
105        let mut exports: Vec<_> = self.exports.iter().collect();
106        exports.sort_by_key(|(k, _)| *k);
107
108        let mut type_schemas: Vec<_> = self.type_schemas.iter().collect();
109        type_schemas.sort_by_key(|(k, _)| *k);
110
111        let mut dep_closure: Vec<_> = self.dependency_closure.iter().collect();
112        dep_closure.sort_by_key(|(k, _)| *k);
113
114        let input = ManifestHashInput {
115            name: &self.name,
116            version: &self.version,
117            exports,
118            type_schemas,
119            required_permission_bits: self.required_permission_bits,
120            dependency_closure: dep_closure,
121        };
122
123        let bytes = rmp_serde::encode::to_vec(&input)
124            .expect("ManifestHashInput serialization should not fail");
125        let digest = Sha256::digest(&bytes);
126        let mut expected = [0u8; 32];
127        expected.copy_from_slice(&digest);
128        self.manifest_hash == expected
129    }
130
131    /// Verify the cryptographic signature on this manifest, if present.
132    ///
133    /// Returns:
134    /// - `Ok(true)` if a valid signature is present
135    /// - `Ok(false)` if no signature is present (unsigned module)
136    /// - `Err(reason)` if a signature is present but invalid
137    ///
138    /// Callers can decide policy: reject unsigned modules, warn, or accept.
139    pub fn verify_signature(&self) -> Result<bool, String> {
140        let sig = match &self.signature {
141            Some(s) => s,
142            None => return Ok(false),
143        };
144
145        // Convert the ModuleSignature to a ModuleSignatureData for verification
146        let sig_data = crate::crypto::ModuleSignatureData {
147            author_key: sig.author_key,
148            signature: sig.signature.clone(),
149            signed_at: sig.signed_at,
150        };
151
152        if sig_data.verify(&self.manifest_hash) {
153            Ok(true)
154        } else {
155            Err(format!(
156                "Invalid signature on manifest '{}' v{}: signature does not match manifest hash",
157                self.name, self.version
158            ))
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_new_manifest_has_zero_hash() {
169        let m = ModuleManifest::new("test".into(), "0.1.0".into());
170        assert_eq!(m.manifest_hash, [0u8; 32]);
171        assert!(m.exports.is_empty());
172        assert!(m.type_schemas.is_empty());
173    }
174
175    #[test]
176    fn test_finalize_produces_nonzero_hash() {
177        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
178        m.add_export("greet".into(), [1u8; 32]);
179        m.finalize();
180        assert_ne!(m.manifest_hash, [0u8; 32]);
181    }
182
183    #[test]
184    fn test_verify_integrity_passes_after_finalize() {
185        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
186        m.add_export("greet".into(), [1u8; 32]);
187        m.add_type_schema("MyType".into(), [2u8; 32]);
188        m.required_permission_bits = 0x03;
189        m.finalize();
190        assert!(m.verify_integrity());
191    }
192
193    #[test]
194    fn test_verify_integrity_fails_after_mutation() {
195        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
196        m.add_export("greet".into(), [1u8; 32]);
197        m.finalize();
198        assert!(m.verify_integrity());
199
200        m.add_export("farewell".into(), [3u8; 32]);
201        assert!(!m.verify_integrity());
202    }
203
204    #[test]
205    fn test_deterministic_hash() {
206        let build = || {
207            let mut m = ModuleManifest::new("det".into(), "0.0.1".into());
208            m.add_export("b_fn".into(), [10u8; 32]);
209            m.add_export("a_fn".into(), [20u8; 32]);
210            m.add_type_schema("Z".into(), [30u8; 32]);
211            m.add_type_schema("A".into(), [40u8; 32]);
212            m.finalize();
213            m.manifest_hash
214        };
215        assert_eq!(build(), build());
216    }
217
218    #[test]
219    fn test_serde_roundtrip() {
220        let mut m = ModuleManifest::new("serde_test".into(), "2.0.0".into());
221        m.add_export("run".into(), [7u8; 32]);
222        m.required_permission_bits = 0xFF;
223        m.finalize();
224
225        let json = serde_json::to_string(&m).expect("serialize");
226        let restored: ModuleManifest = serde_json::from_str(&json).expect("deserialize");
227
228        assert_eq!(restored.name, "serde_test");
229        assert_eq!(restored.version, "2.0.0");
230        assert_eq!(restored.exports.get("run"), Some(&[7u8; 32]));
231        assert_eq!(restored.required_permission_bits, 0xFF);
232        assert!(restored.verify_integrity());
233    }
234
235    #[test]
236    fn test_verify_signature_unsigned() {
237        let mut m = ModuleManifest::new("unsigned".into(), "1.0.0".into());
238        m.add_export("fn_a".into(), [1u8; 32]);
239        m.finalize();
240        // Unsigned module returns Ok(false)
241        assert_eq!(m.verify_signature(), Ok(false));
242    }
243
244    #[test]
245    fn test_verify_signature_valid() {
246        let mut m = ModuleManifest::new("signed".into(), "1.0.0".into());
247        m.add_export("fn_a".into(), [1u8; 32]);
248        m.finalize();
249
250        // Sign the manifest
251        let sig_data = crate::crypto::signing::sign_manifest_hash(
252            &m.manifest_hash,
253            &[42u8; 32],
254        );
255        m.signature = Some(ModuleSignature {
256            author_key: sig_data.author_key,
257            signature: sig_data.signature,
258            signed_at: sig_data.signed_at,
259        });
260
261        assert_eq!(m.verify_signature(), Ok(true));
262    }
263
264    #[test]
265    fn test_verify_signature_invalid() {
266        let mut m = ModuleManifest::new("badsig".into(), "1.0.0".into());
267        m.add_export("fn_a".into(), [1u8; 32]);
268        m.finalize();
269
270        // Create a signature with wrong hash
271        let wrong_hash = [99u8; 32];
272        let sig_data = crate::crypto::signing::sign_manifest_hash(
273            &wrong_hash,
274            &[42u8; 32],
275        );
276        m.signature = Some(ModuleSignature {
277            author_key: sig_data.author_key,
278            signature: sig_data.signature,
279            signed_at: sig_data.signed_at,
280        });
281
282        assert!(m.verify_signature().is_err());
283    }
284}