From b166a6810d5b5200bb5f2ab43940eca59bb088ce Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 14 Apr 2026 21:41:55 +0100 Subject: [PATCH 01/10] fix: handle pcr_digest during encryption The pcr_digest config field was accepted but ignored during encryption because tpm2-policy's TPMPolicyStep::PCRs always reads live PCR values. Work around this by manually constructing a trial policy session with the caller-supplied digest when pcr_digest is present. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/main.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index d55d12a..065a381 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,14 @@ use std::env; use std::io::{self, Read, Write}; use anyhow::{bail, Context, Error, Result}; +use base64::Engine; use josekit::jwe::{alg::direct::DirectJweAlgorithm::Dir, enc::A256GCM}; use serde::{Deserialize, Serialize}; use tpm2_policy::TPMPolicyStep; -use tss_esapi::structures::SensitiveData; +use tss_esapi::interface_types::algorithm::HashingAlgorithm; +use tss_esapi::interface_types::resource_handles::Hierarchy; +use tss_esapi::interface_types::session_handles::AuthSession; +use tss_esapi::structures::{MaxBuffer, PcrSelectionListBuilder, PcrSlot, SensitiveData}; mod cli; mod tpm_objects; @@ -19,6 +23,81 @@ mod utils; use cli::TPM2Config; +/// Compute a policy digest using caller-supplied PCR values instead of +/// reading them from the TPM. This is a workaround until tpm2-policy +/// supports pcr_digest natively. +/// +/// The pcr_digest is the base64url-no-padding encoding of the concatenated +/// raw PCR values (same format as tpm2_pcrread -o output / jose b64 enc). +fn compute_policy_digest_with_pcr_digest( + ctx: &mut tss_esapi::Context, + pcr_digest_b64: &str, + pcr_ids: &[u64], + pcr_hash_alg: HashingAlgorithm, + name_hash_alg: HashingAlgorithm, +) -> Result<(Option, Option)> { + use tss_esapi::constants::SessionType; + use tss_esapi::structures::SymmetricDefinition; + + // Decode the base64url-no-padding pcr_digest + let concatenated_pcr_values = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(pcr_digest_b64) + .context("Error decoding pcr_digest base64")?; + let concatenated_pcr_values = MaxBuffer::try_from(concatenated_pcr_values) + .context("Error converting pcr_digest to MaxBuffer")?; + + // Build PCR selection + // PcrSlot values are bitmask positions (Slot7 = 0x80 = 1 << 7), + // so we need to convert PCR IDs to bitmask form. + let pcr_slots: Result> = pcr_ids + .iter() + .map(|id| { + if *id > 23 { + anyhow::bail!("PCR ID {} out of valid range (0-23)", id); + } + let bitmask = 1u32 << (*id as u32); + PcrSlot::try_from(bitmask).map_err(|e| anyhow::anyhow!("Invalid PCR ID {}: {}", id, e)) + }) + .collect(); + let pcr_sel = PcrSelectionListBuilder::new() + .with_selection(pcr_hash_alg, &pcr_slots?) + .build() + .context("Error building PCR selection")?; + + // Hash the concatenated PCR values + let (hashed_data, _ticket) = ctx.execute_without_session(|context| { + context.hash( + concatenated_pcr_values, + HashingAlgorithm::Sha256, + Hierarchy::Owner, + ) + })?; + + // Create a trial policy session + let trial_session = ctx + .start_auth_session( + None, + None, + None, + SessionType::Trial, + SymmetricDefinition::AES_128_CFB, + name_hash_alg, + )? + .ok_or_else(|| anyhow::anyhow!("Failed to create trial session"))?; + + // Apply the PCR policy and retrieve the digest. The closure ensures + // the trial session is always flushed, even on error. + let result = (|| -> Result { + ctx.policy_pcr(trial_session.try_into()?, hashed_data, pcr_sel)?; + Ok(ctx.policy_get_digest(trial_session.try_into()?)?) + })(); + + let session_handle: tss_esapi::handles::SessionHandle = trial_session.into(); + let _ = ctx.flush_context(session_handle.into()); + + Ok((None, Some(result?))) +} + fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { let key_type = match &cfg.key { None => "ecc", @@ -37,7 +116,22 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { _ => "tpm2plus", }; - let (_, policy_digest) = policy_runner.send_policy(&mut ctx, true)?; + // If pcr_digest is provided, bypass tpm2-policy's PCR reading and + // manually construct the trial policy with the caller-supplied digest. + // This enables sealing to predicted/future PCR values. + let (_, policy_digest) = if let (Some(ref pcr_digest_b64), Some(ref pcr_ids)) = + (&cfg.pcr_digest, &cfg.get_pcr_ids()) + { + compute_policy_digest_with_pcr_digest( + &mut ctx, + pcr_digest_b64, + pcr_ids, + cfg.get_pcr_hash_alg(), + cfg.get_name_hash_alg(), + )? + } else { + policy_runner.send_policy(&mut ctx, true)? + }; let mut jwk = josekit::jwk::Jwk::generate_oct_key(32).context("Error generating random JWK")?; jwk.set_key_operations(vec!["encrypt", "decrypt"]); @@ -241,6 +335,10 @@ This command uses the following configuration properties: pcr_ids: PCR list used for policy. If not present, no PCR policy is used + pcr_digest: base64url-no-pad encoded concatenation of PCR values to seal + against, in ascending PCR index order. Requires pcr_ids. When + provided, uses the given values instead of reading live PCR state + use_policy: Whether to use a policy policy_ref: Reference to search for in signed policy file (default: {}) From a8f28e5aa4299717ffc36c23e914da66586c0859 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Mon, 20 Apr 2026 15:23:03 +0100 Subject: [PATCH 02/10] test: add pcr_digest integration tests Add two test scenarios for the pcr_digest config field: - Positive: pcr_digest matching live swtpm PCR values, with a checker verifying pcr_digest is not leaked into the JWE token. - Negative: pcr_digest not matching live PCR values. Asserts encryption succeeds but decryption fails (TPM refuses to unseal). This is the primary regression guard against pcr_digest being silently ignored. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- tests/integration_test.rs | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2ee47b6..ed6ad84 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -5,6 +5,7 @@ use std::{io::Write, os::unix::process::CommandExt, process::Command}; use anyhow::{bail, Context, Result}; +use base64::Engine; type CheckFunction = dyn Fn(&str) -> Result<()>; struct EncryptFunc { @@ -20,6 +21,10 @@ struct DecryptFunc { const EXENAME: &str = env!("CARGO_BIN_EXE_clevis-pin-tpm2"); +// An arbitrary non-zero 32-byte value that will not match PCR 23's +// initial all-zeros state. The specific byte values do not matter. +const PCR23_SHA256_WRONG_DIGEST: &str = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"; + const CONFIG_STRINGS: &[(&str, &CheckFunction)] = &[ // No sealing (r#"{}"#, &always_success), @@ -31,6 +36,11 @@ const CONFIG_STRINGS: &[(&str, &CheckFunction)] = &[ (r#"{"pcr_ids": [23]}"#, &always_success), // sealed against SHA1 PCR23 (r#"{"pcr_bank": "sha1", "pcr_ids": [23]}"#, &always_success), + // Sealed against PCR23 with caller-supplied pcr_digest matching swtpm state + ( + r#"{"pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#, + &check_no_pcr_digest_in_token, + ), ]; // Check functions @@ -38,6 +48,28 @@ fn always_success(_token: &str) -> Result<()> { Ok(()) } +// Regression guard: verify pcr_digest is not persisted in the JWE token. +fn check_no_pcr_digest_in_token(token: &str) -> Result<()> { + let parts: Vec<&str> = token.trim().split('.').collect(); + if parts.len() != 5 { + bail!("JWE token does not have 5 parts (got {})", parts.len()); + } + let header_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(parts[0]) + .context("Failed to decode JWE protected header")?; + let header: serde_json::Value = + serde_json::from_slice(&header_bytes).context("Failed to parse JWE header as JSON")?; + if let Some(tpm2) = header.get("clevis").and_then(|c| c.get("tpm2")) { + if tpm2.get("pcr_digest").is_some() { + bail!("pcr_digest was leaked into the JWE token header"); + } + } + if header.get("pcr_digest").is_some() { + bail!("pcr_digest was leaked into the top-level JWE header"); + } + Ok(()) +} + fn call_cmd_and_get_output(cmd: &mut Command, input: &str) -> Result { if let Ok(val) = std::env::var("TCTI") { cmd.env("TCTI", &val); @@ -193,4 +225,35 @@ fn pcr_tests() { if failed != 0 { panic!("{} tests failed", failed); } + + // Negative test: sealing with a pcr_digest that does not match the + // live PCR values must encrypt successfully but fail to decrypt (the + // TPM refuses to unseal). This is the primary regression guard against + // the original bug where pcr_digest was silently ignored. + let mismatch_config = format!( + r#"{{"pcr_ids": [23], "pcr_digest": "{}"}}"#, + PCR23_SHA256_WRONG_DIGEST + ); + let encrypt_fn = generate_encrypt_us(false); + let decrypt_fn = generate_decrypt_us(false); + + eprintln!("pcr_digest_mismatch: encrypting with non-matching digest"); + let encrypted = (encrypt_fn.func)(INPUT, &mismatch_config) + .expect("encrypt with mismatched pcr_digest should succeed"); + + let parts: Vec<&str> = encrypted.trim().split('.').collect(); + assert_eq!( + parts.len(), + 5, + "encrypted output is not a valid JWE (expected 5 parts, got {})", + parts.len() + ); + + eprintln!("pcr_digest_mismatch: decrypting (should fail)"); + let result = (decrypt_fn.func)(&encrypted); + assert!( + result.is_err(), + "decrypt should fail when pcr_digest does not match live PCR values" + ); + eprintln!("pcr_digest_mismatch: PASSED"); } From 321b0d48f80cd35c34238dc0c14b4006f16de40f Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Mon, 20 Apr 2026 15:39:27 +0100 Subject: [PATCH 03/10] fix: use session hash for pcr_digest policy The ctx.hash() call in compute_policy_digest_with_pcr_digest hardcoded HashingAlgorithm::Sha256 instead of using the session's name hash algorithm. Per TPM 2.0 Part 3 Section 23.7, the pcrDigest parameter uses the policy session's hash algorithm, not the PCR bank hash. This was accidentally correct for the default SHA-256 case but would produce wrong results with a non-default name hash. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 065a381..1392332 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,13 +64,11 @@ fn compute_policy_digest_with_pcr_digest( .build() .context("Error building PCR selection")?; - // Hash the concatenated PCR values + // Hash the concatenated PCR values using the session hash algorithm + // (per TPM 2.0 Part 3, Section 23.7: pcrDigest uses the session hash, + // not the PCR bank hash). let (hashed_data, _ticket) = ctx.execute_without_session(|context| { - context.hash( - concatenated_pcr_values, - HashingAlgorithm::Sha256, - Hierarchy::Owner, - ) + context.hash(concatenated_pcr_values, name_hash_alg, Hierarchy::Owner) })?; // Create a trial policy session From 666b715a65c8eb8726ecc61bdef6329eeb791543 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Mon, 20 Apr 2026 15:40:45 +0100 Subject: [PATCH 04/10] fix: reject pcr_digest with authorized policy When pcr_digest is provided, the encrypt path bypasses the policy_runner entirely and constructs a PCR-only trial policy. This silently drops the authorized policy branch from an OR'd policy, producing a weaker policy than expected. Reject the combination early in normalize() with a clear error message rather than silently computing the wrong policy digest. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/cli.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 9d336ec..7046c93 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -126,6 +126,9 @@ impl TPM2Config { { eprintln!("To use a policy, please specifiy use_policy: true. Not specifying this will be a fatal error in a next release"); } + if self.pcr_digest.is_some() && self.policy_pubkey_path.is_some() { + bail!("pcr_digest cannot be combined with authorized policy"); + } if (self.policy_pubkey_path.is_some() || self.policy_path.is_some() || self.policy_ref.is_some()) @@ -271,4 +274,15 @@ mod tests { let result = serde_json::from_str::(config_str); assert!(result.is_ok()); } + + #[test] + fn test_pcr_digest_with_policy_rejected() { + let config_str = r#"{"pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "use_policy": true}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("pcr_digest cannot be combined")); + } } From 4b515bda56a410d54672bd992c84c5ccb56ee8c2 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 21 Apr 2026 11:20:50 +0100 Subject: [PATCH 05/10] refactor: return Result from get_hash_alg_from_name The function panicked on unsupported hash algorithm names. This is reachable from the decrypt path via data in stored JWE tokens, meaning a crafted token could crash the process. Return Result instead of panicking, and propagate errors through all callers. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/cli.rs | 9 +++++---- src/main.rs | 12 ++++++------ src/utils.rs | 16 ++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 7046c93..9e46046 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,7 +35,7 @@ impl TryFrom<&TPM2Config> for TPMPolicyStep { match (&cfg.pcr_ids, &cfg.policy_pubkey_path) { (Some(_), Some(pubkey_path)) => Ok(TPMPolicyStep::Or([ Box::new(TPMPolicyStep::PCRs( - cfg.get_pcr_hash_alg(), + cfg.get_pcr_hash_alg()?, cfg.get_pcr_ids().unwrap(), Box::new(TPMPolicyStep::NoStep), )), @@ -52,7 +52,7 @@ impl TryFrom<&TPM2Config> for TPMPolicyStep { Box::new(TPMPolicyStep::NoStep), ])), (Some(_), None) => Ok(TPMPolicyStep::PCRs( - cfg.get_pcr_hash_alg(), + cfg.get_pcr_hash_alg()?, cfg.get_pcr_ids().unwrap(), Box::new(TPMPolicyStep::NoStep), )), @@ -71,13 +71,13 @@ pub(crate) const DEFAULT_POLICY_REF: &str = ""; impl TPM2Config { pub(super) fn get_pcr_hash_alg( &self, - ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { + ) -> anyhow::Result { crate::utils::get_hash_alg_from_name(self.pcr_bank.as_ref()) } pub(super) fn get_name_hash_alg( &self, - ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { + ) -> anyhow::Result { crate::utils::get_hash_alg_from_name(self.hash.as_ref()) } @@ -285,4 +285,5 @@ mod tests { let err = result.unwrap_err(); assert!(err.to_string().contains("pcr_digest cannot be combined")); } + } diff --git a/src/main.rs b/src/main.rs index 1392332..2316529 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,7 +101,7 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { None => "ecc", Some(key_type) => key_type, }; - let key_public = tpm_objects::get_key_public(key_type, cfg.get_name_hash_alg())?; + let key_public = tpm_objects::get_key_public(key_type, cfg.get_name_hash_alg()?)?; let mut ctx = utils::get_tpm2_ctx()?; let key_handle = utils::get_tpm2_primary_key(&mut ctx, key_public)?; @@ -124,8 +124,8 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { &mut ctx, pcr_digest_b64, pcr_ids, - cfg.get_pcr_hash_alg(), - cfg.get_name_hash_alg(), + cfg.get_pcr_hash_alg()?, + cfg.get_name_hash_alg()?, )? } else { policy_runner.send_policy(&mut ctx, true)? @@ -229,7 +229,7 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { match (&cfg.pcr_ids, &cfg.policy_pubkey_path) { (Some(_), Some(pubkey_path)) => Ok(TPMPolicyStep::Or([ Box::new(TPMPolicyStep::PCRs( - utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref()), + utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, cfg.get_pcr_ids().unwrap(), Box::new(TPMPolicyStep::NoStep), )), @@ -246,7 +246,7 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { Box::new(TPMPolicyStep::NoStep), ])), (Some(_), None) => Ok(TPMPolicyStep::PCRs( - utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref()), + utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, cfg.get_pcr_ids().unwrap(), Box::new(TPMPolicyStep::NoStep), )), @@ -283,7 +283,7 @@ fn perform_decrypt(input: Vec) -> Result<()> { let policy = TPMPolicyStep::try_from(&hdr_clevis.tpm2)?; - let name_alg = crate::utils::get_hash_alg_from_name(Some(&hdr_clevis.tpm2.hash)); + let name_alg = crate::utils::get_hash_alg_from_name(Some(&hdr_clevis.tpm2.hash))?; let key_public = tpm_objects::get_key_public(hdr_clevis.tpm2.key.as_str(), name_alg)?; let mut ctx = utils::get_tpm2_ctx()?; diff --git a/src/utils.rs b/src/utils.rs index f1dd3ca..c8dd04a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,7 +6,7 @@ use std::env; use std::fs; use std::str::FromStr; -use anyhow::{Context as anyhow_context, Result}; +use anyhow::{bail, Context as anyhow_context, Result}; use base64::Engine; use serde::Deserialize; use tpm2_policy::{PublicKey, SignedPolicyList, TPMPolicyStep}; @@ -53,15 +53,15 @@ pub(crate) fn get_authorized_policy_step( }) } -pub(crate) fn get_hash_alg_from_name(name: Option<&String>) -> HashingAlgorithm { +pub(crate) fn get_hash_alg_from_name(name: Option<&String>) -> Result { match name { - None => HashingAlgorithm::Sha256, + None => Ok(HashingAlgorithm::Sha256), Some(val) => match val.to_lowercase().as_str() { - "sha1" => HashingAlgorithm::Sha1, - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, - _ => panic!("Unsupported hash algo: {:?}", name), + "sha1" => Ok(HashingAlgorithm::Sha1), + "sha256" => Ok(HashingAlgorithm::Sha256), + "sha384" => Ok(HashingAlgorithm::Sha384), + "sha512" => Ok(HashingAlgorithm::Sha512), + _ => bail!("Unsupported hash algorithm: {:?}", name), }, } } From 1c64674c689a812d72e383d7641b202e20003054 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Mon, 20 Apr 2026 15:42:29 +0100 Subject: [PATCH 06/10] fix: validate pcr_digest input in normalize() Validate the pcr_digest config field early in normalize(): - Reject empty digest (never meaningful) - Reject invalid base64url-no-pad encoding - Reject wrong length when pcr_ids is also set (must match num_pcrs * hash_size for the configured pcr_bank) - Reject unsupported pcr_bank values with bail! instead of letting them reach the error in get_hash_alg_from_name Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/cli.rs | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/utils.rs | 10 +++ 2 files changed, 221 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 9e46046..6c75487 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,6 +5,7 @@ use std::convert::TryFrom; use anyhow::{anyhow, bail, Error, Result}; +use base64::Engine; use serde::{Deserialize, Serialize}; use std::io::IsTerminal; use tpm2_policy::TPMPolicyStep; @@ -109,6 +110,53 @@ impl TPM2Config { if self.pcr_ids.is_some() && self.pcr_bank.is_none() { self.pcr_bank = Some("sha256".to_string()); } + if let Some(ref hash) = self.hash { + crate::utils::get_hash_alg_from_name(Some(hash))?; + } + if let Some(ref bank) = self.pcr_bank { + crate::utils::get_hash_alg_from_name(Some(bank))?; + } + // tpm2-policy 0.6.0 hardcodes SHA-256 for policy sessions on the + // decrypt path, so non-SHA-256 name hash with PCR binding would + // produce tokens that encrypt successfully but can never be unsealed. + if self.pcr_ids.is_some() { + if let Some(ref hash) = self.hash { + if hash.to_lowercase() != "sha256" { + bail!( + "non-SHA-256 hash is not supported with PCR binding \ + (tpm2-policy hardcodes SHA-256 for policy sessions)" + ); + } + } + } + if self.pcr_digest.is_some() && self.pcr_ids.is_none() { + bail!("pcr_digest requires pcr_ids"); + } + if let Some(ref digest) = self.pcr_digest { + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(digest) + .map_err(|e| anyhow!("invalid pcr_digest base64: {}", e))?; + if decoded.is_empty() { + bail!("pcr_digest must not be empty"); + } + if let Some(ref pcr_ids) = self.pcr_ids { + let num_pcrs = match pcr_ids { + serde_json::Value::Array(v) => v.len(), + _ => bail!("pcr_ids has unexpected type (expected array)"), + }; + let hash_size = crate::utils::hash_digest_size(self.pcr_bank.as_ref())?; + let expected = num_pcrs * hash_size; + if decoded.len() != expected { + bail!( + "pcr_digest length {} does not match expected {} ({} PCRs * {} bytes)", + decoded.len(), + expected, + num_pcrs, + hash_size + ); + } + } + } // Make use of the defaults if not specified if self.use_policy.is_some() && self.use_policy.unwrap() { if self.policy_path.is_none() { @@ -171,6 +219,10 @@ impl TPM2Config { if !new.is_u64() { bail!("Non-positive string int"); } + let v = new.as_u64().unwrap(); + if v > 23 { + bail!("PCR ID {} out of valid range (0-23)", v); + } Ok(new) } Err(_) => Err(anyhow!("Unparseable string int")), @@ -181,6 +233,10 @@ impl TPM2Config { if !new.is_u64() { return Err(anyhow!("Non-positive int")); } + let v = new.as_u64().unwrap(); + if v > 23 { + bail!("PCR ID {} out of valid range (0-23)", v); + } Ok(new) } _ => Err(anyhow!("Invalid value in pcr_ids")), @@ -189,9 +245,12 @@ impl TPM2Config { self.pcr_ids = Some(serde_json::Value::Array(newvals?)); } + if let Some(serde_json::Value::Array(ref mut vals)) = self.pcr_ids { + vals.sort_by_key(|v| v.as_u64().unwrap_or(0)); + } + match &self.pcr_ids { None => Ok(()), - // The normalization above would've caught any non-ints Some(serde_json::Value::Array(_)) => Ok(()), _ => Err(anyhow!("Invalid type")), } @@ -286,4 +345,155 @@ mod tests { assert!(err.to_string().contains("pcr_digest cannot be combined")); } + #[test] + fn test_pcr_digest_empty_rejected() { + let config_str = r#"{"pcr_ids": [23], "pcr_digest": ""}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("must not be empty")); + } + + #[test] + fn test_pcr_digest_invalid_base64_rejected() { + let config_str = r#"{"pcr_ids": [23], "pcr_digest": "not!valid!base64"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("invalid pcr_digest")); + } + + #[test] + fn test_pcr_digest_wrong_length_rejected() { + // 27 A's = 20 bytes (SHA-1 size), but pcr_bank defaults to sha256 (32 bytes) + let config_str = r#"{"pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("does not match expected")); + } + + #[test] + fn test_pcr_digest_correct_length_accepted() { + // 43 A's = 32 bytes, matching 1 PCR with default sha256 bank + let config_str = + r#"{"pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_ok()); + } + + #[test] + fn test_pcr_digest_unsupported_bank_rejected() { + let config_str = + r#"{"pcr_ids": [23], "pcr_bank": "md5", "pcr_digest": "AAAAAAAAAAAAAAAA"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Unsupported")); + } + + #[test] + fn test_unsupported_hash_rejected() { + let config_str = r#"{"hash": "md5"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Unsupported")); + } + + #[test] + fn test_pcr_id_out_of_range_rejected() { + let config_str = r#"{"pcr_ids": [24]}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("out of valid range")); + } + + #[test] + fn test_pcr_id_large_value_rejected() { + let config_str = r#"{"pcr_ids": [4294967296]}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("out of valid range")); + } + + #[test] + fn test_pcr_digest_without_pcr_ids_rejected() { + let config_str = r#"{"pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("pcr_digest requires pcr_ids")); + } + + #[test] + fn test_non_sha256_hash_with_pcr_ids_rejected() { + let config_str = r#"{"hash": "sha384", "pcr_ids": [7]}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("non-SHA-256 hash")); + } + + #[test] + fn test_non_sha256_hash_with_pcr_digest_rejected() { + let config_str = + r#"{"hash": "sha384", "pcr_ids": [7], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("non-SHA-256 hash")); + } + + #[test] + fn test_non_sha256_hash_without_pcr_ids_accepted() { + let config_str = r#"{"hash": "sha384"}"#; + let result = serde_json::from_str::(config_str) + .unwrap() + .normalize(); + assert!(result.is_ok()); + } + + #[test] + fn test_pcr_ids_sorted_after_normalize() { + let config_str = r#"{"pcr_ids": [23, 7, 0]}"#; + let cfg = serde_json::from_str::(config_str) + .unwrap() + .normalize() + .unwrap(); + let ids = cfg.get_pcr_ids().unwrap(); + assert_eq!(ids, vec![0, 7, 23]); + } } diff --git a/src/utils.rs b/src/utils.rs index c8dd04a..b189842 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -66,6 +66,16 @@ pub(crate) fn get_hash_alg_from_name(name: Option<&String>) -> Result) -> Result { + match get_hash_alg_from_name(name)? { + HashingAlgorithm::Sha1 => Ok(20), + HashingAlgorithm::Sha256 => Ok(32), + HashingAlgorithm::Sha384 => Ok(48), + HashingAlgorithm::Sha512 => Ok(64), + _ => bail!("Unsupported hash algorithm"), + } +} + pub(crate) fn serialize_as_base64_url_no_pad( bytes: &[u8], serializer: S, From f4c667af52b9e97723243ee658f27305751f87d7 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Mon, 20 Apr 2026 15:43:44 +0100 Subject: [PATCH 07/10] test: add multi-PCR and SHA-1 pcr_digest tests Add integration tests exercising pcr_digest with: - Multiple PCRs (16+23): validates that the concatenated digest length and value are handled correctly for multi-PCR policies. - SHA-1 PCR bank: exercises the non-default hash algorithm path after the session hash fix. - Multi-PCR negative test: sealing to PCR 16+23 with a wrong digest succeeds at encryption but fails at decryption. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- tests/integration_test.rs | 72 +++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index ed6ad84..e34988e 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -25,6 +25,10 @@ const EXENAME: &str = env!("CARGO_BIN_EXE_clevis-pin-tpm2"); // initial all-zeros state. The specific byte values do not matter. const PCR23_SHA256_WRONG_DIGEST: &str = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"; +// An arbitrary non-zero 64-byte value for multi-PCR mismatch testing. +const PCR16_23_SHA256_WRONG_DIGEST: &str = + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ"; + const CONFIG_STRINGS: &[(&str, &CheckFunction)] = &[ // No sealing (r#"{}"#, &always_success), @@ -41,6 +45,16 @@ const CONFIG_STRINGS: &[(&str, &CheckFunction)] = &[ r#"{"pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#, &check_no_pcr_digest_in_token, ), + // Multi-PCR: sealed against PCR 16+23 with caller-supplied pcr_digest + ( + r#"{"pcr_ids": [16, 23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#, + &check_no_pcr_digest_in_token, + ), + // SHA-1 bank: sealed against PCR 23 with caller-supplied pcr_digest + ( + r#"{"pcr_bank": "sha1", "pcr_ids": [23], "pcr_digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#, + &check_no_pcr_digest_in_token, + ), ]; // Check functions @@ -230,30 +244,46 @@ fn pcr_tests() { // live PCR values must encrypt successfully but fail to decrypt (the // TPM refuses to unseal). This is the primary regression guard against // the original bug where pcr_digest was silently ignored. - let mismatch_config = format!( - r#"{{"pcr_ids": [23], "pcr_digest": "{}"}}"#, - PCR23_SHA256_WRONG_DIGEST - ); + let mismatch_cases: &[(&str, &str)] = &[ + ( + &format!( + r#"{{"pcr_ids": [23], "pcr_digest": "{}"}}"#, + PCR23_SHA256_WRONG_DIGEST + ), + "single-PCR", + ), + ( + &format!( + r#"{{"pcr_ids": [16, 23], "pcr_digest": "{}"}}"#, + PCR16_23_SHA256_WRONG_DIGEST + ), + "multi-PCR", + ), + ]; + let encrypt_fn = generate_encrypt_us(false); let decrypt_fn = generate_decrypt_us(false); - eprintln!("pcr_digest_mismatch: encrypting with non-matching digest"); - let encrypted = (encrypt_fn.func)(INPUT, &mismatch_config) - .expect("encrypt with mismatched pcr_digest should succeed"); + for (config, label) in mismatch_cases { + eprintln!("pcr_digest_mismatch ({label}): encrypting with non-matching digest"); + let encrypted = (encrypt_fn.func)(INPUT, config) + .expect("encrypt with mismatched pcr_digest should succeed"); - let parts: Vec<&str> = encrypted.trim().split('.').collect(); - assert_eq!( - parts.len(), - 5, - "encrypted output is not a valid JWE (expected 5 parts, got {})", - parts.len() - ); + let parts: Vec<&str> = encrypted.trim().split('.').collect(); + assert_eq!( + parts.len(), + 5, + "encrypted output is not a valid JWE (expected 5 parts, got {})", + parts.len() + ); - eprintln!("pcr_digest_mismatch: decrypting (should fail)"); - let result = (decrypt_fn.func)(&encrypted); - assert!( - result.is_err(), - "decrypt should fail when pcr_digest does not match live PCR values" - ); - eprintln!("pcr_digest_mismatch: PASSED"); + eprintln!("pcr_digest_mismatch ({label}): decrypting (should fail)"); + let result = (decrypt_fn.func)(&encrypted); + assert!( + result.is_err(), + "decrypt should fail when pcr_digest does not match live PCR values ({})", + label + ); + eprintln!("pcr_digest_mismatch ({label}): PASSED"); + } } From ede290c366e7e441b90ac124fae9e7aff6afd2b2 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 21 Apr 2026 11:21:41 +0100 Subject: [PATCH 08/10] fix: pass name hash to sealed object creation The sealed object's nameAlg was hardcoded to SHA-256, ignoring the user's hash configuration. This caused TPM2_Create to fail with TPM_RC_SIZE when the policy digest size (determined by the session hash) did not match the hardcoded nameAlg. Note: non-SHA-256 hash values with PCR binding are rejected at configuration time because tpm2-policy 0.6.0 hardcodes SHA-256 for policy sessions on the decrypt path. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/main.rs | 11 +++++++---- src/tpm_objects.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2316529..fa35f1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,7 +101,8 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { None => "ecc", Some(key_type) => key_type, }; - let key_public = tpm_objects::get_key_public(key_type, cfg.get_name_hash_alg()?)?; + let name_hash_alg = cfg.get_name_hash_alg()?; + let key_public = tpm_objects::get_key_public(key_type, name_hash_alg)?; let mut ctx = utils::get_tpm2_ctx()?; let key_handle = utils::get_tpm2_primary_key(&mut ctx, key_public)?; @@ -120,12 +121,13 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { let (_, policy_digest) = if let (Some(ref pcr_digest_b64), Some(ref pcr_ids)) = (&cfg.pcr_digest, &cfg.get_pcr_ids()) { + let pcr_hash_alg = cfg.get_pcr_hash_alg()?; compute_policy_digest_with_pcr_digest( &mut ctx, pcr_digest_b64, pcr_ids, - cfg.get_pcr_hash_alg()?, - cfg.get_name_hash_alg()?, + pcr_hash_alg, + name_hash_alg, )? } else { policy_runner.send_policy(&mut ctx, true)? @@ -135,7 +137,8 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { jwk.set_key_operations(vec!["encrypt", "decrypt"]); let jwk_str = serde_json::to_string(&jwk.as_ref())?; - let public = tpm_objects::create_tpm2b_public_sealed_object(policy_digest)?.try_into()?; + let public = + tpm_objects::create_tpm2b_public_sealed_object(policy_digest, name_hash_alg)?.try_into()?; let jwk_str = SensitiveData::try_from(jwk_str.as_bytes().to_vec())?; let jwk_result = ctx.execute_with_nullauth_session(|ctx| { ctx.create(key_handle, public, None, Some(jwk_str), None, None) diff --git a/src/tpm_objects.rs b/src/tpm_objects.rs index a94f8e0..9aa221d 100644 --- a/src/tpm_objects.rs +++ b/src/tpm_objects.rs @@ -65,7 +65,15 @@ pub(super) fn get_key_public(key_type: &str, name_alg: HashingAlgorithm) -> Resu pub(super) fn create_tpm2b_public_sealed_object( policy: Option, + name_hash_alg: HashingAlgorithm, ) -> Result { + let name_alg = match name_hash_alg { + HashingAlgorithm::Sha1 => tss_constants::TPM2_ALG_SHA1, + HashingAlgorithm::Sha256 => tss_constants::TPM2_ALG_SHA256, + HashingAlgorithm::Sha384 => tss_constants::TPM2_ALG_SHA384, + HashingAlgorithm::Sha512 => tss_constants::TPM2_ALG_SHA512, + _ => bail!("Unsupported hash algorithm for sealed object"), + }; let mut object_attributes = ObjectAttributesBuilder::new() .with_fixed_tpm(true) .with_fixed_parent(true) @@ -87,7 +95,7 @@ pub(super) fn create_tpm2b_public_sealed_object( size: std::mem::size_of::() as u16, publicArea: tss_esapi::tss2_esys::TPMT_PUBLIC { type_: tss_constants::TPM2_ALG_KEYEDHASH, - nameAlg: tss_constants::TPM2_ALG_SHA256, + nameAlg: name_alg, objectAttributes: object_attributes.build()?.0, authPolicy: tss_esapi::tss2_esys::TPM2B_DIGEST::from(policy), parameters: params, @@ -173,3 +181,20 @@ pub(super) fn build_tpm2b_public(val: &[u8]) -> Result Date: Tue, 21 Apr 2026 14:25:09 +0100 Subject: [PATCH 09/10] fix: handle invalid pcr_ids in JWE tokens Tpm2Inner::get_pcr_ids() used .parse::().unwrap() on PCR ID strings from deserialized JWE headers. A crafted token with non-numeric PCR IDs (e.g., "pcr_ids": "7,abc") would panic the process during decryption. Return Result and propagate parse errors instead of panicking. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/main.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index fa35f1c..72b9438 100644 --- a/src/main.rs +++ b/src/main.rs @@ -214,14 +214,21 @@ struct Tpm2Inner { } impl Tpm2Inner { - fn get_pcr_ids(&self) -> Option> { - Some( - self.pcr_ids - .as_ref()? - .split(',') - .map(|x| x.parse::().unwrap()) - .collect(), - ) + fn get_pcr_ids(&self) -> Result>> { + match &self.pcr_ids { + None => Ok(None), + Some(ids) => { + let parsed: Result> = ids + .split(',') + .map(|x| { + x.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid PCR ID '{}': {}", x, e)) + }) + .collect(); + Ok(Some(parsed?)) + } + } } } @@ -233,7 +240,7 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { (Some(_), Some(pubkey_path)) => Ok(TPMPolicyStep::Or([ Box::new(TPMPolicyStep::PCRs( utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, - cfg.get_pcr_ids().unwrap(), + cfg.get_pcr_ids()?.ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), Box::new(utils::get_authorized_policy_step( @@ -250,7 +257,7 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { ])), (Some(_), None) => Ok(TPMPolicyStep::PCRs( utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, - cfg.get_pcr_ids().unwrap(), + cfg.get_pcr_ids()?.ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), (None, Some(pubkey_path)) => { From d1ebfd7f786a56201798803988e0f5dea5cb413d Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Wed, 29 Apr 2026 12:45:33 +0100 Subject: [PATCH 10/10] cli: convert TPM2Config::get_pcr_ids() to return Result The methods used panic!() and .unwrap() on pcr_ids array elements, relying on normalize_pcr_ids() having run first. Return Result instead of panicking to be consistent with the Tpm2Inner::get_pcr_ids() pattern and eliminate coupling to normalization ordering. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/cli.rs | 46 ++++++++++++++++++++++++++++++---------------- src/main.rs | 10 ++++++---- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 6c75487..a04b7a6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,7 +37,8 @@ impl TryFrom<&TPM2Config> for TPMPolicyStep { (Some(_), Some(pubkey_path)) => Ok(TPMPolicyStep::Or([ Box::new(TPMPolicyStep::PCRs( cfg.get_pcr_hash_alg()?, - cfg.get_pcr_ids().unwrap(), + cfg.get_pcr_ids()? + .ok_or_else(|| anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), Box::new(get_authorized_policy_step( @@ -54,7 +55,8 @@ impl TryFrom<&TPM2Config> for TPMPolicyStep { ])), (Some(_), None) => Ok(TPMPolicyStep::PCRs( cfg.get_pcr_hash_alg()?, - cfg.get_pcr_ids().unwrap(), + cfg.get_pcr_ids()? + .ok_or_else(|| anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), (None, Some(pubkey_path)) => { @@ -82,26 +84,38 @@ impl TPM2Config { crate::utils::get_hash_alg_from_name(self.hash.as_ref()) } - pub(super) fn get_pcr_ids(&self) -> Option> { + pub(super) fn get_pcr_ids(&self) -> Result>> { match &self.pcr_ids { - None => None, + None => Ok(None), Some(serde_json::Value::Array(vals)) => { - Some(vals.iter().map(|x| x.as_u64().unwrap()).collect()) + let ids: Result> = vals + .iter() + .map(|x| { + x.as_u64() + .ok_or_else(|| anyhow!("non-u64 value in pcr_ids")) + }) + .collect(); + Ok(Some(ids?)) } - _ => panic!("Unexpected type found for pcr_ids"), + _ => bail!("Unexpected type found for pcr_ids"), } } - pub(super) fn get_pcr_ids_str(&self) -> Option { + pub(super) fn get_pcr_ids_str(&self) -> Result> { match &self.pcr_ids { - None => None, - Some(serde_json::Value::Array(vals)) => Some( - vals.iter() - .map(|x| x.as_u64().unwrap().to_string()) - .collect::>() - .join(","), - ), - _ => panic!("Unexpected type found for pcr_ids"), + None => Ok(None), + Some(serde_json::Value::Array(vals)) => { + let strs: Result> = vals + .iter() + .map(|x| { + x.as_u64() + .map(|v| v.to_string()) + .ok_or_else(|| anyhow!("non-u64 value in pcr_ids")) + }) + .collect(); + Ok(Some(strs?.join(","))) + } + _ => bail!("Unexpected type found for pcr_ids"), } } @@ -493,7 +507,7 @@ mod tests { .unwrap() .normalize() .unwrap(); - let ids = cfg.get_pcr_ids().unwrap(); + let ids = cfg.get_pcr_ids().unwrap().unwrap(); assert_eq!(ids, vec![0, 7, 23]); } } diff --git a/src/main.rs b/src/main.rs index 72b9438..c859833 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,7 +119,7 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { // manually construct the trial policy with the caller-supplied digest. // This enables sealing to predicted/future PCR values. let (_, policy_digest) = if let (Some(ref pcr_digest_b64), Some(ref pcr_ids)) = - (&cfg.pcr_digest, &cfg.get_pcr_ids()) + (&cfg.pcr_digest, &cfg.get_pcr_ids()?) { let pcr_hash_alg = cfg.get_pcr_hash_alg()?; compute_policy_digest_with_pcr_digest( @@ -156,7 +156,7 @@ fn perform_encrypt(cfg: TPM2Config, input: Vec) -> Result<()> { jwk_pub, jwk_priv, pcr_bank: cfg.pcr_bank.clone(), - pcr_ids: cfg.get_pcr_ids_str(), + pcr_ids: cfg.get_pcr_ids_str()?, policy_pubkey_path: cfg.policy_pubkey_path, policy_ref: cfg.policy_ref, policy_path: cfg.policy_path, @@ -240,7 +240,8 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { (Some(_), Some(pubkey_path)) => Ok(TPMPolicyStep::Or([ Box::new(TPMPolicyStep::PCRs( utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, - cfg.get_pcr_ids()?.ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, + cfg.get_pcr_ids()? + .ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), Box::new(utils::get_authorized_policy_step( @@ -257,7 +258,8 @@ impl TryFrom<&Tpm2Inner> for TPMPolicyStep { ])), (Some(_), None) => Ok(TPMPolicyStep::PCRs( utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref())?, - cfg.get_pcr_ids()?.ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, + cfg.get_pcr_ids()? + .ok_or_else(|| anyhow::anyhow!("pcr_ids unexpectedly empty"))?, Box::new(TPMPolicyStep::NoStep), )), (None, Some(pubkey_path)) => {