From 7c831ea27ee0680deba536203ce176bbe77f3688 Mon Sep 17 00:00:00 2001 From: GoDeskio Date: Thu, 5 Nov 2026 18:41:51 +0800 Subject: [PATCH] feat: land sign for PR #16 --- crates/encryptoglyph/Cargo.toml | 1 + crates/encryptoglyph/src/lib.rs | 1 + crates/encryptoglyph/src/sign.rs | 38 ++++++++++++++++++++++++++++++ crates/encryptoglyph/tests/sign.rs | 23 ++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 crates/encryptoglyph/src/sign.rs create mode 100644 crates/encryptoglyph/tests/sign.rs diff --git a/crates/encryptoglyph/Cargo.toml b/crates/encryptoglyph/Cargo.toml index a70ebdc..c3d8cf7 100644 --- a/crates/encryptoglyph/Cargo.toml +++ b/crates/encryptoglyph/Cargo.toml @@ -14,6 +14,7 @@ default = [] wasm = ["dep:wasm-bindgen", "getrandom/js"] [dependencies] +ed25519-dalek = { version = "2", features = ["rand_core", "zeroize"] } chacha20poly1305 = "0.10" aes-gcm = "0.10" hkdf = "0.12" diff --git a/crates/encryptoglyph/src/lib.rs b/crates/encryptoglyph/src/lib.rs index 070e499..2bbf716 100644 --- a/crates/encryptoglyph/src/lib.rs +++ b/crates/encryptoglyph/src/lib.rs @@ -24,6 +24,7 @@ pub mod kek; pub mod index; pub mod stream; pub mod shamir; +pub mod sign; pub const MAGIC: &[u8; 2] = b"EG"; pub const VERSION_LEGACY: u8 = 1; diff --git a/crates/encryptoglyph/src/sign.rs b/crates/encryptoglyph/src/sign.rs new file mode 100644 index 0000000..69dc280 --- /dev/null +++ b/crates/encryptoglyph/src/sign.rs @@ -0,0 +1,38 @@ +//! Ed25519 signing for audit receipts, exports and shred receipts (backlog #16). +//! Signatures are domain-separated: the signed message is `"encryptoglyph/sig/v1" | len(context) u32 BE | context | msg`, +//! so a receipt signature can never be replayed as an export signature. +use crate::Error; +use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}; + +const DOMAIN: &[u8] = b"encryptoglyph/sig/v1"; + +fn framed(context: &str, msg: &[u8]) -> Vec { + let mut v = Vec::with_capacity(DOMAIN.len() + 4 + context.len() + msg.len()); + v.extend_from_slice(DOMAIN); + v.extend_from_slice(&(context.len() as u32).to_be_bytes()); + v.extend_from_slice(context.as_bytes()); + v.extend_from_slice(msg); + v +} + +/// Generate a fresh keypair: (secret seed 32 bytes, public key 32 bytes). +pub fn generate() -> Result<([u8; 32], [u8; 32]), Error> { + let mut seed = [0u8; 32]; + getrandom::getrandom(&mut seed).map_err(|_| Error::InvalidArgument)?; + let sk = SigningKey::from_bytes(&seed); + Ok((seed, sk.verifying_key().to_bytes())) +} + +pub fn sign(seed: &[u8], context: &str, msg: &[u8]) -> Result<[u8; 64], Error> { + let seed: [u8; 32] = seed.try_into().map_err(|_| Error::InvalidKey)?; + Ok(SigningKey::from_bytes(&seed).sign(&framed(context, msg)).to_bytes()) +} + +/// Strict verification (rejects non-canonical and small-order points). +pub fn verify(public: &[u8], context: &str, msg: &[u8], sig: &[u8]) -> Result<(), Error> { + let pk: [u8; 32] = public.try_into().map_err(|_| Error::InvalidKey)?; + let pk = VerifyingKey::from_bytes(&pk).map_err(|_| Error::InvalidKey)?; + let sig = Signature::from_slice(sig).map_err(|_| Error::Decrypt)?; + pk.verify_strict(&framed(context, msg), &sig).map_err(|_| Error::Decrypt)?; + Ok(()) +} diff --git a/crates/encryptoglyph/tests/sign.rs b/crates/encryptoglyph/tests/sign.rs new file mode 100644 index 0000000..4b115d9 --- /dev/null +++ b/crates/encryptoglyph/tests/sign.rs @@ -0,0 +1,23 @@ +use encryptoglyph::sign::{generate, sign, verify}; +use encryptoglyph::Error; + +#[test] +fn sign_verify_and_domain_separation() { + let (sk, pk) = generate().unwrap(); + let s = sign(&sk, "audit-receipt", b"entry 1").unwrap(); + assert!(verify(&pk, "audit-receipt", b"entry 1", &s).is_ok()); + assert_eq!(verify(&pk, "export", b"entry 1", &s), Err(Error::Decrypt)); + assert_eq!(verify(&pk, "audit-receipt", b"entry 2", &s), Err(Error::Decrypt)); + let mut bad = s; + bad[0] ^= 1; + assert_eq!(verify(&pk, "audit-receipt", b"entry 1", &bad), Err(Error::Decrypt)); + let (_, other) = generate().unwrap(); + assert_eq!(verify(&other, "audit-receipt", b"entry 1", &s), Err(Error::Decrypt)); + assert_eq!(sign(&[0u8; 5], "x", b""), Err(Error::InvalidKey)); +} + +#[test] +fn deterministic() { + let sk = [3u8; 32]; + assert_eq!(sign(&sk, "c", b"m").unwrap(), sign(&sk, "c", b"m").unwrap()); +}