confium_store_tpm/
config.rs1use 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
20pub const OPT_TPM_DEVICE: &str = "tpm_device";
22
23pub const OPT_HIERARCHY: &str = "hierarchy";
26
27pub const OPT_PARENT_HANDLE: &str = "parent_handle";
30
31pub const OPT_PARENT_PASSWORD: &str = "parent_password";
34
35pub const DEFAULT_HIERARCHY: Hierarchy = Hierarchy::Owner;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum Hierarchy {
46 Owner,
49 Platform,
52 Endorsement,
55}
56
57impl Hierarchy {
58 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct ParentHandle(pub u32);
93
94impl ParentHandle {
95 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 pub fn raw(self) -> u32 {
114 self.0
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct TpmConfig {
126 pub device: Option<PathBuf>,
130
131 pub hierarchy: Hierarchy,
133
134 pub parent_handle: Option<ParentHandle>,
139
140 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 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}