Skip to main content

crablock_core/
format.rs

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;
10// Header = magic bytes + version + manifest length.
11pub const HEADER_SIZE: usize = MAGIC_BYTES.len() + 2 + 4; // magic + version + manifest_len
12pub 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        // We accept both the current version and the older v1 format.
54        // This lets new runtimes keep reading packages created before embedded env support existed.
55        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    // This second encrypted payload is used only for embedded `.env` content.
91    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        // Header
126        bytes.extend_from_slice(&header.to_bytes());
127
128        // Manifest (CBOR)
129        bytes.extend_from_slice(&manifest_bytes);
130
131        // Payload length (u64)
132        bytes.extend_from_slice(&(self.payload.len() as u64).to_be_bytes());
133
134        // Payload
135        bytes.extend_from_slice(&self.payload);
136
137        if header.version >= CURRENT_VERSION {
138            // V2 packages append the optional encrypted env payload after the main payload.
139            // V1 packages skip this section completely.
140            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        // Signature (optional)
146        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        // Signature metadata (optional, appended for backward compatibility)
160        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        // Signatures should not sign themselves.
168        // We clone the package, remove the signature, and serialize the rest.
169        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        // Parse header
199        let header = PackageHeader::from_bytes(&bytes[0..HEADER_SIZE])?;
200
201        // Parse manifest
202        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        // Parse payload length
212        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        // Parse payload
230        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            // In v2 we read the extra env section before the signature trailer.
241            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        // Parse signature (optional)
277        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        // This check protects the encrypted payload before we even try to decrypt it.
364        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            // Embedded env metadata and payload must either both exist or both be absent.
374            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    // This builder is used mainly in tests and helper code to construct a package step by step.
407    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}