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