keylime-agent-rust/0004-rust-keylime-hash-before-certify.patch
Anderson Toshiyuki Sasaki 4b274d7f8c
Hash agent ID before TPM2_Certify qualifying data
This allows using long agent UUID on systems with SHA-256-only TPM.

Resolves: RHEL-169745

Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
2026-05-27 22:20:46 +02:00

341 lines
14 KiB
Diff

From 6a6b1046a2fb5bd0a84982c7777268f4f9c14f25 Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Mon, 11 May 2026 19:01:25 +0200
Subject: [PATCH] agent: Hash agent ID before TPM2_Certify qualifying data
SHA-256 hash the agent ID before passing it as qualifyingData to
TPM2_Certify when certifying the AK with the IAK. This produces a fixed
32-byte value that fits within TPM2B_DATA on all TPMs, including those
that only support SHA-256 (where the limit is 34 bytes).
Without this fix, UUID strings (36 bytes) and other agent IDs exceed the
TPM2B_DATA capacity on SHA-256-only TPMs, causing TPM_RC_SIZE errors
during registration.
Bumps the pull-mode API version to 2.6.
Fixes: https://github.com/keylime/rust-keylime/issues/1226
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime-agent/src/api.rs | 21 +++++
keylime-agent/src/main.rs | 22 +----
keylime-push-model-agent/src/registration.rs | 2 -
keylime/src/agent_registration.rs | 88 +++++++++++++-------
keylime/src/config/base.rs | 2 +-
keylime/src/registrar_client.rs | 10 +++
keylime/src/tpm.rs | 23 +++--
7 files changed, 108 insertions(+), 60 deletions(-)
diff --git a/keylime-agent/src/api.rs b/keylime-agent/src/api.rs
index e9f4562dd..4d135a9b3 100644
--- a/keylime-agent/src/api.rs
+++ b/keylime-agent/src/api.rs
@@ -188,6 +188,25 @@ fn configure_api_v2_5(cfg: &mut web::ServiceConfig) {
)
}
+/// Configure the endpoints supported by API version 2.6
+///
+/// Version 2.6 has the same agent-side endpoints as 2.5. The version bump
+/// reflects a protocol change: the agent now SHA-256 hashes the agent ID
+/// before using it as qualifying data in TPM2_Certify (IAK-based AK
+/// certification). This ensures the qualifying data fits within the
+/// TPM2B_DATA size limit on SHA-256-only TPMs (34 bytes). The certify
+/// operation is deferred until after API version negotiation so the agent
+/// can fall back to raw UUID bytes when the registrar only supports 2.5.
+fn configure_api_v2_6(cfg: &mut web::ServiceConfig) {
+ let version = Version::from_str("2.6").expect("Invalid API version");
+ _ = cfg.app_data(web::Data::new(version));
+ configure_base_endpoints(cfg);
+ _ = cfg.service(
+ web::scope("/agent")
+ .configure(agent_handler::configure_agent_endpoints),
+ )
+}
+
/// Get a scope configured for the given API version
pub(crate) fn get_api_scope(version: &str) -> Result<Scope, APIError> {
match version {
@@ -201,6 +220,8 @@ pub(crate) fn get_api_scope(version: &str) -> Result<Scope, APIError> {
.configure(configure_api_v2_4)),
"2.5" => Ok(web::scope(format!("v{version}").as_ref())
.configure(configure_api_v2_5)),
+ "2.6" => Ok(web::scope(format!("v{version}").as_ref())
+ .configure(configure_api_v2_6)),
_ => Err(APIError::UnsupportedVersion(version.into())),
}
}
diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs
index b05eed214..280a1ac88 100644
--- a/keylime-agent/src/main.rs
+++ b/keylime-agent/src/main.rs
@@ -87,7 +87,7 @@ use tss_esapi::{
handles::KeyHandle,
interface_types::algorithm::{AsymmetricAlgorithm, HashingAlgorithm},
interface_types::resource_handles::Hierarchy,
- structures::{Auth, Data, Digest, MaxBuffer, PublicBuffer},
+ structures::{Auth, Digest, MaxBuffer, PublicBuffer},
traits::Marshall,
Context,
};
@@ -494,24 +494,6 @@ async fn main() -> Result<()> {
None
};
- let (attest, signature) = if let Some(dev_id) = &mut device_id {
- let qualifying_data = Data::try_from(agent_uuid.as_bytes())?;
- let (attest, signature) =
- dev_id.certify(qualifying_data, ak_handle, &mut ctx)?;
-
- info!("AK certified with IAK.");
-
- // // For debugging certify(), the following checks the generated signature
- // let max_b = MaxBuffer::try_from(attest.clone().marshall()?)?;
- // let (hashed_attest, _) = ctx.inner.hash(max_b, HashingAlgorithm::Sha256, Hierarchy::Endorsement,)?;
- // println!("{:?}", hashed_attest);
- // println!("{:?}", signature);
- // println!("{:?}", ctx.inner.verify_signature(iak.as_ref().unwrap().handle, hashed_attest, signature.clone())?); //#[allow_ci]
- (Some(attest), Some(signature))
- } else {
- (None, None)
- };
-
// Load or generate RSA key pair for secure transmission of u, v keys.
// The u, v keys are two halves of the key used to decrypt the workload after
// the Identity and Integrity Quotes sent by the agent are validated
@@ -645,8 +627,6 @@ async fn main() -> Result<()> {
agent_uuid: agent_uuid.clone(),
mtls_cert,
device_id,
- attest,
- signature,
ak_handle,
retry_config: Some(get_retry_config(&config)),
};
diff --git a/keylime-push-model-agent/src/registration.rs b/keylime-push-model-agent/src/registration.rs
index 0189cb15b..0cca01c17 100644
--- a/keylime-push-model-agent/src/registration.rs
+++ b/keylime-push-model-agent/src/registration.rs
@@ -109,8 +109,6 @@ pub async fn register_agent(
agent_uuid,
mtls_cert: None,
device_id: None, // TODO: Check how to proceed with device ID
- attest: None, // TODO: Check how to proceed with attestation, normally, no device ID means no attest
- signature: None, // TODO: Normally, no device ID means no signature
ak_handle: context_info.ak_handle,
retry_config,
};
diff --git a/keylime/src/agent_registration.rs b/keylime/src/agent_registration.rs
index 8a4fe0c3a..9e60f8cf2 100644
--- a/keylime/src/agent_registration.rs
+++ b/keylime/src/agent_registration.rs
@@ -9,10 +9,12 @@ use crate::{
tpm::{self},
};
use base64::{engine::general_purpose, Engine as _};
-use log::{error, info};
+use log::{error, info, warn};
use openssl::x509::X509;
use tss_esapi::{
- handles::KeyHandle, structures::PublicBuffer, traits::Marshall,
+ handles::KeyHandle,
+ structures::{Data, PublicBuffer},
+ traits::Marshall,
};
#[derive(Debug)]
@@ -44,14 +46,12 @@ pub struct AgentRegistration {
pub agent_uuid: String,
pub mtls_cert: Option<X509>,
pub device_id: Option<device_id::DeviceID>,
- pub attest: Option<tss_esapi::structures::Attest>,
- pub signature: Option<tss_esapi::structures::Signature>,
pub ak_handle: KeyHandle,
pub retry_config: Option<RetryConfig>,
}
pub async fn register_agent(
- aa: AgentRegistration,
+ mut aa: AgentRegistration,
ctx: &mut tpm::Context<'_>,
) -> Result<()> {
let iak_pub;
@@ -60,6 +60,28 @@ pub async fn register_agent(
let ek_pub =
&PublicBuffer::try_from(aa.ek_result.public.clone())?.marshall()?;
+ let ac = &aa.agent_registration_config;
+
+ // Build the registrar client first so we can query its supported API
+ // versions before performing TPM operations that depend on that version.
+ // Uses HTTPS if CA certificate is provided, otherwise plain HTTP.
+ let mut builder = RegistrarClientBuilder::new()
+ .registrar_address(ac.registrar_ip.clone())
+ .registrar_port(ac.registrar_port)
+ .retry_config(aa.retry_config.clone());
+
+ if let Some(ca_cert) = &ac.registrar_ca_cert {
+ builder = builder.ca_certificate(ca_cert.clone());
+ }
+ if let Some(disable_tls) = ac.registrar_disable_tls {
+ builder = builder.disable_tls(disable_tls);
+ }
+ if let Some(timeout) = ac.registrar_timeout {
+ builder = builder.timeout(timeout);
+ }
+
+ let mut registrar_client = builder.build().await?;
+
let mut ai_builder = AgentIdentityBuilder::new()
.ak_pub(ak_pub)
.ek_pub(ek_pub)
@@ -81,9 +103,7 @@ pub async fn register_agent(
// Set the IAK/IDevID related fields, if enabled
if aa.agent_registration_config.enable_iak_idevid {
- let (Some(dev_id), Some(attest), Some(signature)) =
- (&aa.device_id, aa.attest, aa.signature)
- else {
+ let Some(dev_id) = aa.device_id.as_mut() else {
error!("IDevID and IAK are enabled but could not be generated");
return Err(Error::ConfigurationGenericError(
"IDevID and IAK are enabled but could not be generated"
@@ -91,6 +111,36 @@ pub async fn register_agent(
));
};
+ // Hash the agent UUID (SHA-256) when the registrar supports API 2.6+.
+ // This keeps qualifying data within the TPM2B_DATA size limit on
+ // SHA-256-only TPMs (34 bytes). For registrars that only support 2.5
+ // or earlier, fall back to the raw UUID bytes for backward compat.
+ let qualifying_data = if registrar_client.supports_api_version("2.6")
+ {
+ let hash = crypto::hash(
+ aa.agent_uuid.as_bytes(),
+ openssl::hash::MessageDigest::sha256(),
+ )?;
+ Data::try_from(hash.as_slice())?
+ } else {
+ let uuid_bytes = aa.agent_uuid.as_bytes();
+ if uuid_bytes.len() > 34 {
+ // TPM2B_DATA limit on SHA-256-only TPMs is 34 bytes.
+ // Raw UUID won't fit; the registrar must support 2.6+.
+ warn!(
+ "Agent UUID is {} bytes (max 34 for pre-2.6 registrar); \
+ registration will likely fail. Update the registrar to 2.6+.",
+ uuid_bytes.len()
+ );
+ }
+ Data::try_from(uuid_bytes)?
+ };
+
+ let (attest, signature) =
+ dev_id.certify(qualifying_data, aa.ak_handle, ctx)?;
+
+ info!("AK certified with IAK.");
+
iak_pub =
PublicBuffer::try_from(dev_id.iak_pubkey.clone())?.marshall()?;
idevid_pub = PublicBuffer::try_from(dev_id.idevid_pubkey.clone())?
@@ -115,28 +165,6 @@ pub async fn register_agent(
// Build the Agent Identity
let ai = ai_builder.build().await?;
- let ac = &aa.agent_registration_config;
-
- // Build the registrar client
- // Uses HTTPS if CA certificate is provided, otherwise plain HTTP
- let mut builder = RegistrarClientBuilder::new()
- .registrar_address(ac.registrar_ip.clone())
- .registrar_port(ac.registrar_port)
- .retry_config(aa.retry_config.clone());
-
- // Add TLS configuration if CA certificate is provided
- if let Some(ca_cert) = &ac.registrar_ca_cert {
- builder = builder.ca_certificate(ca_cert.clone());
- }
- if let Some(disable_tls) = ac.registrar_disable_tls {
- builder = builder.disable_tls(disable_tls);
- }
- if let Some(timeout) = ac.registrar_timeout {
- builder = builder.timeout(timeout);
- }
-
- let mut registrar_client = builder.build().await?;
-
// Request keyblob material
let keyblob = registrar_client.register_agent(&ai).await?;
diff --git a/keylime/src/config/base.rs b/keylime/src/config/base.rs
index 31fdc44ef..eb6cee993 100644
--- a/keylime/src/config/base.rs
+++ b/keylime/src/config/base.rs
@@ -20,7 +20,7 @@ use uuid::Uuid;
pub static CONFIG_VERSION: &str = "2.0";
pub static SUPPORTED_API_VERSIONS: &[&str] =
- &["2.1", "2.2", "2.3", "2.4", "2.5"];
+ &["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"];
pub static DEFAULT_REGISTRAR_API_VERSIONS: &str = "default";
pub static DEFAULT_CONFIG: &str = "/etc/keylime/agent.conf";
diff --git a/keylime/src/registrar_client.rs b/keylime/src/registrar_client.rs
index 7750a381e..04523d1a0 100644
--- a/keylime/src/registrar_client.rs
+++ b/keylime/src/registrar_client.rs
@@ -400,6 +400,16 @@ struct Register<'a> {
}
impl RegistrarClient {
+ /// Check if the registrar supports the given API version.
+ ///
+ /// Returns `false` if the registrar did not advertise supported versions
+ /// (e.g., it does not expose a `/version` endpoint).
+ pub fn supports_api_version(&self, version: &str) -> bool {
+ self.supported_api_versions
+ .as_ref()
+ .is_some_and(|versions| versions.iter().any(|v| v == version))
+ }
+
async fn try_register_agent(
&self,
ai: &AgentIdentity<'_>,
diff --git a/keylime/src/tpm.rs b/keylime/src/tpm.rs
index fb5d843ec..50c94cf5f 100644
--- a/keylime/src/tpm.rs
+++ b/keylime/src/tpm.rs
@@ -3291,14 +3291,25 @@ pub mod tests {
.expect("failed to create IAK")
.handle;
- let qualifying_data = "some_uuid".as_bytes();
+ // Use a realistic UUID string (36 bytes) — the exact input size that
+ // triggers TPM_RC_SIZE on SHA-256-only TPMs without the hash fix.
+ let agent_uuid = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000";
+ let qualifying_data = crate::crypto::hash(
+ agent_uuid.as_bytes(),
+ openssl::hash::MessageDigest::sha256(),
+ )
+ .unwrap(); //#[allow_ci]
+
+ let data = Data::try_from(qualifying_data.as_slice()).unwrap(); //#[allow_ci]
+ let (attest, _signature) = ctx
+ .certify_credential_with_iak(data, ak_handle, iak_handle)
+ .expect("certify_credential_with_iak failed");
- let r = ctx.certify_credential_with_iak(
- Data::try_from(qualifying_data).unwrap(), //#[allow_ci]
- ak_handle,
- iak_handle,
+ assert_eq!(
+ attest.extra_data().value(),
+ qualifying_data.as_slice(),
+ "extra_data in Attest must equal the SHA-256 hash of the agent UUID"
);
- assert!(r.is_ok(), "Result: {r:?}");
// Flush context to free TPM memory
let r = ctx.flush_context(ek_handle.into());