Skip to main content

mnem_core/
sign.rs

1//! Ed25519 signing + revocation-list verification (SPEC §9).
2//!
3//! # Sign
4//!
5//! [`Signer`] wraps an `ed25519-dalek` signing key. Call
6//! [`Signer::sign_commit`] or [`Signer::sign_operation`] on a mutable
7//! [`Commit`] / [`Operation`]; the method canonicalises the object with
8//! the `signature` field absent (SPEC §9.1), computes the Ed25519
9//! signature over the canonical bytes, and re-attaches a [`Signature`]
10//! map that carries `algo = "ed25519"`, the 32-byte public key, and
11//! the 64-byte signature.
12//!
13//! # Verify
14//!
15//! [`Verifier`] checks a signed object in three stages:
16//!
17//! 1. Algorithm + byte-length gate (`algo == "ed25519"`, 32-byte key,
18//!    64-byte signature).
19//! 2. Ed25519 `verify_strict` over the canonical pre-image - identical
20//!    to what [`Signer`] signed.
21//! 3. Revocation check (SPEC §9.2): if the signing key appears in the
22//!    revocation list passed to [`Verifier::with_revocations`] and the
23//!    object's `time` is strictly greater than the revocation's
24//!    `revoked_at`, reject with [`SignError::RevokedKey`].
25//!
26//! The time-of-use semantics mean signatures produced **at or before**
27//! the revocation moment remain valid (SPEC §9.2: "commits signed by
28//! a since-revoked key whose `time` is at or before `revoked_at`
29//! remain valid").
30
31use bytes::Bytes;
32use ed25519_dalek::{Signature as EdSignature, Signer as _, SigningKey, VerifyingKey};
33use serde::{Deserialize, Serialize};
34
35use crate::codec::to_canonical_bytes;
36use crate::error::{Error, SignError};
37use crate::objects::{Commit, Operation, Signature};
38
39/// The `algo` tag emitted for every mnem signature in this crate.
40pub const ALGO_ED25519: &str = "ed25519";
41
42// ---------------- Signer ----------------
43
44/// An Ed25519 signer. Construct from a 32-byte seed via
45/// [`Signer::from_seed_bytes`]. Real-world callers should generate the
46/// seed from an OS-level CSPRNG (getrandom, `rand_core::OsRng`, …).
47pub struct Signer {
48    inner: SigningKey,
49}
50
51impl Signer {
52    /// Build a signer from a 32-byte Ed25519 seed.
53    ///
54    /// The seed is NOT hashed or stretched; pass a uniformly random
55    /// value from a secure RNG. Deterministic seeds are fine for tests
56    /// and smoke demos; use real entropy in production.
57    #[must_use]
58    pub fn from_seed_bytes(seed: [u8; 32]) -> Self {
59        Self {
60            inner: SigningKey::from_bytes(&seed),
61        }
62    }
63
64    /// The 32-byte Ed25519 public key associated with this signer.
65    #[must_use]
66    pub fn public_key_bytes(&self) -> [u8; 32] {
67        *self.inner.verifying_key().as_bytes()
68    }
69
70    /// Sign a [`Commit`] in place.
71    ///
72    /// Clears any existing `signature`, canonicalises the Commit,
73    /// signs the bytes, and attaches the resulting [`Signature`].
74    ///
75    /// # Errors
76    ///
77    /// Codec errors while canonicalising.
78    pub fn sign_commit(&self, commit: &mut Commit) -> Result<(), Error> {
79        let bytes = canonical_bytes_for_commit(commit)?;
80        let sig = self.inner.sign(&bytes);
81        commit.signature = Some(signature_from_parts(
82            self.inner.verifying_key().as_bytes(),
83            &sig.to_bytes(),
84        ));
85        Ok(())
86    }
87
88    /// Sign an [`Operation`] in place. Same protocol as `sign_commit`.
89    ///
90    /// # Errors
91    ///
92    /// Codec errors while canonicalising.
93    pub fn sign_operation(&self, op: &mut Operation) -> Result<(), Error> {
94        let bytes = canonical_bytes_for_operation(op)?;
95        let sig = self.inner.sign(&bytes);
96        op.signature = Some(signature_from_parts(
97            self.inner.verifying_key().as_bytes(),
98            &sig.to_bytes(),
99        ));
100        Ok(())
101    }
102}
103
104// ---------------- Verifier + Revocation ----------------
105
106/// One entry in the repository's revocation list (SPEC §9.2).
107#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Revocation {
109    /// 32-byte Ed25519 public key being revoked.
110    pub public_key: Bytes,
111    /// Microseconds since Unix epoch - the instant the key became
112    /// distrusted. Signatures at `time <= revoked_at` remain valid;
113    /// signatures at `time > revoked_at` are rejected.
114    pub revoked_at: u64,
115    /// Optional free-form rationale (e.g. `"compromised"`, `"rotated"`).
116    pub reason: String,
117}
118
119/// Checks signatures against a (possibly empty) revocation list.
120///
121/// `Verifier::new()` is the trust-everything-algorithmically form.
122/// Repositories with a security posture declare their revocation list
123/// in `.mnem/config.cbor` and load it here via
124/// [`Verifier::with_revocations`].
125#[derive(Debug, Default)]
126pub struct Verifier {
127    revocations: Vec<Revocation>,
128}
129
130impl Verifier {
131    /// Verifier with no revocations.
132    #[must_use]
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Verifier seeded with a revocation list.
138    #[must_use]
139    pub const fn with_revocations(revocations: Vec<Revocation>) -> Self {
140        Self { revocations }
141    }
142
143    /// Verify a [`Commit`]'s signature.
144    ///
145    /// # Errors
146    ///
147    /// Returns the matching [`SignError`] variant on any failure in
148    /// algorithm gate, signature verification, or revocation check.
149    pub fn verify_commit(&self, commit: &Commit) -> Result<(), SignError> {
150        let sig = commit.signature.as_ref().ok_or(SignError::NoSignature)?;
151        let (vk, ed_sig) = extract_verifier_inputs(sig)?;
152        let bytes =
153            canonical_bytes_for_commit(commit).map_err(|e| SignError::Encoding(e.to_string()))?;
154        vk.verify_strict(&bytes, &ed_sig)
155            .map_err(|_| SignError::InvalidSignature)?;
156        self.check_revocation(sig.public_key.as_ref(), commit.time)
157    }
158
159    /// Verify an [`Operation`]'s signature.
160    ///
161    /// # Errors
162    ///
163    /// See [`Verifier::verify_commit`].
164    pub fn verify_operation(&self, op: &Operation) -> Result<(), SignError> {
165        let sig = op.signature.as_ref().ok_or(SignError::NoSignature)?;
166        let (vk, ed_sig) = extract_verifier_inputs(sig)?;
167        let bytes =
168            canonical_bytes_for_operation(op).map_err(|e| SignError::Encoding(e.to_string()))?;
169        vk.verify_strict(&bytes, &ed_sig)
170            .map_err(|_| SignError::InvalidSignature)?;
171        self.check_revocation(sig.public_key.as_ref(), op.time)
172    }
173
174    fn check_revocation(&self, public_key: &[u8], time: u64) -> Result<(), SignError> {
175        for rev in &self.revocations {
176            if rev.public_key.as_ref() == public_key && time > rev.revoked_at {
177                return Err(SignError::RevokedKey {
178                    revoked_at: rev.revoked_at,
179                    time,
180                });
181            }
182        }
183        Ok(())
184    }
185}
186
187// ---------------- Helpers ----------------
188
189fn signature_from_parts(public_key: &[u8; 32], sig: &[u8; 64]) -> Signature {
190    Signature {
191        algo: ALGO_ED25519.into(),
192        public_key: Bytes::copy_from_slice(public_key),
193        sig: Bytes::copy_from_slice(sig),
194    }
195}
196
197fn extract_verifier_inputs(sig: &Signature) -> Result<(VerifyingKey, EdSignature), SignError> {
198    if sig.algo != ALGO_ED25519 {
199        return Err(SignError::WrongAlgorithm {
200            got: sig.algo.clone(),
201        });
202    }
203    let pk_arr: [u8; 32] = sig
204        .public_key
205        .as_ref()
206        .try_into()
207        .map_err(|_| SignError::MalformedKey)?;
208    let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| SignError::MalformedKey)?;
209    let sig_arr: [u8; 64] = sig
210        .sig
211        .as_ref()
212        .try_into()
213        .map_err(|_| SignError::MalformedSignature)?;
214    let ed_sig = EdSignature::from_bytes(&sig_arr);
215    Ok((vk, ed_sig))
216}
217
218fn canonical_bytes_for_commit(commit: &Commit) -> Result<Vec<u8>, Error> {
219    let mut c = commit.clone();
220    c.signature = None;
221    Ok(to_canonical_bytes(&c)?.to_vec())
222}
223
224fn canonical_bytes_for_operation(op: &Operation) -> Result<Vec<u8>, Error> {
225    let mut o = op.clone();
226    o.signature = None;
227    Ok(to_canonical_bytes(&o)?.to_vec())
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::id::{CODEC_RAW, ChangeId, Cid, Multihash};
234
235    fn raw(n: u32) -> Cid {
236        Cid::new(CODEC_RAW, Multihash::sha2_256(&n.to_be_bytes()))
237    }
238
239    fn sample_commit(time: u64) -> Commit {
240        Commit::new(
241            ChangeId::from_bytes_raw([1u8; 16]),
242            raw(1),
243            raw(2),
244            raw(3),
245            "alice@example.org",
246            time,
247            "init",
248        )
249    }
250
251    fn sample_operation(time: u64) -> Operation {
252        Operation::new(raw(1), "alice@example.org", time, "commit: init")
253    }
254
255    #[test]
256    fn sign_then_verify_commit() {
257        let signer = Signer::from_seed_bytes([0x42u8; 32]);
258        let mut c = sample_commit(1_000);
259        signer.sign_commit(&mut c).unwrap();
260        Verifier::new().verify_commit(&c).unwrap();
261    }
262
263    #[test]
264    fn sign_then_verify_operation() {
265        let signer = Signer::from_seed_bytes([0x21u8; 32]);
266        let mut op = sample_operation(1_000);
267        signer.sign_operation(&mut op).unwrap();
268        Verifier::new().verify_operation(&op).unwrap();
269    }
270
271    #[test]
272    fn verify_with_no_signature_errors() {
273        let c = sample_commit(1_000);
274        let err = Verifier::new().verify_commit(&c).unwrap_err();
275        assert!(matches!(err, SignError::NoSignature));
276    }
277
278    #[test]
279    fn tampered_commit_fails_verify() {
280        let signer = Signer::from_seed_bytes([0x42u8; 32]);
281        let mut c = sample_commit(1_000);
282        signer.sign_commit(&mut c).unwrap();
283        // Tamper: change message post-sign.
284        c.message = "I am a thief".into();
285        let err = Verifier::new().verify_commit(&c).unwrap_err();
286        assert!(matches!(err, SignError::InvalidSignature));
287    }
288
289    #[test]
290    fn wrong_algorithm_rejected() {
291        let signer = Signer::from_seed_bytes([0x1u8; 32]);
292        let mut c = sample_commit(1_000);
293        signer.sign_commit(&mut c).unwrap();
294        // Swap algo tag.
295        let sig = c.signature.as_mut().unwrap();
296        sig.algo = "rsa".into();
297        let err = Verifier::new().verify_commit(&c).unwrap_err();
298        assert!(matches!(err, SignError::WrongAlgorithm { .. }));
299    }
300
301    #[test]
302    fn malformed_key_length_rejected() {
303        let signer = Signer::from_seed_bytes([0x1u8; 32]);
304        let mut c = sample_commit(1_000);
305        signer.sign_commit(&mut c).unwrap();
306        let sig = c.signature.as_mut().unwrap();
307        sig.public_key = Bytes::from(vec![0u8; 16]); // too short
308        let err = Verifier::new().verify_commit(&c).unwrap_err();
309        assert!(matches!(err, SignError::MalformedKey));
310    }
311
312    #[test]
313    fn revocation_after_commit_time_still_valid() {
314        let signer = Signer::from_seed_bytes([0x42u8; 32]);
315        let mut c = sample_commit(1_000);
316        signer.sign_commit(&mut c).unwrap();
317        let verifier = Verifier::with_revocations(vec![Revocation {
318            public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
319            revoked_at: 2_000, // strictly after c.time
320            reason: "rotated".into(),
321        }]);
322        verifier.verify_commit(&c).unwrap();
323    }
324
325    #[test]
326    fn revocation_before_commit_time_rejects() {
327        let signer = Signer::from_seed_bytes([0x42u8; 32]);
328        let mut c = sample_commit(1_000);
329        signer.sign_commit(&mut c).unwrap();
330        let verifier = Verifier::with_revocations(vec![Revocation {
331            public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
332            revoked_at: 500, // strictly before c.time
333            reason: "compromised".into(),
334        }]);
335        let err = verifier.verify_commit(&c).unwrap_err();
336        match err {
337            SignError::RevokedKey { revoked_at, time } => {
338                assert_eq!(revoked_at, 500);
339                assert_eq!(time, 1_000);
340            }
341            e => panic!("wrong variant: {e:?}"),
342        }
343    }
344
345    #[test]
346    fn revocation_equals_commit_time_still_valid() {
347        // SPEC §9.2: "signatures whose time <= revoked_at remain valid"
348        let signer = Signer::from_seed_bytes([0x42u8; 32]);
349        let mut c = sample_commit(1_000);
350        signer.sign_commit(&mut c).unwrap();
351        let verifier = Verifier::with_revocations(vec![Revocation {
352            public_key: Bytes::copy_from_slice(&signer.public_key_bytes()),
353            revoked_at: 1_000, // exactly equal
354            reason: "rotated".into(),
355        }]);
356        verifier.verify_commit(&c).unwrap();
357    }
358
359    #[test]
360    fn re_signing_is_idempotent() {
361        // Signing the same commit twice with the same key produces a
362        // valid signature both times.
363        let signer = Signer::from_seed_bytes([0x42u8; 32]);
364        let mut c1 = sample_commit(1_000);
365        signer.sign_commit(&mut c1).unwrap();
366        let mut c2 = sample_commit(1_000);
367        signer.sign_commit(&mut c2).unwrap();
368        Verifier::new().verify_commit(&c1).unwrap();
369        Verifier::new().verify_commit(&c2).unwrap();
370        // Ed25519 is deterministic (RFC 8032), so the signatures match.
371        assert_eq!(c1.signature, c2.signature);
372    }
373}