Skip to main content

rite_runtime/backend/
registry.rs

1//! Backend registry with lazy initialization.
2//!
3//! The registry owns all backend lifecycle state and provides typed access
4//! methods for different backend traits.
5//!
6//! ## Lazy Initialization
7//!
8//! Backends are **declared** (config stored) at startup and **initialized**
9//! (hardware opened) the first time a step actually needs them. This means:
10//!
11//! - Ceremony setup steps (presence checks, clock sync, etc.) run before
12//!   any hardware is touched.
13//! - A missing or unplugged device fails at the step that uses it, not at
14//!   ceremony startup.
15//! - Failed initialization is cached: the hardware is never probed twice.
16//!
17//! ## Design Notes
18//!
19//! `BackendRegistry` stores a `BackendState` enum instead of `Box<dyn Backend>`
20//! directly. A factory closure injected at construction time creates concrete
21//! backends on first use.
22
23use rite_sdk::{
24    AttestationBackend, Backend, BackendConfig, BackendError, CertStoreBackend, KeyStoreBackend,
25    KeyTransportBackend, PivBackend, Pkcs11AdminBackend, Pkcs11Backend, RandomBackend, SignBackend,
26    TpmBackend, YubikeyBackend,
27};
28use std::collections::HashMap;
29
30/// A closure that creates a concrete backend from its name and config.
31///
32/// This is the bridge between `rite-runtime` (which defines the
33/// registry) and `rite-stdlib` (which provides the concrete
34/// implementations). The CLI injects `rite_stdlib::backend::create_backend`
35/// as this closure.
36pub type BackendFactory =
37    Box<dyn Fn(String, &BackendConfig) -> Result<Box<dyn Backend>, BackendError> + Send + Sync>;
38
39enum BackendState {
40    /// Declared but not yet initialized; hardware not yet touched.
41    Uninitialized(BackendConfig),
42    /// Successfully initialized; ready to use.
43    Ready(Box<dyn Backend>),
44    /// Initialization failed; error cached, hardware will not be retried.
45    Failed(String),
46}
47
48/// Backend registry that owns all backend lifecycle state.
49///
50/// Provides typed accessor methods for different backend traits. Backends are
51/// declared at startup (via [`declare`]) and initialized lazily on first use.
52///
53/// Note: This uses `Box<dyn Backend>` for storage to avoid circular
54/// dependencies between rite-runtime and rite-stdlib. Concrete backend
55/// implementations live in rite-stdlib.
56///
57/// [`declare`]: BackendRegistry::declare
58pub struct BackendRegistry {
59    factory: BackendFactory,
60    backends: HashMap<String, BackendState>,
61}
62
63impl BackendRegistry {
64    /// Create a new registry with a backend factory.
65    ///
66    /// The factory is called the first time a declared backend is used.
67    ///
68    /// # Example
69    ///
70    /// ```ignore
71    /// let registry = BackendRegistry::with_factory(
72    ///     Box::new(|name, config| rite_stdlib::backend::create_backend(name, config))
73    /// );
74    /// ```
75    pub fn with_factory(factory: BackendFactory) -> Self {
76        Self {
77            factory,
78            backends: HashMap::new(),
79        }
80    }
81
82    /// Create an empty registry without a factory (for tests).
83    ///
84    /// The factory always returns `BackendError::NotFound`. Use [`register`]
85    /// to insert fully-initialized backends directly.
86    ///
87    /// [`register`]: BackendRegistry::register
88    pub fn new() -> Self {
89        Self {
90            factory: Box::new(|name, _config| Err(BackendError::NotFound(name))),
91            backends: HashMap::new(),
92        }
93    }
94
95    /// Declare a backend from YAML configuration (lazy; no hardware touched).
96    ///
97    /// Stores the config only. The backend is not initialized until first use.
98    ///
99    /// If a backend with the same name already exists, it will be replaced.
100    pub fn declare(&mut self, name: String, config: BackendConfig) {
101        self.backends
102            .insert(name, BackendState::Uninitialized(config));
103    }
104
105    /// Register a fully-initialized backend directly (for tests).
106    ///
107    /// If a backend with the same name already exists, it will be replaced.
108    pub fn register(&mut self, backend: Box<dyn Backend>) {
109        let name = backend.name().to_string();
110        self.backends.insert(name, BackendState::Ready(backend));
111    }
112
113    /// Get backend by name (immutable).
114    ///
115    /// Only returns backends that are already in the `Ready` state. Does not
116    /// trigger initialization. Primarily for testing.
117    pub fn get(&self, name: &str) -> Option<&dyn Backend> {
118        match self.backends.get(name) {
119            Some(BackendState::Ready(b)) => Some(b.as_ref()),
120            _ => None,
121        }
122    }
123
124    /// Get backend by name (mutable), initializing it lazily if needed.
125    ///
126    /// Returns the backend on success, `Err(NotFound)` if the backend was
127    /// never declared, or `Err(HardwareFailure)` if initialization failed.
128    pub fn get_mut(&mut self, name: &str) -> Result<&mut dyn Backend, BackendError> {
129        self.get_ready(name)
130    }
131
132    /// Get backend as `KeyStoreBackend` trait.
133    pub fn get_keystore(&mut self, name: &str) -> Result<&mut dyn KeyStoreBackend, BackendError> {
134        self.get_ready(name)?
135            .as_keystore_mut()
136            .ok_or_else(|| unsupported(name, "KeyStoreBackend"))
137    }
138
139    /// Get backend as `SignBackend` trait.
140    pub fn get_sign(&mut self, name: &str) -> Result<&mut dyn SignBackend, BackendError> {
141        self.get_ready(name)?
142            .as_sign_mut()
143            .ok_or_else(|| unsupported(name, "SignBackend"))
144    }
145
146    /// Get backend as `AttestationBackend` trait (mutable).
147    pub fn get_attest_mut(
148        &mut self,
149        name: &str,
150    ) -> Result<&mut dyn AttestationBackend, BackendError> {
151        self.get_ready(name)?
152            .as_attest_mut()
153            .ok_or_else(|| unsupported(name, "AttestationBackend"))
154    }
155
156    /// Get backend as `PivBackend` trait (mutable).
157    pub fn get_piv(&mut self, name: &str) -> Result<&mut dyn PivBackend, BackendError> {
158        self.get_ready(name)?
159            .as_piv_mut()
160            .ok_or_else(|| unsupported(name, "PivBackend"))
161    }
162
163    /// Get backend as `YubikeyBackend` trait (mutable).
164    pub fn get_yubikey(&mut self, name: &str) -> Result<&mut dyn YubikeyBackend, BackendError> {
165        self.get_ready(name)?
166            .as_yubikey_mut()
167            .ok_or_else(|| unsupported(name, "YubikeyBackend"))
168    }
169
170    /// Get backend as `KeyTransportBackend` trait (mutable).
171    pub fn get_transport(
172        &mut self,
173        name: &str,
174    ) -> Result<&mut dyn KeyTransportBackend, BackendError> {
175        self.get_ready(name)?
176            .as_transport_mut()
177            .ok_or_else(|| unsupported(name, "KeyTransportBackend"))
178    }
179
180    /// Get backend as `CertStoreBackend` trait (mutable).
181    pub fn get_certstore(&mut self, name: &str) -> Result<&mut dyn CertStoreBackend, BackendError> {
182        self.get_ready(name)?
183            .as_certstore_mut()
184            .ok_or_else(|| unsupported(name, "CertStoreBackend"))
185    }
186
187    /// Get backend as `RandomBackend` trait (mutable).
188    pub fn get_random(&mut self, name: &str) -> Result<&mut dyn RandomBackend, BackendError> {
189        self.get_ready(name)?
190            .as_random_mut()
191            .ok_or_else(|| unsupported(name, "RandomBackend"))
192    }
193
194    /// Get backend as `Pkcs11Backend` trait (mutable).
195    pub fn get_pkcs11(&mut self, name: &str) -> Result<&mut dyn Pkcs11Backend, BackendError> {
196        self.get_ready(name)?
197            .as_pkcs11_mut()
198            .ok_or_else(|| unsupported(name, "Pkcs11Backend"))
199    }
200
201    /// Get backend as `Pkcs11AdminBackend` trait (mutable).
202    pub fn get_pkcs11_admin(
203        &mut self,
204        name: &str,
205    ) -> Result<&mut dyn Pkcs11AdminBackend, BackendError> {
206        self.get_ready(name)?
207            .as_pkcs11_admin_mut()
208            .ok_or_else(|| unsupported(name, "Pkcs11AdminBackend"))
209    }
210
211    /// Get backend as `TpmBackend` trait (mutable).
212    pub fn get_tpm(&mut self, name: &str) -> Result<&mut dyn TpmBackend, BackendError> {
213        self.get_ready(name)?
214            .as_tpm_mut()
215            .ok_or_else(|| unsupported(name, "TpmBackend"))
216    }
217
218    /// List all registered backend names.
219    pub fn backend_names(&self) -> impl Iterator<Item = &String> {
220        self.backends.keys()
221    }
222
223    // -------------------------------------------------------------------------
224    // Private helpers
225    // -------------------------------------------------------------------------
226
227    /// Initialize (if needed) and return a mutable reference to the backend.
228    fn get_ready(&mut self, name: &str) -> Result<&mut dyn Backend, BackendError> {
229        self.try_initialize(name)?;
230        match self.backends.get_mut(name) {
231            Some(BackendState::Ready(b)) => Ok(b.as_mut()),
232            Some(BackendState::Failed(msg)) => Err(BackendError::HardwareFailure(msg.clone())),
233            None => Err(BackendError::NotFound(name.to_string())),
234            Some(BackendState::Uninitialized(_)) => {
235                unreachable!("backend should be Ready or Failed after try_initialize")
236            }
237        }
238    }
239
240    /// Initialize a declared backend, caching success or failure.
241    ///
242    /// Uses a two-pass approach to satisfy the borrow checker:
243    /// 1. Clone the config (releases the immutable borrow on `self.backends`).
244    /// 2. Call the factory (no borrow held on `self.backends`).
245    /// 3. Update the entry in place.
246    fn try_initialize(&mut self, name: &str) -> Result<(), BackendError> {
247        // First pass: inspect state (borrow released at end of match).
248        let config = match self.backends.get(name) {
249            Some(BackendState::Uninitialized(config)) => config.clone(),
250            Some(BackendState::Ready(_)) => return Ok(()),
251            Some(BackendState::Failed(msg)) => {
252                return Err(BackendError::HardwareFailure(msg.clone()));
253            }
254            None => return Err(BackendError::NotFound(name.to_string())),
255        };
256
257        // Second pass: call factory and update in place (key already exists).
258        match (self.factory)(name.to_string(), &config) {
259            Ok(backend) => {
260                // Key is guaranteed to exist; we checked above.
261                if let Some(state) = self.backends.get_mut(name) {
262                    *state = BackendState::Ready(backend);
263                }
264                Ok(())
265            }
266            Err(e) => {
267                let msg = e.to_string();
268                if let Some(state) = self.backends.get_mut(name) {
269                    *state = BackendState::Failed(msg);
270                }
271                Err(e)
272            }
273        }
274    }
275}
276
277impl Default for BackendRegistry {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283fn unsupported(name: &str, trait_name: &str) -> BackendError {
284    BackendError::UnsupportedOperation(format!("Backend '{name}' does not implement {trait_name}"))
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use rite_sdk::{Backend, BackendError, KeyId, KeyMetadata, KeySpec, KeyStoreBackend};
291
292    // -------------------------------------------------------------------------
293    // Minimal mock backend for registry tests
294    // -------------------------------------------------------------------------
295
296    struct MinimalMock {
297        name: String,
298    }
299
300    impl Backend for MinimalMock {
301        fn name(&self) -> &str {
302            &self.name
303        }
304        fn provider(&self) -> &'static str {
305            "mock"
306        }
307        fn fingerprint(&self) -> String {
308            format!("mock={}", self.name)
309        }
310        fn as_keystore_mut(&mut self) -> Option<&mut dyn KeyStoreBackend> {
311            Some(self)
312        }
313    }
314
315    impl KeyStoreBackend for MinimalMock {
316        fn generate_key(&mut self, spec: KeySpec) -> Result<KeyMetadata, BackendError> {
317            Ok(KeyMetadata {
318                key_id: KeyId::new("mock-key-1"),
319                algorithm: spec.algorithm,
320                label: spec.label,
321                public_key: None,
322                attestation: None,
323            })
324        }
325        fn import_private_key(
326            &mut self,
327            spec: KeySpec,
328            _key_bytes: &[u8],
329        ) -> Result<KeyMetadata, BackendError> {
330            Ok(KeyMetadata {
331                key_id: KeyId::new("mock-imported-1"),
332                algorithm: spec.algorithm,
333                label: spec.label,
334                public_key: None,
335                attestation: None,
336            })
337        }
338        fn export_public_key(&self, _key_id: &KeyId) -> Result<Vec<u8>, BackendError> {
339            Ok(vec![])
340        }
341        fn list_keys(&self) -> Result<Vec<KeyMetadata>, BackendError> {
342            Ok(vec![])
343        }
344        fn delete_key(&mut self, _key_id: &KeyId) -> Result<(), BackendError> {
345            Ok(())
346        }
347    }
348
349    // -------------------------------------------------------------------------
350    // Tests
351    // -------------------------------------------------------------------------
352
353    #[test]
354    fn registry_not_found_returns_none() {
355        let registry = BackendRegistry::new();
356        assert!(registry.get("nonexistent").is_none());
357    }
358
359    #[test]
360    fn registry_lists_backend_names_empty() {
361        let registry = BackendRegistry::new();
362        assert_eq!(registry.backend_names().count(), 0);
363    }
364
365    #[test]
366    fn register_then_get_returns_backend() {
367        let mut registry = BackendRegistry::new();
368        registry.register(Box::new(MinimalMock {
369            name: "mock".to_string(),
370        }));
371        assert!(registry.get("mock").is_some());
372        assert_eq!(registry.get("mock").unwrap().name(), "mock");
373    }
374
375    #[test]
376    fn register_then_get_keystore_succeeds() {
377        let mut registry = BackendRegistry::new();
378        registry.register(Box::new(MinimalMock {
379            name: "mock".to_string(),
380        }));
381        assert!(registry.get_keystore("mock").is_ok());
382    }
383
384    #[test]
385    fn declare_and_lazy_init_via_factory() {
386        let mut registry = BackendRegistry::with_factory(Box::new(|name, _config| {
387            Ok(Box::new(MinimalMock { name }) as Box<dyn Backend>)
388        }));
389
390        let config = BackendConfig {
391            provider: "mock".to_string(),
392            extra: serde_json::json!({}),
393        };
394
395        // Declare without initializing
396        registry.declare("lazy".to_string(), config);
397
398        // Backend not yet in ready state
399        assert!(registry.get("lazy").is_none());
400
401        // First access triggers initialization
402        let result = registry.get_keystore("lazy");
403        assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
404
405        // Now it's in ready state
406        assert!(registry.get("lazy").is_some());
407    }
408
409    #[test]
410    fn unsupported_trait_returns_error() {
411        // MinimalMock only implements KeyStoreBackend, not SignBackend.
412        let mut registry = BackendRegistry::new();
413        registry.register(Box::new(MinimalMock {
414            name: "mock".to_string(),
415        }));
416
417        let result = registry.get_sign("mock");
418        assert!(result.is_err());
419        match result.err().unwrap() {
420            BackendError::UnsupportedOperation(msg) => {
421                assert!(
422                    msg.contains("SignBackend"),
423                    "Expected UnsupportedOperation for SignBackend, got: {msg}"
424                );
425            }
426            e => panic!("Expected UnsupportedOperation, got: {e}"),
427        }
428    }
429
430    #[test]
431    fn get_mut_undeclared_returns_not_found() {
432        let mut registry = BackendRegistry::new();
433        let result = registry.get_mut("nonexistent");
434        assert!(result.is_err());
435        match result.err().unwrap() {
436            BackendError::NotFound(name) => assert_eq!(name, "nonexistent"),
437            e => panic!("Expected NotFound, got: {e}"),
438        }
439    }
440
441    #[test]
442    fn factory_failure_cached_no_retry() {
443        use std::sync::Arc;
444        use std::sync::atomic::{AtomicUsize, Ordering};
445
446        let call_count = Arc::new(AtomicUsize::new(0));
447        let call_count_clone = Arc::clone(&call_count);
448
449        let mut registry = BackendRegistry::with_factory(Box::new(move |_name, _config| {
450            call_count_clone.fetch_add(1, Ordering::SeqCst);
451            Err(BackendError::HardwareFailure(
452                "device not found".to_string(),
453            ))
454        }));
455
456        let config = BackendConfig {
457            provider: "mock".to_string(),
458            extra: serde_json::json!({}),
459        };
460        registry.declare("failing".to_string(), config);
461
462        // First call triggers factory
463        let _ = registry.get_keystore("failing");
464        assert_eq!(call_count.load(Ordering::SeqCst), 1);
465
466        // Second call must NOT call factory again (error cached)
467        let _ = registry.get_keystore("failing");
468        assert_eq!(
469            call_count.load(Ordering::SeqCst),
470            1,
471            "Factory called more than once"
472        );
473    }
474
475    #[test]
476    fn lazy_init_unsupported_trait_returns_error() {
477        // Factory creates a MinimalMock (only KeyStore), then asking for
478        // AttestationBackend should return UnsupportedOperation.
479        let mut registry = BackendRegistry::with_factory(Box::new(|name, _config| {
480            Ok(Box::new(MinimalMock { name }) as Box<dyn Backend>)
481        }));
482
483        let config = BackendConfig {
484            provider: "yubikey".to_string(),
485            extra: serde_json::json!({}),
486        };
487        registry.declare("yk".to_string(), config);
488
489        let result = registry.get_attest_mut("yk");
490        match result {
491            Err(BackendError::UnsupportedOperation(msg)) => {
492                assert!(
493                    msg.contains("AttestationBackend"),
494                    "Expected AttestationBackend error, got: {msg}"
495                );
496            }
497            Ok(_) => panic!("Expected UnsupportedOperation, got Ok"),
498            Err(e) => panic!("Expected UnsupportedOperation, got: {e:?}"),
499        }
500    }
501}