Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions crates/scout/src/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,17 @@ pub async fn run(
let mut att_key_handle_opt: Option<KeyHandle> = None;
let mut tss_ctx_opt: Option<Context> = None;

if !is_dpu {
// A host with no TPM cannot attest, so gate on actual TPM presence (not just is_dpu) and skip
// the flow rather than hard failing in create_context_from_path.
let do_attestation = !is_dpu && tpm::tpm_present(tpm_path);
if !is_dpu && !do_attestation {
tracing::warn!(
tpm_path = ?tpm_path,
"Host has no TPM device; skipping attestation key setup"
);
}

if do_attestation {
// set the max auth fail to 256 as a stop gap measure to prevent machines from failing during
// repeated reingestion cycle
crate::tpm::set_tpm_max_auth_fail()?;
Expand Down Expand Up @@ -100,7 +110,7 @@ pub async fn run(

// If we are not on a DPU and have some post-registration things to do,
// we do them here.
if !is_dpu {
if do_attestation {
// If we have received back an attestation key challenge, this means
// that Carbide has requested an attestation, so do it!
//
Expand Down
58 changes: 58 additions & 0 deletions crates/scout/src/tpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ pub(crate) fn set_tpm_max_auth_fail() -> Result<(), CarbideClientError> {
Ok(())
}

/// Kernel device paths to probe for `tpm_path`. An explicit `/dev/` path (optionally written with a
/// `device` TCTI prefix) resolves to just itself, anything else falls back to the standard nodes.
fn tpm_device_candidates(tpm_path: &str) -> Vec<&str> {
let conf = tpm_path.strip_prefix("device:").unwrap_or(tpm_path);
if conf.starts_with("/dev/") {
vec![conf]
} else {
vec!["/dev/tpmrm0", "/dev/tpm0"]
}
}

/// True when a kernel TPM device exists for `tpm_path`. Socket TCTIs such as swtpm and mssim are not
/// detected because the lab does not use them.
pub(crate) fn tpm_present(tpm_path: &str) -> bool {
// try_exists tells a clean absent (Ok(false)) apart from an IO error. On error we assume the
// device is present rather than silently treating the host as having no TPM.
let dev_exists = |path: &str| {
Path::new(path).try_exists().unwrap_or_else(|e| {
tracing::warn!(path = %path, error = %e, "tpm_present: cannot stat TPM device; assuming present");
true
})
};
tpm_device_candidates(tpm_path)
.iter()
.any(|&p| dev_exists(p))
}

/// Clears the TPM storage hierarchies via TPM2_Clear (lockout authorization), after dictionary
/// lockout setup.
pub(crate) fn clear_tpm(tpm_path: &str) -> Result<(), CarbideClientError> {
Expand Down Expand Up @@ -164,4 +191,35 @@ mod tests {
);
}
}

#[test]
fn tpm_device_candidates_cases() {
let cases: &[(&str, &[&str])] = &[
// explicit device file, with and without the device prefix
("device:/dev/tpmrm0", &["/dev/tpmrm0"]),
("device:/dev/tpm0", &["/dev/tpm0"]),
("/dev/tpmrm0", &["/dev/tpmrm0"]),
// socket and default TCTIs fall back to the standard nodes
(
"mssim:host=localhost,port=2321",
&["/dev/tpmrm0", "/dev/tpm0"],
),
("swtpm:path=/tmp/swtpm-sock", &["/dev/tpmrm0", "/dev/tpm0"]),
("device:", &["/dev/tpmrm0", "/dev/tpm0"]),
("", &["/dev/tpmrm0", "/dev/tpm0"]),
];
for (input, want) in cases {
assert_eq!(tpm_device_candidates(input), *want, "input={input:?}");
}
}

#[test]
fn tpm_present_probes_explicit_device_path() {
// /dev/null always exists on the Linux hosts scout runs on, so an explicit path pointing at
// it reports present, and a bogus /dev path reports absent.
assert!(tpm_present("device:/dev/null"));
assert!(tpm_present("/dev/null"));
assert!(!tpm_present("device:/dev/forge_scout_nonexistent_tpm"));
assert!(!tpm_present("/dev/forge_scout_nonexistent_tpm"));
}
}