use crate::storage::schema::Value;
use crate::storage::signed_writes::{
verify_insert, InsertSignatureFields, SignedWriteError, SignerHistoryAction,
SignerHistoryEntry, SignerRegistry, RESERVED_SIGNATURE_COL, RESERVED_SIGNER_PUBKEY_COL,
SIGNATURE_LEN, SIGNER_PUBKEY_LEN,
};
use crate::storage::unified::UnifiedStore;
use std::time::{SystemTime, UNIX_EPOCH};
const ENABLED_SUFFIX: &str = "signed_writes.enabled";
const ALLOWED_SUFFIX: &str = "signed_writes.allowed_json";
const HISTORY_SUFFIX: &str = "signed_writes.history_json";
fn key(name: &str, suffix: &str) -> String {
format!("red.collection.{name}.{suffix}")
}
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn hex_decode_32(s: &str) -> Option<[u8; SIGNER_PUBKEY_LEN]> {
if s.len() != SIGNER_PUBKEY_LEN * 2 {
return None;
}
let mut out = [0u8; SIGNER_PUBKEY_LEN];
for i in 0..SIGNER_PUBKEY_LEN {
out[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}
fn action_str(a: SignerHistoryAction) -> &'static str {
match a {
SignerHistoryAction::Add => "add",
SignerHistoryAction::Revoke => "revoke",
}
}
fn action_from_str(s: &str) -> Option<SignerHistoryAction> {
match s {
"add" => Some(SignerHistoryAction::Add),
"revoke" => Some(SignerHistoryAction::Revoke),
_ => None,
}
}
fn entry_to_json(e: &SignerHistoryEntry) -> crate::serde_json::Value {
let mut obj = crate::serde_json::Map::new();
obj.insert(
"action".to_string(),
crate::serde_json::Value::String(action_str(e.action).to_string()),
);
obj.insert(
"pubkey".to_string(),
crate::serde_json::Value::String(hex_encode(&e.pubkey)),
);
obj.insert(
"actor".to_string(),
crate::serde_json::Value::String(e.actor.clone()),
);
obj.insert(
"ts_unix_ms".to_string(),
crate::serde_json::Value::Number(e.ts_unix_ms as f64),
);
crate::serde_json::Value::Object(obj)
}
fn entry_from_json(v: &crate::serde_json::Value) -> Option<SignerHistoryEntry> {
let obj = v.as_object()?;
let action = action_from_str(obj.get("action")?.as_str()?)?;
let pubkey = hex_decode_32(obj.get("pubkey")?.as_str()?)?;
let actor = obj.get("actor")?.as_str()?.to_string();
let ts_unix_ms = obj.get("ts_unix_ms")?.as_u64()? as u128;
Some(SignerHistoryEntry {
action,
pubkey,
actor,
ts_unix_ms,
})
}
pub fn is_signed(store: &UnifiedStore, collection: &str) -> bool {
matches!(
store.get_config(&key(collection, ENABLED_SUFFIX)),
Some(Value::Boolean(true)) | Some(Value::Text(_))
)
}
pub fn install(
store: &UnifiedStore,
collection: &str,
initial: &[[u8; SIGNER_PUBKEY_LEN]],
actor: &str,
) {
if is_signed(store, collection) {
return;
}
let reg = SignerRegistry::from_initial(initial, actor.to_string(), now_ms());
write_registry(store, collection, ®);
store.set_config_tree(
&key(collection, ENABLED_SUFFIX),
&crate::serde_json::Value::Bool(true),
);
}
fn write_registry(store: &UnifiedStore, collection: &str, reg: &SignerRegistry) {
let allowed: Vec<crate::serde_json::Value> = reg
.allowed()
.map(|pk| crate::serde_json::Value::String(hex_encode(pk)))
.collect();
let history: Vec<crate::serde_json::Value> =
reg.history().iter().map(entry_to_json).collect();
store.set_config_tree(
&key(collection, ALLOWED_SUFFIX),
&crate::serde_json::Value::String(crate::serde_json::Value::Array(allowed).to_string()),
);
store.set_config_tree(
&key(collection, HISTORY_SUFFIX),
&crate::serde_json::Value::String(crate::serde_json::Value::Array(history).to_string()),
);
}
fn read_latest_config(store: &UnifiedStore, full_key: &str) -> Option<Value> {
let manager = store.get_collection("red_config")?;
let mut all = manager.query_all(|_| true);
all.sort_by(|a, b| b.id.raw().cmp(&a.id.raw()));
for entity in all {
let crate::storage::unified::EntityData::Row(row) = &entity.data else {
continue;
};
let Some(named) = &row.named else { continue };
let matches = matches!(
named.get("key"),
Some(Value::Text(s)) if s.as_ref() == full_key
);
if matches {
return named.get("value").cloned();
}
}
None
}
fn read_registry(store: &UnifiedStore, collection: &str) -> SignerRegistry {
let allowed_json = match read_latest_config(store, &key(collection, ALLOWED_SUFFIX)) {
Some(Value::Text(s)) => s.to_string(),
_ => "[]".to_string(),
};
let history_json = match read_latest_config(store, &key(collection, HISTORY_SUFFIX)) {
Some(Value::Text(s)) => s.to_string(),
_ => "[]".to_string(),
};
let parsed_allowed: Vec<[u8; SIGNER_PUBKEY_LEN]> = match crate::utils::json::parse_json(
&allowed_json,
) {
Ok(v) => match crate::serde_json::Value::from(v) {
crate::serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str().and_then(hex_decode_32))
.collect(),
_ => Vec::new(),
},
Err(_) => Vec::new(),
};
let parsed_history: Vec<SignerHistoryEntry> = match crate::utils::json::parse_json(
&history_json,
) {
Ok(v) => match crate::serde_json::Value::from(v) {
crate::serde_json::Value::Array(arr) => {
arr.iter().filter_map(entry_from_json).collect()
}
_ => Vec::new(),
},
Err(_) => Vec::new(),
};
SignerRegistry::from_persisted_parts(parsed_allowed, parsed_history)
}
pub fn registry(store: &UnifiedStore, collection: &str) -> SignerRegistry {
read_registry(store, collection)
}
pub fn add_signer(
store: &UnifiedStore,
collection: &str,
pubkey: [u8; SIGNER_PUBKEY_LEN],
actor: &str,
) -> bool {
let mut reg = read_registry(store, collection);
let changed = reg.add_signer(pubkey, actor.to_string(), now_ms());
if changed {
write_registry(store, collection, ®);
}
changed
}
pub fn revoke_signer(
store: &UnifiedStore,
collection: &str,
pubkey: &[u8; SIGNER_PUBKEY_LEN],
actor: &str,
) -> bool {
let mut reg = read_registry(store, collection);
let changed = reg.revoke_signer(pubkey, actor.to_string(), now_ms());
if changed {
write_registry(store, collection, ®);
}
changed
}
pub const RESERVED_COLUMNS: &[&str] = &[RESERVED_SIGNER_PUBKEY_COL, RESERVED_SIGNATURE_COL];
pub struct SignerColumn {
pub raw_value: Value,
pub bytes: Vec<u8>,
}
pub fn split_signature_fields(
fields: Vec<(String, Value)>,
) -> (Option<SignerColumn>, Option<SignerColumn>, Vec<(String, Value)>) {
let mut pubkey: Option<SignerColumn> = None;
let mut signature: Option<SignerColumn> = None;
let mut residual: Vec<(String, Value)> = Vec::with_capacity(fields.len());
for (k, v) in fields {
if k == RESERVED_SIGNER_PUBKEY_COL {
let bytes = match &v {
Value::Blob(b) => Some(b.clone()),
Value::Text(s) => decode_hex(s.as_ref()),
_ => None,
};
if let Some(bytes) = bytes {
pubkey = Some(SignerColumn { raw_value: v, bytes });
}
continue;
}
if k == RESERVED_SIGNATURE_COL {
let bytes = match &v {
Value::Blob(b) => Some(b.clone()),
Value::Text(s) => decode_hex(s.as_ref()),
_ => None,
};
if let Some(bytes) = bytes {
signature = Some(SignerColumn { raw_value: v, bytes });
}
continue;
}
residual.push((k, v));
}
(pubkey, signature, residual)
}
fn decode_hex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
for i in (0..s.len()).step_by(2) {
out.push(u8::from_str_radix(&s[i..i + 2], 16).ok()?);
}
Some(out)
}
pub fn verify_row(
registry: &SignerRegistry,
signer_pubkey: Option<&[u8]>,
signature: Option<&[u8]>,
canonical_payload: &[u8],
) -> Result<(), SignedWriteError> {
verify_insert(
registry,
&InsertSignatureFields {
signer_pubkey,
signature,
},
canonical_payload,
)
}
pub fn map_error(err: SignedWriteError) -> crate::api::RedDBError {
let body = match &err {
SignedWriteError::MissingSignatureFields { fields } => {
format!("SignedWriteError:MissingSignatureFields:{}", fields.join(","))
}
SignedWriteError::UnknownSigner { pubkey } => {
format!("SignedWriteError:UnknownSigner:{}", hex_encode(pubkey))
}
SignedWriteError::RevokedSigner { pubkey } => {
format!("SignedWriteError:RevokedSigner:{}", hex_encode(pubkey))
}
SignedWriteError::InvalidSignature => "SignedWriteError:InvalidSignature".to_string(),
SignedWriteError::MalformedSignerPubkey => {
"SignedWriteError:MalformedSignerPubkey".to_string()
}
SignedWriteError::MalformedSignature => "SignedWriteError:MalformedSignature".to_string(),
};
crate::api::RedDBError::InvalidOperation(body)
}
pub const SIGNATURE_BYTES: usize = SIGNATURE_LEN;
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn pubkey_of(sk: &SigningKey) -> [u8; SIGNER_PUBKEY_LEN] {
sk.verifying_key().to_bytes()
}
fn make_store() -> UnifiedStore {
UnifiedStore::new()
}
#[test]
fn install_and_read_roundtrip_preserves_registry() {
let store = make_store();
let pk1 = pubkey_of(&signing_key(1));
let pk2 = pubkey_of(&signing_key(2));
install(&store, "sc", &[pk1, pk2], "@system/create");
assert!(is_signed(&store, "sc"));
let reg = registry(&store, "sc");
assert_eq!(reg.allowed_len(), 2);
assert!(reg.is_allowed(&pk1));
assert!(reg.is_allowed(&pk2));
assert_eq!(reg.history().len(), 2);
}
#[test]
fn add_signer_persists_and_records_history() {
let store = make_store();
let pk1 = pubkey_of(&signing_key(1));
install(&store, "sc", &[pk1], "@system/create");
let pk2 = pubkey_of(&signing_key(2));
assert!(add_signer(&store, "sc", pk2, "admin:alice"));
assert!(!add_signer(&store, "sc", pk2, "admin:alice"));
let reg = registry(&store, "sc");
assert!(reg.is_allowed(&pk2));
assert_eq!(reg.history().len(), 2);
let last = reg.history().last().unwrap();
assert_eq!(last.action, SignerHistoryAction::Add);
assert_eq!(last.actor, "admin:alice");
}
#[test]
fn revoke_signer_blocks_future_inserts_but_history_preserved() {
let store = make_store();
let sk = signing_key(7);
let pk = pubkey_of(&sk);
install(&store, "sc", &[pk], "@system/create");
assert!(revoke_signer(&store, "sc", &pk, "admin:bob"));
let reg = registry(&store, "sc");
assert!(!reg.is_allowed(&pk));
assert!(reg.ever_added(&pk));
let last = reg.history().last().unwrap();
assert_eq!(last.action, SignerHistoryAction::Revoke);
assert_eq!(last.actor, "admin:bob");
}
#[test]
fn split_signature_fields_extracts_blob_columns() {
let fields = vec![
("name".to_string(), Value::text("alice".to_string())),
(RESERVED_SIGNER_PUBKEY_COL.to_string(), Value::Blob(vec![0x11; 32])),
(RESERVED_SIGNATURE_COL.to_string(), Value::Blob(vec![0x22; 64])),
];
let (pk, sig, residual) = split_signature_fields(fields);
assert_eq!(pk.as_ref().unwrap().bytes.len(), 32);
assert!(matches!(pk.unwrap().raw_value, Value::Blob(_)));
assert_eq!(sig.as_ref().unwrap().bytes.len(), 64);
assert!(matches!(sig.unwrap().raw_value, Value::Blob(_)));
assert_eq!(residual.len(), 1);
assert_eq!(residual[0].0, "name");
}
#[test]
fn split_signature_fields_accepts_hex_text() {
let pk_hex = "11".repeat(32);
let sig_hex = "22".repeat(64);
let fields = vec![
(RESERVED_SIGNER_PUBKEY_COL.to_string(), Value::text(pk_hex)),
(RESERVED_SIGNATURE_COL.to_string(), Value::text(sig_hex)),
];
let (pk, sig, residual) = split_signature_fields(fields);
assert_eq!(pk.as_ref().unwrap().bytes, vec![0x11; 32]);
assert!(matches!(pk.unwrap().raw_value, Value::Text(_)));
assert_eq!(sig.as_ref().unwrap().bytes, vec![0x22; 64]);
assert!(matches!(sig.unwrap().raw_value, Value::Text(_)));
assert!(residual.is_empty());
}
#[test]
fn map_error_carries_variant_prefix() {
let pk = [0u8; SIGNER_PUBKEY_LEN];
match map_error(SignedWriteError::UnknownSigner { pubkey: pk }) {
crate::api::RedDBError::InvalidOperation(s) => {
assert!(s.starts_with("SignedWriteError:UnknownSigner"));
}
other => panic!("unexpected mapping: {other:?}"),
}
match map_error(SignedWriteError::InvalidSignature) {
crate::api::RedDBError::InvalidOperation(s) => {
assert_eq!(s, "SignedWriteError:InvalidSignature");
}
other => panic!("unexpected mapping: {other:?}"),
}
}
#[test]
fn verify_row_accepts_valid_signature_over_canonical_payload() {
let sk = signing_key(3);
let pk = pubkey_of(&sk);
let store = make_store();
install(&store, "sc", &[pk], "@system/create");
let payload = b"hello-world";
let sig = sk.sign(payload).to_bytes();
let reg = registry(&store, "sc");
verify_row(®, Some(&pk), Some(&sig), payload).unwrap();
}
#[test]
fn verify_row_rejects_tampered_payload() {
let sk = signing_key(4);
let pk = pubkey_of(&sk);
let store = make_store();
install(&store, "sc", &[pk], "@system/create");
let payload = b"hello-world";
let sig = sk.sign(payload).to_bytes();
let reg = registry(&store, "sc");
let err = verify_row(®, Some(&pk), Some(&sig), b"tampered").unwrap_err();
assert_eq!(err, SignedWriteError::InvalidSignature);
}
}