Skip to main content

confium_store_pkcs11/
config.rs

1//! Configuration types for the PKCS#11 backend.
2//!
3//! The backend is configured entirely through the
4//! [`Options`](confium_store::backend::Options) map passed to
5//! [`StoreBackend::open`](confium_store::backend::StoreBackend::open).
6//! This module names those option keys and parses them into a typed
7//! [`Config`].
8
9use confium_store::backend::Options;
10use confium_store::error::{Error, Result};
11
12/// Options key naming the PKCS#11 module path. Required: there is no
13/// sensible default for the location of a vendor's `.so` / `.dylib`.
14pub const OPT_PKCS11_MODULE: &str = "pkcs11_module";
15
16/// Options key naming the HSM slot, as a decimal `u64`. Required: slot
17/// discovery by label is supported, but a concrete slot must still be
18/// resolved before opening a session.
19pub const OPT_SLOT_ID: &str = "slot_id";
20
21/// Options key carrying the user PIN. Optional at config time — if
22/// absent, callers are expected to prompt the operator and supply the
23/// PIN before the first HSM operation that requires it.
24pub const OPT_PIN: &str = "pin";
25
26/// Options key naming a token label, used for slot discovery when
27/// `slot_id` is not known in advance. Optional.
28pub const OPT_TOKEN_LABEL: &str = "token_label";
29
30/// Typed view over the PKCS#11 backend's open-time options.
31///
32/// Construct with [`Config::from_options`]. All fields are owned so
33/// the value can outlive the borrowed `Options` map.
34#[derive(Debug, Clone)]
35pub struct Config {
36    /// Filesystem path to the PKCS#11 shared object. Required.
37    pub pkcs11_module: String,
38    /// HSM slot id. Required.
39    pub slot_id: u64,
40    /// User PIN. `None` means the caller will prompt for it.
41    pub pin: Option<String>,
42    /// Token label, used for slot discovery. `None` means slot id is
43    /// authoritative.
44    pub token_label: Option<String>,
45}
46
47impl Config {
48    /// Parse a [`Config`] out of an [`Options`] map.
49    ///
50    /// Returns [`Error::Wrapped`] with a descriptive message when a
51    /// required option is missing or malformed. We use `Wrapped`
52    /// rather than adding PKCS#11-specific error variants so the
53    /// backend does not leak its config grammar into the shared
54    /// Store error enum — the Store contract is that backends surface
55    /// failures through the existing variants.
56    pub fn from_options(opts: &Options) -> Result<Self> {
57        let pkcs11_module = opts
58            .get(OPT_PKCS11_MODULE)
59            .map(String::as_str)
60            .filter(|s| !s.is_empty())
61            .ok_or_else(|| Error::Wrapped {
62                message: format!(
63                    "PKCS#11 backend requires the '{OPT_PKCS11_MODULE}' option \
64                     (path to the PKCS#11 shared object)"
65                ),
66            })?
67            .to_string();
68
69        let slot_id = opts
70            .get(OPT_SLOT_ID)
71            .ok_or_else(|| Error::Wrapped {
72                message: format!(
73                    "PKCS#11 backend requires the '{OPT_SLOT_ID}' option \
74                     (HSM slot id, decimal u64)"
75                ),
76            })
77            .and_then(|raw| {
78                raw.parse::<u64>().map_err(|_| Error::Wrapped {
79                    message: format!(
80                        "PKCS#11 backend: '{OPT_SLOT_ID}' must be a decimal u64, \
81                         got '{raw}'"
82                    ),
83                })
84            })?;
85
86        let pin = opts.get(OPT_PIN).filter(|s| !s.is_empty()).cloned();
87        let token_label = opts.get(OPT_TOKEN_LABEL).filter(|s| !s.is_empty()).cloned();
88
89        Ok(Config {
90            pkcs11_module,
91            slot_id,
92            pin,
93            token_label,
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::collections::HashMap;
102
103    fn opts() -> Options {
104        let mut o = Options::new();
105        o.insert(
106            OPT_PKCS11_MODULE.to_string(),
107            "/opt/hsm/libpkcs11.so".into(),
108        );
109        o.insert(OPT_SLOT_ID.to_string(), "0".into());
110        o.insert(OPT_PIN.to_string(), "123456".into());
111        o.insert(OPT_TOKEN_LABEL.to_string(), "confium".into());
112        o
113    }
114
115    #[test]
116    fn parses_all_fields() {
117        let cfg = Config::from_options(&opts()).expect("valid config");
118        assert_eq!(cfg.pkcs11_module, "/opt/hsm/libpkcs11.so");
119        assert_eq!(cfg.slot_id, 0);
120        assert_eq!(cfg.pin.as_deref(), Some("123456"));
121        assert_eq!(cfg.token_label.as_deref(), Some("confium"));
122    }
123
124    #[test]
125    fn pin_and_label_optional() {
126        let mut o = opts();
127        o.remove(OPT_PIN);
128        o.remove(OPT_TOKEN_LABEL);
129        let cfg = Config::from_options(&o).expect("still valid");
130        assert!(cfg.pin.is_none());
131        assert!(cfg.token_label.is_none());
132    }
133
134    #[test]
135    fn empty_pin_treated_as_absent() {
136        let mut o = opts();
137        o.insert(OPT_PIN.to_string(), String::new());
138        let cfg = Config::from_options(&o).expect("empty pin is absent");
139        assert!(cfg.pin.is_none());
140    }
141
142    #[test]
143    fn missing_module_is_error() {
144        let mut o = opts();
145        o.remove(OPT_PKCS11_MODULE);
146        let err = Config::from_options(&o).unwrap_err();
147        assert!(matches!(err, Error::Wrapped { .. }));
148        assert!(format!("{err}").contains(OPT_PKCS11_MODULE));
149    }
150
151    #[test]
152    fn missing_slot_is_error() {
153        let mut o = opts();
154        o.remove(OPT_SLOT_ID);
155        let err = Config::from_options(&o).unwrap_err();
156        assert!(format!("{err}").contains(OPT_SLOT_ID));
157    }
158
159    #[test]
160    fn non_numeric_slot_is_error() {
161        let mut o = opts();
162        o.insert(OPT_SLOT_ID.to_string(), "not-a-number".into());
163        let err = Config::from_options(&o).unwrap_err();
164        assert!(format!("{err}").contains("must be a decimal u64"));
165    }
166
167    #[test]
168    fn empty_options_map_errors() {
169        let empty: Options = HashMap::new();
170        let err = Config::from_options(&empty).unwrap_err();
171        assert!(matches!(err, Error::Wrapped { .. }));
172    }
173}