use ed25519_dalek::VerifyingKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct ApproverId(pub String);
impl std::fmt::Display for ApproverId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApproverRegistryError {
#[error("approver registry load: {0}")]
Load(String),
#[error("approver `{approver}`: pubkey is not 32 bytes")]
BadKey {
approver: ApproverId,
},
}
pub trait ApproverRegistry: Send + Sync {
fn lookup(&self, approver: &ApproverId) -> Option<VerifyingKey>;
}
pub struct StaticApproverRegistry {
keys: HashMap<ApproverId, VerifyingKey>,
}
impl StaticApproverRegistry {
#[must_use]
pub fn from_map(keys: HashMap<ApproverId, VerifyingKey>) -> Self {
Self { keys }
}
pub fn from_yaml(path: impl AsRef<Path>) -> Result<Self, ApproverRegistryError> {
let body = std::fs::read_to_string(path.as_ref())
.map_err(|e| ApproverRegistryError::Load(format!("read: {e}")))?;
let raw: HashMap<String, String> = serde_yaml::from_str(&body)
.map_err(|e| ApproverRegistryError::Load(format!("parse: {e}")))?;
let mut keys = HashMap::with_capacity(raw.len());
for (id, b64) in raw {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&b64)
.map_err(|e| ApproverRegistryError::Load(format!("base64 `{id}`: {e}")))?;
let approver = ApproverId(id);
let pubkey_bytes: [u8; 32] =
bytes
.try_into()
.map_err(|_| ApproverRegistryError::BadKey {
approver: approver.clone(),
})?;
let vk = VerifyingKey::from_bytes(&pubkey_bytes).map_err(|_| {
ApproverRegistryError::BadKey {
approver: approver.clone(),
}
})?;
keys.insert(approver, vk);
}
Ok(Self { keys })
}
}
impl ApproverRegistry for StaticApproverRegistry {
fn lookup(&self, approver: &ApproverId) -> Option<VerifyingKey> {
self.keys.get(approver).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn deterministic_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
#[test]
fn round_trip_via_yaml() {
let sk = deterministic_key(1);
let pk = sk.verifying_key();
use base64::Engine as _;
let pk_b64 = base64::engine::general_purpose::STANDARD.encode(pk.to_bytes());
let yaml = format!("alice@insurer.de: \"{pk_b64}\"\n");
let path = std::env::temp_dir().join("klieo-test-approvers.yaml");
std::fs::write(&path, yaml).unwrap();
let reg = StaticApproverRegistry::from_yaml(&path).expect("load");
let resolved = reg
.lookup(&ApproverId("alice@insurer.de".into()))
.expect("found");
assert_eq!(resolved.to_bytes(), pk.to_bytes());
}
#[test]
fn missing_approver_returns_none() {
let reg = StaticApproverRegistry::from_map(HashMap::new());
assert!(reg.lookup(&ApproverId("ghost".into())).is_none());
}
}