//! The Joule Code coding-operation cargo (SPEC §2, `spec/cargo.md`). //! //! A [`CodingOp`] is a signed transition that moves a source tree from a prior //! state to a successor via an ordered edit set. Identity is the content //! address of its canonical form excluding the signature; the signature binds //! `op_id ‖ prior` so it cannot be replayed onto a different base. use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use joule_code_schema::{ContentId, SchemaError, Tier, ALG_ED25519}; use serde::{Deserialize, Serialize}; /// Errors from building, signing, or verifying a coding operation. #[derive(Debug, thiserror::Error)] pub enum CargoError { #[error(transparent)] Schema(#[from] SchemaError), /// The edit list is empty (SPEC §2 requires non-empty). #[error("edits must be non-empty")] NoEdits, /// Edits are not in ascending `(path, start)` order (non-canonical). #[error("edits are not in canonical (path, start) order")] EditsMisordered, /// Two edits to the same file overlap. #[error("edits overlap")] EditsOverlap, /// The algorithm identifier is not known to this implementation. #[error("unknown algorithm id: {0}")] UnknownAlgorithm(u8), /// A malformed key or signature. #[error("bad key or signature: {0}")] Crypto(String), /// The recomputed `op_id` did not match the signed one. #[error("op_id mismatch (tampered op)")] OpIdMismatch, /// The signature did not verify against `op_id ‖ prior`. #[error("signature does not bind op_id ‖ prior")] BadSignature, } /// Where an edit applies within a file. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum Anchor { /// A half-open byte range against the prior blob. ByteRange { start: u64, end: u64 }, /// A content-addressed hunk (rename-stable). Hunk { hunk_id: ContentId }, } /// One edit: a replacement of an anchored region of one file. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Edit { /// Tree-relative, forward-slash path. pub path: String, /// Where the replacement applies. pub anchor: Anchor, /// The new bytes for the anchored region. pub replacement: Vec, } impl Edit { /// Sort key for canonical ordering: `(path, start)`. Hunk-anchored edits /// sort after byte-range edits at the same path (start = u64::MAX). fn sort_key(&self) -> (&str, u64) { let start = match &self.anchor { Anchor::ByteRange { start, .. } => *start, Anchor::Hunk { .. } => u64::MAX, }; (self.path.as_str(), start) } fn byte_span(&self) -> Option<(u64, u64)> { match &self.anchor { Anchor::ByteRange { start, end } => Some((*start, *end)), Anchor::Hunk { .. } => None, } } } /// A coding operation: base + edits + author + intent + closing tier (SPEC §2). /// The signature is carried separately in [`SignedOp`]. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CodingOp { /// Signature algorithm id (`0x01` = Ed25519). pub alg: u8, /// The tree state this op builds on. pub prior: ContentId, /// Ordered, non-empty, disjoint edits. pub edits: Vec, /// The author's Ed25519 verifying key. pub author: [u8; 32], /// The request that produced the op (advisory). pub intent: String, /// The cascade tier that closed the request. pub tier: Tier, } impl CodingOp { /// Validate canonical structure (SPEC §2, `spec/cargo.md` §4): known alg, /// non-empty edits, ascending `(path, start)` order, no overlap. pub fn validate(&self) -> Result<(), CargoError> { if self.alg != ALG_ED25519 { return Err(CargoError::UnknownAlgorithm(self.alg)); } if self.edits.is_empty() { return Err(CargoError::NoEdits); } for pair in self.edits.windows(2) { if pair[0].sort_key() > pair[1].sort_key() { return Err(CargoError::EditsMisordered); } } // Overlap check for byte-range edits within the same path. for i in 0..self.edits.len() { for j in (i + 1)..self.edits.len() { if self.edits[i].path != self.edits[j].path { continue; } if let (Some((a0, a1)), Some((b0, b1))) = (self.edits[i].byte_span(), self.edits[j].byte_span()) { if a0 < b1 && b0 < a1 { return Err(CargoError::EditsOverlap); } } } } Ok(()) } /// The operation id: content address of the canonical form (SPEC §3). /// Validates structure first so a non-canonical op has no id. pub fn op_id(&self) -> Result { self.validate()?; Ok(ContentId::of_canonical(self)?) } /// The signature preimage: `op_id ‖ prior` (SPEC §2). fn preimage(&self) -> Result, CargoError> { let op_id = self.op_id()?; let mut pre = op_id.as_str().as_bytes().to_vec(); pre.extend_from_slice(self.prior.as_str().as_bytes()); Ok(pre) } /// Sign this op with the author's signing key, producing a [`SignedOp`]. pub fn sign(self, key: &SigningKey) -> Result { let sig = key.sign(&self.preimage()?); Ok(SignedOp { op: self, sig: sig.to_bytes().to_vec(), }) } } /// A coding operation plus its signature (SPEC §2). The signature is an /// Ed25519 signature (64 bytes); it is held as `Vec` because `serde` does /// not derive for arrays larger than 32, and verification checks the length. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SignedOp { pub op: CodingOp, pub sig: Vec, } impl SignedOp { /// Verify against a stated `prior` (the base the verifier believes is /// current). Recomputes `op_id`, rebuilds `op_id ‖ prior`, and checks the /// signature against `op.author`. Rejects replay onto a different base and /// tampered edits (SPEC §2, `spec/cargo.md` §4). pub fn verify(&self, expected_prior: &ContentId) -> Result<(), CargoError> { if &self.op.prior != expected_prior { return Err(CargoError::BadSignature); } let vk = VerifyingKey::from_bytes(&self.op.author) .map_err(|e| CargoError::Crypto(e.to_string()))?; let sig_arr: [u8; 64] = self .sig .as_slice() .try_into() .map_err(|_| CargoError::Crypto("signature must be 64 bytes".into()))?; let sig = Signature::from_bytes(&sig_arr); vk.verify(&self.op.preimage()?, &sig) .map_err(|_| CargoError::BadSignature) } } #[cfg(test)] mod tests { use super::*; use ed25519_dalek::SigningKey; fn sample(prior: &str) -> CodingOp { let sk = SigningKey::from_bytes(&[7u8; 32]); CodingOp { alg: ALG_ED25519, prior: ContentId(format!("b3:{prior}")), edits: vec![Edit { path: "src/lib.rs".into(), anchor: Anchor::ByteRange { start: 0, end: 0 }, replacement: b"pub fn add() {}".to_vec(), }], author: sk.verifying_key().to_bytes(), intent: "add add()".into(), tier: Tier::L3, } } #[test] fn sign_then_verify_roundtrips() { let sk = SigningKey::from_bytes(&[7u8; 32]); let op = sample("AAAA"); let prior = op.prior.clone(); let signed = op.sign(&sk).unwrap(); assert!(signed.verify(&prior).is_ok()); } #[test] fn replay_onto_different_base_rejected() { let sk = SigningKey::from_bytes(&[7u8; 32]); let signed = sample("AAAA").sign(&sk).unwrap(); let other = ContentId("b3:CCCC".into()); assert!(signed.verify(&other).is_err()); } #[test] fn overlapping_edits_have_no_op_id() { let mut op = sample("AAAA"); op.edits = vec![ Edit { path: "f".into(), anchor: Anchor::ByteRange { start: 0, end: 10 }, replacement: vec![1] }, Edit { path: "f".into(), anchor: Anchor::ByteRange { start: 5, end: 15 }, replacement: vec![2] }, ]; assert!(matches!(op.op_id(), Err(CargoError::EditsOverlap))); } #[test] fn unknown_alg_fails_closed() { let mut op = sample("AAAA"); op.alg = 0xFF; assert!(matches!(op.op_id(), Err(CargoError::UnknownAlgorithm(0xFF)))); } }