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 && h.actor == "@system/create-collection"
366                && h.ts_unix_ms == 10));
367    }
368
369    #[test]
370    fn add_signer_is_idempotent() {
371        let sk = fixed_signing_key(7);
372        let pk = pubkey_bytes(&sk);
373        let mut reg = SignerRegistry::default();
374        assert!(reg.add_signer(pk, "alice", 1));
375        assert!(!reg.add_signer(pk, "alice-again", 2)); // dup → no-op
376        assert_eq!(reg.history().len(), 1);
377    }
378
379    #[test]
380    fn revoke_signer_records_history_and_blocks_future_inserts() {
381        let sk = fixed_signing_key(3);
382        let pk = pubkey_bytes(&sk);
383        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
384        assert!(reg.is_allowed(&pk));
385        assert!(reg.revoke_signer(&pk, "bob-admin", 100));
386        assert!(!reg.is_allowed(&pk));
387        assert!(reg.ever_added(&pk));
388        assert_eq!(reg.history().len(), 2);
389        assert_eq!(reg.history()[1].action, SignerHistoryAction::Revoke);
390        // Idempotent revoke of an already-revoked key returns false.
391        assert!(!reg.revoke_signer(&pk, "bob-admin", 200));
392    }
393
394    #[test]
395    fn missing_fields_lists_both_missing() {
396        let reg = SignerRegistry::default();
397        let err = verify_insert(&reg, &InsertSignatureFields::default(), b"payload").unwrap_err();
398        match err {
399            SignedWriteError::MissingSignatureFields { fields } => {
400                assert!(fields.contains(&RESERVED_SIGNER_PUBKEY_COL));
401                assert!(fields.contains(&RESERVED_SIGNATURE_COL));
402            }
403            other => panic!("expected MissingSignatureFields, got {other:?}"),
404        }
405    }
406
407    #[test]
408    fn missing_signature_only_is_reported() {
409        let sk = fixed_signing_key(5);
410        let pk = pubkey_bytes(&sk);
411        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
412        let err = verify_insert(
413            &reg,
414            &InsertSignatureFields {
415                signer_pubkey: Some(&pk),
416                signature: None,
417            },
418            b"x",
419        )
420        .unwrap_err();
421        assert!(matches!(
422            err,
423            SignedWriteError::MissingSignatureFields { ref fields }
424                if fields == &vec![RESERVED_SIGNATURE_COL]
425        ));
426    }
427
428    #[test]
429    fn unknown_signer_rejected() {
430        let sk_allowed = fixed_signing_key(1);
431        let sk_stranger = fixed_signing_key(2);
432        let reg = SignerRegistry::from_initial(&[pubkey_bytes(&sk_allowed)], "@system", 0);
433        let payload = b"hello";
434        let sig = sk_stranger.sign(payload).to_bytes();
435        let pk = pubkey_bytes(&sk_stranger);
436        let err = verify_insert(
437            &reg,
438            &InsertSignatureFields {
439                signer_pubkey: Some(&pk),
440                signature: Some(&sig),
441            },
442            payload,
443        )
444        .unwrap_err();
445        assert_eq!(err, SignedWriteError::UnknownSigner { pubkey: pk });
446    }
447
448    #[test]
449    fn revoked_signer_distinguished_from_unknown() {
450        let sk = fixed_signing_key(9);
451        let pk = pubkey_bytes(&sk);
452        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
453        assert!(reg.revoke_signer(&pk, "ops", 1));
454        let payload = b"after-revoke";
455        let sig = sk.sign(payload).to_bytes();
456        let err = verify_insert(
457            &reg,
458            &InsertSignatureFields {
459                signer_pubkey: Some(&pk),
460                signature: Some(&sig),
461            },
462            payload,
463        )
464        .unwrap_err();
465        assert_eq!(err, SignedWriteError::RevokedSigner { pubkey: pk });
466    }
467
468    #[test]
469    fn valid_signature_accepted() {
470        let sk = fixed_signing_key(4);
471        let pk = pubkey_bytes(&sk);
472        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
473        let payload = b"row-canon-bytes";
474        let sig = sk.sign(payload).to_bytes();
475        verify_insert(
476            &reg,
477            &InsertSignatureFields {
478                signer_pubkey: Some(&pk),
479                signature: Some(&sig),
480            },
481            payload,
482        )
483        .unwrap();
484    }
485
486    #[test]
487    fn tampered_payload_rejected_as_invalid_signature() {
488        let sk = fixed_signing_key(6);
489        let pk = pubkey_bytes(&sk);
490        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
491        let signed_payload = b"original";
492        let sig = sk.sign(signed_payload).to_bytes();
493        let err = verify_insert(
494            &reg,
495            &InsertSignatureFields {
496                signer_pubkey: Some(&pk),
497                signature: Some(&sig),
498            },
499            b"tampered",
500        )
501        .unwrap_err();
502        assert_eq!(err, SignedWriteError::InvalidSignature);
503    }
504
505    #[test]
506    fn malformed_signature_length() {
507        let sk = fixed_signing_key(8);
508        let pk = pubkey_bytes(&sk);
509        let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
510        let err = verify_insert(
511            &reg,
512            &InsertSignatureFields {
513                signer_pubkey: Some(&pk),
514                signature: Some(&[0u8; 10][..]),
515            },
516            b"x",
517        )
518        .unwrap_err();
519        assert_eq!(err, SignedWriteError::MalformedSignature);
520    }
521
522    #[test]
523    fn malformed_signer_pubkey_length() {
524        let reg = SignerRegistry::default();
525        let err = verify_insert(
526            &reg,
527            &InsertSignatureFields {
528                signer_pubkey: Some(&[0u8; 7][..]),
529                signature: Some(&[0u8; SIGNATURE_LEN][..]),
530            },
531            b"x",
532        )
533        .unwrap_err();
534        assert_eq!(err, SignedWriteError::MalformedSignerPubkey);
535    }
536
537    #[test]
538    fn past_record_re_verifies_after_signer_revoked() {
539        // Acceptance: "past records still readable + re-verifiable"
540        // after revoke. `reverify_row` doesn't consult the registry.
541        let sk = fixed_signing_key(11);
542        let pk = pubkey_bytes(&sk);
543        let payload = b"committed-row";
544        let sig = sk.sign(payload).to_bytes();
545
546        let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
547        // Insert succeeded at write time.
548        verify_insert(
549            &reg,
550            &InsertSignatureFields {
551                signer_pubkey: Some(&pk),
552                signature: Some(&sig),
553            },
554            payload,
555        )
556        .unwrap();
557        // Operator revokes the signer later.
558        reg.revoke_signer(&pk, "ops", 999);
559        // Future inserts blocked …
560        let blocked = verify_insert(
561            &reg,
562            &InsertSignatureFields {
563                signer_pubkey: Some(&pk),
564                signature: Some(&sig),
565            },
566            payload,
567        )
568        .unwrap_err();
569        assert_eq!(blocked, SignedWriteError::RevokedSigner { pubkey: pk });
570        // … but the historical row still re-verifies fine.
571        reverify_row(&pk, &sig, payload).unwrap();
572    }
573
574    #[test]
575    fn error_display_strings_are_stable() {
576        // The runtime maps these onto HTTP error bodies; pin the
577        // strings so renaming a variant trips a test.
578        assert_eq!(
579            SignedWriteError::UnknownSigner { pubkey: [0u8; 32] }.to_string(),
580            "UnknownSigner"
581        );
582        assert_eq!(
583            SignedWriteError::RevokedSigner { pubkey: [0u8; 32] }.to_string(),
584            "RevokedSigner"
585        );
586        assert_eq!(
587            SignedWriteError::InvalidSignature.to_string(),
588            "InvalidSignature"
589        );
590        assert_eq!(
591            SignedWriteError::MalformedSignature.to_string(),
592            "MalformedSignature"
593        );
594        assert_eq!(
595            SignedWriteError::MalformedSignerPubkey.to_string(),
596            "MalformedSignerPubkey"
597        );
598        assert_eq!(
599            SignedWriteError::MissingSignatureFields {
600                fields: vec![RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL],
601            }
602            .to_string(),
603            format!(
604                "MissingSignatureFields: {}, {}",
605                RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL
606            ),
607        );
608    }
609}