1use std::path::Path;
2
3use crate::crypto::compute_sha256;
4use crate::error::{CrablockError, Result};
5use crate::manifest::Manifest;
6
7pub const MAGIC_BYTES: &[u8] = b"ENCPKG1";
8pub const CURRENT_VERSION: u16 = 2;
9pub const LEGACY_VERSION: u16 = 1;
10pub const HEADER_SIZE: usize = MAGIC_BYTES.len() + 2 + 4; pub const PACKAGE_EXTENSION: &str = "crablock";
13
14pub fn validate_package_path(path: &Path) -> Result<()> {
15 if path.extension().and_then(|ext| ext.to_str()) == Some(PACKAGE_EXTENSION) {
16 return Ok(());
17 }
18
19 Err(CrablockError::InvalidPackageExtension {
20 path: path.display().to_string(),
21 expected: format!(".{PACKAGE_EXTENSION}"),
22 })
23}
24
25#[derive(Debug, Clone)]
26pub struct PackageHeader {
27 pub magic: Vec<u8>,
28 pub version: u16,
29 pub manifest_len: u32,
30}
31
32impl PackageHeader {
33 pub fn new(manifest_len: u32) -> Self {
34 Self {
35 magic: MAGIC_BYTES.to_vec(),
36 version: CURRENT_VERSION,
37 manifest_len,
38 }
39 }
40
41 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
42 if bytes.len() < HEADER_SIZE {
43 return Err(CrablockError::InvalidFormat("Header too short".to_string()));
44 }
45
46 let magic = bytes[0..MAGIC_BYTES.len()].to_vec();
47 if magic != MAGIC_BYTES {
48 return Err(CrablockError::InvalidMagic);
49 }
50
51 let version = u16::from_be_bytes([bytes[MAGIC_BYTES.len()], bytes[MAGIC_BYTES.len() + 1]]);
52
53 if version != CURRENT_VERSION && version != LEGACY_VERSION {
56 return Err(CrablockError::VersionMismatch {
57 expected: CURRENT_VERSION,
58 found: version,
59 });
60 }
61
62 let manifest_len = u32::from_be_bytes([
63 bytes[MAGIC_BYTES.len() + 2],
64 bytes[MAGIC_BYTES.len() + 3],
65 bytes[MAGIC_BYTES.len() + 4],
66 bytes[MAGIC_BYTES.len() + 5],
67 ]);
68
69 Ok(Self {
70 magic,
71 version,
72 manifest_len,
73 })
74 }
75
76 pub fn to_bytes(&self) -> Vec<u8> {
77 let mut bytes = Vec::with_capacity(HEADER_SIZE);
78 bytes.extend_from_slice(&self.magic);
79 bytes.extend_from_slice(&self.version.to_be_bytes());
80 bytes.extend_from_slice(&self.manifest_len.to_be_bytes());
81 bytes
82 }
83}
84
85#[derive(Debug, Clone)]
86pub struct Package {
87 pub header: PackageHeader,
88 pub manifest: Manifest,
89 pub payload: Vec<u8>,
90 pub embedded_env_payload: Option<Vec<u8>>,
92 pub signature: Option<Vec<u8>>,
93 pub signature_algorithm: Option<String>,
94 pub signing_pubkey_fingerprint: Option<String>,
95}
96
97impl Package {
98 pub fn new(manifest: Manifest, payload: Vec<u8>, signature: Option<Vec<u8>>) -> Self {
99 let manifest_bytes = manifest.to_cbor().unwrap_or_default();
100 let header = PackageHeader::new(manifest_bytes.len() as u32);
101 let signature_algorithm = manifest.signature_algorithm.clone();
102 let signing_pubkey_fingerprint = manifest.signing_pubkey_fingerprint.clone();
103
104 Self {
105 header,
106 manifest,
107 payload,
108 embedded_env_payload: None,
109 signature,
110 signature_algorithm,
111 signing_pubkey_fingerprint,
112 }
113 }
114
115 pub fn to_bytes(&self) -> Result<Vec<u8>> {
116 let manifest_bytes = self.manifest.to_cbor()?;
117 let header = PackageHeader {
118 magic: MAGIC_BYTES.to_vec(),
119 version: self.header.version,
120 manifest_len: manifest_bytes.len() as u32,
121 };
122
123 let mut bytes = Vec::new();
124
125 bytes.extend_from_slice(&header.to_bytes());
127
128 bytes.extend_from_slice(&manifest_bytes);
130
131 bytes.extend_from_slice(&(self.payload.len() as u64).to_be_bytes());
133
134 bytes.extend_from_slice(&self.payload);
136
137 if header.version >= CURRENT_VERSION {
138 let embedded_env_payload = self.embedded_env_payload.as_deref().unwrap_or(&[]);
141 bytes.extend_from_slice(&(embedded_env_payload.len() as u64).to_be_bytes());
142 bytes.extend_from_slice(embedded_env_payload);
143 }
144
145 if let Some(sig) = &self.signature {
147 if sig.len() > u16::MAX as usize {
148 return Err(CrablockError::InvalidFormat(format!(
149 "Signature too large: {} bytes",
150 sig.len()
151 )));
152 }
153 bytes.extend_from_slice(&(sig.len() as u16).to_be_bytes());
154 bytes.extend_from_slice(sig);
155 } else {
156 bytes.extend_from_slice(&0u16.to_be_bytes());
157 }
158
159 Self::push_sized_string(&mut bytes, self.signature_algorithm.as_ref())?;
161 Self::push_sized_string(&mut bytes, self.signing_pubkey_fingerprint.as_ref())?;
162
163 Ok(bytes)
164 }
165
166 pub fn canonical_signing_bytes(&self) -> Result<Vec<u8>> {
167 let mut canonical = self.clone();
170 canonical.signature = None;
171 canonical.to_bytes()
172 }
173
174 fn push_sized_string(out: &mut Vec<u8>, value: Option<&String>) -> Result<()> {
175 if let Some(value) = value {
176 let raw = value.as_bytes();
177 if raw.len() > u8::MAX as usize {
178 return Err(CrablockError::InvalidFormat(format!(
179 "Metadata field too long: {} bytes",
180 raw.len()
181 )));
182 }
183 out.push(raw.len() as u8);
184 out.extend_from_slice(raw);
185 } else {
186 out.push(0u8);
187 }
188 Ok(())
189 }
190
191 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
192 if bytes.len() < HEADER_SIZE {
193 return Err(CrablockError::InvalidFormat(
194 "Package too short".to_string(),
195 ));
196 }
197
198 let header = PackageHeader::from_bytes(&bytes[0..HEADER_SIZE])?;
200
201 let manifest_start = HEADER_SIZE;
203 let manifest_end = manifest_start + header.manifest_len as usize;
204 if manifest_end > bytes.len() {
205 return Err(CrablockError::InvalidFormat(
206 "Manifest extends beyond package".to_string(),
207 ));
208 }
209 let manifest = Manifest::from_cbor(&bytes[manifest_start..manifest_end])?;
210
211 let payload_len_start = manifest_end;
213 if payload_len_start + 8 > bytes.len() {
214 return Err(CrablockError::InvalidFormat(
215 "Missing payload length".to_string(),
216 ));
217 }
218 let payload_len = u64::from_be_bytes([
219 bytes[payload_len_start],
220 bytes[payload_len_start + 1],
221 bytes[payload_len_start + 2],
222 bytes[payload_len_start + 3],
223 bytes[payload_len_start + 4],
224 bytes[payload_len_start + 5],
225 bytes[payload_len_start + 6],
226 bytes[payload_len_start + 7],
227 ]) as usize;
228
229 let payload_start = payload_len_start + 8;
231 let payload_end = payload_start + payload_len;
232 if payload_end > bytes.len() {
233 return Err(CrablockError::InvalidFormat(
234 "Payload extends beyond package".to_string(),
235 ));
236 }
237 let payload = bytes[payload_start..payload_end].to_vec();
238
239 let (embedded_env_payload, signature_offset) = if header.version >= CURRENT_VERSION {
240 if payload_end + 8 > bytes.len() {
242 return Err(CrablockError::InvalidFormat(
243 "Missing embedded env payload length".to_string(),
244 ));
245 }
246
247 let env_payload_len = u64::from_be_bytes([
248 bytes[payload_end],
249 bytes[payload_end + 1],
250 bytes[payload_end + 2],
251 bytes[payload_end + 3],
252 bytes[payload_end + 4],
253 bytes[payload_end + 5],
254 bytes[payload_end + 6],
255 bytes[payload_end + 7],
256 ]) as usize;
257 let env_payload_start = payload_end + 8;
258 let env_payload_end = env_payload_start + env_payload_len;
259 if env_payload_end > bytes.len() {
260 return Err(CrablockError::InvalidFormat(
261 "Embedded env payload extends beyond package".to_string(),
262 ));
263 }
264
265 let embedded_env_payload = if env_payload_len > 0 {
266 Some(bytes[env_payload_start..env_payload_end].to_vec())
267 } else {
268 None
269 };
270
271 (embedded_env_payload, env_payload_end)
272 } else {
273 (None, payload_end)
274 };
275
276 let mut signature = None;
278 let mut signature_algorithm = None;
279 let mut signing_pubkey_fingerprint = None;
280 if signature_offset + 2 <= bytes.len() {
281 let sig_len =
282 u16::from_be_bytes([bytes[signature_offset], bytes[signature_offset + 1]]) as usize;
283 let sig_start = signature_offset + 2;
284 let sig_end = sig_start + sig_len;
285 if sig_end > bytes.len() {
286 return Err(CrablockError::InvalidFormat(
287 "Signature extends beyond package".to_string(),
288 ));
289 }
290
291 if sig_len > 0 {
292 signature = Some(bytes[sig_start..sig_end].to_vec());
293 }
294
295 let mut cursor = sig_end;
296 if cursor < bytes.len() {
297 let (algorithm, next) = Self::read_sized_string(bytes, cursor, "algorithm")?;
298 signature_algorithm = algorithm;
299 cursor = next;
300 }
301 if cursor < bytes.len() {
302 let (fingerprint, next) = Self::read_sized_string(bytes, cursor, "fingerprint")?;
303 signing_pubkey_fingerprint = fingerprint;
304 cursor = next;
305 }
306 if cursor != bytes.len() {
307 return Err(CrablockError::InvalidFormat(
308 "Unexpected trailing bytes after signature metadata".to_string(),
309 ));
310 }
311 }
312
313 let mut manifest = manifest;
314 if manifest.signature_algorithm.is_none() {
315 manifest.signature_algorithm = signature_algorithm.clone();
316 }
317 if manifest.signing_pubkey_fingerprint.is_none() {
318 manifest.signing_pubkey_fingerprint = signing_pubkey_fingerprint.clone();
319 }
320
321 Ok(Self {
322 header,
323 manifest,
324 payload,
325 embedded_env_payload,
326 signature,
327 signature_algorithm,
328 signing_pubkey_fingerprint,
329 })
330 }
331
332 fn read_sized_string(
333 bytes: &[u8],
334 cursor: usize,
335 field_name: &str,
336 ) -> Result<(Option<String>, usize)> {
337 if cursor >= bytes.len() {
338 return Err(CrablockError::InvalidFormat(format!(
339 "Missing signature {field_name} length"
340 )));
341 }
342 let len = bytes[cursor] as usize;
343 let start = cursor + 1;
344 let end = start + len;
345 if end > bytes.len() {
346 return Err(CrablockError::InvalidFormat(format!(
347 "Signature {field_name} metadata extends beyond package"
348 )));
349 }
350 if len == 0 {
351 return Ok((None, end));
352 }
353
354 let value = std::str::from_utf8(&bytes[start..end]).map_err(|e| {
355 CrablockError::InvalidFormat(format!(
356 "Signature {field_name} metadata is not UTF-8: {e}"
357 ))
358 })?;
359 Ok((Some(value.to_string()), end))
360 }
361
362 pub fn verify_hashes(&self) -> Result<()> {
363 let computed_payload_hash = compute_sha256(&self.payload);
365 if computed_payload_hash != self.manifest.payload_hash_sha256 {
366 return Err(CrablockError::HashMismatch {
367 expected: self.manifest.payload_hash_sha256.clone(),
368 computed: computed_payload_hash,
369 });
370 }
371
372 if let Some(embedded_env) = self.manifest.embedded_env.as_ref() {
373 let embedded_env_payload = self.embedded_env_payload.as_ref().ok_or_else(|| {
375 CrablockError::InvalidFormat(
376 "Manifest declares embedded env but package payload is missing".to_string(),
377 )
378 })?;
379 let computed_env_hash = compute_sha256(embedded_env_payload);
380 if computed_env_hash != embedded_env.payload_hash_sha256 {
381 return Err(CrablockError::HashMismatch {
382 expected: embedded_env.payload_hash_sha256.clone(),
383 computed: computed_env_hash,
384 });
385 }
386 }
387
388 Ok(())
389 }
390
391 pub fn write_to_file(&self, path: &Path) -> Result<()> {
392 validate_package_path(path)?;
393 let bytes = self.to_bytes()?;
394 std::fs::write(path, bytes)?;
395 Ok(())
396 }
397
398 pub fn read_from_file(path: &Path) -> Result<Self> {
399 validate_package_path(path)?;
400 let bytes = std::fs::read(path)?;
401 Self::from_bytes(&bytes)
402 }
403}
404
405pub struct PackageBuilder {
406 manifest: Option<Manifest>,
408 payload: Option<Vec<u8>>,
409 signature: Option<Vec<u8>>,
410 embedded_env_payload: Option<Vec<u8>>,
411 signature_algorithm: Option<String>,
412 signing_pubkey_fingerprint: Option<String>,
413}
414
415impl PackageBuilder {
416 pub fn new() -> Self {
417 Self {
418 manifest: None,
419 payload: None,
420 signature: None,
421 embedded_env_payload: None,
422 signature_algorithm: None,
423 signing_pubkey_fingerprint: None,
424 }
425 }
426
427 pub fn manifest(mut self, manifest: Manifest) -> Self {
428 self.manifest = Some(manifest);
429 self
430 }
431
432 pub fn payload(mut self, payload: Vec<u8>) -> Self {
433 self.payload = Some(payload);
434 self
435 }
436
437 pub fn signature(mut self, signature: Vec<u8>) -> Self {
438 self.signature = Some(signature);
439 self
440 }
441
442 pub fn embedded_env_payload(mut self, embedded_env_payload: Vec<u8>) -> Self {
443 self.embedded_env_payload = Some(embedded_env_payload);
444 self
445 }
446
447 pub fn signature_algorithm(mut self, signature_algorithm: String) -> Self {
448 self.signature_algorithm = Some(signature_algorithm);
449 self
450 }
451
452 pub fn signing_pubkey_fingerprint(mut self, fingerprint: String) -> Self {
453 self.signing_pubkey_fingerprint = Some(fingerprint);
454 self
455 }
456
457 pub fn build(self) -> Result<Package> {
458 let manifest = self
459 .manifest
460 .ok_or_else(|| CrablockError::InvalidFormat("Missing manifest".to_string()))?;
461 let payload = self
462 .payload
463 .ok_or_else(|| CrablockError::InvalidFormat("Missing payload".to_string()))?;
464
465 let mut package = Package::new(manifest, payload, self.signature);
466 package.embedded_env_payload = self.embedded_env_payload;
467 package.signature_algorithm = self.signature_algorithm;
468 package.signing_pubkey_fingerprint = self.signing_pubkey_fingerprint;
469 Ok(package)
470 }
471}
472
473impl Default for PackageBuilder {
474 fn default() -> Self {
475 Self::new()
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::crypto::EncryptionAlgorithm;
483 use std::path::Path;
484
485 #[test]
486 fn test_package_roundtrip() {
487 let manifest = Manifest::new(
488 "test_app".to_string(),
489 100,
490 EncryptionAlgorithm::Aes256Gcm,
491 &[0u8; 12],
492 "artifact_hash",
493 "payload_hash",
494 );
495
496 let package = Package::new(manifest, vec![1, 2, 3, 4, 5], None);
497 let bytes = package.to_bytes().unwrap();
498 let restored = Package::from_bytes(&bytes).unwrap();
499
500 assert_eq!(package.manifest.package_id, restored.manifest.package_id);
501 assert_eq!(package.payload, restored.payload);
502 }
503
504 #[test]
505 fn test_invalid_magic() {
506 let bytes = vec![0u8; 20];
507 let result = Package::from_bytes(&bytes);
508 assert!(matches!(result, Err(CrablockError::InvalidMagic)));
509 }
510
511 #[test]
512 fn test_package_with_signature() {
513 let manifest = Manifest::new(
514 "test_app".to_string(),
515 100,
516 EncryptionAlgorithm::ChaCha20Poly1305,
517 &[1u8; 12],
518 "hash1",
519 "hash2",
520 );
521
522 let signature = vec![0xAB; 64];
523 let mut package = Package::new(manifest, vec![1, 2, 3], Some(signature.clone()));
524 package.signature_algorithm = Some("ed25519".to_string());
525 package.signing_pubkey_fingerprint = Some("abc123".to_string());
526 let bytes = package.to_bytes().unwrap();
527 let restored = Package::from_bytes(&bytes).unwrap();
528
529 assert_eq!(restored.signature, Some(signature));
530 assert_eq!(restored.signature_algorithm.as_deref(), Some("ed25519"));
531 assert_eq!(
532 restored.signing_pubkey_fingerprint.as_deref(),
533 Some("abc123")
534 );
535 }
536
537 #[test]
538 fn test_package_with_embedded_env_roundtrip() {
539 let manifest = Manifest::new(
540 "test_app".to_string(),
541 100,
542 EncryptionAlgorithm::Aes256Gcm,
543 &[2u8; 12],
544 "hash1",
545 "hash2",
546 );
547
548 let mut package = Package::new(manifest, vec![1, 2, 3], None);
549 package.embedded_env_payload = Some(vec![9, 9, 9]);
550
551 let bytes = package.to_bytes().unwrap();
552 let restored = Package::from_bytes(&bytes).unwrap();
553
554 assert_eq!(restored.embedded_env_payload, Some(vec![9, 9, 9]));
555 }
556
557 #[test]
558 fn test_canonical_bytes_exclude_signature() {
559 let manifest = Manifest::new(
560 "test_app".to_string(),
561 100,
562 EncryptionAlgorithm::Aes256Gcm,
563 &[0u8; 12],
564 "hash1",
565 "hash2",
566 );
567 let mut package = Package::new(manifest.clone(), vec![9, 8, 7], Some(vec![0xAB; 64]));
568 package.signature_algorithm = Some("ed25519".to_string());
569 package.signing_pubkey_fingerprint = Some("fingerprint".to_string());
570
571 let canonical = package.canonical_signing_bytes().unwrap();
572
573 let mut expected = Package::new(manifest, vec![9, 8, 7], None);
574 expected.signature_algorithm = Some("ed25519".to_string());
575 expected.signing_pubkey_fingerprint = Some("fingerprint".to_string());
576 let expected_bytes = expected.to_bytes().unwrap();
577
578 assert_eq!(canonical, expected_bytes);
579 }
580
581 #[test]
582 fn test_legacy_signed_package_without_metadata_parses() {
583 let manifest = Manifest::new(
584 "test_app".to_string(),
585 100,
586 EncryptionAlgorithm::Aes256Gcm,
587 &[0u8; 12],
588 "hash1",
589 "hash2",
590 );
591 let payload = vec![1, 2, 3];
592 let signature = vec![0xAA; 64];
593 let manifest_bytes = manifest.to_cbor().unwrap();
594 let header = PackageHeader {
595 magic: MAGIC_BYTES.to_vec(),
596 version: LEGACY_VERSION,
597 manifest_len: manifest_bytes.len() as u32,
598 };
599
600 let mut bytes = Vec::new();
601 bytes.extend_from_slice(&header.to_bytes());
602 bytes.extend_from_slice(&manifest_bytes);
603 bytes.extend_from_slice(&(payload.len() as u64).to_be_bytes());
604 bytes.extend_from_slice(&payload);
605 bytes.extend_from_slice(&(signature.len() as u16).to_be_bytes());
606 bytes.extend_from_slice(&signature);
607
608 let restored = Package::from_bytes(&bytes).unwrap();
609 assert_eq!(restored.signature, Some(signature));
610 assert!(restored.signature_algorithm.is_none());
611 assert!(restored.signing_pubkey_fingerprint.is_none());
612 }
613
614 #[test]
615 fn test_validate_package_path_accepts_crablock_extension() {
616 assert!(validate_package_path(Path::new("app.crablock")).is_ok());
617 }
618
619 #[test]
620 fn test_validate_package_path_rejects_non_crablock_extension() {
621 let result = validate_package_path(Path::new("app.pkg"));
622
623 assert!(matches!(
624 result,
625 Err(CrablockError::InvalidPackageExtension { .. })
626 ));
627 }
628}