klieo_ops/
key_registry.rs1use ed25519_dalek::VerifyingKey;
9use std::collections::HashMap;
10use std::path::Path;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum KeyRegistryError {
17 #[error("key registry load: {0}")]
19 Load(String),
20 #[error("key registry: pubkey for `{id}` is not a valid 32-byte Ed25519 key")]
22 BadKey {
23 id: String,
25 },
26}
27
28pub trait Ed25519KeyRegistry<Id>: Send + Sync
30where
31 Id: Eq + std::hash::Hash + std::fmt::Display,
32{
33 fn lookup(&self, id: &Id) -> Option<VerifyingKey>;
36}
37
38pub struct StaticEd25519KeyRegistry<Id>
40where
41 Id: Eq + std::hash::Hash + std::fmt::Display,
42{
43 keys: HashMap<Id, VerifyingKey>,
44}
45
46impl<Id> StaticEd25519KeyRegistry<Id>
47where
48 Id: Eq + std::hash::Hash + std::fmt::Display,
49{
50 #[must_use]
52 pub fn from_map(keys: HashMap<Id, VerifyingKey>) -> Self {
53 Self { keys }
54 }
55}
56
57impl<Id> Ed25519KeyRegistry<Id> for StaticEd25519KeyRegistry<Id>
58where
59 Id: Eq + std::hash::Hash + std::fmt::Display + Send + Sync,
60{
61 fn lookup(&self, id: &Id) -> Option<VerifyingKey> {
62 self.keys.get(id).copied()
63 }
64}
65
66pub fn load_ed25519_yaml<Id>(
72 path: impl AsRef<Path>,
73 make_id: impl Fn(String) -> Id,
74) -> Result<HashMap<Id, VerifyingKey>, KeyRegistryError>
75where
76 Id: Eq + std::hash::Hash + std::fmt::Display,
77{
78 let body = std::fs::read_to_string(path.as_ref())
79 .map_err(|e| KeyRegistryError::Load(format!("read: {e}")))?;
80 let raw: HashMap<String, String> =
81 serde_yaml::from_str(&body).map_err(|e| KeyRegistryError::Load(format!("parse: {e}")))?;
82 let mut keys = HashMap::with_capacity(raw.len());
83 for (id_raw, b64) in raw {
84 use base64::Engine as _;
85 let bytes = base64::engine::general_purpose::STANDARD
86 .decode(b64)
87 .map_err(|e| KeyRegistryError::Load(format!("base64 `{id_raw}`: {e}")))?;
88 let id = make_id(id_raw);
89 let pk_bytes: [u8; 32] = bytes
90 .try_into()
91 .map_err(|_| KeyRegistryError::BadKey { id: id.to_string() })?;
92 let vk = VerifyingKey::from_bytes(&pk_bytes)
93 .map_err(|_| KeyRegistryError::BadKey { id: id.to_string() })?;
94 keys.insert(id, vk);
95 }
96 Ok(keys)
97}
98
99#[cfg(feature = "hot-reload")]
100pub use hot_reload::HotReloadableEd25519Registry;
101
102#[cfg(feature = "hot-reload")]
103mod hot_reload {
104 use super::{load_ed25519_yaml, Ed25519KeyRegistry, KeyRegistryError};
118 use arc_swap::ArcSwap;
119 use ed25519_dalek::VerifyingKey;
120 use notify_debouncer_mini::{new_debouncer, notify, DebounceEventResult};
121 use std::collections::HashMap;
122 use std::path::Path;
123 use std::sync::Arc;
124 use std::time::Duration;
125
126 #[non_exhaustive]
134 pub struct HotReloadableEd25519Registry<Id>
135 where
136 Id: Eq + std::hash::Hash + std::fmt::Display + Send + Sync + 'static,
137 {
138 keys: Arc<ArcSwap<HashMap<Id, VerifyingKey>>>,
139 _debouncer: notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>,
141 }
142
143 impl<Id> HotReloadableEd25519Registry<Id>
144 where
145 Id: Eq + std::hash::Hash + std::fmt::Display + Send + Sync + Clone + 'static,
146 {
147 pub fn watch_yaml(
153 path: impl AsRef<Path>,
154 make_id: impl Fn(String) -> Id + Send + Sync + 'static,
155 ) -> Result<Self, KeyRegistryError> {
156 let path = path.as_ref().to_path_buf();
157 let initial = load_ed25519_yaml(&path, &make_id)?;
158 let keys = Arc::new(ArcSwap::from_pointee(initial));
159
160 let keys_for_watch = keys.clone();
161 let path_for_watch = path.clone();
162 let make_id_arc: Arc<dyn Fn(String) -> Id + Send + Sync> = Arc::new(make_id);
163
164 let mut debouncer = new_debouncer(
165 Duration::from_millis(500),
166 move |result: DebounceEventResult| {
167 match result {
168 Err(err) => {
169 tracing::warn!(
170 target: "klieo.ops.key_registry.hot_reload",
171 error = %err,
172 "watcher error; keeping previous snapshot"
173 );
174 return;
175 }
176 Ok(events) => {
177 let relevant = events.iter().any(|e| {
179 matches!(e.kind, notify_debouncer_mini::DebouncedEventKind::Any)
180 });
181 if !relevant {
182 return;
183 }
184 }
185 }
186 match load_ed25519_yaml(&path_for_watch, |s| (make_id_arc)(s)) {
187 Ok(new_keys) => {
188 keys_for_watch.store(Arc::new(new_keys));
189 tracing::info!(
190 target: "klieo.ops.key_registry.hot_reload",
191 path = %path_for_watch.display(),
192 "key registry reloaded"
193 );
194 }
195 Err(err) => {
196 tracing::warn!(
197 target: "klieo.ops.key_registry.hot_reload",
198 path = %path_for_watch.display(),
199 error = %err,
200 "reload failed; keeping previous snapshot"
201 );
202 }
203 }
204 },
205 )
206 .map_err(|e| KeyRegistryError::Load(format!("debouncer init: {e}")))?;
207
208 debouncer
209 .watcher()
210 .watch(&path, notify::RecursiveMode::NonRecursive)
211 .map_err(|e| KeyRegistryError::Load(format!("watch start: {e}")))?;
212
213 Ok(Self {
214 keys,
215 _debouncer: debouncer,
216 })
217 }
218 }
219
220 impl<Id> Ed25519KeyRegistry<Id> for HotReloadableEd25519Registry<Id>
221 where
222 Id: Eq + std::hash::Hash + std::fmt::Display + Send + Sync + 'static,
223 {
224 fn lookup(&self, id: &Id) -> Option<VerifyingKey> {
225 self.keys.load().get(id).copied()
226 }
227 }
228}