Fix pcr_digest handling and harden error paths#42
Conversation
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
Tpm2Inner::get_pcr_ids() used .parse::<u64>().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 <scorreia@redhat.com>
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 <scorreia@redhat.com>
sarroutbi
left a comment
There was a problem hiding this comment.
Can you please evaluate next findings provided by Gemini?:
1. Robust Session Management (src/main.rs)
This ensures the TPM trial session is flushed even if the policy calculation fails, preventing session slot exhaustion.
// Replace the session handling logic in compute_policy_digest_with_pcr_digest
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"))?;
let session_handle: tss_esapi::handles::SessionHandle = trial_session.into();
// Execute policy operations and guarantee session flush
let result = {
let res = (|| -> Result<tss_esapi::structures::Digest> {
ctx.policy_pcr(session_handle, hashed_data, pcr_sel)?;
Ok(ctx.policy_get_digest(session_handle)?)
})();
// Ensure the session is flushed regardless of success or failure of the policy steps
let _ = ctx.flush_context(session_handle.into());
res
};
Ok((None, Some(result?)))2. Strict Algorithm and Length Validation (src/cli.rs)
This prevents "silent" encryption success where the resulting object can never be decrypted due to algorithm or ordering mismatches.
// Add/update these checks inside TPM2Config::normalize()
if let Some(ref digest_b64) = self.pcr_digest {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(digest_b64)
.map_err(|e| anyhow::anyhow!("invalid pcr_digest base64: {}", e))?;
let pcr_bank_alg = self.get_pcr_hash_alg()?;
let name_alg = self.get_name_hash_alg()?;
// 1. Ensure the Name Algorithm matches the PCR Bank to avoid complex double-hash mismatches
if pcr_bank_alg != name_alg {
bail!("When using pcr_digest, 'hash' (nameAlg) must match 'pcr_bank' to ensure policy consistency.");
}
// 2. Validate exact length based on number of PCRs and hash size
if let Some(ref pcr_ids_val) = self.pcr_ids {
let num_pcrs = match pcr_ids_val {
serde_json::Value::Array(v) => v.len(),
_ => bail!("pcr_ids must be an array"),
};
let hash_size = crate::utils::hash_digest_size(self.pcr_bank.as_ref())?;
let expected_len = num_pcrs * hash_size;
if decoded.len() != expected_len {
bail!(
"pcr_digest length mismatch: expected {} bytes ({} PCRs * {} bytes), but got {} bytes. \
Ensure values are concatenated in ascending numerical order of PCR index.",
expected_len,
num_pcrs,
hash_size,
decoded.len()
);
}
}
}3. Safer PCR ID Parsing (src/main.rs)
Prevents process panics when encountering malformed or trailing-comma strings in stored JWE headers.
// Replace Tpm2Inner::get_pcr_ids in src/main.rs
fn get_pcr_ids(&self) -> Result<Option<Vec<u64>>> {
match &self.pcr_ids {
None => Ok(None),
Some(ids) => {
let parsed: Result<Vec<u64>> = ids
.split(',')
.filter(|s| !s.trim().is_empty()) // Prevent errors on trailing or double commas
.map(|x| {
x.trim()
.parse::<u64>()
.map_err(|e| anyhow::anyhow!("Invalid PCR ID '{}': {}", x, e))
})
.collect();
Ok(Some(parsed?))
}
}
}|
Thanks for the thorough review! Here are my thoughts on each item: 1. Robust Session ManagementThe suggested pattern converts The current code keeps the value as 2. Strict Algorithm and Length ValidationThe base64url-no-pad decoding, empty-digest rejection, and length validation ( Regarding the Adding this restriction would break the existing (and tested) configuration The actual consistency constraint is already enforced: non-SHA-256 3. Safer PCR ID ParsingThe current code already handles trailing/double commas without panicking. For input like Silently filtering empty segments would be the wrong approach for this code path. |
Thanks for the thorough reply ! Changes LGTM |
|
Thank you for the review! |
The
pcr_digestconfiguration field was accepted but silently ignored during encryption because tpm2-policy always reads live PCR values. This series fixes that by manually constructing a trial policy session with the caller-supplied digest whenpcr_digestis present, enabling sealing to predicted or future PCR values (e.g., before a kernel update). Input validation innormalize()now catches malformed configurations early with clear error messages: invalid base64, wrong digest length, out-of-range PCR IDs, and incompatible option combinations.The series also fixes several panic paths reachable from untrusted input. On the decrypt path,
get_hash_alg_from_name()panicked on unsupported hash names andTpm2Inner::get_pcr_ids()panicked on non-numeric PCR ID strings, both reachable via crafted JWE tokens stored on disk. These now return proper errors instead of crashing the process during early-boot decryption. The sealed object'snameAlgwas hardcoded to SHA-256, causingTPM2_Createto fail when the policy digest size didn't match; it now respects the user's hash configuration. Non-SHA-256 hash with PCR binding is explicitly rejected since tpm2-policy 0.6.0 hardcodes SHA-256 for policy sessions on the decrypt path.