1use crate::error::{EngramError, Result};
63use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
64use serde::{Deserialize, Serialize};
65use sha2::{Digest, Sha256};
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Manifest {
70 pub version: String,
72
73 pub id: String,
75
76 pub name: String,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub description: Option<String>,
82
83 pub author: Author,
85
86 pub metadata: Metadata,
88
89 #[serde(default)]
91 pub capabilities: Vec<String>,
92
93 #[serde(default)]
95 pub files: Vec<FileEntry>,
96
97 #[serde(default)]
99 pub signatures: Vec<SignatureEntry>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Author {
105 pub name: String,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub email: Option<String>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub url: Option<String>,
112}
113
114impl Author {
115 pub fn new(name: impl Into<String>) -> Self {
117 Self {
118 name: name.into(),
119 email: None,
120 url: None,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Metadata {
128 pub version: String,
130
131 pub created: u64,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub modified: Option<u64>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub license: Option<String>,
141
142 #[serde(default)]
144 pub tags: Vec<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct FileEntry {
150 pub path: String,
152
153 pub sha256: String,
155
156 pub size: u64,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub mime_type: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct SignatureEntry {
167 pub algorithm: String,
169
170 pub public_key: String,
172
173 pub signature: String,
175
176 pub timestamp: u64,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub signer: Option<String>,
182}
183
184impl Manifest {
185 pub fn new(id: String, name: String, author: Author, version: String) -> Self {
187 Self {
188 version: "0.4.0".to_string(),
189 id,
190 name,
191 description: None,
192 author,
193 metadata: Metadata {
194 version,
195 created: std::time::SystemTime::now()
196 .duration_since(std::time::UNIX_EPOCH)
197 .unwrap()
198 .as_secs(),
199 modified: None,
200 license: None,
201 tags: Vec::new(),
202 },
203 capabilities: Vec::new(),
204 files: Vec::new(),
205 signatures: Vec::new(),
206 }
207 }
208
209 pub fn add_file(&mut self, path: String, data: &[u8], mime_type: Option<String>) {
211 let sha256 = hex::encode(Sha256::digest(data));
212 self.files.push(FileEntry {
213 path,
214 sha256,
215 size: data.len() as u64,
216 mime_type,
217 });
218 }
219
220 pub fn to_json(&self) -> Result<Vec<u8>> {
222 serde_json::to_vec_pretty(self).map_err(EngramError::from)
223 }
224
225 pub fn from_json(data: &[u8]) -> Result<Self> {
227 serde_json::from_slice(data).map_err(EngramError::from)
228 }
229
230 pub fn canonical_hash(&self) -> Result<[u8; 32]> {
235 let mut manifest_copy = self.clone();
237 manifest_copy.signatures.clear();
238
239 let json = serde_json::to_vec(&manifest_copy)?;
241
242 Ok(Sha256::digest(&json).into())
244 }
245
246 pub fn sign(&mut self, signing_key: &SigningKey, signer: Option<String>) -> Result<()> {
248 let hash = self.canonical_hash()?;
249 let signature = signing_key.sign(&hash);
250
251 let public_key = signing_key.verifying_key();
252
253 self.signatures.push(SignatureEntry {
254 algorithm: "ed25519".to_string(),
255 public_key: hex::encode(public_key.to_bytes()),
256 signature: hex::encode(signature.to_bytes()),
257 timestamp: std::time::SystemTime::now()
258 .duration_since(std::time::UNIX_EPOCH)
259 .unwrap()
260 .as_secs(),
261 signer,
262 });
263
264 Ok(())
265 }
266
267 pub fn verify_signatures(&self) -> Result<Vec<bool>> {
269 let mut results = Vec::new();
270 let hash = self.canonical_hash()?;
271
272 for sig_entry in &self.signatures {
273 let result = self.verify_signature_entry(sig_entry, &hash);
274 results.push(result.is_ok());
275 }
276
277 Ok(results)
278 }
279
280 fn verify_signature_entry(&self, entry: &SignatureEntry, hash: &[u8; 32]) -> Result<()> {
282 if entry.algorithm != "ed25519" {
283 return Err(EngramError::InvalidSignature);
284 }
285
286 let public_key_bytes =
288 hex::decode(&entry.public_key).map_err(|_| EngramError::InvalidPublicKey)?;
289 let public_key_array: [u8; 32] = public_key_bytes
290 .try_into()
291 .map_err(|_| EngramError::InvalidPublicKey)?;
292 let public_key = VerifyingKey::from_bytes(&public_key_array)?;
293
294 let signature_bytes =
296 hex::decode(&entry.signature).map_err(|_| EngramError::InvalidSignature)?;
297 let signature_array: [u8; 64] = signature_bytes
298 .try_into()
299 .map_err(|_| EngramError::InvalidSignature)?;
300 let signature = Signature::from_bytes(&signature_array);
301
302 public_key.verify(hash, &signature)?;
304
305 Ok(())
306 }
307
308 pub fn is_fully_signed(&self) -> Result<bool> {
310 if self.signatures.is_empty() {
311 return Ok(false);
312 }
313
314 let results = self.verify_signatures()?;
315 Ok(results.iter().all(|&valid| valid))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use rand::rngs::OsRng;
323
324 #[test]
325 fn test_manifest_creation() {
326 let manifest = Manifest::new(
327 "test-engram".to_string(),
328 "Test Engram".to_string(),
329 Author {
330 name: "Test Author".to_string(),
331 email: Some("test@example.com".to_string()),
332 url: None,
333 },
334 "0.1.0".to_string(),
335 );
336
337 assert_eq!(manifest.version, "0.4.0");
338 assert_eq!(manifest.id, "test-engram");
339 assert_eq!(manifest.author.name, "Test Author");
340 }
341
342 #[test]
343 fn test_add_file() {
344 let mut manifest = Manifest::new(
345 "test".to_string(),
346 "Test".to_string(),
347 Author {
348 name: "Test".to_string(),
349 email: None,
350 url: None,
351 },
352 "0.1.0".to_string(),
353 );
354
355 let data = b"Hello, World!";
356 manifest.add_file("test.txt".to_string(), data, Some("text/plain".to_string()));
357
358 assert_eq!(manifest.files.len(), 1);
359 assert_eq!(manifest.files[0].path, "test.txt");
360 assert_eq!(manifest.files[0].size, 13);
361 }
362
363 #[test]
364 fn test_signature_roundtrip() {
365 let mut manifest = Manifest::new(
366 "test".to_string(),
367 "Test".to_string(),
368 Author {
369 name: "Test".to_string(),
370 email: None,
371 url: None,
372 },
373 "0.1.0".to_string(),
374 );
375
376 let mut csprng = OsRng;
378 let signing_key = SigningKey::generate(&mut csprng);
379
380 manifest
382 .sign(&signing_key, Some("Test Signer".to_string()))
383 .unwrap();
384
385 assert_eq!(manifest.signatures.len(), 1);
386 assert_eq!(manifest.signatures[0].algorithm, "ed25519");
387
388 let results = manifest.verify_signatures().unwrap();
390 assert_eq!(results.len(), 1);
391 assert!(results[0]);
392
393 assert!(manifest.is_fully_signed().unwrap());
394 }
395
396 #[test]
397 fn test_json_roundtrip() {
398 let manifest = Manifest::new(
399 "test".to_string(),
400 "Test".to_string(),
401 Author {
402 name: "Test".to_string(),
403 email: None,
404 url: None,
405 },
406 "0.1.0".to_string(),
407 );
408
409 let json = manifest.to_json().unwrap();
410 let parsed = Manifest::from_json(&json).unwrap();
411
412 assert_eq!(parsed.id, manifest.id);
413 assert_eq!(parsed.name, manifest.name);
414 }
415}