use std::path::PathBuf;
use confium_store::backend::Options;
use confium_store::error::{Result, WrappedSnafu};
#[cfg(test)]
use confium_store::error::Error;
pub const OPT_TPM_DEVICE: &str = "tpm_device";
pub const OPT_HIERARCHY: &str = "hierarchy";
pub const OPT_PARENT_HANDLE: &str = "parent_handle";
pub const OPT_PARENT_PASSWORD: &str = "parent_password";
pub const DEFAULT_HIERARCHY: Hierarchy = Hierarchy::Owner;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Hierarchy {
Owner,
Platform,
Endorsement,
}
impl Hierarchy {
pub fn from_wire(value: &str) -> Result<Self> {
match value.to_ascii_lowercase().as_str() {
"owner" | "o" => Ok(Hierarchy::Owner),
"platform" | "p" => Ok(Hierarchy::Platform),
"endorsement" | "e" => Ok(Hierarchy::Endorsement),
other => Err(WrappedSnafu {
message: format!("unknown TPM hierarchy: {other:?}"),
}
.build()),
}
}
pub fn as_wire(self) -> &'static str {
match self {
Hierarchy::Owner => "owner",
Hierarchy::Platform => "platform",
Hierarchy::Endorsement => "endorsement",
}
}
}
impl std::fmt::Display for Hierarchy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_wire())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ParentHandle(pub u32);
impl ParentHandle {
pub fn from_wire(value: &str) -> Result<Self> {
let stripped = value
.trim()
.trim_start_matches("0x")
.trim_start_matches("0X");
let raw = u32::from_str_radix(stripped, 16).map_err(|e| {
WrappedSnafu {
message: format!("invalid parent handle {value:?}: {e}"),
}
.build()
})?;
Ok(ParentHandle(raw))
}
pub fn raw(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TpmConfig {
pub device: Option<PathBuf>,
pub hierarchy: Hierarchy,
pub parent_handle: Option<ParentHandle>,
pub parent_password: Vec<u8>,
}
impl Default for TpmConfig {
fn default() -> Self {
Self {
device: None,
hierarchy: DEFAULT_HIERARCHY,
parent_handle: None,
parent_password: Vec::new(),
}
}
}
impl TpmConfig {
pub fn from_options(opts: &Options) -> Result<Self> {
let device = opts.get(OPT_TPM_DEVICE).map(PathBuf::from);
let hierarchy = opts
.get(OPT_HIERARCHY)
.map(|v| Hierarchy::from_wire(v))
.transpose()?
.unwrap_or(DEFAULT_HIERARCHY);
let parent_handle = opts
.get(OPT_PARENT_HANDLE)
.map(|v| ParentHandle::from_wire(v))
.transpose()?;
let parent_password = opts
.get(OPT_PARENT_PASSWORD)
.map(String::as_bytes)
.map(Vec::from)
.unwrap_or_default();
Ok(Self {
device,
hierarchy,
parent_handle,
parent_password,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn defaults_are_sane() {
let cfg = TpmConfig::default();
assert_eq!(cfg.hierarchy, Hierarchy::Owner);
assert!(cfg.device.is_none());
assert!(cfg.parent_handle.is_none());
assert!(cfg.parent_password.is_empty());
}
#[test]
fn parses_full_options_map() {
let mut opts: Options = HashMap::new();
opts.insert(OPT_TPM_DEVICE.into(), "/dev/tpmrmis0".into());
opts.insert(OPT_HIERARCHY.into(), "endorsement".into());
opts.insert(OPT_PARENT_HANDLE.into(), "0x81000001".into());
opts.insert(OPT_PARENT_PASSWORD.into(), "hunter2".into());
let cfg = TpmConfig::from_options(&opts).expect("parse");
assert_eq!(
cfg.device.as_deref(),
Some(std::path::Path::new("/dev/tpmrmis0"))
);
assert_eq!(cfg.hierarchy, Hierarchy::Endorsement);
assert_eq!(cfg.parent_handle.unwrap().raw(), 0x8100_0001);
assert_eq!(cfg.parent_password, b"hunter2");
}
#[test]
fn hierarchy_is_case_insensitive() {
for (wire, expected) in [
("Owner", Hierarchy::Owner),
("PLATFORM", Hierarchy::Platform),
("Endorsement", Hierarchy::Endorsement),
] {
assert_eq!(Hierarchy::from_wire(wire).unwrap(), expected);
}
}
#[test]
fn hierarchy_accepts_short_forms() {
assert_eq!(Hierarchy::from_wire("o").unwrap(), Hierarchy::Owner);
assert_eq!(Hierarchy::from_wire("p").unwrap(), Hierarchy::Platform);
assert_eq!(Hierarchy::from_wire("e").unwrap(), Hierarchy::Endorsement);
}
#[test]
fn unknown_hierarchy_errors() {
let err = Hierarchy::from_wire("nonsense").unwrap_err();
assert!(matches!(err, Error::Wrapped { .. }));
}
#[test]
fn parent_handle_parses_with_and_without_prefix() {
assert_eq!(
ParentHandle::from_wire("0x81000001").unwrap().raw(),
0x8100_0001
);
assert_eq!(
ParentHandle::from_wire("81000001").unwrap().raw(),
0x8100_0001
);
assert_eq!(
ParentHandle::from_wire("0X81000002").unwrap().raw(),
0x8100_0002
);
}
#[test]
fn parent_handle_rejects_garbage() {
let err = ParentHandle::from_wire("not-a-handle").unwrap_err();
assert!(matches!(err, Error::Wrapped { .. }));
}
#[test]
fn empty_options_uses_defaults() {
let opts: Options = HashMap::new();
let cfg = TpmConfig::from_options(&opts).expect("parse");
assert_eq!(cfg, TpmConfig::default());
}
#[test]
fn display_round_trips_through_from_wire() {
for h in [
Hierarchy::Owner,
Hierarchy::Platform,
Hierarchy::Endorsement,
] {
let wire = h.to_string();
assert_eq!(Hierarchy::from_wire(&wire).unwrap(), h);
}
}
}