//! Sealed artifact format definition. //! //! Binary layout (big-endian): //! //! ```text //! Offset Size Field //! ────── ───── ───────────────────────────────────────────────────────── //! 0 8 magic: b"ENGRAM01" //! 8 2 version: u16 (currently 1) //! 10 * JSON-encoded SealedArtifact body (algorithm_id, nonce, …) //! ``` //! //! The body is JSON so the format is self-describing and forward-compatible. //! Future versions can add new fields without breaking older parsers. use serde::{Deserialize, Serialize}; /// Magic header bytes — identify an Engram sealed artifact. pub const MAGIC: [u8; 8] = *b"ENGRAM01"; /// Current artifact format version. pub const FORMAT_VERSION: u16 = 1; // ── Configuration ───────────────────────────────────────────────────────────── /// Which algorithm was used to seal the artifact. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SealAlgorithm { /// AES-256-GCM — current default, quantum-resistant at 256-bit. Aes256Gcm, /// CRYSTALS-Kyber 768 — when ml-kem crate stabilizes. MlKem768, /// CRYSTALS-Kyber 1024 — when ml-kem crate stabilizes. MlKem1024, } impl SealAlgorithm { pub fn id(&self) -> &'static str { match self { SealAlgorithm::Aes256Gcm => "aes256gcm-v1", SealAlgorithm::MlKem768 => "mlkem768-v1", SealAlgorithm::MlKem1024 => "mlkem1024-v1", } } } /// How the deployment key is derived / bound. #[derive(Debug, Clone)] pub enum DeploymentBinding { /// Read the seal key from an environment variable (e.g. `ENGRAM_SEAL_KEY`). EnvironmentKey(String), /// Bind to this machine's hostname + OS + CPU model (BLAKE3 hash of all three). MachineFingerprint, /// No binding — key is the zero vector. For testing only; offers no security. None, } /// Configuration for the sealing operation. #[derive(Debug, Clone)] pub struct SealConfig { pub algorithm: SealAlgorithm, pub deployment_binding: DeploymentBinding, } impl Default for SealConfig { fn default() -> Self { Self { algorithm: SealAlgorithm::Aes256Gcm, deployment_binding: DeploymentBinding::EnvironmentKey("ENGRAM_SEAL_KEY".into()), } } } // ── Artifact ────────────────────────────────────────────────────────────────── /// A quantum-sealed bytecode artifact. /// /// This is the output of `el seal` / the `prod` compilation target. /// It is serialized to disk as: `MAGIC || VERSION_u16_be || JSON(body)`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SealedArtifact { /// Algorithm identifier — matches [`SealAlgorithm::id()`]. pub algorithm_id: String, /// BLAKE3-keyed MAC over `(algorithm_id || nonce || ciphertext)`. /// Used to detect tampering before attempting decryption. /// In the PQ upgrade path, this becomes an ML-DSA signature. pub signature: Vec, /// The binding-protected symmetric key. /// /// In the current scheme: `symmetric_key XOR BLAKE3(binding_material)`. /// Without the deployment key, the binding material cannot be derived, /// so the symmetric key cannot be recovered. /// /// In the ML-KEM upgrade: this becomes the KEM-encapsulated key ciphertext. pub encapsulated_key: Vec, /// 96-bit AES-GCM nonce. pub nonce: Vec, /// Encrypted bytecode (AES-256-GCM ciphertext including the 128-bit auth tag). pub ciphertext: Vec, /// BLAKE3 hash of the binding material. Allows the unsealer to verify /// it is running in the correct deployment environment before decryption. /// `None` if [`DeploymentBinding::None`] was used. pub deployment_fingerprint: Option>, } impl SealedArtifact { /// Serialize to the on-disk wire format: `MAGIC || version_be16 || JSON`. pub fn to_bytes(&self) -> Result, serde_json::Error> { let mut out = Vec::with_capacity(256); out.extend_from_slice(&MAGIC); out.extend_from_slice(&FORMAT_VERSION.to_be_bytes()); let json = serde_json::to_vec(self)?; out.extend_from_slice(&json); Ok(out) } /// Deserialize from the on-disk wire format. pub fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() < 10 { return Err(crate::SealError::Serialization("artifact too short".into())); } let magic: [u8; 8] = bytes[..8].try_into().unwrap(); if magic != MAGIC { return Err(crate::SealError::InvalidMagic(magic)); } // Version check (bytes 8..10) — currently we only support v1 let version = u16::from_be_bytes([bytes[8], bytes[9]]); if version != FORMAT_VERSION { return Err(crate::SealError::UnsupportedAlgorithm(format!("format v{version}"))); } let body = &bytes[10..]; serde_json::from_slice(body).map_err(|e| crate::SealError::Serialization(e.to_string())) } }