1use std::collections::BTreeSet;
23
24use ed25519_dalek::{Signature, Verifier, VerifyingKey};
25
26pub const RESERVED_SIGNER_PUBKEY_COL: &str = "signer_pubkey";
29
30pub const RESERVED_SIGNATURE_COL: &str = "signature";
33
34pub const SIGNER_PUBKEY_LEN: usize = 32;
36
37pub const SIGNATURE_LEN: usize = 64;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum SignedWriteError {
53 MissingSignatureFields { fields: Vec<&'static str> },
57 UnknownSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
61 RevokedSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
66 InvalidSignature,
69 MalformedSignerPubkey,
72 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SignerHistoryAction {
96 Add,
99 Revoke,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SignerHistoryEntry {
108 pub action: SignerHistoryAction,
109 pub pubkey: [u8; SIGNER_PUBKEY_LEN],
110 pub actor: String,
114 pub ts_unix_ms: u128,
118}
119
120#[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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
253pub struct InsertSignatureFields<'a> {
254 pub signer_pubkey: Option<&'a [u8]>,
255 pub signature: Option<&'a [u8]>,
256}
257
258pub 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 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 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
319pub 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)); 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 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(®, &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 ®,
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 ®,
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 ®,
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 ®,
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 ®,
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 ®,
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 ®,
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 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 verify_insert(
549 ®,
550 &InsertSignatureFields {
551 signer_pubkey: Some(&pk),
552 signature: Some(&sig),
553 },
554 payload,
555 )
556 .unwrap();
557 reg.revoke_signer(&pk, "ops", 999);
559 let blocked = verify_insert(
561 ®,
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 reverify_row(&pk, &sig, payload).unwrap();
572 }
573
574 #[test]
575 fn error_display_strings_are_stable() {
576 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}