Skip to main content

confium_store_tpm/
config.rs

1//! Configuration model for the TPM 2.0 backend.
2//!
3//! [`TpmConfig`] captures everything the backend needs to locate and
4//! authorise against a TPM: the device path, the hierarchy the parent
5//! key lives under, the parent key's persistent handle, and the
6//! authorisation value for that parent. The wire form (the
7//! [`Options`](confium_store::backend::Options) string map surfaced at
8//! the FFI boundary) is parsed by [`TpmConfig::from_options`].
9//!
10//! See `TODO.roadmap/18-hardware-keystore-backends.md` for the design.
11
12use std::path::PathBuf;
13
14use confium_store::backend::Options;
15use confium_store::error::{Result, WrappedSnafu};
16
17#[cfg(test)]
18use confium_store::error::Error;
19
20/// Options key naming the TPM device path.
21pub const OPT_TPM_DEVICE: &str = "tpm_device";
22
23/// Options key naming the hierarchy (one of `owner`, `platform`,
24/// `endorsement`).
25pub const OPT_HIERARCHY: &str = "hierarchy";
26
27/// Options key naming the persistent parent handle, as a hex string
28/// (e.g. `0x81000001`).
29pub const OPT_PARENT_HANDLE: &str = "parent_handle";
30
31/// Options key carrying the authorisation value for the parent key.
32/// Empty by default — typical for owner-hierarchy parent keys.
33pub const OPT_PARENT_PASSWORD: &str = "parent_password";
34
35/// Default hierarchy when [`OPT_HIERARCHY`] is absent.
36pub const DEFAULT_HIERARCHY: Hierarchy = Hierarchy::Owner;
37
38/// A TPM 2.0 hierarchy. Maps to the three persistent hierarchies
39/// defined by the TPM 2.0 specification: owner, platform, and
40/// endorsement. The parent key under which Confium wraps its sealed
41/// objects lives in one of these.
42///
43/// Wire form is the lowercase ASCII name (see [`Hierarchy::from_wire`]).
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum Hierarchy {
46    /// The owner hierarchy (`TPM_RH_OWNER`). Default for Confium;
47    /// parent key authorisation usually uses an empty auth value.
48    Owner,
49    /// The platform hierarchy (`TPM_RH_PLATFORM`). Controlled by the
50    /// platform firmware; clears on every reboot.
51    Platform,
52    /// The endorsement hierarchy (`TPM_RH_ENDORSEMENT`). Used for
53    /// privacy-sensitive operations (attestation, EK-derived keys).
54    Endorsement,
55}
56
57impl Hierarchy {
58    /// Decode the wire value used by the FFI / options map. Accepts the
59    /// case-insensitive ASCII name; unknown values map to
60    /// [`Error::Wrapped`].
61    pub fn from_wire(value: &str) -> Result<Self> {
62        match value.to_ascii_lowercase().as_str() {
63            "owner" | "o" => Ok(Hierarchy::Owner),
64            "platform" | "p" => Ok(Hierarchy::Platform),
65            "endorsement" | "e" => Ok(Hierarchy::Endorsement),
66            other => Err(WrappedSnafu {
67                message: format!("unknown TPM hierarchy: {other:?}"),
68            }
69            .build()),
70        }
71    }
72
73    /// The wire name this hierarchy serialises to (lowercase ASCII).
74    pub fn as_wire(self) -> &'static str {
75        match self {
76            Hierarchy::Owner => "owner",
77            Hierarchy::Platform => "platform",
78            Hierarchy::Endorsement => "endorsement",
79        }
80    }
81}
82
83impl std::fmt::Display for Hierarchy {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.write_str(self.as_wire())
86    }
87}
88
89/// A persistent TPM handle. Wraps a `u32` so the wire encoding (hex
90/// string) is localised here and the call site reads naturally.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct ParentHandle(pub u32);
93
94impl ParentHandle {
95    /// Parse a hex-encoded persistent handle (with or without a `0x`
96    /// prefix). Used to read [`OPT_PARENT_HANDLE`] from the options map.
97    pub fn from_wire(value: &str) -> Result<Self> {
98        let stripped = value
99            .trim()
100            .trim_start_matches("0x")
101            .trim_start_matches("0X");
102        let raw = u32::from_str_radix(stripped, 16).map_err(|e| {
103            WrappedSnafu {
104                message: format!("invalid parent handle {value:?}: {e}"),
105            }
106            .build()
107        })?;
108        Ok(ParentHandle(raw))
109    }
110
111    /// The raw `u32` handle, suitable for passing to `tss-esapi` as a
112    /// `ESYS_TR` persistent handle.
113    pub fn raw(self) -> u32 {
114        self.0
115    }
116}
117
118/// Resolved configuration for the TPM backend.
119///
120/// Produced by [`TpmConfig::from_options`]; consumed by
121/// [`crate::backend::TpmBackend::open`]. Holds the post-parse, typed
122/// form of every option the backend reads, so the open path does not
123/// re-do string parsing on every call.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct TpmConfig {
126    /// Path to the TPM device. `None` means "let `tss-esapi`
127    /// auto-detect" (the common case on Linux where `tcti=tabrmd`
128    /// finds the system TPM ResourceManager).
129    pub device: Option<PathBuf>,
130
131    /// Hierarchy the parent key lives under.
132    pub hierarchy: Hierarchy,
133
134    /// Persistent handle of the parent (wrapping) key. May be `None`
135    /// at config time — the backend will then create a transient
136    /// parent on the fly and evict it on close. Persistent handles
137    /// survive reboot and are the production setting.
138    pub parent_handle: Option<ParentHandle>,
139
140    /// Authorisation value for the parent key. Empty by default
141    /// (matches the typical owner-hierarchy deployment).
142    pub parent_password: Vec<u8>,
143}
144
145impl Default for TpmConfig {
146    fn default() -> Self {
147        Self {
148            device: None,
149            hierarchy: DEFAULT_HIERARCHY,
150            parent_handle: None,
151            parent_password: Vec::new(),
152        }
153    }
154}
155
156impl TpmConfig {
157    /// Parse the backend config out of the
158    /// [`Options`](confium_store::backend::Options) string map. Unknown
159    /// keys are ignored; malformed values surface as
160    /// [`Error::Wrapped`].
161    pub fn from_options(opts: &Options) -> Result<Self> {
162        let device = opts.get(OPT_TPM_DEVICE).map(PathBuf::from);
163
164        let hierarchy = opts
165            .get(OPT_HIERARCHY)
166            .map(|v| Hierarchy::from_wire(v))
167            .transpose()?
168            .unwrap_or(DEFAULT_HIERARCHY);
169
170        let parent_handle = opts
171            .get(OPT_PARENT_HANDLE)
172            .map(|v| ParentHandle::from_wire(v))
173            .transpose()?;
174
175        let parent_password = opts
176            .get(OPT_PARENT_PASSWORD)
177            .map(String::as_bytes)
178            .map(Vec::from)
179            .unwrap_or_default();
180
181        Ok(Self {
182            device,
183            hierarchy,
184            parent_handle,
185            parent_password,
186        })
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::collections::HashMap;
194
195    #[test]
196    fn defaults_are_sane() {
197        let cfg = TpmConfig::default();
198        assert_eq!(cfg.hierarchy, Hierarchy::Owner);
199        assert!(cfg.device.is_none());
200        assert!(cfg.parent_handle.is_none());
201        assert!(cfg.parent_password.is_empty());
202    }
203
204    #[test]
205    fn parses_full_options_map() {
206        let mut opts: Options = HashMap::new();
207        opts.insert(OPT_TPM_DEVICE.into(), "/dev/tpmrmis0".into());
208        opts.insert(OPT_HIERARCHY.into(), "endorsement".into());
209        opts.insert(OPT_PARENT_HANDLE.into(), "0x81000001".into());
210        opts.insert(OPT_PARENT_PASSWORD.into(), "hunter2".into());
211
212        let cfg = TpmConfig::from_options(&opts).expect("parse");
213        assert_eq!(
214            cfg.device.as_deref(),
215            Some(std::path::Path::new("/dev/tpmrmis0"))
216        );
217        assert_eq!(cfg.hierarchy, Hierarchy::Endorsement);
218        assert_eq!(cfg.parent_handle.unwrap().raw(), 0x8100_0001);
219        assert_eq!(cfg.parent_password, b"hunter2");
220    }
221
222    #[test]
223    fn hierarchy_is_case_insensitive() {
224        for (wire, expected) in [
225            ("Owner", Hierarchy::Owner),
226            ("PLATFORM", Hierarchy::Platform),
227            ("Endorsement", Hierarchy::Endorsement),
228        ] {
229            assert_eq!(Hierarchy::from_wire(wire).unwrap(), expected);
230        }
231    }
232
233    #[test]
234    fn hierarchy_accepts_short_forms() {
235        assert_eq!(Hierarchy::from_wire("o").unwrap(), Hierarchy::Owner);
236        assert_eq!(Hierarchy::from_wire("p").unwrap(), Hierarchy::Platform);
237        assert_eq!(Hierarchy::from_wire("e").unwrap(), Hierarchy::Endorsement);
238    }
239
240    #[test]
241    fn unknown_hierarchy_errors() {
242        let err = Hierarchy::from_wire("nonsense").unwrap_err();
243        assert!(matches!(err, Error::Wrapped { .. }));
244    }
245
246    #[test]
247    fn parent_handle_parses_with_and_without_prefix() {
248        assert_eq!(
249            ParentHandle::from_wire("0x81000001").unwrap().raw(),
250            0x8100_0001
251        );
252        assert_eq!(
253            ParentHandle::from_wire("81000001").unwrap().raw(),
254            0x8100_0001
255        );
256        assert_eq!(
257            ParentHandle::from_wire("0X81000002").unwrap().raw(),
258            0x8100_0002
259        );
260    }
261
262    #[test]
263    fn parent_handle_rejects_garbage() {
264        let err = ParentHandle::from_wire("not-a-handle").unwrap_err();
265        assert!(matches!(err, Error::Wrapped { .. }));
266    }
267
268    #[test]
269    fn empty_options_uses_defaults() {
270        let opts: Options = HashMap::new();
271        let cfg = TpmConfig::from_options(&opts).expect("parse");
272        assert_eq!(cfg, TpmConfig::default());
273    }
274
275    #[test]
276    fn display_round_trips_through_from_wire() {
277        for h in [
278            Hierarchy::Owner,
279            Hierarchy::Platform,
280            Hierarchy::Endorsement,
281        ] {
282            let wire = h.to_string();
283            assert_eq!(Hierarchy::from_wire(&wire).unwrap(), h);
284        }
285    }
286}