Skip to main content

reddb_server/storage/
signed_writes.rs

1//! Signed Writes — pure logic for `CREATE COLLECTION ... SIGNED_BY (...)`.
2//!
3//! A collection with a non-empty signer registry rejects every INSERT
4//! that does not carry a valid Ed25519 signature produced by one of the
5//! currently-allowed signer keys. This module provides the
6//! deterministic, side-effect-free pieces of that contract:
7//!
8//! * The reserved column names + byte widths injected on `SIGNED_BY`
9//!   collections.
10//! * A [`SignerRegistry`] holding the *currently* allowed keys plus an
11//!   append-only [`SignerHistoryEntry`] log of admin mutations.
12//! * The [`SignedWriteError`] taxonomy that the engine maps onto HTTP
13//!   400 / 401 responses.
14//! * [`verify_insert`] — the single entry point the insert path will
15//!   call to validate one row.
16//!
17//! Issue #522 wires this into the runtime insert path, REST error
18//! mapping, and the catalog persistence of the registry. This file is
19//! intentionally self-contained so the wiring is a thin adapter on top
20//! of audited logic.
21
22use std::collections::BTreeSet;
23
24use ed25519_dalek::{Signature, Verifier, VerifyingKey};
25
26/// Reserved column auto-added to every signed-writes collection. Holds
27/// the 32-byte Ed25519 public key the signer used to sign the row.
28pub const RESERVED_SIGNER_PUBKEY_COL: &str = "signer_pubkey";
29
30/// Reserved column auto-added to every signed-writes collection. Holds
31/// the 64-byte raw Ed25519 signature over the canonical payload.
32pub const RESERVED_SIGNATURE_COL: &str = "signature";
33
34/// Length of a raw Ed25519 public key, in bytes.
35pub const SIGNER_PUBKEY_LEN: usize = 32;
36
37/// Length of a raw Ed25519 signature, in bytes.
38pub const SIGNATURE_LEN: usize = 64;
39
40/// Failure modes for a signed-writes INSERT. The runtime maps each
41/// variant onto an HTTP status:
42///
43/// | Variant                  | HTTP |
44/// |--------------------------|------|
45/// | `MissingSignatureFields` |  400 |
46/// | `UnknownSigner`          |  401 |
47/// | `RevokedSigner`          |  401 |
48/// | `InvalidSignature`       |  401 |
49/// | `MalformedSignerPubkey`  |  400 |
50/// | `MalformedSignature`     |  400 |
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum SignedWriteError {
53    /// Row omitted `signer_pubkey` and/or `signature` on a collection
54    /// that requires them. Carries the missing column name(s) for the
55    /// error response.
56    MissingSignatureFields { fields: Vec<&'static str> },
57    /// `signer_pubkey` was a valid Ed25519 key but is not in the
58    /// collection's current allowed-signer set AND has never appeared
59    /// in the history — i.e. an entirely unknown key.
60    UnknownSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
61    /// `signer_pubkey` was previously allowed (appears in history with
62    /// an `Add` event) but has since been revoked. Distinguished from
63    /// `UnknownSigner` so operators can tell "never seen" from
64    /// "revoked" in audit logs.
65    RevokedSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
66    /// Signature parsed as 64 bytes but did NOT verify against the
67    /// supplied `signer_pubkey` + canonical payload.
68    InvalidSignature,
69    /// `signer_pubkey` was present but not 32 bytes / not a valid
70    /// Ed25519 public key encoding.
71    MalformedSignerPubkey,
72    /// `signature` was present but not 64 bytes.
73    MalformedSignature,
74}
75
76impl std::fmt::Display for SignedWriteError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::MissingSignatureFields { fields } => {
80                write!(f, "MissingSignatureFields: {}", fields.join(", "))
81            }
82            Self::UnknownSigner { .. } => f.write_str("UnknownSigner"),
83            Self::RevokedSigner { .. } => f.write_str("RevokedSigner"),
84            Self::InvalidSignature => f.write_str("InvalidSignature"),
85            Self::MalformedSignerPubkey => f.write_str("MalformedSignerPubkey"),
86            Self::MalformedSignature => f.write_str("MalformedSignature"),
87        }
88    }
89}
90
91impl std::error::Error for SignedWriteError {}
92
93/// Action recorded in [`SignerRegistry::history`].
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SignerHistoryAction {
96    /// Initial registration at `CREATE COLLECTION` time or via
97    /// `ALTER COLLECTION ... ADD SIGNER`.
98    Add,
99    /// Removed via `ALTER COLLECTION ... REVOKE SIGNER`. Past rows
100    /// signed by this key remain readable and re-verifiable; only
101    /// *new* inserts are rejected.
102    Revoke,
103}
104
105/// One entry in the append-only admin history of a signer registry.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SignerHistoryEntry {
108    pub action: SignerHistoryAction,
109    pub pubkey: [u8; SIGNER_PUBKEY_LEN],
110    /// Principal that performed the mutation. Free-form so the
111    /// caller can pass user IDs, role names, or system markers like
112    /// `"@system/create-collection"` for the genesis entry.
113    pub actor: String,
114    /// Wall-clock ms-since-epoch the engine recorded when applying
115    /// the mutation. The registry never inspects this; it exists for
116    /// audit.
117    pub ts_unix_ms: u128,
118}
119
120/// Mutable signer registry attached to a `SIGNED_BY` collection.
121///
122/// Invariants:
123///
124/// 1. `allowed` is the *exact* set of keys that may produce new
125///    signatures. Empty set ⇒ collection rejects every insert (the
126///    runtime treats an empty `SIGNED_BY` list as a parse error, so
127///    in practice this only happens after every key is revoked —
128///    intentional kill-switch behaviour).
129/// 2. `history` is append-only. `add_signer` / `revoke_signer` push
130///    new entries; nothing ever pops.
131/// 3. `add_signer` of an already-allowed key is a no-op (no history
132///    entry written) so that idempotent DDL replays don't flood the
133///    log. `revoke_signer` of an unknown key returns `false`.
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct SignerRegistry {
136    allowed: BTreeSet<[u8; SIGNER_PUBKEY_LEN]>,
137    history: Vec<SignerHistoryEntry>,
138}
139
140impl SignerRegistry {
141    /// Build a registry from the initial `SIGNED_BY (...)` list parsed
142    /// at `CREATE COLLECTION` time. Each key receives one
143    /// `SignerHistoryAction::Add` entry with the supplied actor /
144    /// timestamp so the audit trail is non-empty from genesis.
145    pub fn from_initial(
146        initial: &[[u8; SIGNER_PUBKEY_LEN]],
147        actor: impl Into<String>,
148        ts_unix_ms: u128,
149    ) -> Self {
150        let actor = actor.into();
151        let mut reg = Self::default();
152        for pk in initial {
153            if reg.allowed.insert(*pk) {
154                reg.history.push(SignerHistoryEntry {
155                    action: SignerHistoryAction::Add,
156                    pubkey: *pk,
157                    actor: actor.clone(),
158                    ts_unix_ms,
159                });
160            }
161        }
162        reg
163    }
164
165    /// Rebuild a registry from previously-persisted state. Used by the
166    /// runtime adapter when loading the registry off `red_config` — the
167    /// caller is responsible for the storage format; this constructor
168    /// only stitches the in-memory invariants back together.
169    pub fn from_persisted_parts(
170        allowed: Vec<[u8; SIGNER_PUBKEY_LEN]>,
171        history: Vec<SignerHistoryEntry>,
172    ) -> Self {
173        Self {
174            allowed: allowed.into_iter().collect(),
175            history,
176        }
177    }
178
179    /// Snapshot of the currently-allowed signers, in stable order.
180    pub fn allowed(&self) -> impl Iterator<Item = &[u8; SIGNER_PUBKEY_LEN]> {
181        self.allowed.iter()
182    }
183
184    pub fn allowed_len(&self) -> usize {
185        self.allowed.len()
186    }
187
188    pub fn history(&self) -> &[SignerHistoryEntry] {
189        &self.history
190    }
191
192    pub fn is_allowed(&self, pubkey: &[u8; SIGNER_PUBKEY_LEN]) -> bool {
193        self.allowed.contains(pubkey)
194    }
195
196    /// Returns true if this key was added at any point in the past
197    /// (even if later revoked). Used by [`verify_insert`] to
198    /// distinguish `UnknownSigner` from `RevokedSigner`.
199    pub fn ever_added(&self, pubkey: &[u8; SIGNER_PUBKEY_LEN]) -> bool {
200        self.history
201            .iter()
202            .any(|e| e.action == SignerHistoryAction::Add && &e.pubkey == pubkey)
203    }
204
205    /// Add `pubkey` to the allowed set. Returns `true` if the key was
206    /// newly added (history entry written), `false` if it was already
207    /// allowed (idempotent no-op).
208    pub fn add_signer(
209        &mut self,
210        pubkey: [u8; SIGNER_PUBKEY_LEN],
211        actor: impl Into<String>,
212        ts_unix_ms: u128,
213    ) -> bool {
214        if !self.allowed.insert(pubkey) {
215            return false;
216        }
217        self.history.push(SignerHistoryEntry {
218            action: SignerHistoryAction::Add,
219            pubkey,
220            actor: actor.into(),
221            ts_unix_ms,
222        });
223        true
224    }
225
226    /// Remove `pubkey` from the allowed set. Returns `true` if the key
227    /// was present (and a `Revoke` history entry written), `false` if
228    /// it was unknown. Past rows signed by `pubkey` remain valid and
229    /// re-verifiable — only future inserts are rejected.
230    pub fn revoke_signer(
231        &mut self,
232        pubkey: &[u8; SIGNER_PUBKEY_LEN],
233        actor: impl Into<String>,
234        ts_unix_ms: u128,
235    ) -> bool {
236        if !self.allowed.remove(pubkey) {
237            return false;
238        }
239        self.history.push(SignerHistoryEntry {
240            action: SignerHistoryAction::Revoke,
241            pubkey: *pubkey,
242            actor: actor.into(),
243            ts_unix_ms,
244        });
245        true
246    }
247}
248
249/// Result of looking at the row-supplied signer + signature columns
250/// before verification. `None` for either side means the caller passed
251/// `NULL` / omitted column entirely.
252#[derive(Debug, Clone, Default)]
253pub struct InsertSignatureFields<'a> {
254    pub signer_pubkey: Option<&'a [u8]>,
255    pub signature: Option<&'a [u8]>,
256}
257
258/// Top-level insert-time verification.
259///
260/// 1. Both columns must be present (else `MissingSignatureFields`).
261/// 2. `signer_pubkey` must be exactly 32 bytes and a valid Ed25519
262///    point encoding (else `MalformedSignerPubkey`).
263/// 3. `signature` must be exactly 64 bytes (else `MalformedSignature`).
264/// 4. `signer_pubkey` must be in the registry's *current* allowed set
265///    (else `UnknownSigner` or `RevokedSigner` depending on history).
266/// 5. The signature must verify against `signer_pubkey` over
267///    `canonical_payload` (else `InvalidSignature`).
268///
269/// `canonical_payload` is the engine's content-hash encoding of the
270/// row WITHOUT the reserved `signer_pubkey` / `signature` columns —
271/// the same bytes the client signed.
272pub fn verify_insert(
273    registry: &SignerRegistry,
274    fields: &InsertSignatureFields<'_>,
275    canonical_payload: &[u8],
276) -> Result<(), SignedWriteError> {
277    let mut missing: Vec<&'static str> = Vec::new();
278    if fields.signer_pubkey.is_none() {
279        missing.push(RESERVED_SIGNER_PUBKEY_COL);
280    }
281    if fields.signature.is_none() {
282        missing.push(RESERVED_SIGNATURE_COL);
283    }
284    if !missing.is_empty() {
285        return Err(SignedWriteError::MissingSignatureFields { fields: missing });
286    }
287    // unwraps safe by the missing-check above.
288    let pubkey_bytes = fields.signer_pubkey.unwrap();
289    let sig_bytes = fields.signature.unwrap();
290
291    let pubkey_arr: [u8; SIGNER_PUBKEY_LEN] = pubkey_bytes
292        .try_into()
293        .map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
294    if sig_bytes.len() != SIGNATURE_LEN {
295        return Err(SignedWriteError::MalformedSignature);
296    }
297    let sig_arr: [u8; SIGNATURE_LEN] = sig_bytes
298        .try_into()
299        .map_err(|_| SignedWriteError::MalformedSignature)?;
300
301    if !registry.is_allowed(&pubkey_arr) {
302        return Err(if registry.ever_added(&pubkey_arr) {
303            SignedWriteError::RevokedSigner { pubkey: pubkey_arr }
304        } else {
305            SignedWriteError::UnknownSigner { pubkey: pubkey_arr }
306        });
307    }
308
309    // Construct the verifying key — if pubkey_arr is not a valid
310    // Ed25519 point encoding, surface MalformedSignerPubkey.
311    let vk = VerifyingKey::from_bytes(&pubkey_arr)
312        .map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
313    let signature = Signature::from_bytes(&sig_arr);
314
315    vk.verify(canonical_payload, &signature)
316        .map_err(|_| SignedWriteError::InvalidSignature)
317}
318
319/// Re-verify a previously-accepted row by its stored
320/// `signer_pubkey` + `signature` + canonical payload. Used by
321/// integrity scans (`/admin/verify-collection`) — does NOT consult the
322/// registry, so rows signed by since-revoked keys still re-verify Ok.
323/// This is the property the issue calls out:
324/// > Insert with revoked signer → 401 RevokedSigner; past records still
325/// > readable + re-verifiable
326pub fn reverify_row(
327    signer_pubkey: &[u8; SIGNER_PUBKEY_LEN],
328    signature: &[u8; SIGNATURE_LEN],
329    canonical_payload: &[u8],
330) -> Result<(), SignedWriteError> {
331    let vk = VerifyingKey::from_bytes(signer_pubkey)
332        .map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
333    let sig = Signature::from_bytes(signature);
334    vk.verify(canonical_payload, &sig)
335        .map_err(|_| SignedWriteError::InvalidSignature)
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use ed25519_dalek::{Signer, SigningKey};
342
343    fn fixed_signing_key(seed: u8) -> SigningKey {
344        SigningKey::from_bytes(&[seed; 32])
345    }
346
347    fn pubkey_bytes(sk: &SigningKey) -> [u8; SIGNER_PUBKEY_LEN] {
348        sk.verifying_key().to_bytes()
349    }
350
351    #[test]
352    fn from_initial_seeds_history_and_allowed_set() {
353        let sk_a = fixed_signing_key(1);
354        let sk_b = fixed_signing_key(2);
355        let reg = SignerRegistry::from_initial(
356            &[pubkey_bytes(&sk_a), pubkey_bytes(&sk_b)],
357            "@system/create-collection",
358            10,
359        );
360        assert_eq!(reg.allowed_len(), 2);
361        assert_eq!(reg.history().len(), 2);
362        assert!(reg
363            .history()
364            .iter()
365            .all(|h| h.action == SignerHistoryAction::Add
366                && h.actor == "@system/create-collection"
367                && h.ts_unix_ms == 10));
368    }
369
370    #[test]
371    fn add_signer_is_idempotent() {
372        let sk = fixed_signing_key(7);
373        let pk = pubkey_bytes(&sk);
374        let mut reg = SignerRegistry::default();
375        assert!(reg.add_signer(pk, "alice", 1));
376        assert!(!reg.add_signer(pk, "alice-again", 2)); // dup → no-op
377        assert_eq!(reg.history().len(), 1);
378    }
379
380    #[test]
381    fn revoke_signer_records_history_and_blocks_future_inserts() {
382        let sk = fixed_signing_key(3);
383        let pk = pubkey_bytes(&sk);
384        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
385        assert!(reg.is_allowed(&pk));
386        assert!(reg.revoke_signer(&pk, "bob-admin", 100));
387        assert!(!reg.is_allowed(&pk));
388        assert!(reg.ever_added(&pk));
389        assert_eq!(reg.history().len(), 2);
390        assert_eq!(reg.history()[1].action, SignerHistoryAction::Revoke);
391        // Idempotent revoke of an already-revoked key returns false.
392        assert!(!reg.revoke_signer(&pk, "bob-admin", 200));
393    }
394
395    #[test]
396    fn missing_fields_lists_both_missing() {
397        let reg = SignerRegistry::default();
398        let err = verify_insert(&reg, &InsertSignatureFields::default(), b"payload").unwrap_err();
399        match err {
400            SignedWriteError::MissingSignatureFields { fields } => {
401                assert!(fields.contains(&RESERVED_SIGNER_PUBKEY_COL));
402                assert!(fields.contains(&RESERVED_SIGNATURE_COL));
403            }
404            other => panic!("expected MissingSignatureFields, got {other:?}"),
405        }
406    }
407
408    #[test]
409    fn missing_signature_only_is_reported() {
410        let sk = fixed_signing_key(5);
411        let pk = pubkey_bytes(&sk);
412        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
413        let err = verify_insert(
414            &reg,
415            &InsertSignatureFields {
416                signer_pubkey: Some(&pk),
417                signature: None,
418            },
419            b"x",
420        )
421        .unwrap_err();
422        assert!(matches!(
423            err,
424            SignedWriteError::MissingSignatureFields { ref fields }
425                if fields == &vec![RESERVED_SIGNATURE_COL]
426        ));
427    }
428
429    #[test]
430    fn unknown_signer_rejected() {
431        let sk_allowed = fixed_signing_key(1);
432        let sk_stranger = fixed_signing_key(2);
433        let reg = SignerRegistry::from_initial(&[pubkey_bytes(&sk_allowed)], "@system", 0);
434        let payload = b"hello";
435        let sig = sk_stranger.sign(payload).to_bytes();
436        let pk = pubkey_bytes(&sk_stranger);
437        let err = verify_insert(
438            &reg,
439            &InsertSignatureFields {
440                signer_pubkey: Some(&pk),
441                signature: Some(&sig),
442            },
443            payload,
444        )
445        .unwrap_err();
446        assert_eq!(err, SignedWriteError::UnknownSigner { pubkey: pk });
447    }
448
449    #[test]
450    fn revoked_signer_distinguished_from_unknown() {
451        let sk = fixed_signing_key(9);
452        let pk = pubkey_bytes(&sk);
453        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
454        assert!(reg.revoke_signer(&pk, "ops", 1));
455        let payload = b"after-revoke";
456        let sig = sk.sign(payload).to_bytes();
457        let err = verify_insert(
458            &reg,
459            &InsertSignatureFields {
460                signer_pubkey: Some(&pk),
461                signature: Some(&sig),
462            },
463            payload,
464        )
465        .unwrap_err();
466        assert_eq!(err, SignedWriteError::RevokedSigner { pubkey: pk });
467    }
468
469    #[test]
470    fn valid_signature_accepted() {
471        let sk = fixed_signing_key(4);
472        let pk = pubkey_bytes(&sk);
473        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
474        let payload = b"row-canon-bytes";
475        let sig = sk.sign(payload).to_bytes();
476        verify_insert(
477            &reg,
478            &InsertSignatureFields {
479                signer_pubkey: Some(&pk),
480                signature: Some(&sig),
481            },
482            payload,
483        )
484        .unwrap();
485    }
486
487    #[test]
488    fn tampered_payload_rejected_as_invalid_signature() {
489        let sk = fixed_signing_key(6);
490        let pk = pubkey_bytes(&sk);
491        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
492        let signed_payload = b"original";
493        let sig = sk.sign(signed_payload).to_bytes();
494        let err = verify_insert(
495            &reg,
496            &InsertSignatureFields {
497                signer_pubkey: Some(&pk),
498                signature: Some(&sig),
499            },
500            b"tampered",
501        )
502        .unwrap_err();
503        assert_eq!(err, SignedWriteError::InvalidSignature);
504    }
505
506    #[test]
507    fn malformed_signature_length() {
508        let sk = fixed_signing_key(8);
509        let pk = pubkey_bytes(&sk);
510        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
511        let err = verify_insert(
512            &reg,
513            &InsertSignatureFields {
514                signer_pubkey: Some(&pk),
515                signature: Some(&[0u8; 10][..]),
516            },
517            b"x",
518        )
519        .unwrap_err();
520        assert_eq!(err, SignedWriteError::MalformedSignature);
521    }
522
523    #[test]
524    fn malformed_signer_pubkey_length() {
525        let reg = SignerRegistry::default();
526        let err = verify_insert(
527            &reg,
528            &InsertSignatureFields {
529                signer_pubkey: Some(&[0u8; 7][..]),
530                signature: Some(&[0u8; SIGNATURE_LEN][..]),
531            },
532            b"x",
533        )
534        .unwrap_err();
535        assert_eq!(err, SignedWriteError::MalformedSignerPubkey);
536    }
537
538    #[test]
539    fn past_record_re_verifies_after_signer_revoked() {
540        // Acceptance: "past records still readable + re-verifiable"
541        // after revoke. `reverify_row` doesn't consult the registry.
542        let sk = fixed_signing_key(11);
543        let pk = pubkey_bytes(&sk);
544        let payload = b"committed-row";
545        let sig = sk.sign(payload).to_bytes();
546
547        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
548        // Insert succeeded at write time.
549        verify_insert(
550            &reg,
551            &InsertSignatureFields {
552                signer_pubkey: Some(&pk),
553                signature: Some(&sig),
554            },
555            payload,
556        )
557        .unwrap();
558        // Operator revokes the signer later.
559        reg.revoke_signer(&pk, "ops", 999);
560        // Future inserts blocked …
561        let blocked = verify_insert(
562            &reg,
563            &InsertSignatureFields {
564                signer_pubkey: Some(&pk),
565                signature: Some(&sig),
566            },
567            payload,
568        )
569        .unwrap_err();
570        assert_eq!(blocked, SignedWriteError::RevokedSigner { pubkey: pk });
571        // … but the historical row still re-verifies fine.
572        reverify_row(&pk, &sig, payload).unwrap();
573    }
574
575    #[test]
576    fn error_display_strings_are_stable() {
577        // The runtime maps these onto HTTP error bodies; pin the
578        // strings so renaming a variant trips a test.
579        assert_eq!(
580            SignedWriteError::UnknownSigner { pubkey: [0u8; 32] }.to_string(),
581            "UnknownSigner"
582        );
583        assert_eq!(
584            SignedWriteError::RevokedSigner { pubkey: [0u8; 32] }.to_string(),
585            "RevokedSigner"
586        );
587        assert_eq!(
588            SignedWriteError::InvalidSignature.to_string(),
589            "InvalidSignature"
590        );
591        assert_eq!(
592            SignedWriteError::MalformedSignature.to_string(),
593            "MalformedSignature"
594        );
595        assert_eq!(
596            SignedWriteError::MalformedSignerPubkey.to_string(),
597            "MalformedSignerPubkey"
598        );
599        assert_eq!(
600            SignedWriteError::MissingSignatureFields {
601                fields: vec![RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL],
602            }
603            .to_string(),
604            format!(
605                "MissingSignatureFields: {}, {}",
606                RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL
607            ),
608        );
609    }
610}