use std::collections::BTreeSet;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
pub const RESERVED_SIGNER_PUBKEY_COL: &str = "signer_pubkey";
pub const RESERVED_SIGNATURE_COL: &str = "signature";
pub const SIGNER_PUBKEY_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignedWriteError {
MissingSignatureFields { fields: Vec<&'static str> },
UnknownSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
RevokedSigner { pubkey: [u8; SIGNER_PUBKEY_LEN] },
InvalidSignature,
MalformedSignerPubkey,
MalformedSignature,
}
impl std::fmt::Display for SignedWriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingSignatureFields { fields } => {
write!(f, "MissingSignatureFields: {}", fields.join(", "))
}
Self::UnknownSigner { .. } => f.write_str("UnknownSigner"),
Self::RevokedSigner { .. } => f.write_str("RevokedSigner"),
Self::InvalidSignature => f.write_str("InvalidSignature"),
Self::MalformedSignerPubkey => f.write_str("MalformedSignerPubkey"),
Self::MalformedSignature => f.write_str("MalformedSignature"),
}
}
}
impl std::error::Error for SignedWriteError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignerHistoryAction {
Add,
Revoke,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignerHistoryEntry {
pub action: SignerHistoryAction,
pub pubkey: [u8; SIGNER_PUBKEY_LEN],
pub actor: String,
pub ts_unix_ms: u128,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SignerRegistry {
allowed: BTreeSet<[u8; SIGNER_PUBKEY_LEN]>,
history: Vec<SignerHistoryEntry>,
}
impl SignerRegistry {
pub fn from_initial(
initial: &[[u8; SIGNER_PUBKEY_LEN]],
actor: impl Into<String>,
ts_unix_ms: u128,
) -> Self {
let actor = actor.into();
let mut reg = Self::default();
for pk in initial {
if reg.allowed.insert(*pk) {
reg.history.push(SignerHistoryEntry {
action: SignerHistoryAction::Add,
pubkey: *pk,
actor: actor.clone(),
ts_unix_ms,
});
}
}
reg
}
pub fn from_persisted_parts(
allowed: Vec<[u8; SIGNER_PUBKEY_LEN]>,
history: Vec<SignerHistoryEntry>,
) -> Self {
Self {
allowed: allowed.into_iter().collect(),
history,
}
}
pub fn allowed(&self) -> impl Iterator<Item = &[u8; SIGNER_PUBKEY_LEN]> {
self.allowed.iter()
}
pub fn allowed_len(&self) -> usize {
self.allowed.len()
}
pub fn history(&self) -> &[SignerHistoryEntry] {
&self.history
}
pub fn is_allowed(&self, pubkey: &[u8; SIGNER_PUBKEY_LEN]) -> bool {
self.allowed.contains(pubkey)
}
pub fn ever_added(&self, pubkey: &[u8; SIGNER_PUBKEY_LEN]) -> bool {
self.history
.iter()
.any(|e| e.action == SignerHistoryAction::Add && &e.pubkey == pubkey)
}
pub fn add_signer(
&mut self,
pubkey: [u8; SIGNER_PUBKEY_LEN],
actor: impl Into<String>,
ts_unix_ms: u128,
) -> bool {
if !self.allowed.insert(pubkey) {
return false;
}
self.history.push(SignerHistoryEntry {
action: SignerHistoryAction::Add,
pubkey,
actor: actor.into(),
ts_unix_ms,
});
true
}
pub fn revoke_signer(
&mut self,
pubkey: &[u8; SIGNER_PUBKEY_LEN],
actor: impl Into<String>,
ts_unix_ms: u128,
) -> bool {
if !self.allowed.remove(pubkey) {
return false;
}
self.history.push(SignerHistoryEntry {
action: SignerHistoryAction::Revoke,
pubkey: *pubkey,
actor: actor.into(),
ts_unix_ms,
});
true
}
}
#[derive(Debug, Clone, Default)]
pub struct InsertSignatureFields<'a> {
pub signer_pubkey: Option<&'a [u8]>,
pub signature: Option<&'a [u8]>,
}
pub fn verify_insert(
registry: &SignerRegistry,
fields: &InsertSignatureFields<'_>,
canonical_payload: &[u8],
) -> Result<(), SignedWriteError> {
let mut missing: Vec<&'static str> = Vec::new();
if fields.signer_pubkey.is_none() {
missing.push(RESERVED_SIGNER_PUBKEY_COL);
}
if fields.signature.is_none() {
missing.push(RESERVED_SIGNATURE_COL);
}
if !missing.is_empty() {
return Err(SignedWriteError::MissingSignatureFields { fields: missing });
}
let pubkey_bytes = fields.signer_pubkey.unwrap();
let sig_bytes = fields.signature.unwrap();
let pubkey_arr: [u8; SIGNER_PUBKEY_LEN] = pubkey_bytes
.try_into()
.map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
if sig_bytes.len() != SIGNATURE_LEN {
return Err(SignedWriteError::MalformedSignature);
}
let sig_arr: [u8; SIGNATURE_LEN] = sig_bytes
.try_into()
.map_err(|_| SignedWriteError::MalformedSignature)?;
if !registry.is_allowed(&pubkey_arr) {
return Err(if registry.ever_added(&pubkey_arr) {
SignedWriteError::RevokedSigner { pubkey: pubkey_arr }
} else {
SignedWriteError::UnknownSigner { pubkey: pubkey_arr }
});
}
let vk = VerifyingKey::from_bytes(&pubkey_arr)
.map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
let signature = Signature::from_bytes(&sig_arr);
vk.verify(canonical_payload, &signature)
.map_err(|_| SignedWriteError::InvalidSignature)
}
pub fn reverify_row(
signer_pubkey: &[u8; SIGNER_PUBKEY_LEN],
signature: &[u8; SIGNATURE_LEN],
canonical_payload: &[u8],
) -> Result<(), SignedWriteError> {
let vk = VerifyingKey::from_bytes(signer_pubkey)
.map_err(|_| SignedWriteError::MalformedSignerPubkey)?;
let sig = Signature::from_bytes(signature);
vk.verify(canonical_payload, &sig)
.map_err(|_| SignedWriteError::InvalidSignature)
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
fn fixed_signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn pubkey_bytes(sk: &SigningKey) -> [u8; SIGNER_PUBKEY_LEN] {
sk.verifying_key().to_bytes()
}
#[test]
fn from_initial_seeds_history_and_allowed_set() {
let sk_a = fixed_signing_key(1);
let sk_b = fixed_signing_key(2);
let reg = SignerRegistry::from_initial(
&[pubkey_bytes(&sk_a), pubkey_bytes(&sk_b)],
"@system/create-collection",
10,
);
assert_eq!(reg.allowed_len(), 2);
assert_eq!(reg.history().len(), 2);
assert!(reg
.history()
.iter()
.all(|h| h.action == SignerHistoryAction::Add && h.actor == "@system/create-collection"
&& h.ts_unix_ms == 10));
}
#[test]
fn add_signer_is_idempotent() {
let sk = fixed_signing_key(7);
let pk = pubkey_bytes(&sk);
let mut reg = SignerRegistry::default();
assert!(reg.add_signer(pk, "alice", 1));
assert!(!reg.add_signer(pk, "alice-again", 2)); assert_eq!(reg.history().len(), 1);
}
#[test]
fn revoke_signer_records_history_and_blocks_future_inserts() {
let sk = fixed_signing_key(3);
let pk = pubkey_bytes(&sk);
let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
assert!(reg.is_allowed(&pk));
assert!(reg.revoke_signer(&pk, "bob-admin", 100));
assert!(!reg.is_allowed(&pk));
assert!(reg.ever_added(&pk));
assert_eq!(reg.history().len(), 2);
assert_eq!(reg.history()[1].action, SignerHistoryAction::Revoke);
assert!(!reg.revoke_signer(&pk, "bob-admin", 200));
}
#[test]
fn missing_fields_lists_both_missing() {
let reg = SignerRegistry::default();
let err = verify_insert(®, &InsertSignatureFields::default(), b"payload").unwrap_err();
match err {
SignedWriteError::MissingSignatureFields { fields } => {
assert!(fields.contains(&RESERVED_SIGNER_PUBKEY_COL));
assert!(fields.contains(&RESERVED_SIGNATURE_COL));
}
other => panic!("expected MissingSignatureFields, got {other:?}"),
}
}
#[test]
fn missing_signature_only_is_reported() {
let sk = fixed_signing_key(5);
let pk = pubkey_bytes(&sk);
let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: None,
},
b"x",
)
.unwrap_err();
assert!(matches!(
err,
SignedWriteError::MissingSignatureFields { ref fields }
if fields == &vec![RESERVED_SIGNATURE_COL]
));
}
#[test]
fn unknown_signer_rejected() {
let sk_allowed = fixed_signing_key(1);
let sk_stranger = fixed_signing_key(2);
let reg = SignerRegistry::from_initial(&[pubkey_bytes(&sk_allowed)], "@system", 0);
let payload = b"hello";
let sig = sk_stranger.sign(payload).to_bytes();
let pk = pubkey_bytes(&sk_stranger);
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap_err();
assert_eq!(err, SignedWriteError::UnknownSigner { pubkey: pk });
}
#[test]
fn revoked_signer_distinguished_from_unknown() {
let sk = fixed_signing_key(9);
let pk = pubkey_bytes(&sk);
let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
assert!(reg.revoke_signer(&pk, "ops", 1));
let payload = b"after-revoke";
let sig = sk.sign(payload).to_bytes();
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap_err();
assert_eq!(err, SignedWriteError::RevokedSigner { pubkey: pk });
}
#[test]
fn valid_signature_accepted() {
let sk = fixed_signing_key(4);
let pk = pubkey_bytes(&sk);
let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
let payload = b"row-canon-bytes";
let sig = sk.sign(payload).to_bytes();
verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap();
}
#[test]
fn tampered_payload_rejected_as_invalid_signature() {
let sk = fixed_signing_key(6);
let pk = pubkey_bytes(&sk);
let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
let signed_payload = b"original";
let sig = sk.sign(signed_payload).to_bytes();
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
b"tampered",
)
.unwrap_err();
assert_eq!(err, SignedWriteError::InvalidSignature);
}
#[test]
fn malformed_signature_length() {
let sk = fixed_signing_key(8);
let pk = pubkey_bytes(&sk);
let reg = SignerRegistry::from_initial(&[pk], "@system", 0);
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&[0u8; 10][..]),
},
b"x",
)
.unwrap_err();
assert_eq!(err, SignedWriteError::MalformedSignature);
}
#[test]
fn malformed_signer_pubkey_length() {
let reg = SignerRegistry::default();
let err = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&[0u8; 7][..]),
signature: Some(&[0u8; SIGNATURE_LEN][..]),
},
b"x",
)
.unwrap_err();
assert_eq!(err, SignedWriteError::MalformedSignerPubkey);
}
#[test]
fn past_record_re_verifies_after_signer_revoked() {
let sk = fixed_signing_key(11);
let pk = pubkey_bytes(&sk);
let payload = b"committed-row";
let sig = sk.sign(payload).to_bytes();
let mut reg = SignerRegistry::from_initial(&[pk], "@system", 0);
verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap();
reg.revoke_signer(&pk, "ops", 999);
let blocked = verify_insert(
®,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap_err();
assert_eq!(blocked, SignedWriteError::RevokedSigner { pubkey: pk });
reverify_row(&pk, &sig, payload).unwrap();
}
#[test]
fn error_display_strings_are_stable() {
assert_eq!(
SignedWriteError::UnknownSigner { pubkey: [0u8; 32] }.to_string(),
"UnknownSigner"
);
assert_eq!(
SignedWriteError::RevokedSigner { pubkey: [0u8; 32] }.to_string(),
"RevokedSigner"
);
assert_eq!(
SignedWriteError::InvalidSignature.to_string(),
"InvalidSignature"
);
assert_eq!(
SignedWriteError::MalformedSignature.to_string(),
"MalformedSignature"
);
assert_eq!(
SignedWriteError::MalformedSignerPubkey.to_string(),
"MalformedSignerPubkey"
);
assert_eq!(
SignedWriteError::MissingSignatureFields {
fields: vec![RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL],
}
.to_string(),
format!(
"MissingSignatureFields: {}, {}",
RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL
),
);
}
}