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