diff --git a/crates/scout/src/register.rs b/crates/scout/src/register.rs index 7474663244..ea2aefe5b6 100644 --- a/crates/scout/src/register.rs +++ b/crates/scout/src/register.rs @@ -51,7 +51,17 @@ pub async fn run( let mut att_key_handle_opt: Option = None; let mut tss_ctx_opt: Option = 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()?; @@ -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! // diff --git a/crates/scout/src/tpm.rs b/crates/scout/src/tpm.rs index 2d23bcf0db..d74e9664cb 100644 --- a/crates/scout/src/tpm.rs +++ b/crates/scout/src/tpm.rs @@ -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> { @@ -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")); + } }