Update to 7.14.3

Resolves: RHEL-180618

Signed-off-by: Sergio Correia <scorreia@redhat.com>
This commit is contained in:
Sergio Correia 2026-06-01 13:03:50 +00:00
parent 86d98c9da8
commit a498566a20
No known key found for this signature in database
29 changed files with 1267 additions and 9704 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@
/keylime-selinux-38.1.0.tar.gz
/v7.12.1.tar.gz
/keylime-selinux-42.1.2.tar.gz
/v7.14.2.tar.gz
/keylime-selinux-44.1.0.tar.gz
/v7.14.3.tar.gz

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
From c60460eccab93863dbd1fd0b748e5a275c8e6737 Mon Sep 17 00:00:00 2001
From 1f73ef67a77d7355df524b6c120d6247f8f40e7f Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Tue, 3 Jun 2025 21:29:15 +0100
Subject: [PATCH 5/6] Restore RHEL-9 version of create_allowlist.sh
Date: Mon, 1 Jun 2026 13:56:43 +0100
Subject: [PATCH 2/4] Restore RHEL-9 version of create_allowlist.sh
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
@ -9,7 +9,7 @@ Signed-off-by: Sergio Correia <scorreia@redhat.com>
1 file changed, 104 insertions(+), 231 deletions(-)
diff --git a/scripts/create_runtime_policy.sh b/scripts/create_runtime_policy.sh
index 90ba50b..c0b641d 100755
index 462073a..c0b641d 100755
--- a/scripts/create_runtime_policy.sh
+++ b/scripts/create_runtime_policy.sh
@@ -1,282 +1,155 @@
@ -303,7 +303,7 @@ index 90ba50b..c0b641d 100755
- # If we are on an ostree system change where we look for initramfs image
- loc=$(grep -E "/ostree/[^/]([^/]*)" -o /proc/cmdline | head -n 1 | cut -d / -f 3)
- INITRAMFS_LOC="/boot/ostree/${loc}/"
- announce "--- The location of initramfs was overriden from \"${X}\" to \"$INITRAMFS_LOC\""
- announce "--- The location of initramfs was overridden from \"${X}\" to \"$INITRAMFS_LOC\""
- fi
-
- announce "--- Creating allowlist for init ram disks found under \"$INITRAMFS_LOC\" to $ALLOWLIST_DIR/${OUTPUT} ..."
@ -400,5 +400,5 @@ index 90ba50b..c0b641d 100755
+# Clean up
+rm -rf /tmp/ima
--
2.47.1
2.54.0

View File

@ -1,58 +0,0 @@
From 5c5c7f7f7180111485b24061af4c0395476958b5 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Thu, 22 May 2025 11:25:15 -0400
Subject: [PATCH 2/6] tests: fix rpm repo tests from create-runtime-policy
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
.../create-runtime-policy/setup-rpm-tests | 28 +++++++++++++------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/test/data/create-runtime-policy/setup-rpm-tests b/test/data/create-runtime-policy/setup-rpm-tests
index 708438c..b62729b 100755
--- a/test/data/create-runtime-policy/setup-rpm-tests
+++ b/test/data/create-runtime-policy/setup-rpm-tests
@@ -217,20 +217,32 @@ create_rpm() {
# https://github.com/rpm-software-management/rpm/commit/96467dce18f264b278e17ffe1859c88d9b5aa4b6
_pkgname="DUMMY-${_name}-${_version}-${_rel}.noarch.rpm"
- _expected_pkg="${RPMSDIR}/noarch/${_pkgname}"
- [ -e "${_expected_pkg}" ] && return 0
+ # For some reason, it may not store the built package within the
+ # noarch directory, but directly in RPMS, so let's check both
+ # locations.
+ _expected_pkg="${RPMSDIR}/noarch/${_pkgname} ${RPMSDIR}/${_pkgname}"
+ for _expected in ${_expected_pkg}; do
+ if [ -e "${_expected}" ]; then
+ echo "(create_rpm) CREATED RPM: ${_expected}" >&2
+ return 0
+ fi
+ done
# OK, the package was not built where it should. Let us see if
# it was built in ~/rpmbuild instead, and if that is the case,
# copy it to the expected location.
- _bad_location_pkg="${HOME}/rpmbuild/RPMS/noarch/${_pkgname}"
- if [ -e "${_bad_location_pkg}" ]; then
- echo "WARNING: the package ${_pkgname} was built into ~/rpmbuild despite rpmbuild being instructed to build it at a different location. Probably a fallout from https://github.com/rpm-software-management/rpm/commit/96467dce" >&2
- install -D -m644 "${_bad_location_pkg}" "${_expected_pkg}"
- return 0
- fi
+ _bad_location_pkg="${HOME}/rpmbuild/RPMS/noarch/${_pkgname} ${HOME}/rpmbuild/RPMS/${_pkgname}"
+ for _bad_l in ${_bad_location_pkg}; do
+ if [ -e "${_bad_l}" ]; then
+ echo "WARNING: the package ${_pkgname} was built into ~/rpmbuild despite rpmbuild being instructed to build it at a different location. Probably a fallout from https://github.com/rpm-software-management/rpm/commit/96467dce" >&2
+ install -D -m644 "${_bad_l}" "${RPMSDIR}/noarch/${_pkgname}"
+ echo "(create_rpm) CREATED RPM: ${RPMSDIR}/noarch/${_pkgname}" >&2
+ return 0
+ fi
+ done
# Should not be here.
+ echo "create_rpm() ended with error; probably an issue with the location where the RPMs were built" >&2
return 1
}
--
2.47.1

View File

@ -1,7 +1,7 @@
From 733db4036f2142152795fc51b761f05e39594b08 Mon Sep 17 00:00:00 2001
From 8356c4b5af97afa7a8ba57998865767793de2b2f Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Tue, 27 May 2025 09:31:54 +0000
Subject: [PATCH 6/6] Revert "default" server_key_password for
Subject: [PATCH 3/4] Revert "default" server_key_password for
verifier/registrar
Signed-off-by: Sergio Correia <scorreia@redhat.com>
@ -11,7 +11,7 @@ Signed-off-by: Sergio Correia <scorreia@redhat.com>
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/templates/2.0/mapping.json b/templates/2.0/mapping.json
index 80dcdde..8fce124 100644
index 57f72d9..0004c4f 100644
--- a/templates/2.0/mapping.json
+++ b/templates/2.0/mapping.json
@@ -232,7 +232,7 @@
@ -33,7 +33,7 @@ index 80dcdde..8fce124 100644
"server_cert": {
"section": "registrar",
diff --git a/templates/2.1/mapping.json b/templates/2.1/mapping.json
index 956a53a..88e3fb6 100644
index 6f5c11f..5b5f895 100644
--- a/templates/2.1/mapping.json
+++ b/templates/2.1/mapping.json
@@ -262,7 +262,7 @@
@ -62,5 +62,5 @@ index 956a53a..88e3fb6 100644
\ No newline at end of file
+}
--
2.47.1
2.54.0

View File

@ -1,52 +0,0 @@
From 4e7cd6b75de27897ecc8e7329732cd945f7adfd0 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Thu, 22 May 2025 18:27:04 +0100
Subject: [PATCH 3/6] tests: skip measured-boot related tests for s390x and
ppc64le
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
test/test_create_mb_policy.py | 2 ++
test/test_mba_parsing.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py
index eaed0e3..b00d8e7 100644
--- a/test/test_create_mb_policy.py
+++ b/test/test_create_mb_policy.py
@@ -5,6 +5,7 @@ Copyright 2024 Red Hat, Inc.
import argparse
import os
+import platform
import unittest
from keylime.policy import create_mb_policy
@@ -12,6 +13,7 @@ from keylime.policy import create_mb_policy
DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data", "create-mb-policy"))
+@unittest.skipIf(platform.machine() in ["ppc64le", "s390x"], "ppc64le and s390x are not supported")
class CreateMeasuredBootPolicy_Test(unittest.TestCase):
def test_event_to_sha256(self):
test_cases = [
diff --git a/test/test_mba_parsing.py b/test/test_mba_parsing.py
index 670a602..e157116 100644
--- a/test/test_mba_parsing.py
+++ b/test/test_mba_parsing.py
@@ -1,10 +1,12 @@
import os
+import platform
import unittest
from keylime.common.algorithms import Hash
from keylime.mba import mba
+@unittest.skipIf(platform.machine() in ["ppc64le", "s390x"], "ppc64le and s390x are not supported")
class TestMBAParsing(unittest.TestCase):
def test_parse_bootlog(self):
"""Test parsing binary measured boot event log"""
--
2.47.1

View File

@ -0,0 +1,192 @@
From ea7db68f2fd12737d156a28cfc088e244ac14489 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Mon, 6 Jul 2026 08:23:50 +0000
Subject: [PATCH 4/4] Disable push model (agent-driven attestation) for RHEL 9
Push mode is not yet supported in this build. Gate it off at entry
points: remove API v3.0 from supported versions, hardcode verifier
operating mode to pull, remove --push-model tenant CLI flag. Push
code remains in tree as dead code for easy re-enablement later.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/api_version.py | 4 ++--
keylime/cmd/verifier.py | 11 +++++------
keylime/tenant.py | 23 +----------------------
keylime/web/verifier_server.py | 2 +-
test/test_registrar_server.py | 31 -------------------------------
test/test_version_controller.py | 6 +++---
6 files changed, 12 insertions(+), 65 deletions(-)
diff --git a/keylime/api_version.py b/keylime/api_version.py
index df65736..5773be7 100644
--- a/keylime/api_version.py
+++ b/keylime/api_version.py
@@ -7,8 +7,8 @@ from packaging import version
VersionType = Union[int, float, str]
CURRENT_VERSION: str = "2.6"
-VERSIONS: List[str] = ["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "3.0"]
-LATEST_VERSIONS: Dict[str, str] = {"1": "1.0", "2": "2.6", "3": "3.0"}
+VERSIONS: List[str] = ["1.0", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"]
+LATEST_VERSIONS: Dict[str, str] = {"1": "1.0", "2": "2.6"}
DEPRECATED_VERSIONS: List[str] = ["1.0"]
diff --git a/keylime/cmd/verifier.py b/keylime/cmd/verifier.py
index 1e6cc44..8f2730d 100644
--- a/keylime/cmd/verifier.py
+++ b/keylime/cmd/verifier.py
@@ -10,14 +10,13 @@ logger = keylime_logging.init_logging("verifier")
def _log_startup_info() -> None:
mode = config.get("verifier", "mode", fallback="pull")
- logger.info("Starting Keylime verifier in %s mode...", mode.upper())
-
- # Temporary warning when enabling push mode
if mode == "push":
- logger.warning(
- "Push mode is experimental. Please report issues at "
- "https://github.com/keylime/keylime/issues/?q=label:push-mode"
+ logger.error(
+ "Push mode (agent-driven attestation) is not supported in this build. "
+ "Falling back to pull mode."
)
+ mode = "pull"
+ logger.info("Starting Keylime verifier in %s mode...", mode.upper())
# Log REST API versions supported by the verifier
api_version.log_api_versions(logger)
diff --git a/keylime/tenant.py b/keylime/tenant.py
index 4cb3698..b905b4e 100644
--- a/keylime/tenant.py
+++ b/keylime/tenant.py
@@ -337,15 +337,7 @@ class Tenant:
if self.agent_port is None and self.registrar_data["port"] is not None:
self.agent_port = self.registrar_data["port"]
- # Set push_model based on explicit --push-model flag
- # Default to PULL mode (False) if flag not provided
- if not args.get("push_model", False):
- self.push_model = False
- else:
- self.push_model = True
-
- # If ip/port are still None, that's okay for push-mode (verifier will handle it)
- # The verifier's mode config determines whether ip/port are required
+ self.push_model = False
# Initialize ID strings early (will be updated after API detection with full info)
self.agent_fid_str = f"Agent {self.agent_uuid}"
@@ -1751,13 +1743,6 @@ def main() -> None:
"addmbpolicy,showmbpolicy,deletembpolicy,updatembpolicy,"
"listmbpolicy. defaults to add",
)
- parser.add_argument(
- "--push-model",
- action="store_true",
- dest="push_model",
- default=False,
- help="Enable push model (avoid requests to keylime-agent)",
- )
parser.add_argument(
"-t", "--targethost", action="store", dest="agent_ip", help="the IP address of the host to provision"
)
@@ -1983,12 +1968,6 @@ def main() -> None:
config.check_version("tenant", logger=logger)
mytenant = Tenant()
- # Set push_model based on explicit --push-model flag
- # This must be done before dispatching to commands (status, add, etc.)
- if hasattr(args, "push_model") and args.push_model:
- mytenant.push_model = True
- else:
- mytenant.push_model = False
if args.agent_uuid is not None:
mytenant.agent_uuid = args.agent_uuid
diff --git a/keylime/web/verifier_server.py b/keylime/web/verifier_server.py
index 11e2e5c..8d42a61 100755
--- a/keylime/web/verifier_server.py
+++ b/keylime/web/verifier_server.py
@@ -183,7 +183,7 @@ class VerifierServer(Server):
def _setup(self) -> None:
self._set_component("verifier")
self._use_config("verifier")
- self._set_operating_mode(from_config="mode", fallback="pull")
+ self._set_operating_mode(value="pull")
self._set_bind_interface(from_config="ip")
self._set_http_port(value=None) # verifier does not accept insecure connections
self._set_https_port(from_config="port")
diff --git a/test/test_registrar_server.py b/test/test_registrar_server.py
index 17ee926..618f4d4 100644
--- a/test/test_registrar_server.py
+++ b/test/test_registrar_server.py
@@ -123,25 +123,6 @@ class TestRegistrarServerV3Routes(unittest.TestCase):
self.assertFalse(route.requires_auth)
self.assertEqual(route.auth_action, Action.ACTIVATE_AGENT)
- def test_v3_0_routes_also_match(self):
- """Test that all v3 routes also match with the /v3.0/ prefix."""
- cases = [
- ("get", "/v3.0/", VersionController, "show_version_root"),
- ("get", "/v3.0/agents", AgentsController, "index"),
- ("get", "/v3.0/agents/test-agent-id", AgentsController, "show"),
- ("delete", "/v3.0/agents/test-agent-id", AgentsController, "delete"),
- ("post", "/v3.0/agents", AgentsController, "create"),
- ("post", "/v3.0/agents/test-agent-id/activate", AgentsController, "activate"),
- ]
- for method, path, expected_controller, expected_action in cases:
- with self.subTest(method=method, path=path):
- route = self.server.first_matching_route(method, path)
- self.assertIsNotNone(route, f"No route found for {method.upper()} {path}")
- assert route is not None
- self.assertEqual(route.controller, expected_controller)
- self.assertEqual(route.action, expected_action)
-
-
class TestRegistrarServerV3NoCompatRoutes(unittest.TestCase):
"""Test that v3 does NOT include the backwards-compatibility routes from v2.
@@ -167,18 +148,6 @@ class TestRegistrarServerV3NoCompatRoutes(unittest.TestCase):
route = self.server.first_matching_route("put", "/v3/agents/some-agent-id")
self.assertIsNone(route)
- def test_v3_0_no_compat_routes(self):
- """Test that v3.0 prefix also has no compat routes."""
- cases = [
- ("post", "/v3.0/agents/some-agent-id"),
- ("put", "/v3.0/agents/some-agent-id/activate"),
- ("put", "/v3.0/agents/some-agent-id"),
- ]
- for method, path in cases:
- with self.subTest(method=method, path=path):
- route = self.server.first_matching_route(method, path)
- self.assertIsNone(route, f"Unexpected compat route found for {method.upper()} {path}")
-
class TestRegistrarServerV3MethodNotAllowed(unittest.TestCase):
"""Test that legacy methods on v3 paths produce 405, not 404.
diff --git a/test/test_version_controller.py b/test/test_version_controller.py
index 8a78c2a..ffdca11 100644
--- a/test/test_version_controller.py
+++ b/test/test_version_controller.py
@@ -34,12 +34,12 @@ class TestVersionControllerVersion(unittest.TestCase):
self.assertEqual(data["current_version"], keylime_api_version.current_version())
self.assertEqual(data["supported_versions"], keylime_api_version.all_versions())
- def test_version_includes_v3(self):
- """Test that the supported versions list includes v3.0."""
+ def test_version_does_not_include_v3(self):
+ """Test that v3.0 is not in supported versions (push model disabled)."""
self.controller.version() # pylint: disable=not-callable
data = self.mock_respond.call_args[0][2]
- self.assertIn("3.0", data["supported_versions"])
+ self.assertNotIn("3.0", data["supported_versions"])
class TestVersionControllerShowVersionRoot(unittest.TestCase):
--
2.54.0

View File

@ -1,52 +0,0 @@
From 7ca86e1c0d68f45915d9f583ffaf149285905005 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Tue, 3 Jun 2025 10:50:48 +0100
Subject: [PATCH 4/6] templates: duplicate str_to_version() in the adjust
script
As a follow-up of upstream PR#1486, duplicate the str_to_version()
method in adjust.py so that we do not need the keylime modules in
order for the configuration upgrade script to run.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
templates/2.0/adjust.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/templates/2.0/adjust.py b/templates/2.0/adjust.py
index 6008e4c..24ba898 100644
--- a/templates/2.0/adjust.py
+++ b/templates/2.0/adjust.py
@@ -4,9 +4,27 @@ import logging
import re
from configparser import RawConfigParser
from logging import Logger
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, Tuple, Union
-from keylime.common.version import str_to_version
+
+def str_to_version(v_str: str) -> Union[Tuple[int, int], None]:
+ """
+ Validates the string format and converts the provided string to a tuple of
+ ints which can be sorted and compared.
+
+ :returns: Tuple with version number parts converted to int. In case of
+ invalid version string, returns None
+ """
+
+ # Strip to remove eventual quotes and spaces
+ v_str = v_str.strip('" ')
+
+ m = re.match(r"^(\d+)\.(\d+)$", v_str)
+
+ if not m:
+ return None
+
+ return (int(m.group(1)), int(m.group(2)))
def adjust(
--
2.47.1

View File

@ -0,0 +1,102 @@
From e06745c7c2a28bf0c796f516f8fdb529176d630c Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Thu, 16 Jul 2026 08:56:54 +0000
Subject: [PATCH 5/5] fix: handle invalid config values for typed server
options
When a typed configuration option (int, float, bool) contains an empty
or invalid value, configparser raises ValueError instead of using the
fallback. This causes the verifier to crash on startup when e.g.
max_workers has an empty value after a config upgrade.
Catch ValueError in _set_option for typed config reads and fall back to
the caller-provided default with a warning. Also add a default fallback
of 0 for max_workers (meaning use all CPUs).
Fixes: keylime/keylime#1929
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/web/base/server.py | 44 +++++++++++++++++++++++++++-----------
1 file changed, 32 insertions(+), 12 deletions(-)
diff --git a/keylime/web/base/server.py b/keylime/web/base/server.py
index 9ac3a3c..eee8313 100644
--- a/keylime/web/base/server.py
+++ b/keylime/web/base/server.py
@@ -449,9 +449,34 @@ class Server(ABC):
"""Sets config component (i.e., namespace) used to locate config values when setting server options."""
self.__config_component = component
+ _TYPED_CONFIG_GETTERS: dict[type, Callable[..., Any]] = {
+ int: config.getint,
+ float: config.getfloat,
+ bool: config.getboolean,
+ }
+
+ def _get_typed_config_value(self, config_name: str, data_type: type, fallback: Any) -> Any:
+ """Read a typed config value, falling back gracefully on ValueError."""
+ getter = self._TYPED_CONFIG_GETTERS[data_type]
+ try:
+ return getter(self.config_component, config_name, fallback=fallback) # type: ignore
+ except ValueError:
+ if fallback is None:
+ raise
+ raw_value = config.get(self.config_component, config_name, fallback="<unknown>") # type: ignore[arg-type]
+ logger.warning(
+ "Cannot parse '%s' as %s for option '%s' (component: %s), using fallback: %s",
+ raw_value,
+ data_type.__name__,
+ config_name,
+ self.config_component,
+ fallback,
+ )
+ return fallback
+
def _set_option(self, name: str, **kwargs: Any) -> None:
"""Sets server option by name either from given value or by obtaining it from user config. If the value is
- falsy, uses the given fallback value instead or ``None`` if no fallback is provided. Examples::
+ ``None``, uses the given fallback value instead. Examples::
# Set option using provided value
self._set_option("bind_interface", value="0.0.0.0")
@@ -488,22 +513,14 @@ class Server(ABC):
config_name = __match_val["from_config"][0]
data_type = __match_val["from_config"][1]
value = config.get(self.config_component, config_name, fallback=fallback) # type: ignore
- elif (isinstance(__match_val, dict) and "from_config" in __match_val) and __match_val["from_config"][1] is int:
- config_name = __match_val["from_config"][0]
- data_type = __match_val["from_config"][1]
- value = config.getint(self.config_component, config_name, fallback=fallback) # type: ignore
- elif (isinstance(__match_val, dict) and "from_config" in __match_val) and __match_val["from_config"][1] is float:
+ elif (isinstance(__match_val, dict) and "from_config" in __match_val) and __match_val["from_config"][1] in self._TYPED_CONFIG_GETTERS:
config_name = __match_val["from_config"][0]
data_type = __match_val["from_config"][1]
- value = config.getfloat(self.config_component, config_name, fallback=fallback) # type: ignore
- elif (isinstance(__match_val, dict) and "from_config" in __match_val) and __match_val["from_config"][1] is bool:
- config_name = __match_val["from_config"][0]
- data_type = __match_val["from_config"][1]
- value = config.getboolean(self.config_component, config_name, fallback=fallback) # type: ignore
+ value = self._get_typed_config_value(config_name, data_type, fallback)
else:
raise TypeError(f"invalid arguments given when setting option '{name}' for {self.__class__.__name__}")
- setattr(self, attr_name, value or fallback)
+ setattr(self, attr_name, value if value is not None else fallback)
def _set_operating_mode(self, **kwargs: Any) -> None:
"""Sets operating mode of the server (push or pull)."""
@@ -578,6 +595,9 @@ class Server(ABC):
if "from_config" in kwargs:
kwargs.update({"from_config": (kwargs["from_config"], int)})
+ if "fallback" not in kwargs:
+ kwargs.update({"fallback": 0})
+
self._set_option("max_workers", **kwargs)
def _set_ssl_ctx(self, **kwargs: Any) -> None:
--
2.54.0

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
From d14e0a132cfedd081bffa7a990b9401d5e257cac Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Fri, 8 Aug 2025 16:40:01 +0100
Subject: [PATCH 8/9] mb: support EV_EFI_HANDOFF_TABLES events on PCR1
Allow EV_EFI_HANDOFF_TABLES events on PCR1 alongside the existing
EV_EFI_HANDOFF_TABLES2 support to handle different firmware
implementations, in the example policy.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/mba/elchecking/example.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/keylime/mba/elchecking/example.py b/keylime/mba/elchecking/example.py
index 2c6f699..a3d918a 100644
--- a/keylime/mba/elchecking/example.py
+++ b/keylime/mba/elchecking/example.py
@@ -185,6 +185,7 @@ class Example(policies.Policy):
# We only expect one EV_NO_ACTION event at the start.
dispatcher.set((0, "EV_NO_ACTION"), tests.OnceTest(tests.AcceptAll()))
dispatcher.set((1, "EV_CPU_MICROCODE"), tests.OnceTest(tests.AcceptAll()))
+ dispatcher.set((1, "EV_EFI_HANDOFF_TABLES"), tests.OnceTest(tests.AcceptAll()))
dispatcher.set((1, "EV_EFI_HANDOFF_TABLES2"), tests.OnceTest(tests.AcceptAll()))
dispatcher.set((0, "EV_S_CRTM_VERSION"), events_final.get("s_crtms"))
dispatcher.set((0, "EV_EFI_PLATFORM_FIRMWARE_BLOB"), events_final.get("platform_firmware_blobs"))
--
2.47.3

View File

@ -1,356 +0,0 @@
From 607b97ac8d414cb57b1ca89925631d41bd7ac04c Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Fri, 8 Aug 2025 16:41:54 +0100
Subject: [PATCH 9/9] mb: support vendor_db as logged by newer shim versions
- Updated example policy to properly handle different event structures
for vendor_db validation:
- KeySubsetMulti for EV_EFI_VARIABLE_DRIVER_CONFIG (has SignatureType field)
- SignatureSetMember for EV_EFI_VARIABLE_AUTHORITY (direct signature format)
- Added method to extract vendor_db from EV_EFI_VARIABLE_AUTHORITY events
in reference state generation (keylime-policy create measured-boot and
the legacy create_mb_refstate script)
- Made vendor_db optional for backward compatibility
This fixes attestation failures when vendor_db variables are present but
missing from reference states or validated with incorrect test types.
See: https://github.com/rhboot/shim/pull/728
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/mba/elchecking/example.py | 45 +++++++++
keylime/policy/create_mb_policy.py | 30 ++++++
scripts/create_mb_refstate | 30 ++++++
test/test_create_mb_policy.py | 142 +++++++++++++++++++++++++++++
4 files changed, 247 insertions(+)
diff --git a/keylime/mba/elchecking/example.py b/keylime/mba/elchecking/example.py
index a3d918a..5a933ac 100644
--- a/keylime/mba/elchecking/example.py
+++ b/keylime/mba/elchecking/example.py
@@ -21,6 +21,7 @@ from . import policies, tests
# kek - list of allowed KEK keys
# db - list of allowed db keys
# dbx - list of required dbx keys
+# vendor_db - list of allowed vendor_db keys (optional, for newer shim versions)
# mokdig - list of allowed digests of MoKList (PCR 14 EV_IPL)
# mokxdig - list of allowed digests of MoKListX (PCR 14 EV_IPL)
# kernels - list of allowed {
@@ -121,6 +122,10 @@ class Example(policies.Policy):
if req not in refstate:
raise Exception(f"refstate lacks {req}")
+ # vendor_db is optional for backward compatibility
+ if "vendor_db" not in refstate:
+ refstate["vendor_db"] = []
+
dispatcher = tests.Dispatcher(("PCRIndex", "EventType"))
vd_driver_config = tests.VariableDispatch()
vd_authority = tests.VariableDispatch()
@@ -268,6 +273,34 @@ class Example(policies.Policy):
"db",
db_test,
)
+ # Support vendor_db as logged by newer shim versions
+ # See: https://github.com/rhboot/shim/pull/728
+ if not has_secureboot and not refstate["vendor_db"]:
+ vendor_db_test = tests.OnceTest(tests.AcceptAll())
+ else:
+ vendor_db_test = tests.OnceTest(
+ tests.Or(
+ tests.KeySubsetMulti(
+ ["a159c0a5-e494-a74a-87b5-ab155c2bf072", "2616c4c1-4c50-9240-aca9-41f936934328"],
+ sigs_strip0x(refstate["vendor_db"]),
+ ),
+ tests.KeySubsetMulti(
+ ["a5c059a1-94e4-4aa7-87b5-ab155c2bf072", "c1c41626-504c-4092-aca9-41f936934328"],
+ sigs_strip0x(refstate["vendor_db"]),
+ ),
+ )
+ )
+
+ vd_driver_config.set(
+ "cbb219d7-3a3d-9645-a3bc-dad00e67656f",
+ "vendor_db",
+ vendor_db_test,
+ )
+ vd_driver_config.set(
+ "d719b2cb-3d3a-4596-a3bc-dad00e67656f",
+ "vendor_db",
+ vendor_db_test,
+ )
if not has_secureboot and not refstate["dbx"]:
dbx_test = tests.OnceTest(tests.AcceptAll())
@@ -295,6 +328,18 @@ class Example(policies.Policy):
vd_db_test = tests.OnceTest(tests.AcceptAll())
vd_authority.set("cbb219d7-3a3d-9645-a3bc-dad00e67656f", "db", vd_db_test)
vd_authority.set("d719b2cb-3d3a-4596-a3bc-dad00e67656f", "db", vd_db_test)
+ # Support vendor_db as logged by newer shim versions in EV_EFI_VARIABLE_AUTHORITY events
+ # See: https://github.com/rhboot/shim/pull/728
+ # EV_EFI_VARIABLE_AUTHORITY events have different structure than EV_EFI_VARIABLE_DRIVER_CONFIG
+ # They contain direct signature data without SignatureType field
+ if not has_secureboot and not refstate["vendor_db"]:
+ vendor_db_authority_test = tests.OnceTest(tests.AcceptAll())
+ else:
+ vendor_db_authority_test = tests.OnceTest(
+ tests.IterateTest(tests.SignatureSetMember(sigs_strip0x(refstate["vendor_db"])))
+ )
+ vd_authority.set("cbb219d7-3a3d-9645-a3bc-dad00e67656f", "vendor_db", vendor_db_authority_test)
+ vd_authority.set("d719b2cb-3d3a-4596-a3bc-dad00e67656f", "vendor_db", vendor_db_authority_test)
# Accept all SbatLevels of the Shim, because we already checked the hash of the Shim itself.
vd_sbat_level_test = tests.OnceTest(tests.AcceptAll())
vd_authority.set("50ab5d60-46e0-0043-abb6-3dd810dd8b23", "SbatLevel", vd_sbat_level_test)
diff --git a/keylime/policy/create_mb_policy.py b/keylime/policy/create_mb_policy.py
index 859e652..b2b48f7 100644
--- a/keylime/policy/create_mb_policy.py
+++ b/keylime/policy/create_mb_policy.py
@@ -93,6 +93,35 @@ def get_keys(events: List[Dict[str, Any]]) -> Dict[str, List[Any]]:
return out
+def get_vendor_db(events: List[Dict[str, Any]]) -> Dict[str, List[Any]]:
+ """Get vendor_db signatures from EV_EFI_VARIABLE_AUTHORITY events."""
+ out: Dict[str, List[Any]] = {"vendor_db": []}
+
+ for event in events:
+ if "EventType" not in event:
+ continue
+ if event["EventType"] != "EV_EFI_VARIABLE_AUTHORITY":
+ continue
+ if "Event" not in event or "UnicodeName" not in event["Event"]:
+ continue
+
+ event_name = event["Event"]["UnicodeName"].lower()
+ if event_name == "vendor_db":
+ data = None
+ if "VariableData" in event["Event"]:
+ data = event["Event"]["VariableData"]
+
+ if data is not None:
+ # VariableData for EV_EFI_VARIABLE_AUTHORITY is a list of signatures
+ for entry in data:
+ if "SignatureOwner" in entry and "SignatureData" in entry:
+ out["vendor_db"].append(
+ {"SignatureOwner": entry["SignatureOwner"], "SignatureData": f"0x{entry['SignatureData']}"}
+ )
+
+ return out
+
+
def get_kernel(events: List[Dict[str, Any]], secure_boot: bool) -> Dict[str, List[Dict[str, Any]]]:
"""Extract digest for Shim, Grub, Linux Kernel and initrd."""
out = []
@@ -259,6 +288,7 @@ def create_mb_refstate(args: argparse.Namespace) -> Optional[Dict[str, object]]:
}
],
**get_keys(events),
+ **get_vendor_db(events),
**get_mok(events),
**get_kernel(events, has_secureboot),
}
diff --git a/scripts/create_mb_refstate b/scripts/create_mb_refstate
index 23cafb9..c98e61d 100755
--- a/scripts/create_mb_refstate
+++ b/scripts/create_mb_refstate
@@ -78,6 +78,35 @@ def get_keys(events):
return out
+def get_vendor_db(events):
+ """Get vendor_db signatures from EV_EFI_VARIABLE_AUTHORITY events."""
+ out = {"vendor_db": []}
+
+ for event in events:
+ if "EventType" not in event:
+ continue
+ if event["EventType"] != "EV_EFI_VARIABLE_AUTHORITY":
+ continue
+ if "Event" not in event or "UnicodeName" not in event["Event"]:
+ continue
+
+ event_name = event["Event"]["UnicodeName"].lower()
+ if event_name == "vendor_db":
+ data = None
+ if "VariableData" in event["Event"]:
+ data = event["Event"]["VariableData"]
+
+ if data is not None:
+ # VariableData for EV_EFI_VARIABLE_AUTHORITY is a list of signatures
+ for entry in data:
+ if "SignatureOwner" in entry and "SignatureData" in entry:
+ out["vendor_db"].append(
+ {"SignatureOwner": entry["SignatureOwner"], "SignatureData": f"0x{entry['SignatureData']}"}
+ )
+
+ return out
+
+
def get_kernel(events, secure_boot):
"""
Extract digest for Shim, Grub, Linux Kernel and initrd.
@@ -197,6 +226,7 @@ def main():
}
],
**get_keys(events),
+ **get_vendor_db(events),
**get_mok(events),
**get_kernel(events, has_secureboot),
}
diff --git a/test/test_create_mb_policy.py b/test/test_create_mb_policy.py
index b00d8e7..cd32bda 100644
--- a/test/test_create_mb_policy.py
+++ b/test/test_create_mb_policy.py
@@ -364,6 +364,148 @@ class CreateMeasuredBootPolicy_Test(unittest.TestCase):
for c in test_cases:
self.assertDictEqual(create_mb_policy.get_mok(c["events"]), c["expected"])
+ def test_get_vendor_db(self):
+ test_cases = [
+ {"events": [], "expected": {"vendor_db": []}},
+ # No EV_EFI_VARIABLE_AUTHORITY events.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_DRIVER_CONFIG",
+ "Event": {"UnicodeName": "vendor_db", "VariableData": []},
+ }
+ ],
+ "expected": {"vendor_db": []},
+ },
+ # Good vendor_db event with EV_EFI_VARIABLE_AUTHORITY.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_AUTHORITY",
+ "Event": {
+ "UnicodeName": "vendor_db",
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ }
+ ],
+ },
+ }
+ ],
+ "expected": {
+ "vendor_db": [
+ {"SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", "SignatureData": "0xsig-data-1"}
+ ]
+ },
+ },
+ # Multiple vendor_db signatures.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_AUTHORITY",
+ "Event": {
+ "UnicodeName": "vendor_db",
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ },
+ {
+ "SignatureOwner": "77fa9abd-0359-4d32-bd60-28f4e78f784b",
+ "SignatureData": "sig-data-2",
+ },
+ ],
+ },
+ }
+ ],
+ "expected": {
+ "vendor_db": [
+ {"SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b", "SignatureData": "0xsig-data-1"},
+ {"SignatureOwner": "77fa9abd-0359-4d32-bd60-28f4e78f784b", "SignatureData": "0xsig-data-2"},
+ ]
+ },
+ },
+ # Missing EventType.
+ {
+ "events": [
+ {
+ "Event": {
+ "UnicodeName": "vendor_db",
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ }
+ ],
+ }
+ }
+ ],
+ "expected": {"vendor_db": []},
+ },
+ # Wrong EventType.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_DRIVER_CONFIG",
+ "Event": {
+ "UnicodeName": "vendor_db",
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ }
+ ],
+ },
+ }
+ ],
+ "expected": {"vendor_db": []},
+ },
+ # Missing Event.
+ {
+ "events": [{"EventType": "EV_EFI_VARIABLE_AUTHORITY"}],
+ "expected": {"vendor_db": []},
+ },
+ # Missing UnicodeName.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_AUTHORITY",
+ "Event": {
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ }
+ ]
+ },
+ }
+ ],
+ "expected": {"vendor_db": []},
+ },
+ # Wrong UnicodeName.
+ {
+ "events": [
+ {
+ "EventType": "EV_EFI_VARIABLE_AUTHORITY",
+ "Event": {
+ "UnicodeName": "db",
+ "VariableData": [
+ {
+ "SignatureOwner": "0223eddb-9079-4388-af77-2d65b1c35d3b",
+ "SignatureData": "sig-data-1",
+ }
+ ],
+ },
+ }
+ ],
+ "expected": {"vendor_db": []},
+ },
+ ]
+
+ for c in test_cases:
+ self.assertDictEqual(create_mb_policy.get_vendor_db(c["events"]), c["expected"])
+
def test_get_kernel(self):
test_cases = [
{"events": [], "secureboot": False, "expected": {}},
--
2.47.3

View File

@ -1,42 +0,0 @@
From 1b7191098ca3f6d72c6ad218564ae0938a87efd4 Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Mon, 18 Aug 2025 12:22:55 +0000
Subject: [PATCH 10/13] verifier: Gracefully shutdown on signal
Wait for the processes to finish when interrupted by a signal. Do not
call exit(0) in the signal handler.
Assisted-by: Claude 4 Sonnet
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/cloud_verifier_tornado.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
index 7553ac8..7065661 100644
--- a/keylime/cloud_verifier_tornado.py
+++ b/keylime/cloud_verifier_tornado.py
@@ -2138,7 +2138,7 @@ def main() -> None:
revocation_notifier.stop_broker()
for p in processes:
p.join()
- sys.exit(0)
+ # Do not call sys.exit(0) here as it interferes with multiprocessing cleanup
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
@@ -2159,3 +2159,11 @@ def main() -> None:
process = Process(target=server_process, args=(task_id, active_agents))
process.start()
processes.append(process)
+
+ # Wait for all worker processes to complete
+ try:
+ for p in processes:
+ p.join()
+ except KeyboardInterrupt:
+ # Signal handler will take care of cleanup
+ pass
--
2.47.3

View File

@ -1,308 +0,0 @@
From af9ac50f5acf1a7d4ad285956b60e60c3c4416b7 Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Wed, 23 Jul 2025 15:39:49 +0200
Subject: [PATCH 11/13] revocations: Try to send notifications on shutdown
During verifier shutdown, try to send any pending revocation
notification in a best-effort manner. In future, the pending revocation
notifications should be persisted to be processed during next startup.
Assisted-by: Claude 4 Sonnet
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/cloud_verifier_tornado.py | 7 +
keylime/revocation_notifier.py | 239 ++++++++++++++++++++++--------
2 files changed, 184 insertions(+), 62 deletions(-)
diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
index 7065661..89aa703 100644
--- a/keylime/cloud_verifier_tornado.py
+++ b/keylime/cloud_verifier_tornado.py
@@ -2109,6 +2109,10 @@ def main() -> None:
# Stop server to not accept new incoming connections
server.stop()
+ # Gracefully shutdown webhook workers to prevent connection errors
+ if "webhook" in revocation_notifier.get_notifiers():
+ revocation_notifier.shutdown_webhook_workers()
+
# Wait for all connections to be closed and then stop ioloop
async def stop() -> None:
await server.close_all_connections()
@@ -2136,6 +2140,9 @@ def main() -> None:
def sig_handler(*_: Any) -> None:
if run_revocation_notifier:
revocation_notifier.stop_broker()
+ # Gracefully shutdown webhook workers to prevent connection errors
+ if "webhook" in revocation_notifier.get_notifiers():
+ revocation_notifier.shutdown_webhook_workers()
for p in processes:
p.join()
# Do not call sys.exit(0) here as it interferes with multiprocessing cleanup
diff --git a/keylime/revocation_notifier.py b/keylime/revocation_notifier.py
index 5a7cc4b..c154028 100644
--- a/keylime/revocation_notifier.py
+++ b/keylime/revocation_notifier.py
@@ -18,6 +18,174 @@ broker_proc: Optional[Process] = None
_SOCKET_PATH = "/var/run/keylime/keylime.verifier.ipc"
+# Global webhook manager instance (initialized when needed)
+_webhook_manager: Optional["WebhookNotificationManager"] = None
+
+
+class WebhookNotificationManager:
+ """Manages webhook worker threads and graceful shutdown for revocation notifications."""
+
+ def __init__(self) -> None:
+ self._shutdown_event = threading.Event()
+ self._workers: Set[threading.Thread] = set()
+ self._workers_lock = threading.Lock()
+
+ def notify_webhook(self, tosend: Dict[str, Any]) -> None:
+ """Send webhook notification with worker thread management."""
+ url = config.get("verifier", "webhook_url", section="revocations", fallback="")
+ # Check if a url was specified
+ if url == "":
+ return
+
+ # Similarly to notify(), let's convert `tosend' to str to prevent
+ # possible issues with json handling by python-requests.
+ tosend = json.bytes_to_str(tosend)
+
+ def worker_webhook(tosend: Dict[str, Any], url: str) -> None:
+ is_shutdown_mode = False
+ try:
+ interval = config.getfloat("verifier", "retry_interval")
+ exponential_backoff = config.getboolean("verifier", "exponential_backoff")
+
+ max_retries = config.getint("verifier", "max_retries")
+ if max_retries <= 0:
+ logger.info("Invalid value found in 'max_retries' option for verifier, using default value")
+ max_retries = 5
+
+ # During shutdown, use fewer retries but still make best effort
+ if self._shutdown_event.is_set():
+ is_shutdown_mode = True
+ max_retries = min(max_retries, 3) # Reduce retries during shutdown but still try
+ logger.info(
+ "Shutdown mode: attempting to send critical revocation notification with %d retries",
+ max_retries,
+ )
+
+ # Get TLS options from the configuration
+ (cert, key, trusted_ca, key_password), verify_server_cert = web_util.get_tls_options(
+ "verifier", is_client=True, logger=logger
+ )
+
+ # Generate the TLS context using the obtained options
+ tls_context = web_util.generate_tls_context(
+ cert, key, trusted_ca, key_password, is_client=True, logger=logger
+ )
+
+ logger.info("Sending revocation event via webhook to %s ...", url)
+ for i in range(max_retries):
+ next_retry = retry.retry_time(exponential_backoff, interval, i, logger)
+
+ with RequestsClient(
+ url,
+ verify_server_cert,
+ tls_context,
+ ) as client:
+ try:
+ res = client.post("", json=tosend, timeout=5)
+ except requests.exceptions.SSLError as ssl_error:
+ if "TLSV1_ALERT_UNKNOWN_CA" in str(ssl_error):
+ logger.warning(
+ "Keylime does not recognize certificate from peer. Check if verifier 'trusted_server_ca' is configured correctly"
+ )
+
+ raise ssl_error from ssl_error
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
+ # During shutdown, only suppress errors on the final attempt after all retries exhausted
+ if is_shutdown_mode and i == max_retries - 1:
+ logger.warning(
+ "Final attempt to send revocation notification failed during shutdown: %s", e
+ )
+ return
+ # Otherwise, let the retry logic handle it
+ raise e
+
+ if res and res.status_code in [200, 202]:
+ if is_shutdown_mode:
+ logger.info("Successfully sent revocation notification during shutdown")
+ break
+
+ logger.debug(
+ "Unable to publish revocation message %d times via webhook, "
+ "trying again in %d seconds. "
+ "Server returned status code: %s",
+ i + 1,
+ next_retry,
+ res.status_code,
+ )
+
+ # During shutdown, use shorter retry intervals to complete faster
+ if is_shutdown_mode:
+ next_retry = min(next_retry, 2.0) # Cap retry interval during shutdown
+
+ time.sleep(next_retry)
+
+ except Exception as e:
+ # Only suppress errors during final shutdown phase and log appropriately
+ if is_shutdown_mode:
+ logger.warning("Failed to send revocation notification during shutdown: %s", e)
+ else:
+ logger.error("Error in webhook worker: %s", e)
+ finally:
+ # Remove this worker from the active set
+ current_thread = threading.current_thread()
+ with self._workers_lock:
+ self._workers.discard(current_thread)
+
+ w = functools.partial(worker_webhook, tosend, url)
+ t = threading.Thread(target=w, daemon=True)
+
+ # Add this worker to the active set
+ with self._workers_lock:
+ self._workers.add(t)
+
+ t.start()
+
+ def shutdown_workers(self) -> None:
+ """Signal webhook workers to shut down gracefully and wait for them to complete.
+
+ This gives workers time to complete their critical revocation notifications
+ before the service shuts down completely.
+ """
+ logger.info("Shutting down webhook workers gracefully...")
+ self._shutdown_event.set()
+
+ # Give workers generous time to complete critical revocation notifications
+ timeout = 30.0 # Increased timeout for critical security notifications
+ end_time = time.time() + timeout
+
+ with self._workers_lock:
+ workers_to_wait = list(self._workers)
+
+ if workers_to_wait:
+ logger.info("Waiting for %d webhook workers to complete revocation notifications...", len(workers_to_wait))
+
+ for worker in workers_to_wait:
+ remaining_time = max(0, end_time - time.time())
+ if remaining_time > 0:
+ logger.debug(
+ "Waiting for webhook worker %s to complete (timeout: %.1f seconds)", worker.name, remaining_time
+ )
+ worker.join(timeout=remaining_time)
+ if worker.is_alive():
+ logger.warning("Webhook worker %s did not complete within timeout", worker.name)
+ else:
+ logger.warning("Timeout exceeded while waiting for webhook workers")
+ break
+
+ # Clean up completed workers
+ with self._workers_lock:
+ self._workers.clear()
+
+ logger.info("Webhook workers shutdown complete")
+
+
+def _get_webhook_manager() -> WebhookNotificationManager:
+ """Get the global webhook manager instance, creating it if needed."""
+ global _webhook_manager
+ if _webhook_manager is None:
+ _webhook_manager = WebhookNotificationManager()
+ return _webhook_manager
+
# return the revocation notification methods for cloud verifier
def get_notifiers() -> Set[str]:
@@ -83,6 +251,12 @@ def stop_broker() -> None:
broker_proc.kill() # pylint: disable=E1101
+def shutdown_webhook_workers() -> None:
+ """Convenience function to shutdown webhook workers using the global manager."""
+ manager = _get_webhook_manager()
+ manager.shutdown_workers()
+
+
def notify(tosend: Dict[str, Any]) -> None:
assert "zeromq" in get_notifiers()
try:
@@ -127,68 +301,9 @@ def notify(tosend: Dict[str, Any]) -> None:
def notify_webhook(tosend: Dict[str, Any]) -> None:
- url = config.get("verifier", "webhook_url", section="revocations", fallback="")
- # Check if a url was specified
- if url == "":
- return
-
- # Similarly to notify(), let's convert `tosend' to str to prevent
- # possible issues with json handling by python-requests.
- tosend = json.bytes_to_str(tosend)
-
- def worker_webhook(tosend: Dict[str, Any], url: str) -> None:
- interval = config.getfloat("verifier", "retry_interval")
- exponential_backoff = config.getboolean("verifier", "exponential_backoff")
-
- max_retries = config.getint("verifier", "max_retries")
- if max_retries <= 0:
- logger.info("Invalid value found in 'max_retries' option for verifier, using default value")
- max_retries = 5
-
- # Get TLS options from the configuration
- (cert, key, trusted_ca, key_password), verify_server_cert = web_util.get_tls_options(
- "verifier", is_client=True, logger=logger
- )
-
- # Generate the TLS context using the obtained options
- tls_context = web_util.generate_tls_context(cert, key, trusted_ca, key_password, is_client=True, logger=logger)
-
- logger.info("Sending revocation event via webhook to %s ...", url)
- for i in range(max_retries):
- next_retry = retry.retry_time(exponential_backoff, interval, i, logger)
-
- with RequestsClient(
- url,
- verify_server_cert,
- tls_context,
- ) as client:
- try:
- res = client.post("", json=tosend, timeout=5)
- except requests.exceptions.SSLError as ssl_error:
- if "TLSV1_ALERT_UNKNOWN_CA" in str(ssl_error):
- logger.warning(
- "Keylime does not recognize certificate from peer. Check if verifier 'trusted_server_ca' is configured correctly"
- )
-
- raise ssl_error from ssl_error
-
- if res and res.status_code in [200, 202]:
- break
-
- logger.debug(
- "Unable to publish revocation message %d times via webhook, "
- "trying again in %d seconds. "
- "Server returned status code: %s",
- i + 1,
- next_retry,
- res.status_code,
- )
-
- time.sleep(next_retry)
-
- w = functools.partial(worker_webhook, tosend, url)
- t = threading.Thread(target=w, daemon=True)
- t.start()
+ """Send webhook notification using the global webhook manager."""
+ manager = _get_webhook_manager()
+ manager.notify_webhook(tosend)
cert_key = None
--
2.47.3

View File

@ -1,45 +0,0 @@
From 5fb4484b07a7ba3fcdf451bf816b5f07a40d6d97 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Wed, 4 Jun 2025 19:52:37 +0100
Subject: [PATCH 12/13] requests_client: close the session at the end of the
resource manager
We had an issue in the past in which the webhook worker would not
properly close the opened session. This was fixed in #1456 (Close
session in worker_webhook function).
At some later point, in #1566 (revocation_notifier: Take into account CA
certificates added via configuration), some refactoring around the
webhook_worker() in revocation_notifier happened and it started using
the RequestsClient resource manager.
However, the RequestsClient does not close the session at its end, which
in turns makes that the old issue of not closing properly the session
in the webhook_worker() returned.
We now issue a session.close() at the end of the RequestsClient.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/requests_client.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/keylime/requests_client.py b/keylime/requests_client.py
index 16615f7..b7da484 100644
--- a/keylime/requests_client.py
+++ b/keylime/requests_client.py
@@ -40,7 +40,10 @@ class RequestsClient:
return self
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
- pass
+ try:
+ self.session.close()
+ except Exception:
+ pass
def request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
return self.session.request(method, self.base_url + url, **kwargs)
--
2.47.3

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,382 +0,0 @@
From 7a723f0938edf9ccc597507a4230922e9235cf18 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Wed, 24 Sep 2025 07:20:53 +0100
Subject: [PATCH 13/18] algorithms: add support for specific ECC curve
algorithms
Extended the Encrypt enum to support specific ECC curves including:
- ecc192 (P-192)
- ecc224 (P-224)
- ecc256 (P-256)
- ecc384 (P-384)
- ecc521 (P-521)
This enables Keylime to accept and validate different ECC curves
for TPM attestation operations.
Also, when agent reports specific algorithm like 'ecc256' but tenant
configuration uses generic 'ecc', the is_accepted function now uses
bidirectional normalization to properly match algorithms.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/common/algorithms.py | 28 +++-
test/test_algorithms.py | 275 ++++++++++++++++++++++++++++++++++-
2 files changed, 301 insertions(+), 2 deletions(-)
diff --git a/keylime/common/algorithms.py b/keylime/common/algorithms.py
index db12c26..bb22fb6 100644
--- a/keylime/common/algorithms.py
+++ b/keylime/common/algorithms.py
@@ -9,7 +9,18 @@ def is_accepted(algorithm: str, accepted: List[Any]) -> bool:
@param algorithm: algorithm to be checked
@param accepted: a list of acceptable algorithms
"""
- return algorithm in accepted
+ # Check direct match first.
+ if algorithm in accepted:
+ return True
+
+ # Check if any accepted algorithm normalizes to the same value as our algorithm
+ # This handles backwards compatibility cases like "ecc" accepting "ecc256".
+ normalized_algorithm = Encrypt.normalize(algorithm)
+ for accepted_alg in accepted:
+ if Encrypt.normalize(str(accepted_alg)) == normalized_algorithm:
+ return True
+
+ return False
class Hash(str, enum.Enum):
@@ -74,11 +85,26 @@ class Hash(str, enum.Enum):
class Encrypt(str, enum.Enum):
RSA = "rsa"
ECC = "ecc"
+ ECC192 = "ecc192"
+ ECC224 = "ecc224"
+ ECC256 = "ecc256"
+ ECC384 = "ecc384"
+ ECC521 = "ecc521"
@staticmethod
def is_recognized(algorithm: str) -> bool:
+ # Handle aliases to match agent behavior
+ if algorithm == "ecc":
+ algorithm = "ecc256" # Default ECC alias maps to P-256, same as the agent.
return algorithm in list(Encrypt)
+ @staticmethod
+ def normalize(algorithm: str) -> str:
+ """Normalize algorithm string to handle aliases, matching the agent behavior"""
+ if algorithm == "ecc":
+ return "ecc256" # Default ECC alias maps to P-256.
+ return algorithm
+
class Sign(str, enum.Enum):
RSASSA = "rsassa"
diff --git a/test/test_algorithms.py b/test/test_algorithms.py
index b5a29c7..8a31fa9 100644
--- a/test/test_algorithms.py
+++ b/test/test_algorithms.py
@@ -2,7 +2,7 @@ import os
import tempfile
import unittest
-from keylime.common.algorithms import Encrypt, Hash, Sign
+from keylime.common.algorithms import Encrypt, Hash, Sign, is_accepted
class TestHash(unittest.TestCase):
@@ -117,11 +117,88 @@ class TestEncrypt(unittest.TestCase):
"enc": "ecc",
"valid": True,
},
+ {
+ "enc": "ecc192",
+ "valid": True,
+ },
+ {
+ "enc": "ecc224",
+ "valid": True,
+ },
+ {
+ "enc": "ecc256",
+ "valid": True,
+ },
+ {
+ "enc": "ecc384",
+ "valid": True,
+ },
+ {
+ "enc": "ecc521",
+ "valid": True,
+ },
]
for c in test_cases:
self.assertEqual(Encrypt.is_recognized(c["enc"]), c["valid"], msg=f"enc = {c['enc']}")
+ def test_enum_membership(self):
+ """Test that all ECC curve algorithms are members of the Encrypt enum"""
+ self.assertTrue(Encrypt.RSA in Encrypt)
+ self.assertTrue(Encrypt.ECC in Encrypt)
+ self.assertTrue(Encrypt.ECC192 in Encrypt)
+ self.assertTrue(Encrypt.ECC224 in Encrypt)
+ self.assertTrue(Encrypt.ECC256 in Encrypt)
+ self.assertTrue(Encrypt.ECC384 in Encrypt)
+ self.assertTrue(Encrypt.ECC521 in Encrypt)
+
+ def test_normalize(self):
+ """Test the normalize method for handling ECC aliases"""
+ test_cases = [
+ {
+ "input": "ecc",
+ "expected": "ecc256",
+ },
+ {
+ "input": "ecc192",
+ "expected": "ecc192",
+ },
+ {
+ "input": "ecc224",
+ "expected": "ecc224",
+ },
+ {
+ "input": "ecc256",
+ "expected": "ecc256",
+ },
+ {
+ "input": "ecc384",
+ "expected": "ecc384",
+ },
+ {
+ "input": "ecc521",
+ "expected": "ecc521",
+ },
+ {
+ "input": "rsa",
+ "expected": "rsa",
+ },
+ ]
+
+ for c in test_cases:
+ self.assertEqual(Encrypt.normalize(c["input"]), c["expected"], msg=f"input = {c['input']}")
+
+ def test_normalize_ecc_alias_behavior(self):
+ """Test that ECC alias normalization matches agent behavior"""
+ # Test that "ecc" is recognized through alias handling
+ self.assertTrue(Encrypt.is_recognized("ecc"))
+
+ # Test that normalize converts "ecc" to "ecc256" (P-256)
+ self.assertEqual(Encrypt.normalize("ecc"), "ecc256")
+
+ # Test that direct ecc256 works
+ self.assertTrue(Encrypt.is_recognized("ecc256"))
+
class TestSign(unittest.TestCase):
def test_is_recognized(self):
@@ -158,3 +235,199 @@ class TestSign(unittest.TestCase):
for c in test_cases:
self.assertEqual(Sign.is_recognized(c["sign"]), c["valid"], msg=f"sign = {c['sign']}")
+
+
+class TestIsAccepted(unittest.TestCase):
+ def test_direct_algorithm_matching(self):
+ """Test that direct algorithm matches work correctly"""
+ test_cases = [
+ {
+ "algorithm": "ecc256",
+ "accepted": ["ecc256"],
+ "expected": True,
+ },
+ {
+ "algorithm": "rsa",
+ "accepted": ["rsa"],
+ "expected": True,
+ },
+ {
+ "algorithm": "ecc384",
+ "accepted": ["ecc256", "ecc384"],
+ "expected": True,
+ },
+ {
+ "algorithm": "ecc521",
+ "accepted": ["ecc256"],
+ "expected": False,
+ },
+ {
+ "algorithm": "unknown",
+ "accepted": ["rsa", "ecc256"],
+ "expected": False,
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(result, c["expected"], msg=f"algorithm='{c['algorithm']}', accepted={c['accepted']}")
+
+ def test_backwards_compatibility_ecc_normalization(self):
+ """Test backwards compatibility: 'ecc' in accepted list should accept specific ECC algorithms"""
+ test_cases = [
+ {
+ "algorithm": "ecc256",
+ "accepted": ["ecc"],
+ "expected": True,
+ "desc": "ecc256 should be accepted when 'ecc' is in accepted list",
+ },
+ {
+ "algorithm": "ecc384",
+ "accepted": ["ecc"],
+ "expected": False,
+ "desc": "ecc384 should NOT be accepted when only 'ecc' is in accepted list (ecc maps to ecc256)",
+ },
+ {
+ "algorithm": "ecc521",
+ "accepted": ["ecc"],
+ "expected": False,
+ "desc": "ecc521 should NOT be accepted when only 'ecc' is in accepted list",
+ },
+ {
+ "algorithm": "ecc192",
+ "accepted": ["ecc"],
+ "expected": False,
+ "desc": "ecc192 should NOT be accepted when only 'ecc' is in accepted list",
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(
+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}"
+ )
+
+ def test_forward_compatibility_ecc_normalization(self):
+ """Test forward compatibility: specific ECC in accepted list should accept 'ecc' algorithm"""
+ test_cases = [
+ {
+ "algorithm": "ecc",
+ "accepted": ["ecc256"],
+ "expected": True,
+ "desc": "ecc should be accepted when 'ecc256' is in accepted list (both normalize to ecc256)",
+ },
+ {
+ "algorithm": "ecc",
+ "accepted": ["ecc384"],
+ "expected": False,
+ "desc": "ecc should NOT be accepted when only 'ecc384' is in accepted list",
+ },
+ {
+ "algorithm": "ecc",
+ "accepted": ["ecc521"],
+ "expected": False,
+ "desc": "ecc should NOT be accepted when only 'ecc521' is in accepted list",
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(
+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}"
+ )
+
+ def test_bidirectional_algorithm_matching(self):
+ """Test bidirectional matching scenarios that happen in real usage"""
+ test_cases = [
+ {
+ "algorithm": "ecc256",
+ "accepted": ["rsa", "ecc"],
+ "expected": True,
+ "desc": "Agent reports ecc256, tenant config has generic 'ecc'",
+ },
+ {
+ "algorithm": "ecc",
+ "accepted": ["rsa", "ecc256"],
+ "expected": True,
+ "desc": "Agent reports generic 'ecc', tenant config has specific 'ecc256'",
+ },
+ {
+ "algorithm": "ecc384",
+ "accepted": ["rsa", "ecc"],
+ "expected": False,
+ "desc": "Agent reports ecc384, tenant has generic 'ecc' (should not match)",
+ },
+ {
+ "algorithm": "ecc",
+ "accepted": ["rsa", "ecc384"],
+ "expected": False,
+ "desc": "Agent reports generic 'ecc', tenant has ecc384 (should not match)",
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(
+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}"
+ )
+
+ def test_mixed_algorithm_types(self):
+ """Test mixing different algorithm types in accepted list"""
+ test_cases = [
+ {
+ "algorithm": "rsa",
+ "accepted": ["ecc", "rsa"],
+ "expected": True,
+ },
+ {
+ "algorithm": "ecc256",
+ "accepted": ["rsa", "ecc"],
+ "expected": True,
+ },
+ {
+ "algorithm": "ecc384",
+ "accepted": ["rsa", "ecc256", "ecc384"],
+ "expected": True,
+ },
+ {
+ "algorithm": "unknown",
+ "accepted": ["rsa", "ecc", "ecc384"],
+ "expected": False,
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(result, c["expected"], msg=f"algorithm='{c['algorithm']}', accepted={c['accepted']}")
+
+ def test_edge_cases(self):
+ """Test edge cases and boundary conditions"""
+ test_cases = [
+ {"algorithm": "", "accepted": ["ecc"], "expected": False, "desc": "Empty algorithm string"},
+ {"algorithm": "ecc256", "accepted": [], "expected": False, "desc": "Empty accepted list"},
+ {"algorithm": "ecc256", "accepted": [""], "expected": False, "desc": "Accepted list with empty string"},
+ {
+ "algorithm": "ECC256",
+ "accepted": ["ecc256"],
+ "expected": False,
+ "desc": "Case sensitivity - uppercase should not match",
+ },
+ {
+ "algorithm": "ecc256",
+ "accepted": ["ecc"],
+ "expected": True,
+ "desc": "ecc256 algorithm should match ecc in accepted list",
+ },
+ {
+ "algorithm": "ecc",
+ "accepted": ["ecc256"],
+ "expected": True,
+ "desc": "ecc algorithm should match ecc256 in accepted list",
+ },
+ ]
+
+ for c in test_cases:
+ result = is_accepted(c["algorithm"], c["accepted"])
+ self.assertEqual(
+ result, c["expected"], msg=f"{c['desc']} - algorithm='{c['algorithm']}', accepted={c['accepted']}"
+ )
--
2.47.3

View File

@ -1,87 +0,0 @@
From eecd2f73642f784b19cb1bb9c78c6d0b1e486dda Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Fri, 26 Sep 2025 00:03:49 +0100
Subject: [PATCH 14/18] algorithms: add support for specific RSA algorithms
Similar to the previous change for ECC, now we extend the Encrypt enum
to support the following specific RSA algorithms:
- RSA1024
- RSA2048
- RSA3072
- RSA4096
Map also 'rsa' to 'rsa2048' for backwards compatibility.
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/common/algorithms.py | 8 ++++++++
test/test_algorithms.py | 13 ++++++++++++-
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/keylime/common/algorithms.py b/keylime/common/algorithms.py
index bb22fb6..32a1ec1 100644
--- a/keylime/common/algorithms.py
+++ b/keylime/common/algorithms.py
@@ -84,6 +84,10 @@ class Hash(str, enum.Enum):
class Encrypt(str, enum.Enum):
RSA = "rsa"
+ RSA1024 = "rsa1024"
+ RSA2048 = "rsa2048"
+ RSA3072 = "rsa3072"
+ RSA4096 = "rsa4096"
ECC = "ecc"
ECC192 = "ecc192"
ECC224 = "ecc224"
@@ -96,6 +100,8 @@ class Encrypt(str, enum.Enum):
# Handle aliases to match agent behavior
if algorithm == "ecc":
algorithm = "ecc256" # Default ECC alias maps to P-256, same as the agent.
+ if algorithm == "rsa":
+ algorithm = "rsa2048" # Default RSA alias maps to RSA-2048, same as the agent.
return algorithm in list(Encrypt)
@staticmethod
@@ -103,6 +109,8 @@ class Encrypt(str, enum.Enum):
"""Normalize algorithm string to handle aliases, matching the agent behavior"""
if algorithm == "ecc":
return "ecc256" # Default ECC alias maps to P-256.
+ if algorithm == "rsa":
+ return "rsa2048" # Default RSA alias maps to RSA-2048.
return algorithm
diff --git a/test/test_algorithms.py b/test/test_algorithms.py
index 8a31fa9..5542c0f 100644
--- a/test/test_algorithms.py
+++ b/test/test_algorithms.py
@@ -181,7 +181,7 @@ class TestEncrypt(unittest.TestCase):
},
{
"input": "rsa",
- "expected": "rsa",
+ "expected": "rsa2048",
},
]
@@ -199,6 +199,17 @@ class TestEncrypt(unittest.TestCase):
# Test that direct ecc256 works
self.assertTrue(Encrypt.is_recognized("ecc256"))
+ def test_normalize_rsa_alias_behavior(self):
+ """Test that RSA alias normalization matches agent behavior"""
+ # Test that "rsa" is recognized through alias handling
+ self.assertTrue(Encrypt.is_recognized("rsa"))
+
+ # Test that normalize converts "rsa" to "rsa2048"
+ self.assertEqual(Encrypt.normalize("rsa"), "rsa2048")
+
+ # Test that direct rsa2048 works
+ self.assertTrue(Encrypt.is_recognized("rsa2048"))
+
class TestSign(unittest.TestCase):
def test_is_recognized(self):
--
2.47.3

View File

@ -1,43 +0,0 @@
From 690a2059be01993f5e7f65a01d994e53b82211e4 Mon Sep 17 00:00:00 2001
From: Thore Sommer <mail@thson.de>
Date: Mon, 3 Mar 2025 15:44:37 +0100
Subject: [PATCH 15/18] tpm_util: fix quote signature extraction for ECDSA
Signed-off-by: Thore Sommer <mail@thson.de>
---
keylime/tpm/tpm_util.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py
index cdecd32..25c40e0 100644
--- a/keylime/tpm/tpm_util.py
+++ b/keylime/tpm/tpm_util.py
@@ -223,9 +223,7 @@ def checkquote(
pcrblob: The state of the PCRs that were quoted; Intel tpm2-tools specific format
exp_hash_alg: The hash that was expected to have been used for quoting
"""
- sig_alg, hash_alg, sig_size = struct.unpack_from(">HHH", sigblob, 0)
-
- (signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6)
+ sig_alg, hash_alg = struct.unpack_from(">HH", sigblob, 0)
pubkey = serialization.load_pem_public_key(aikblob, backend=backends.default_backend())
if not isinstance(pubkey, (RSAPublicKey, EllipticCurvePublicKey)):
@@ -236,6 +234,14 @@ def checkquote(
if isinstance(pubkey, EllipticCurvePublicKey) and sig_alg not in [tpm2_objects.TPM_ALG_ECDSA]:
raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}' for EC keys")
+ if sig_alg in [tpm2_objects.TPM_ALG_RSASSA]:
+ (sig_size,) = struct.unpack_from(">H", sigblob, 4)
+ (signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6)
+ elif sig_alg in [tpm2_objects.TPM_ALG_ECDSA]:
+ signature = ecdsa_der_from_tpm(sigblob)
+ else:
+ raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}'")
+
hashfunc = tpm2_objects.HASH_FUNCS.get(hash_alg)
if not hashfunc:
raise ValueError(f"Unsupported hash with id {hash_alg:#x} in signature blob")
--
2.47.3

View File

@ -1,516 +0,0 @@
From c0aaf2ad80e2ec714b46ae1ba94678791d58b02d Mon Sep 17 00:00:00 2001
From: rpm-build <rpm-build>
Date: Wed, 4 Feb 2026 01:00:00 +0100
Subject: [PATCH] tpm: fix ECC P-521 coordinate validation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
P-521 coordinates can vary from 65-66 bytes due to TPM implementations
padding 521-bit values to byte boundaries or stripping leading zeros.
The previous validation was too strict, rejecting valid coordinates.
Enhanced validation:
- Accepts P-521 coordinate range 65-66 bytes (520-528 bits)
- Validates against actual NIST prime moduli per SEC1 §2.3.5 and
FIPS 186-4 App D (coordinates must be < field prime p)
- Strict rejection of unknown curves for security
The enhanced approach prevents false validation of coordinates that are
the correct byte length but exceed the curve's field prime.
Backported from upstream commit 0219550c3a29db85b202a02b17590260a41a262f
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/tpm/tpm2_objects.py | 42 +++-
test/test_tpm2_objects.py | 417 ++++++++++++++++++++++++++++++++++++
2 files changed, 455 insertions(+), 4 deletions(-)
create mode 100644 test/test_tpm2_objects.py
diff --git a/keylime/tpm/tpm2_objects.py b/keylime/tpm/tpm2_objects.py
index fcc5bb5..9170628 100644
--- a/keylime/tpm/tpm2_objects.py
+++ b/keylime/tpm/tpm2_objects.py
@@ -31,6 +31,16 @@ TPM_ECC_NIST_P256 = 0x0003
TPM_ECC_NIST_P384 = 0x0004
TPM_ECC_NIST_P521 = 0x0005
+# ECC curve prime moduli lookup table (coordinates must be < p)
+# This structure supports NIST curves and can be extended for other curves.
+ECC_CURVE_PRIMES = {
+ TPM_ECC_NIST_P192: 2**192 - 2**64 - 1, # P-192 prime
+ TPM_ECC_NIST_P224: 2**224 - 2**96 + 1, # P-224 prime
+ TPM_ECC_NIST_P256: 2**256 - 2**224 + 2**192 + 2**96 - 1, # P-256 prime
+ TPM_ECC_NIST_P384: 2**384 - 2**128 - 2**96 + 2**32 - 1, # P-384 prime
+ TPM_ECC_NIST_P521: 2**521 - 1, # P-521 prime
+}
+
TPM_ALG_RSA = 0x0001
TPM_ALG_ECC = 0x0023
@@ -318,10 +328,34 @@ def pubkey_parms_from_tpm2b_public(
if len(rest) != 0:
raise ValueError("Misparsed: more contents after X and Y")
- if (len(x) * 8) != curve.key_size:
- raise ValueError(f"Misparsed either X or curve: {len(x)}*8 != {curve.key_size}")
- if (len(y) * 8) != curve.key_size:
- raise ValueError(f"Misparsed either Y or curve curve: {len(y)}*8 != {curve.key_size}")
+ # ECC coordinates can vary in byte length due to:
+ # 1. Padding to byte boundaries (most common)
+ # 2. Leading zero stripping in some encodings
+ # Validate both byte length and actual coordinate value.
+ max_bytes = (curve.key_size + 7) // 8
+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes
+
+ # Get the actual prime modulus for the curve
+ prime_p = ECC_CURVE_PRIMES.get(curve_id)
+ if prime_p is None:
+ raise ValueError(f"Unsupported curve ID {curve_id:#x}: prime modulus not known")
+
+ for label, coord in (("X", x), ("Y", y)):
+ coord_len = len(coord)
+ if coord_len < min_bytes or coord_len > max_bytes:
+ raise ValueError(
+ f"Misparsed {label} coordinate: got {coord_len} bytes, "
+ f"expected {min_bytes}-{max_bytes} for {curve.key_size}-bit curve"
+ )
+
+ coord_int = int.from_bytes(coord, "big")
+ # Coordinates must be reduced modulo the field prime p
+ # (SEC1 §2.3.5, FIPS 186-4 App D). Reject values >= p.
+ if coord_int >= prime_p:
+ raise ValueError(
+ f"{label} coordinate too large: {coord_int.bit_length()} bits, "
+ f"must be < {prime_p.bit_length()}-bit prime modulus"
+ )
bx = int.from_bytes(x, byteorder="big")
by = int.from_bytes(y, byteorder="big")
diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py
new file mode 100644
index 0000000..9cc6b70
--- /dev/null
+++ b/test/test_tpm2_objects.py
@@ -0,0 +1,417 @@
+import struct
+import unittest
+
+from cryptography.hazmat.primitives.asymmetric import ec
+
+from keylime.tpm.tpm2_objects import (
+ ECC_CURVE_PRIMES,
+ TPM_ECC_NIST_P192,
+ TPM_ECC_NIST_P224,
+ TPM_ECC_NIST_P256,
+ TPM_ECC_NIST_P384,
+ TPM_ECC_NIST_P521,
+ _curve_from_curve_id,
+ _pack_in_tpm2b,
+ pubkey_parms_from_tpm2b_public,
+)
+
+
+class TestTpm2Objects(unittest.TestCase):
+ def test_p521_coordinate_validation_logic(self):
+ """Test the specific coordinate validation logic for P-521"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521)
+
+ # Test the updated validation logic
+ max_bytes = (curve.key_size + 7) // 8 # Should be 66 bytes for P-521
+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes # Should be 65 bytes for P-521
+
+ self.assertEqual(max_bytes, 66)
+ self.assertEqual(min_bytes, 65) # P-521 is not byte-aligned, so allows 65-66 bytes
+
+ # Test coordinate sizes that should be accepted (65-66 bytes for P-521)
+ valid_sizes = [65, 66]
+
+ for size in valid_sizes:
+ # Check that the validation logic would accept this size
+ should_pass = min_bytes <= size <= max_bytes
+ self.assertTrue(should_pass, f"Size {size} bytes should be valid for P-521")
+
+ # Test coordinate sizes that should be rejected
+ invalid_sizes = [64, 67, 32, 68]
+
+ for size in invalid_sizes:
+ # This should fail: not in the valid range
+ should_fail = size < min_bytes or size > max_bytes
+ self.assertTrue(should_fail, f"Size {size} bytes should be invalid for P-521")
+
+ def test_p256_coordinate_validation_logic(self):
+ """Test the coordinate validation logic for P-256 to ensure no regression"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P256)
+
+ max_bytes = (curve.key_size + 7) // 8 # Should be 32 bytes for P-256
+ min_bytes = (
+ max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes
+ ) # Should be 32 bytes for P-256 (byte-aligned)
+
+ self.assertEqual(max_bytes, 32)
+ self.assertEqual(min_bytes, 32) # P-256 is byte-aligned, so only accepts 32 bytes
+
+ # 32 bytes should be accepted
+ size = 32
+ should_pass = min_bytes <= size <= max_bytes
+ self.assertTrue(should_pass, f"P-256 should accept {size} bytes")
+
+ # Other sizes should be rejected
+ invalid_sizes = [31, 33, 64]
+ for size in invalid_sizes:
+ should_fail = size < min_bytes or size > max_bytes
+ self.assertTrue(should_fail, f"P-256 should reject {size} bytes")
+
+ def test_p384_coordinate_validation_logic(self):
+ """Test the coordinate validation logic for P-384 to ensure no regression"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P384)
+
+ max_bytes = (curve.key_size + 7) // 8 # Should be 48 bytes for P-384
+ min_bytes = (
+ max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes
+ ) # Should be 48 bytes for P-384 (byte-aligned)
+
+ self.assertEqual(max_bytes, 48)
+ self.assertEqual(min_bytes, 48) # P-384 is byte-aligned, so only accepts 48 bytes
+
+ # 48 bytes should be accepted
+ size = 48
+ should_pass = min_bytes <= size <= max_bytes
+ self.assertTrue(should_pass, f"P-384 should accept {size} bytes")
+
+ def test_coordinate_size_calculation(self):
+ """Test that coordinate size calculations are correct for different curves"""
+ # P-256: 256 bits -> (256 + 7) // 8 = 32 bytes
+ curve_p256 = _curve_from_curve_id(TPM_ECC_NIST_P256)
+ expected_p256 = (curve_p256.key_size + 7) // 8
+ self.assertEqual(expected_p256, 32)
+ self.assertEqual(curve_p256.key_size, 256)
+
+ # P-384: 384 bits -> (384 + 7) // 8 = 48 bytes
+ curve_p384 = _curve_from_curve_id(TPM_ECC_NIST_P384)
+ expected_p384 = (curve_p384.key_size + 7) // 8
+ self.assertEqual(expected_p384, 48)
+ self.assertEqual(curve_p384.key_size, 384)
+
+ # P-521: 521 bits -> (521 + 7) // 8 = 66 bytes
+ curve_p521 = _curve_from_curve_id(TPM_ECC_NIST_P521)
+ expected_p521 = (curve_p521.key_size + 7) // 8
+ self.assertEqual(expected_p521, 66)
+ self.assertEqual(curve_p521.key_size, 521)
+
+ def test_p521_specific_fix(self):
+ """Test the specific scenario that was fixed: P-521 with 66-byte coordinates"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521)
+
+ # The key issue: P-521 has 521 bits
+ self.assertEqual(curve.key_size, 521)
+
+ # TPMs pad to 66 bytes (528 bits)
+ tpm_padded_size = 66
+ tpm_padded_bits = tpm_padded_size * 8
+ self.assertEqual(tpm_padded_bits, 528)
+
+ # The old validation would reject: (66 * 8) != 521
+ old_validation_fails = tpm_padded_bits != curve.key_size
+ self.assertTrue(old_validation_fails, "Old validation would incorrectly reject 66-byte coordinates")
+
+ # The new validation should accept: len(x) == expected_bytes OR (len(x) * 8) == curve.key_size
+ expected_bytes = (curve.key_size + 7) // 8
+ new_validation_passes = (tpm_padded_size == expected_bytes) or (tpm_padded_bits == curve.key_size)
+ self.assertTrue(new_validation_passes, "New validation should accept 66-byte coordinates")
+
+ def test_validation_before_and_after_fix(self):
+ """Test that demonstrates the fix by comparing old vs new validation logic"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521)
+
+ # Test multiple coordinate sizes that P-521 can have
+ test_sizes = [65, 66] # 65 bytes (leading zero stripped), 66 bytes (padded)
+
+ max_bytes = (curve.key_size + 7) // 8 # 66 bytes
+ min_bytes = max_bytes - 1 if curve.key_size % 8 != 0 else max_bytes # 65 bytes for P-521
+
+ for coordinate_size in test_sizes:
+ # Old validation logic (strict bit size match) - would require exactly 65.125 bytes
+ # which is impossible since we can't have fractional bytes
+
+ # New validation logic (accept range for non-byte-aligned curves)
+ new_logic_passes = min_bytes <= coordinate_size <= max_bytes
+ self.assertTrue(new_logic_passes, f"New logic should accept {coordinate_size}-byte coordinates for P-521")
+
+ # Verify the calculations
+ self.assertEqual(max_bytes, 66)
+ self.assertEqual(min_bytes, 65)
+
+ def test_p521_coordinate_range_validation(self):
+ """Test that P-521 accepts coordinates in the range 65-66 bytes (520-528 bits)"""
+ curve = _curve_from_curve_id(TPM_ECC_NIST_P521)
+
+ # P-521: 521 bits, padded to 66 bytes (528 bits), or 65 bytes with leading zero stripped
+ max_bytes = (curve.key_size + 7) // 8 # 66 bytes
+ min_bytes = max_bytes - 1 # 65 bytes (since 521 % 8 != 0)
+
+ # Test all valid sizes
+ valid_sizes = [65, 66]
+ for size in valid_sizes:
+ is_valid = min_bytes <= size <= max_bytes
+ self.assertTrue(is_valid, f"P-521 should accept {size} bytes ({size * 8} bits)")
+
+ # Test invalid sizes
+ invalid_sizes = [64, 67, 68, 32]
+ for size in invalid_sizes:
+ is_invalid = size < min_bytes or size > max_bytes
+ self.assertTrue(is_invalid, f"P-521 should reject {size} bytes ({size * 8} bits)")
+
+ def test_coordinate_value_validation(self):
+ """Test that coordinate values are validated against actual prime moduli"""
+ # Test P-521 with actual prime
+ # curve_p521 = _curve_from_curve_id(TPM_ECC_NIST_P521) # Not needed for this test
+ p521_prime = ECC_CURVE_PRIMES[TPM_ECC_NIST_P521]
+
+ # Test valid coordinate value (within range)
+ valid_coord_int = p521_prime - 1 # Largest valid value
+ valid_coord_bytes = valid_coord_int.to_bytes(66, "big") # 66 bytes, padded
+
+ # Test the validation logic
+ coord_int = int.from_bytes(valid_coord_bytes, "big")
+ is_valid_value = coord_int < p521_prime
+ self.assertTrue(is_valid_value, "Coordinate value should be valid for P-521")
+
+ # Test invalid coordinate value (>= prime)
+ invalid_coord_int = p521_prime # Equal to prime (invalid)
+ invalid_coord_bytes = invalid_coord_int.to_bytes(66, "big") # 66 bytes, but value too large
+
+ coord_int = int.from_bytes(invalid_coord_bytes, "big")
+ is_invalid_value = coord_int >= p521_prime
+ self.assertTrue(is_invalid_value, "Coordinate value >= prime should be invalid for P-521")
+
+ def test_prime_constants_accuracy(self):
+ """Test that our hardcoded prime constants are correct"""
+ # Verify the NIST prime values
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192 - 2**64 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224 - 2**96 + 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256 - 2**224 + 2**192 + 2**96 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384 - 2**128 - 2**96 + 2**32 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1)
+
+ # Verify they are actually less than 2^m for all curves except P-521
+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192)
+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224)
+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256)
+ self.assertLess(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1) # P-521 is special case
+
+ def test_prime_lookup_table(self):
+ """Test that the prime lookup table works correctly"""
+ # Test known curves
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P192], 2**192 - 2**64 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P224], 2**224 - 2**96 + 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P256], 2**256 - 2**224 + 2**192 + 2**96 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P384], 2**384 - 2**128 - 2**96 + 2**32 - 1)
+ self.assertEqual(ECC_CURVE_PRIMES[TPM_ECC_NIST_P521], 2**521 - 1)
+
+ # Test rejection of unknown curve
+ unknown_curve_id = 0x9999
+ unknown_prime = ECC_CURVE_PRIMES.get(unknown_curve_id)
+ self.assertIsNone(unknown_prime, "Unknown curves should not be in ECC_CURVE_PRIMES")
+
+ def test_error_message_formatting(self):
+ """Test that error messages use bit_length() instead of full integers"""
+ # Create a large coordinate value
+ large_value = ECC_CURVE_PRIMES[TPM_ECC_NIST_P521] # This would be hundreds of digits
+
+ # Verify bit_length() is much more reasonable than the full number
+ bit_length = large_value.bit_length()
+ self.assertEqual(bit_length, 521) # Much more readable than 150+ digit number
+
+ # The error message should use bit lengths, not full integers
+ expected_msg_pattern = f"coordinate too large: {bit_length} bits"
+ self.assertIn("521 bits", expected_msg_pattern)
+
+ def test_unknown_curve_rejection(self):
+ """Test that unknown curves are strictly rejected"""
+ # This tests the design decision to be strict rather than use fallbacks
+ unknown_curve_id = 0x9999
+
+ # The strict approach: unknown curves should not have fallback behavior
+ # This ensures we only validate curves we explicitly understand
+ result = ECC_CURVE_PRIMES.get(unknown_curve_id)
+ self.assertIsNone(result, "Unknown curves should be explicitly rejected, not given fallback primes")
+
+
+class TestEccPublicKeySecurityValidation(unittest.TestCase):
+ """Test that ECC public key validation includes all required security checks:
+ 1. Point is on the curve
+ 2. Point is not zero or infinity
+ 3. Point is not in a small subgroup (not applicable to NIST curves with cofactor=1)
+ """
+
+ def create_ecc_tpm2b_public(self, x: int, y: int, curve_id: int = TPM_ECC_NIST_P256) -> bytes:
+ """Helper to create a TPM2B_PUBLIC structure for ECC key with given coordinates"""
+ # Get coordinate size based on curve
+ curve = _curve_from_curve_id(curve_id)
+ coord_bytes = (curve.key_size + 7) // 8
+
+ # Convert coordinates to bytes
+ x_bytes = x.to_bytes(coord_bytes, "big")
+ y_bytes = y.to_bytes(coord_bytes, "big")
+
+ # Build TPMT_PUBLIC structure
+ # alg_type (TPM_ALG_ECC = 0x0023)
+ tpmt = struct.pack(">H", 0x0023)
+ # name_alg (TPM_ALG_SHA256 = 0x000B)
+ tpmt += struct.pack(">H", 0x000B)
+ # object_attributes (4 bytes)
+ tpmt += struct.pack(">I", 0x00040072)
+ # auth_policy (empty TPM2B)
+ tpmt += struct.pack(">H", 0)
+ # symmetric (TPM_ALG_NULL)
+ tpmt += struct.pack(">H", 0x0010)
+ # scheme (TPM_ALG_NULL)
+ tpmt += struct.pack(">H", 0x0010)
+ # curve_id
+ tpmt += struct.pack(">H", curve_id)
+ # kdf_scheme (TPM_ALG_NULL)
+ tpmt += struct.pack(">H", 0x0010)
+ # x coordinate (TPM2B)
+ tpmt += _pack_in_tpm2b(x_bytes)
+ # y coordinate (TPM2B)
+ tpmt += _pack_in_tpm2b(y_bytes)
+
+ # Wrap in TPM2B_PUBLIC
+ return _pack_in_tpm2b(tpmt)
+
+ def test_point_on_curve_validation(self):
+ """Test that points not on the curve are rejected (Security Check #1)"""
+ # For P-256, the curve equation is: y² = x³ - 3x + b (mod p)
+ # Choose coordinates that don't satisfy this equation
+ x = 1
+ y = 1 # (1, 1) is not on the P-256 curve
+
+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256)
+
+ # The cryptography library should reject this point as not being on the curve
+ with self.assertRaises(ValueError) as cm:
+ pubkey_parms_from_tpm2b_public(tpm2b_public)
+ self.assertIn("invalid ec key", str(cm.exception).lower())
+
+ def test_point_at_infinity_validation(self):
+ """Test that the point at infinity (0, 0) is rejected (Security Check #2)"""
+ # The point at infinity should be rejected
+ x = 0
+ y = 0
+
+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256)
+
+ # The cryptography library should reject the point at infinity
+ with self.assertRaises(ValueError) as cm:
+ pubkey_parms_from_tpm2b_public(tpm2b_public)
+ self.assertIn("invalid ec key", str(cm.exception).lower())
+
+ def test_valid_point_accepted(self):
+ """Test that a valid point on the curve is accepted"""
+ # Generate a valid key and extract its coordinates
+ private_key = ec.generate_private_key(ec.SECP256R1())
+ public_key = private_key.public_key()
+ numbers = public_key.public_numbers()
+
+ # Create TPM2B_PUBLIC with valid coordinates
+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, TPM_ECC_NIST_P256)
+
+ # Should parse successfully
+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public)
+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey)
+
+ # Verify the coordinates match
+ assert isinstance(parsed_key, ec.EllipticCurvePublicKey) # Type narrowing for pyright
+ parsed_numbers = parsed_key.public_numbers()
+ self.assertEqual(parsed_numbers.x, numbers.x)
+ self.assertEqual(parsed_numbers.y, numbers.y)
+
+ def test_small_subgroup_not_applicable_to_nist_curves(self):
+ """Test documenting that small subgroup checks are not needed for NIST curves (Security Check #3)
+
+ NIST P-curves (P-192, P-224, P-256, P-384, P-521) all have cofactor h=1,
+ meaning the entire curve has prime order. There are no small subgroups to check.
+
+ Curves with cofactor > 1 (like Curve25519 with h=8) require additional validation
+ to ensure the point is not in a small subgroup, but this is not applicable to
+ the NIST curves used by TPMs.
+ """
+ # This test documents the cofactor=1 property for all supported NIST curves
+ # The cryptography library's point validation is sufficient for these curves
+
+ test_curves = [
+ (TPM_ECC_NIST_P192, ec.SECP192R1()),
+ (TPM_ECC_NIST_P224, ec.SECP224R1()),
+ (TPM_ECC_NIST_P256, ec.SECP256R1()),
+ (TPM_ECC_NIST_P384, ec.SECP384R1()),
+ (TPM_ECC_NIST_P521, ec.SECP521R1()),
+ ]
+
+ for curve_id, curve_obj in test_curves:
+ with self.subTest(curve=curve_obj.name):
+ try:
+ # Generate a valid key for this curve
+ # Note: P-192 may not be supported in newer OpenSSL versions
+ private_key = ec.generate_private_key(curve_obj)
+ except Exception: # pylint: disable=broad-except
+ # Skip this specific curve if not supported by OpenSSL (e.g., P-192)
+ self.skipTest(f"Curve {curve_obj.name} not supported by OpenSSL")
+
+ public_key = private_key.public_key()
+ numbers = public_key.public_numbers()
+
+ # Create TPM2B_PUBLIC and verify it parses successfully
+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, curve_id)
+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public)
+
+ # All NIST curves have cofactor = 1, so no small subgroup attacks possible
+ # The point validation by the cryptography library is sufficient
+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey)
+
+ def test_coordinate_exceeds_field_prime_rejected(self):
+ """Test that coordinates >= field prime are rejected"""
+ # Use a coordinate value that's >= the field prime for P-256
+ p256_prime = ECC_CURVE_PRIMES[TPM_ECC_NIST_P256]
+
+ # x coordinate exceeds the field prime
+ x = p256_prime + 1
+ y = 1
+
+ tpm2b_public = self.create_ecc_tpm2b_public(x, y, TPM_ECC_NIST_P256)
+
+ # Should be rejected during coordinate validation
+ with self.assertRaises(ValueError) as cm:
+ pubkey_parms_from_tpm2b_public(tpm2b_public)
+ # Will fail either at coordinate validation or curve validation
+ self.assertTrue(
+ "coordinate too large" in str(cm.exception).lower() or "invalid ec key" in str(cm.exception).lower()
+ )
+
+ def test_p521_point_validation(self):
+ """Test point validation works correctly for P-521 (non-byte-aligned curve)"""
+ # Generate a valid P-521 key
+ private_key = ec.generate_private_key(ec.SECP521R1())
+ public_key = private_key.public_key()
+ numbers = public_key.public_numbers()
+
+ # Valid point should be accepted
+ tpm2b_public = self.create_ecc_tpm2b_public(numbers.x, numbers.y, TPM_ECC_NIST_P521)
+ parsed_key, _ = pubkey_parms_from_tpm2b_public(tpm2b_public)
+ self.assertIsInstance(parsed_key, ec.EllipticCurvePublicKey)
+
+ # Invalid point should be rejected
+ tpm2b_public_invalid = self.create_ecc_tpm2b_public(1, 1, TPM_ECC_NIST_P521)
+ with self.assertRaises(ValueError) as cm:
+ pubkey_parms_from_tpm2b_public(tpm2b_public_invalid)
+ self.assertIn("invalid ec key", str(cm.exception).lower())
+
+
+if __name__ == "__main__":
+ unittest.main()
--
2.52.0

View File

@ -1,222 +0,0 @@
From a4c32b7a84c93df86284b95e735923beeb18ca94 Mon Sep 17 00:00:00 2001
From: rpm-build <rpm-build>
Date: Wed, 4 Feb 2026 18:58:40 +0100
Subject: [PATCH] tpm: fix ECC P-521 credential activation with consistent
marshaling
The TPM credential activation was failing for P-521 curves due to
inconsistent ECC point marshaling in tpms_ecc_point_marshal().
The function used bit_length() which varies for P-521 coordinates
(520-521 bits), producing different blob sizes and causing TPM
integrity check failures during ActivateCredential operations.
Backported from upstream commit 1db525b7abf62e6d2d9450817477fd0911e083d4
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/tpm/tpm2_objects.py | 11 +--
keylime/tpm/tpm_util.py | 7 +-
test/test_tpm2_objects.py | 130 ++++++++++++++++++++++++++++++++++++
3 files changed, 142 insertions(+), 6 deletions(-)
diff --git a/keylime/tpm/tpm2_objects.py b/keylime/tpm/tpm2_objects.py
index 9170628..d33ebaa 100644
--- a/keylime/tpm/tpm2_objects.py
+++ b/keylime/tpm/tpm2_objects.py
@@ -597,9 +597,12 @@ def unmarshal_tpml_pcr_selection(tpml_pcr_selection: bytes) -> Tuple[Dict[int, i
def tpms_ecc_point_marshal(public_key: EllipticCurvePublicKey) -> bytes:
pn = public_key.public_numbers()
+ curve = public_key.curve
- sz = (pn.x.bit_length() + 7) // 8
- secret = struct.pack(">H", sz) + pn.x.to_bytes(sz, "big")
+ # Use fixed coordinate size based on curve to ensure consistent marshaling
+ # This is critical for P-521 where bit_length() can vary (520-521 bits)
+ # leading to credential activation failures due to inconsistent blob sizes
+ coord_size = (curve.key_size + 7) // 8
- sz = (pn.y.bit_length() + 7) // 8
- return secret + struct.pack(">H", sz) + pn.y.to_bytes(sz, "big")
+ secret = struct.pack(">H", coord_size) + pn.x.to_bytes(coord_size, "big")
+ return secret + struct.pack(">H", coord_size) + pn.y.to_bytes(coord_size, "big")
diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py
index 25c40e0..fbbe557 100644
--- a/keylime/tpm/tpm_util.py
+++ b/keylime/tpm/tpm_util.py
@@ -318,11 +318,14 @@ def crypt_secret_encrypt_ecc(public_key: EllipticCurvePublicKey, hashfunc: hashe
digest_size = hashfunc.digest_size
+ # Use fixed coordinate size for consistent marshaling
+ coord_size = (public_key.curve.key_size + 7) // 8
+
x = my_public_key.public_numbers().x
- party_x = x.to_bytes((x.bit_length() + 7) >> 3, "big")
+ party_x = x.to_bytes(coord_size, "big")
x = public_key.public_numbers().x
- party_y = x.to_bytes((x.bit_length() + 7) >> 3, "big")
+ party_y = x.to_bytes(coord_size, "big")
data = crypt_kdfe(hashfunc, ecc_secret_x, "IDENTITY", party_x, party_y, digest_size << 3)
diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py
index 9cc6b70..aa91f8d 100644
--- a/test/test_tpm2_objects.py
+++ b/test/test_tpm2_objects.py
@@ -1,6 +1,7 @@
import struct
import unittest
+from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from keylime.tpm.tpm2_objects import (
@@ -13,7 +14,9 @@ from keylime.tpm.tpm2_objects import (
_curve_from_curve_id,
_pack_in_tpm2b,
pubkey_parms_from_tpm2b_public,
+ tpms_ecc_point_marshal,
)
+from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc
class TestTpm2Objects(unittest.TestCase):
@@ -413,5 +416,132 @@ class TestEccPublicKeySecurityValidation(unittest.TestCase):
self.assertIn("invalid ec key", str(cm.exception).lower())
+class TestEccMarshaling(unittest.TestCase):
+ """Test ECC point marshaling consistency fixes"""
+
+ def test_p521_marshaling_consistency(self):
+ """Test that P-521 marshaling produces consistent blob sizes regardless of coordinate values"""
+ # Generate multiple P-521 keys to test with different coordinate values
+ keys = []
+ for _ in range(10):
+ private_key = ec.generate_private_key(ec.SECP521R1())
+ keys.append(private_key.public_key())
+
+ # Marshal all keys and check that blob sizes are consistent
+ blob_sizes = []
+ for key in keys:
+ blob = tpms_ecc_point_marshal(key)
+ blob_sizes.append(len(blob))
+
+ # All blobs should be the same size for P-521
+ self.assertEqual(len(set(blob_sizes)), 1, "All P-521 marshaled blobs should have the same size")
+
+ # Expected size: 2 bytes (x size) + 66 bytes (x coord) + 2 bytes (y size) + 66 bytes (y coord) = 136 bytes
+ expected_size = 2 + 66 + 2 + 66
+ self.assertEqual(blob_sizes[0], expected_size, f"P-521 marshaled blob should be {expected_size} bytes")
+
+ def test_marshaling_coordinate_sizes(self):
+ """Test that marshaled coordinates use fixed sizes based on curve key size"""
+ # Test P-521: 521 bits -> (521 + 7) // 8 = 66 bytes per coordinate
+ p521_key = ec.generate_private_key(ec.SECP521R1()).public_key()
+ p521_blob = tpms_ecc_point_marshal(p521_key)
+
+ # Parse the blob to check coordinate sizes
+ x_size = struct.unpack(">H", p521_blob[:2])[0]
+ y_size = struct.unpack(">H", p521_blob[2 + x_size : 2 + x_size + 2])[0]
+
+ self.assertEqual(x_size, 66, "P-521 X coordinate should be 66 bytes")
+ self.assertEqual(y_size, 66, "P-521 Y coordinate should be 66 bytes")
+
+ # Test P-256: 256 bits -> (256 + 7) // 8 = 32 bytes per coordinate
+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key()
+ p256_blob = tpms_ecc_point_marshal(p256_key)
+
+ x_size = struct.unpack(">H", p256_blob[:2])[0]
+ y_size = struct.unpack(">H", p256_blob[2 + x_size : 2 + x_size + 2])[0]
+
+ self.assertEqual(x_size, 32, "P-256 X coordinate should be 32 bytes")
+ self.assertEqual(y_size, 32, "P-256 Y coordinate should be 32 bytes")
+
+ def test_p521_credential_activation_consistency(self):
+ """Test the specific issue: P-521 credential activation with consistent marshaling"""
+ # This test verifies the fix for credential activation failures
+ # Generate two P-521 keys with potentially different bit lengths for coordinates
+ key1 = ec.generate_private_key(ec.SECP521R1()).public_key()
+ key2 = ec.generate_private_key(ec.SECP521R1()).public_key()
+
+ # Marshal both keys
+ blob1 = tpms_ecc_point_marshal(key1)
+ blob2 = tpms_ecc_point_marshal(key2)
+
+ # The critical fix: both blobs should be the same size regardless of coordinate bit lengths
+ self.assertEqual(
+ len(blob1), len(blob2), "P-521 marshaled blobs must be same size regardless of coordinate bit lengths"
+ )
+
+ # Both should use the fixed coordinate size (66 bytes)
+ expected_total_size = 2 + 66 + 2 + 66 # size_x + x + size_y + y
+ self.assertEqual(len(blob1), expected_total_size)
+ self.assertEqual(len(blob2), expected_total_size)
+
+ def test_marshaling_format_correctness(self):
+ """Test that marshaling follows the correct TPM format: size(2) + coord(n) + size(2) + coord(n)"""
+ key = ec.generate_private_key(ec.SECP521R1()).public_key()
+ blob = tpms_ecc_point_marshal(key)
+
+ # Parse the blob structure
+ if len(blob) < 4:
+ self.fail("Marshaled blob too short")
+
+ x_size = struct.unpack(">H", blob[:2])[0]
+ self.assertEqual(x_size, 66, "X coordinate size should be 66 for P-521")
+
+ if len(blob) < 2 + x_size + 2:
+ self.fail("Marshaled blob missing Y coordinate size")
+
+ y_size = struct.unpack(">H", blob[2 + x_size : 2 + x_size + 2])[0]
+ self.assertEqual(y_size, 66, "Y coordinate size should be 66 for P-521")
+
+ # Total size should be: 2 + 66 + 2 + 66 = 136
+ expected_total = 2 + x_size + 2 + y_size
+ self.assertEqual(len(blob), expected_total, "Total marshaled blob size incorrect")
+
+ def test_crypt_secret_encrypt_ecc_consistency(self):
+ """Test that crypt_secret_encrypt_ecc produces consistent results with fixed coordinate sizes"""
+ # Generate a P-521 key to test with
+ public_key = ec.generate_private_key(ec.SECP521R1()).public_key()
+ hashfunc = hashes.SHA256()
+
+ # Call the function multiple times and check consistency
+ results = []
+ for _ in range(5):
+ data, point = crypt_secret_encrypt_ecc(public_key, hashfunc)
+ results.append((data, point))
+
+ # Check that all returned points have consistent marshaling
+ # (the data will be different due to random key generation, but point marshaling should be consistent)
+ point_sizes = [len(point) for _, point in results]
+ self.assertEqual(len(set(point_sizes)), 1, "All marshaled points should have the same size")
+
+ # For P-521, the marshaled point should be 136 bytes (2+66+2+66)
+ expected_point_size = 2 + 66 + 2 + 66
+ self.assertEqual(
+ point_sizes[0], expected_point_size, f"P-521 marshaled point should be {expected_point_size} bytes"
+ )
+
+ # All data results should be different (due to random ephemeral keys)
+ data_results = [data for data, _ in results]
+ self.assertEqual(
+ len(set(data_results)),
+ len(data_results),
+ "All data results should be different due to random ephemeral keys",
+ )
+
+ # All data results should have the same length (SHA256 digest size)
+ data_sizes = [len(data) for data, _ in results]
+ self.assertEqual(len(set(data_sizes)), 1, "All data results should have the same size")
+ self.assertEqual(data_sizes[0], hashfunc.digest_size, "Data size should match hash digest size")
+
+
if __name__ == "__main__":
unittest.main()
--
2.52.0

View File

@ -1,372 +0,0 @@
From ee4192df70384fa6b23f359a287e042103ba4ea9 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Thu, 25 Sep 2025 14:37:10 +0100
Subject: [PATCH 18/18] tpm: fix ECC signature parsing to support
variable-length coordinates
The previous ECC signature validation implementation incorrectly assumed
fixed-length coordinate encoding, causing failures with mathematically
correct variable-length coordinates (especially P-521 curves where
coordinates are typically 65-66 bytes).
This commit fixes the ecdsa_der_from_tpm() function to properly handle
TSS ESAPI signature format where the sig_size field contains only the
r component size, followed by the s component with its own size header.
This enables proper ECC attestation for the supported NIST curves.
Assisted-by: Claude 4 Sonnet
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/tpm/tpm_main.py | 2 +-
keylime/tpm/tpm_util.py | 105 +++++++++++++++++++---
test/test_tpm2_objects.py | 184 +++++++++++++++++++++++++++++++++++++-
3 files changed, 277 insertions(+), 14 deletions(-)
diff --git a/keylime/tpm/tpm_main.py b/keylime/tpm/tpm_main.py
index 6f2e89f..ecbacbe 100644
--- a/keylime/tpm/tpm_main.py
+++ b/keylime/tpm/tpm_main.py
@@ -91,7 +91,7 @@ class Tpm:
if isinstance(iak_pub, EllipticCurvePublicKey):
if sig_alg in [tpm2_objects.TPM_ALG_ECDSA]:
try:
- der_sig = tpm_util.ecdsa_der_from_tpm(iak_sign)
+ der_sig = tpm_util.ecdsa_der_from_tpm(iak_sign, iak_pub)
tpm_util.verify(iak_pub, der_sig, digest, hashfunc)
logger.info("Agent %s AIK verified with IAK", uuid)
return True
diff --git a/keylime/tpm/tpm_util.py b/keylime/tpm/tpm_util.py
index fbbe557..f554f94 100644
--- a/keylime/tpm/tpm_util.py
+++ b/keylime/tpm/tpm_util.py
@@ -59,6 +59,43 @@ logger = keylime_logging.init_logging("tpm_util")
SupportedKeyTypes = Union[RSAPublicKey, EllipticCurvePublicKey]
+# ECC signature parsing constants.
+# Raw signature sizes for different ECC curves (r||s concatenated format).
+# r and s are the two mathematical components of an ECDSA signature.
+ECC_SECP192R1_SIGNATURE_SIZE = 48 # 24 bytes each for r,s.
+ECC_SECP224R1_SIGNATURE_SIZE = 56 # 28 bytes each for r,s.
+ECC_SECP256R1_SIGNATURE_SIZE = 64 # 32 bytes each for r,s.
+ECC_SECP384R1_SIGNATURE_SIZE = 96 # 48 bytes each for r,s.
+ECC_SECP521R1_SIGNATURE_SIZE = 132 # 66 bytes each for r,s.
+
+# TPM2B_ECDSA_SIGNATURE format constants.
+TPM2B_SIZE_FIELD_LENGTH = 2 # 2 bytes for size field.
+TPM2B_MIN_HEADER_SIZE = 4 # Minimum: 2 bytes r_size + 2 bytes s_size.
+
+# DER encoding constants.
+DER_SEQUENCE_TAG = 0x30
+
+# Signature blob header offset (skip alg, hash_alg, sig_size headers).
+SIGNATURE_BLOB_HEADER_SIZE = 6
+
+# ECC curve to signature size mapping for raw r||s format.
+ECC_RAW_SIGNATURE_SIZES = {
+ "secp192r1": ECC_SECP192R1_SIGNATURE_SIZE,
+ "secp224r1": ECC_SECP224R1_SIGNATURE_SIZE,
+ "secp256r1": ECC_SECP256R1_SIGNATURE_SIZE,
+ "secp384r1": ECC_SECP384R1_SIGNATURE_SIZE,
+ "secp521r1": ECC_SECP521R1_SIGNATURE_SIZE,
+}
+
+# ECC curve coordinate size ranges (min, max) for validation.
+ECC_COORDINATE_SIZE_RANGES = {
+ "secp192r1": (1, 24), # 192 bits = 24 bytes max
+ "secp224r1": (1, 28), # 224 bits = 28 bytes max
+ "secp256r1": (1, 32), # 256 bits = 32 bytes max
+ "secp384r1": (1, 48), # 384 bits = 48 bytes max
+ "secp521r1": (1, 66), # 521 bits = 66 bytes max (521 bits, not 512)
+}
+
def verify(
pubkey: SupportedKeyTypes,
@@ -106,17 +143,59 @@ def der_len(encoded_int_len: int) -> bytes:
return bytes((0x80 | len(bin_str),)) + bin_str
-def ecdsa_der_from_tpm(sigblob: bytes) -> bytes:
- _, _, sig_size_r = struct.unpack_from(">HHH", sigblob, 0)
- sig_r = sigblob[6 : 6 + sig_size_r]
- encoded_sig_r = der_int(sig_r)
- sigblob = sigblob[6 + sig_size_r :]
- sig_size_s = struct.unpack_from(">H", sigblob, 0)[0]
- sig_s = sigblob[2 : 2 + sig_size_s]
- encoded_sig_s = der_int(sig_s)
- total_size = len(encoded_sig_r) + len(encoded_sig_s)
- der_sig = bytes.fromhex(f"30{total_size:x}") + encoded_sig_r + encoded_sig_s
- return der_sig
+def ecdsa_der_from_tpm(sigblob: bytes, pubkey: EllipticCurvePublicKey) -> bytes:
+ """Convert ECC signature from TPM format to DER format for cryptographic verification.
+
+ This function handles TSS ESAPI signature format where the signature header's
+ sig_size field contains the size of the r component, followed by the s component
+ with its own size header.
+
+ Parameters
+ ----------
+ sigblob: TPM signature blob containing signature headers and signature data
+ pubkey: ECC public key to determine expected signature size
+
+ Returns
+ -------
+ DER-encoded ECDSA signature suitable for cryptographic library verification
+
+ Raises
+ ------
+ ValueError: If signature format cannot be parsed or is invalid
+ """
+ # Extract signature header information.
+ _sig_alg, _hash_alg, sig_size_r = struct.unpack_from(">HHH", sigblob, 0)
+
+ # Extract the r component (size is in sig_size_r field).
+ sig_r = sigblob[SIGNATURE_BLOB_HEADER_SIZE : SIGNATURE_BLOB_HEADER_SIZE + sig_size_r]
+
+ # The s component follows immediately after r, with its own size header.
+ s_offset = SIGNATURE_BLOB_HEADER_SIZE + sig_size_r
+ if s_offset + 2 <= len(sigblob):
+ sig_size_s = struct.unpack_from(">H", sigblob, s_offset)[0]
+ s_start = s_offset + 2
+ if s_start + sig_size_s <= len(sigblob):
+ sig_s = sigblob[s_start : s_start + sig_size_s]
+
+ # Validate coordinate sizes against curve requirements.
+ curve_name = pubkey.curve.name
+ coordinate_range = ECC_COORDINATE_SIZE_RANGES.get(curve_name)
+ if coordinate_range:
+ min_size, max_size = coordinate_range
+ if not min_size <= len(sig_r) <= max_size:
+ raise ValueError(f"Invalid r coordinate size {len(sig_r)} for curve {curve_name}")
+ if not min_size <= len(sig_s) <= max_size:
+ raise ValueError(f"Invalid s coordinate size {len(sig_s)} for curve {curve_name}")
+
+ # Convert to DER format.
+ encoded_sig_r = der_int(sig_r)
+ encoded_sig_s = der_int(sig_s)
+ total_size = len(encoded_sig_r) + len(encoded_sig_s)
+ der_length = der_len(total_size)
+ der_sig = bytes([DER_SEQUENCE_TAG]) + der_length + encoded_sig_r + encoded_sig_s
+ return der_sig
+
+ raise ValueError("Unable to parse ECC signature from TPM format")
def __get_pcrs_from_blob(pcrblob: bytes) -> Tuple[int, Dict[int, int], List[bytes]]:
@@ -238,7 +317,9 @@ def checkquote(
(sig_size,) = struct.unpack_from(">H", sigblob, 4)
(signature,) = struct.unpack_from(f"{sig_size}s", sigblob, 6)
elif sig_alg in [tpm2_objects.TPM_ALG_ECDSA]:
- signature = ecdsa_der_from_tpm(sigblob)
+ if not isinstance(pubkey, EllipticCurvePublicKey):
+ raise ValueError(f"ECDSA signature algorithm requires EllipticCurvePublicKey, got {type(pubkey)}")
+ signature = ecdsa_der_from_tpm(sigblob, pubkey)
else:
raise ValueError(f"Unsupported quote signature algorithm '{sig_alg:#x}'")
diff --git a/test/test_tpm2_objects.py b/test/test_tpm2_objects.py
index 48d6a43..c0e4c0a 100644
--- a/test/test_tpm2_objects.py
+++ b/test/test_tpm2_objects.py
@@ -16,7 +16,7 @@ from keylime.tpm.tpm2_objects import (
pubkey_parms_from_tpm2b_public,
tpms_ecc_point_marshal,
)
-from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc
+from keylime.tpm.tpm_util import crypt_secret_encrypt_ecc, der_int, der_len, ecdsa_der_from_tpm
class TestTpm2Objects(unittest.TestCase):
@@ -543,5 +543,187 @@ class TestEccMarshaling(unittest.TestCase):
self.assertEqual(data_sizes[0], hashfunc.digest_size, "Data size should match hash digest size")
+class TestEccSignatureParsing(unittest.TestCase):
+ """Test ECC signature parsing improvements for variable-length coordinates"""
+
+ def create_test_signature_blob(self, sig_r: bytes, sig_s: bytes) -> bytes:
+ """Create a test TPM signature blob with given r and s components"""
+ # TPM signature format: sig_alg(2) + hash_alg(2) + sig_size_r(2) + r_data + sig_size_s(2) + s_data
+ sig_alg = 0x0018 # TPM_ALG_ECDSA
+ hash_alg = 0x000B # TPM_ALG_SHA256
+
+ blob = struct.pack(">HHH", sig_alg, hash_alg, len(sig_r))
+ blob += sig_r
+ blob += struct.pack(">H", len(sig_s))
+ blob += sig_s
+
+ return blob
+
+ def test_p521_variable_length_coordinates(self):
+ """Test that P-521 signatures with variable-length coordinates are parsed correctly"""
+ # Generate a P-521 key for testing
+ private_key = ec.generate_private_key(ec.SECP521R1())
+ public_key = private_key.public_key()
+
+ # Test with 65-byte coordinates (leading zero stripped)
+ sig_r_65 = b"\x00" * 1 + b"\x01" * 64 # 65 bytes
+ sig_s_65 = b"\x00" * 1 + b"\x02" * 64 # 65 bytes
+
+ blob_65 = self.create_test_signature_blob(sig_r_65, sig_s_65)
+
+ # Should parse successfully
+ der_sig_65 = ecdsa_der_from_tpm(blob_65, public_key)
+ self.assertIsInstance(der_sig_65, bytes)
+ self.assertTrue(len(der_sig_65) > 0)
+
+ # Test with 66-byte coordinates (full padding)
+ sig_r_66 = b"\x00" * 2 + b"\x01" * 64 # 66 bytes
+ sig_s_66 = b"\x00" * 2 + b"\x02" * 64 # 66 bytes
+
+ blob_66 = self.create_test_signature_blob(sig_r_66, sig_s_66)
+
+ # Should parse successfully
+ der_sig_66 = ecdsa_der_from_tpm(blob_66, public_key)
+ self.assertIsInstance(der_sig_66, bytes)
+ self.assertTrue(len(der_sig_66) > 0)
+
+ def test_coordinate_size_validation(self):
+ """Test that coordinate size validation works for different curves"""
+ # Test P-256 with valid coordinates
+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key()
+
+ # Valid P-256 coordinates (32 bytes each)
+ sig_r_32 = b"\x01" * 32
+ sig_s_32 = b"\x02" * 32
+ blob_p256_valid = self.create_test_signature_blob(sig_r_32, sig_s_32)
+
+ # Should parse successfully
+ der_sig = ecdsa_der_from_tpm(blob_p256_valid, p256_key)
+ self.assertIsInstance(der_sig, bytes)
+
+ # Test P-256 with invalid coordinates (too large)
+ sig_r_invalid = b"\x01" * 50 # Too large for P-256
+ sig_s_invalid = b"\x02" * 50 # Too large for P-256
+ blob_p256_invalid = self.create_test_signature_blob(sig_r_invalid, sig_s_invalid)
+
+ # Should raise ValueError
+ with self.assertRaises(ValueError) as cm:
+ ecdsa_der_from_tpm(blob_p256_invalid, p256_key)
+ self.assertIn("Invalid r coordinate size", str(cm.exception))
+
+ def test_signature_parsing_edge_cases(self):
+ """Test edge cases in signature parsing"""
+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key()
+
+ # Test with truncated blob (missing s component)
+ truncated_blob = struct.pack(">HHH", 0x0018, 0x000B, 32) + b"\x01" * 32
+ # Missing s component
+
+ with self.assertRaises(ValueError) as cm:
+ ecdsa_der_from_tpm(truncated_blob, p256_key)
+ self.assertIn("Unable to parse ECC signature", str(cm.exception))
+
+ # Test with blob too short for s size header
+ short_blob = struct.pack(">HHH", 0x0018, 0x000B, 32) + b"\x01" * 32 + b"\x00" # Only 1 byte for s size
+
+ with self.assertRaises(ValueError) as cm:
+ ecdsa_der_from_tpm(short_blob, p256_key)
+ self.assertIn("Unable to parse ECC signature", str(cm.exception))
+
+ def test_der_encoding_correctness(self):
+ """Test that DER encoding produces correctly formatted output"""
+ p256_key = ec.generate_private_key(ec.SECP256R1()).public_key()
+
+ # Create test coordinates
+ sig_r = b"\x01" * 32
+ sig_s = b"\x02" * 32
+ blob = self.create_test_signature_blob(sig_r, sig_s)
+
+ der_sig = ecdsa_der_from_tpm(blob, p256_key)
+
+ # DER signature should start with SEQUENCE tag (0x30)
+ self.assertEqual(der_sig[0], 0x30, "DER signature should start with SEQUENCE tag")
+
+ # Should be parseable as DER format
+ # The structure should be: 0x30 + length + INTEGER(r) + INTEGER(s)
+ self.assertTrue(len(der_sig) >= 6, "DER signature should have minimum length")
+
+ def test_multiple_curve_support(self):
+ """Test that signature parsing works for multiple curve types"""
+ test_cases = [
+ (ec.SECP256R1(), 32),
+ (ec.SECP384R1(), 48),
+ (ec.SECP521R1(), 66),
+ ]
+
+ for curve, coord_size in test_cases:
+ with self.subTest(curve=curve.name):
+ private_key = ec.generate_private_key(curve)
+ public_key = private_key.public_key()
+
+ # Create test signature with appropriate coordinate size
+ sig_r = b"\x01" * coord_size
+ sig_s = b"\x02" * coord_size
+ blob = self.create_test_signature_blob(sig_r, sig_s)
+
+ # Should parse successfully
+ der_sig = ecdsa_der_from_tpm(blob, public_key)
+ self.assertIsInstance(der_sig, bytes)
+ self.assertTrue(len(der_sig) > 0)
+ self.assertEqual(der_sig[0], 0x30) # DER SEQUENCE tag
+
+ def test_der_int_encoding(self):
+ """Test DER integer encoding helper function"""
+ # Test positive number that doesn't need padding
+ test_bytes = b"\x7F" # 127, no padding needed
+ der_encoded = der_int(test_bytes)
+ expected = b"\x02\x01\x7F" # INTEGER tag + length + value
+ self.assertEqual(der_encoded, expected)
+
+ # Test positive number that needs zero padding (high bit set)
+ test_bytes = b"\xFF" # 255, needs zero padding
+ der_encoded = der_int(test_bytes)
+ expected = b"\x02\x02\x00\xFF" # INTEGER tag + length + zero padding + value
+ self.assertEqual(der_encoded, expected)
+
+ def test_der_len_encoding(self):
+ """Test DER length encoding helper function"""
+ # Test short form (< 128)
+ short_len = der_len(50)
+ self.assertEqual(short_len, b"\x32") # 50 in hex
+
+ # Test long form (>= 128)
+ long_len = der_len(300) # 0x012C
+ expected = b"\x82\x01\x2C" # Long form: 0x80 | 2 bytes, then 0x012C
+ self.assertEqual(long_len, expected)
+
+ def test_signature_format_validation_comprehensive(self):
+ """Comprehensive test of signature format validation"""
+ p521_key = ec.generate_private_key(ec.SECP521R1()).public_key()
+
+ # Test minimum valid coordinate sizes for P-521
+ valid_sizes = [65, 66]
+ for size in valid_sizes:
+ sig_r = b"\x01" * size
+ sig_s = b"\x02" * size
+ blob = self.create_test_signature_blob(sig_r, sig_s)
+
+ # Should not raise exception
+ der_sig = ecdsa_der_from_tpm(blob, p521_key)
+ self.assertIsInstance(der_sig, bytes)
+
+ # Test invalid coordinate sizes for P-521 (outside the 1-66 range)
+ invalid_sizes = [0, 67, 100] # 0 is too small, 67+ is too large
+ for size in invalid_sizes:
+ with self.subTest(size=size):
+ sig_r = b"\x01" * size if size > 0 else b""
+ sig_s = b"\x02" * size if size > 0 else b""
+ blob = self.create_test_signature_blob(sig_r, sig_s)
+
+ with self.assertRaises(ValueError) as cm:
+ ecdsa_der_from_tpm(blob, p521_key)
+ self.assertIn("coordinate size", str(cm.exception))
+
+
if __name__ == "__main__":
unittest.main()
--
2.47.3

View File

@ -1,20 +0,0 @@
diff --git a/keylime/web/base/server.py b/keylime/web/base/server.py
index 1d9a9c2..859b23a 100644
--- a/keylime/web/base/server.py
+++ b/keylime/web/base/server.py
@@ -2,7 +2,6 @@ import asyncio
import multiprocessing
from abc import ABC, abstractmethod
from functools import wraps
-from ssl import CERT_OPTIONAL
from typing import TYPE_CHECKING, Any, Callable, Optional
import tornado
@@ -252,7 +251,6 @@ class Server(ABC):
self._https_port = config.getint(component, "tls_port", fallback=0)
self._max_upload_size = config.getint(component, "max_upload_size", fallback=104857600)
self._ssl_ctx = web_util.init_mtls(component)
- self._ssl_ctx.verify_mode = CERT_OPTIONAL
def _get(self, pattern: str, controller: type["Controller"], action: str, allow_insecure: bool = False) -> None:
"""Creates a new route to handle incoming GET requests issued for paths which match the given

View File

@ -1,950 +0,0 @@
From b5f5387cbe19df3a937359263fa7c705c47a1855 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Fri, 17 Apr 2026 09:27:23 +0100
Subject: [PATCH 23/23] Backport tenant version negotiation mechanism
Original PRs:
- https://github.com/keylime/keylime/pull/1838
- https://github.com/keylime/keylime/pull/1845
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/api_version.py | 41 ++++-
keylime/cloud_verifier_tornado.py | 93 ++++++++--
keylime/registrar_client.py | 63 ++++++-
keylime/tenant.py | 280 +++++++++++++++++++++++++-----
test/test_api_version.py | 65 +++++++
5 files changed, 470 insertions(+), 72 deletions(-)
diff --git a/keylime/api_version.py b/keylime/api_version.py
index 1936260..de383c5 100644
--- a/keylime/api_version.py
+++ b/keylime/api_version.py
@@ -1,6 +1,6 @@
import re
from logging import Logger
-from typing import Dict, List, Union
+from typing import Dict, List, Optional, Union
from packaging import version
@@ -34,6 +34,45 @@ def all_versions() -> List[str]:
return VERSIONS.copy()
+def negotiate_version(
+ remote_versions: Union[str, List[str]],
+ local_versions: Optional[List[str]] = None,
+ raise_on_error: bool = False,
+) -> Optional[str]:
+ """
+ Negotiate highest API version supported by both local and remote components.
+
+ Args:
+ remote_versions: Single version string or list from remote component
+ local_versions: Versions supported locally (default: all_versions())
+ raise_on_error: If True, raise ValueError when no compatible version found.
+ If False (default), return None.
+
+ Returns:
+ Highest mutually supported version, or None if incompatible and raise_on_error=False
+
+ Raises:
+ ValueError: If no compatible version found and raise_on_error=True
+ """
+ if local_versions is None:
+ local_versions = all_versions()
+
+ if isinstance(remote_versions, str):
+ remote_versions = [remote_versions]
+
+ common = set(remote_versions) & set(local_versions)
+
+ if not common:
+ if raise_on_error:
+ raise ValueError(
+ f"No compatible API version found. "
+ f"Local supports: {local_versions}, Remote supports: {remote_versions}"
+ )
+ return None
+
+ return max(common, key=version.parse)
+
+
def is_supported_version(version_type: VersionType) -> bool:
try:
v_obj = version.parse(str(version_type))
diff --git a/keylime/cloud_verifier_tornado.py b/keylime/cloud_verifier_tornado.py
index 67ba8af..81db22f 100644
--- a/keylime/cloud_verifier_tornado.py
+++ b/keylime/cloud_verifier_tornado.py
@@ -518,6 +518,22 @@ class AgentsHandler(BaseHandler):
logger.warning("POST returning 400 response. Expected non zero content length.")
else:
json_body = json.loads(self.request.body)
+
+ # Validate supported_version from tenant
+ supported_version = json_body.get("supported_version")
+ if supported_version:
+ if not keylime_api_version.is_supported_version(supported_version):
+ logger.warning(
+ "Agent %s requested API version %s which is not supported by verifier. "
+ "Verifier supports: %s. Will attempt version negotiation on first contact.",
+ agent_id,
+ supported_version,
+ keylime_api_version.all_versions(),
+ )
+ supported_version = keylime_api_version.current_version()
+ else:
+ supported_version = keylime_api_version.current_version()
+
agent_data = {
"v": json_body.get("v", None),
"ip": json_body["cloudagent_ip"],
@@ -531,7 +547,7 @@ class AgentsHandler(BaseHandler):
"accept_tpm_hash_algs": json_body["accept_tpm_hash_algs"],
"accept_tpm_encryption_algs": json_body["accept_tpm_encryption_algs"],
"accept_tpm_signing_algs": json_body["accept_tpm_signing_algs"],
- "supported_version": json_body["supported_version"],
+ "supported_version": supported_version,
"ak_tpm": json_body["ak_tpm"],
"mtls_cert": json_body.get("mtls_cert", None),
"hash_alg": "",
@@ -1422,7 +1438,11 @@ class MbpolicyHandler(BaseHandler):
async def update_agent_api_version(agent: Dict[str, Any], timeout: float = 60.0) -> Union[Dict[str, Any], None]:
+ """
+ Query agent's /version endpoint and negotiate compatible API version.
+ """
agent_id = agent["agent_id"]
+ old_version = agent.get("supported_version")
logger.info("Agent %s API version bump detected, trying to update stored API version", agent_id)
kwargs = {}
@@ -1447,31 +1467,67 @@ async def update_agent_api_version(agent: Dict[str, Any], timeout: float = 60.0)
try:
json_response = json.loads(response.body)
- new_version = json_response["results"]["supported_version"]
- old_version = agent["supported_version"]
- # Only update the API version to use if it is supported by the verifier
- if new_version in keylime_api_version.all_versions():
- new_version_tuple = str_to_version(new_version)
- old_version_tuple = str_to_version(old_version)
+ # Try new format first (list of versions)
+ agent_versions = json_response["results"].get("supported_versions")
- assert new_version_tuple, f"Agent {agent_id} version {new_version} is invalid"
- assert old_version_tuple, f"Agent {agent_id} version {old_version} is invalid"
+ # Fall back to old format (single version)
+ if agent_versions is None:
+ agent_versions = json_response["results"].get("supported_version")
- # Check that the new version is greater than current version
- if new_version_tuple <= old_version_tuple:
- logger.warning(
- "Agent %s API version %s is lower or equal to previous version %s",
+ if agent_versions:
+ # Negotiate compatible version
+ negotiated = keylime_api_version.negotiate_version(agent_versions)
+
+ if negotiated is None:
+ logger.error(
+ "No compatible API version between verifier and agent %s. "
+ "Agent supports: %s, Verifier supports: %s",
agent_id,
- new_version,
- old_version,
+ agent_versions,
+ keylime_api_version.all_versions(),
)
return None
- logger.info("Agent %s new API version %s is supported", agent_id, new_version)
+ # Check if version actually changed
+ if negotiated == old_version:
+ logger.debug("Agent %s already using negotiated version %s", agent_id, negotiated)
+ return agent
+
+ # Validate negotiated version
+ if not keylime_api_version.validate_version(negotiated):
+ logger.error("Negotiated version %s for agent %s is invalid", negotiated, agent_id)
+ return None
+
+ # Check that the negotiated version is greater than current version (prevent downgrade)
+ negotiated_tuple = str_to_version(negotiated)
+ if not negotiated_tuple:
+ logger.error("Agent %s negotiated version %s is invalid", agent_id, negotiated)
+ return None
+
+ # Only check for downgrade if there was a previous version and successful attestation.
+ # If attestation_count == 0, the stored version might be a fallback guess from the tenant,
+ # not a version the agent actually supported, so we allow the "downgrade".
+ attestation_count = agent.get("attestation_count", 0)
+ if old_version is not None and attestation_count > 0:
+ old_version_tuple = str_to_version(old_version)
+ if not old_version_tuple:
+ logger.error("Agent %s stored version %s is invalid", agent_id, old_version)
+ return None
+
+ if negotiated_tuple <= old_version_tuple:
+ logger.warning(
+ "Agent %s API version %s is lower or equal to previous version %s",
+ agent_id,
+ negotiated,
+ old_version,
+ )
+ return None
+
+ logger.info("Agent %s new API version %s is supported", agent_id, negotiated)
with session_context() as session:
- agent["supported_version"] = new_version
+ agent["supported_version"] = negotiated
# Remove keys that should not go to the DB
agent_db = dict(agent)
@@ -1481,8 +1537,9 @@ async def update_agent_api_version(agent: Dict[str, Any], timeout: float = 60.0)
session.query(VerfierMain).filter_by(agent_id=agent_id).update(agent_db) # pyright: ignore
# session.commit() is automatically called by context manager
+
else:
- logger.warning("Agent %s new API version %s is not supported", agent_id, new_version)
+ logger.warning("Agent %s did not provide version information", agent_id)
return None
except SQLAlchemyError as e:
diff --git a/keylime/registrar_client.py b/keylime/registrar_client.py
index 97fbc2a..e32707b 100644
--- a/keylime/registrar_client.py
+++ b/keylime/registrar_client.py
@@ -25,7 +25,7 @@ class RegistrarData(TypedDict):
logger = keylime_logging.init_logging("registrar_client")
-api_version = keylime_api_version.current_version()
+API_VERSION = keylime_api_version.current_version()
MANDATORY_FIELDS = ["aik_tpm", "regcount", "ek_tpm", "ip", "port"]
@@ -38,24 +38,61 @@ def check_mandatory_fields(results: Dict[str, Any]) -> bool:
return True
+def getVersions(
+ registrar_ip: str,
+ registrar_port: str,
+ tls_context: Optional[ssl.SSLContext],
+) -> Optional[Dict[str, Any]]:
+ """
+ Fetch supported API versions from the registrar's /version endpoint.
+
+ :returns: JSON structure containing version info, or None on failure
+ """
+ try:
+ client = RequestsClient(f"{bracketize_ipv6(registrar_ip)}:{registrar_port}", True, tls_context=tls_context)
+ response = client.get("/version")
+
+ if response.status_code != 200:
+ logger.warning("Failed to get versions from registrar: %s", response.status_code)
+ return None
+
+ response_body: Dict[str, Any] = response.json()
+ if "results" not in response_body:
+ logger.warning("Unexpected response format from registrar /version endpoint")
+ return None
+
+ results: Dict[str, Any] = response_body["results"]
+ return results
+
+ except Exception as e:
+ logger.warning("Error fetching versions from registrar: %s", e)
+ return None
+
+
def getData(
- registrar_ip: str, registrar_port: str, agent_id: str, tls_context: Optional[ssl.SSLContext]
+ registrar_ip: str,
+ registrar_port: str,
+ agent_id: str,
+ tls_context: Optional[ssl.SSLContext],
+ api_version: Optional[str] = None,
) -> Optional[RegistrarData]:
"""
Get the agent data from the registrar.
This is called by the tenant code
+ :param api_version: Optional API version to use. If None, uses current version.
:returns: JSON structure containing the agent data
"""
# make absolutely sure you don't ask for data that contains AIK keys unauthenticated
if not tls_context:
raise Exception("It is unsafe to use this interface to query AIKs without server-authenticated TLS.")
+ version = api_version or API_VERSION
response = None
try:
client = RequestsClient(f"{bracketize_ipv6(registrar_ip)}:{registrar_port}", True, tls_context=tls_context)
- response = client.get(f"/v{api_version}/agents/{agent_id}")
+ response = client.get(f"/v{version}/agents/{agent_id}")
response_body = response.json()
if response.status_code == 404:
@@ -106,18 +143,23 @@ def getData(
def doRegistrarDelete(
- registrar_ip: str, registrar_port: str, agent_id: str, tls_context: Optional[ssl.SSLContext]
+ registrar_ip: str,
+ registrar_port: str,
+ agent_id: str,
+ tls_context: Optional[ssl.SSLContext],
+ api_version: Optional[str] = None,
) -> Dict[str, Any]:
"""
Delete the given agent from the registrar.
This is called by the tenant code
+ :param api_version: Optional API version to use. If None, uses current version.
:returns: The request response body
"""
-
+ version = api_version or API_VERSION
client = RequestsClient(f"{bracketize_ipv6(registrar_ip)}:{registrar_port}", True, tls_context=tls_context)
- response = client.delete(f"/v{api_version}/agents/{agent_id}")
+ response = client.delete(f"/v{version}/agents/{agent_id}")
response_body: Dict[str, Any] = response.json()
if response.status_code == 200:
@@ -130,17 +172,22 @@ def doRegistrarDelete(
def doRegistrarList(
- registrar_ip: str, registrar_port: str, tls_context: Optional[ssl.SSLContext]
+ registrar_ip: str,
+ registrar_port: str,
+ tls_context: Optional[ssl.SSLContext],
+ api_version: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""
Get the list of registered agents from the registrar.
This is called by the tenant code
+ :param api_version: Optional API version to use. If None, uses current version.
:returns: The request response body
"""
+ version = api_version or API_VERSION
client = RequestsClient(f"{bracketize_ipv6(registrar_ip)}:{registrar_port}", True, tls_context=tls_context)
- response = client.get(f"/v{api_version}/agents/")
+ response = client.get(f"/v{version}/agents/")
response_body: Dict[str, Any] = response.json()
if response.status_code != 200:
diff --git a/keylime/tenant.py b/keylime/tenant.py
index feb46dc..08b8a51 100644
--- a/keylime/tenant.py
+++ b/keylime/tenant.py
@@ -54,6 +54,7 @@ class Tenant:
registrar_data: Optional[registrar_client.RegistrarData] = None
api_version: Optional[str] = None
+ push_model: bool = False
# uuid_service_generate_locally = None
agent_uuid: str = ""
@@ -76,7 +77,10 @@ class Tenant:
mb_policy = None
mb_policy_name: str = ""
- supported_version: Optional[str] = None
+ supported_version: Optional[str] = None # Deprecated: use agent_api_version
+ agent_api_version: Optional[str] = None
+ verifier_api_version: Optional[str] = None
+ registrar_api_version: Optional[str] = None
client_cert = None
client_key = None
@@ -177,6 +181,103 @@ class Tenant:
if self.registrar_ip:
self.registrar_fid_str = f"{self.registrar_fid_str} ({self.registrar_ip}:{self.registrar_port})"
+ def _fetch_server_versions(
+ self, base_url: str, tls_context: Optional[ssl.SSLContext], server_name: str
+ ) -> Optional[List[str]]:
+ """Fetch supported versions from a server's /version endpoint."""
+ try:
+ client = RequestsClient(base_url, True, tls_context=tls_context)
+ response = client.get("/version", timeout=self.request_timeout)
+
+ if response.status_code == 410:
+ logger.debug("%s returned 410 (push mode), falling back to current version", server_name)
+ return None
+
+ if response.status_code != 200:
+ logger.warning("Failed to get versions from %s: %s", server_name, response.status_code)
+ return None
+
+ response_body: Dict[str, Any] = response.json()
+ if "results" not in response_body or "supported_versions" not in response_body["results"]:
+ logger.warning("Unexpected response format from %s /version endpoint", server_name)
+ return None
+
+ versions: List[str] = response_body["results"]["supported_versions"]
+ return versions
+
+ except Exception as e:
+ logger.warning("Error fetching versions from %s: %s", server_name, e)
+ return None
+
+ def _negotiate_server_version(self, server_versions: Optional[List[str]], server_name: str) -> str:
+ """Negotiate API version with a server.
+
+ In pull mode, API version 3.0+ is excluded since it is for push attestation only.
+ In push mode, all versions including 3.0 are available.
+ """
+ if self.push_model:
+ local_versions = None
+ else:
+ local_versions = [v for v in keylime_api_version.all_versions() if keylime_api_version.major(v) < 3]
+
+ if server_versions is None:
+ if self.api_version is None:
+ raise UserError("Tenant API version is not set")
+ logger.debug("Cannot negotiate version with %s, using current version %s", server_name, self.api_version)
+ return self.api_version
+
+ try:
+ negotiated = keylime_api_version.negotiate_version(server_versions, local_versions, raise_on_error=True)
+ if negotiated is None:
+ raise UserError(f"Failed to negotiate API version with {server_name}")
+ logger.debug("Negotiated API version %s with %s", negotiated, server_name)
+ return negotiated
+ except ValueError:
+ supported = local_versions if local_versions is not None else keylime_api_version.all_versions()
+ raise UserError(
+ f"No compatible API version with {server_name}. "
+ f"Tenant supports: {supported}, "
+ f"Server supports: {server_versions}"
+ ) from None
+
+ def negotiate_verifier_version(self) -> None:
+ """Fetch and negotiate API version with the verifier."""
+ server_versions = self._fetch_server_versions(self.verifier_base_url, self.tls_context, "verifier")
+ self.verifier_api_version = self._negotiate_server_version(server_versions, "verifier")
+ logger.info("Using API version %s for verifier communication", self.verifier_api_version)
+
+ def negotiate_registrar_version(self) -> None:
+ """Fetch and negotiate API version with the registrar."""
+ if not self.registrar_ip or not self.registrar_port:
+ raise UserError("registrar_ip and registrar_port must be configured for version negotiation")
+
+ base_url = f"{bracketize_ipv6(self.registrar_ip)}:{self.registrar_port}"
+ server_versions = self._fetch_server_versions(base_url, self.tls_context, "registrar")
+ self.registrar_api_version = self._negotiate_server_version(server_versions, "registrar")
+ logger.info("Using API version %s for registrar communication", self.registrar_api_version)
+
+ def ensure_verifier_version(self) -> str:
+ """Ensure verifier API version is negotiated and return it.
+
+ Performs lazy negotiation: only negotiates if not already done.
+ """
+ if self.verifier_api_version is None:
+ self.negotiate_verifier_version()
+ if self.verifier_api_version is None:
+ raise UserError("Failed to negotiate API version with verifier")
+ return self.verifier_api_version
+
+ def ensure_registrar_version(self) -> str:
+ """Ensure registrar API version is negotiated and return it.
+
+ Performs lazy negotiation: only negotiates if not already done.
+ """
+ if self.registrar_api_version is None:
+ self.negotiate_registrar_version()
+ if self.registrar_api_version is None:
+ raise UserError("Failed to negotiate API version with registrar")
+ return self.registrar_api_version
+
def init_add(self, args: Dict[str, Any]) -> None:
"""Set up required values. Command line options can overwrite these config values
@@ -192,7 +293,11 @@ class Tenant:
if not self.registrar_ip or not self.registrar_port:
raise UserError("registrar_ip and registrar_port have both to be set in the configuration")
self.registrar_data = registrar_client.getData(
- self.registrar_ip, self.registrar_port, self.agent_uuid, self.tls_context
+ self.registrar_ip,
+ self.registrar_port,
+ self.agent_uuid,
+ self.tls_context,
+ api_version=self.ensure_registrar_version(),
)
if self.registrar_data is None:
@@ -218,13 +323,15 @@ class Tenant:
self.set_full_id_str()
- # Auto-detection for API version
- self.supported_version = args["supported_version"]
+ # Auto-detection for agent API version
+ self.agent_api_version = args.get("agent_api_version")
+ self.supported_version = self.agent_api_version
# Default to 1.0 if the agent did not send a mTLS certificate
- if self.registrar_data.get("mtls_cert", None) is None and self.supported_version is None:
- self.supported_version = "1.0"
- else:
- # Try to connect to the agent to get supported version
+ if self.registrar_data.get("mtls_cert", None) is None and self.agent_api_version is None:
+ self.agent_api_version = "1.0"
+ # Try to contact agent to get API version if in pull-mode and we have contact info
+ # Skip in push-mode since agent will send attestations to verifier directly
+ elif not self.push_model and self.agent_ip is not None and self.agent_port is not None:
if self.registrar_data["mtls_cert"] == "disabled":
self.enable_agent_mtls = False
logger.warning(
@@ -269,18 +376,66 @@ class Tenant:
if res and res.status_code == 200:
try:
data = res.json()
- api_version = data["results"]["supported_version"]
- if keylime_api_version.validate_version(api_version) and self.supported_version is None:
- self.supported_version = api_version
+
+ # Try new format first (list of versions)
+ agent_versions = data["results"].get("supported_versions")
+
+ # Fall back to old format (single version) for backward compatibility
+ if agent_versions is None:
+ agent_versions = data["results"].get("supported_version")
+
+ if agent_versions:
+ negotiated = keylime_api_version.negotiate_version(agent_versions)
+
+ if negotiated is None:
+ logger.error(
+ "No compatible API version between tenant and agent %s. "
+ "Agent supports: %s, Tenant supports: %s",
+ self.agent_uuid,
+ agent_versions,
+ keylime_api_version.all_versions(),
+ )
+ raise UserError(
+ f"Agent {self.agent_uuid} has no compatible API version. "
+ f"Agent supports: {agent_versions}, "
+ f"Tenant supports: {keylime_api_version.all_versions()}"
+ )
+
+ if keylime_api_version.validate_version(negotiated) and self.agent_api_version is None:
+ self.agent_api_version = negotiated
+ logger.info(
+ "Negotiated API version %s with agent %s",
+ negotiated,
+ self.agent_uuid,
+ )
+ elif not keylime_api_version.validate_version(negotiated):
+ logger.warning(
+ "Negotiated version %s is invalid, using current: %s",
+ negotiated,
+ keylime_api_version.current_version(),
+ )
+ if self.agent_api_version is None:
+ self.agent_api_version = keylime_api_version.current_version()
else:
- logger.warning("API version provided by the agent is not valid")
- except (TypeError, KeyError):
- pass
+ logger.warning("Agent did not provide version information")
- if self.supported_version is None:
- api_version = keylime_api_version.current_version()
- logger.warning("Could not detect supported API version. Defaulting to %s", api_version)
- self.supported_version = api_version
+ except (TypeError, KeyError) as e:
+ logger.warning("Failed to parse agent version response: %s", e)
+
+ if self.agent_api_version is None:
+ fallback_version = keylime_api_version.current_version()
+ logger.warning(
+ "Could not detect supported API version. Defaulting to %s (push_model=%s, agent_ip=%s, agent_port=%s)",
+ fallback_version,
+ self.push_model,
+ self.agent_ip,
+ self.agent_port,
+ )
+ self.agent_api_version = fallback_version
+
+ # Keep supported_version in sync for backward compatibility
+ self.supported_version = self.agent_api_version
+ logger.info("Using API version %s for agent communication", self.agent_api_version)
# Now set the cv_agent_ip
if "cv_agent_ip" in args and args["cv_agent_ip"] is not None:
@@ -516,7 +671,7 @@ class Tenant:
quote,
self.registrar_data["aik_tpm"],
hash_alg=hash_alg,
- compressed=(self.supported_version == "1.0"),
+ compressed=(self.agent_api_version == "1.0"),
)
if failure:
if self.registrar_data["regcount"] > 1:
@@ -601,12 +756,14 @@ class Tenant:
"accept_tpm_signing_algs": self.accept_tpm_signing_algs,
"ak_tpm": self.registrar_data["aik_tpm"],
"mtls_cert": self.registrar_data.get("mtls_cert", None),
- "supported_version": self.supported_version,
+ "supported_version": self.agent_api_version,
}
json_message = json.dumps(data)
do_cv = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = do_cv.post(
- (f"/v{self.api_version}/agents/{self.agent_uuid}"), data=json_message, timeout=self.request_timeout
+ (f"/v{self.ensure_verifier_version()}/agents/{self.agent_uuid}"),
+ data=json_message,
+ timeout=self.request_timeout,
)
if response.status_code == 503:
@@ -671,7 +828,9 @@ class Tenant:
do_cvstatus = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = do_cvstatus.get((f"/v{self.api_version}/agents/{self.agent_uuid}"), timeout=self.request_timeout)
+ response = do_cvstatus.get(
+ (f"/v{self.ensure_verifier_version()}/agents/{self.agent_uuid}"), timeout=self.request_timeout
+ )
response_json = Tenant._jsonify_response(response, print_response=False, raise_except=True)
@@ -728,7 +887,9 @@ class Tenant:
self.set_full_id_str()
- response = do_cvstatus.get(f"/v{self.api_version}/agents/?verifier={verifier_id}", timeout=self.request_timeout)
+ response = do_cvstatus.get(
+ f"/v{self.ensure_verifier_version()}/agents/?verifier={verifier_id}", timeout=self.request_timeout
+ )
response_json = Tenant._jsonify_response(response, print_response=False, raise_except=True)
@@ -776,7 +937,8 @@ class Tenant:
self.set_full_id_str()
response = do_cvstatus.get(
- f"/v{self.api_version}/agents/?bulk={True}&verifier={verifier_id}", timeout=self.request_timeout
+ f"/v{self.ensure_verifier_version()}/agents/?bulk={True}&verifier={verifier_id}",
+ timeout=self.request_timeout,
)
response_json = Tenant._jsonify_response(response, print_response=False)
@@ -823,7 +985,9 @@ class Tenant:
self.set_full_id_str()
do_cvdelete = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = do_cvdelete.delete(f"/v{self.api_version}/agents/{self.agent_uuid}", timeout=self.request_timeout)
+ response = do_cvdelete.delete(
+ f"/v{self.ensure_verifier_version()}/agents/{self.agent_uuid}", timeout=self.request_timeout
+ )
response_json = Tenant._jsonify_response(response, print_response=False, raise_except=True)
@@ -894,7 +1058,13 @@ class Tenant:
self.set_full_id_str()
- agent_info = registrar_client.getData(self.registrar_ip, self.registrar_port, self.agent_uuid, self.tls_context)
+ agent_info = registrar_client.getData(
+ self.registrar_ip,
+ self.registrar_port,
+ self.agent_uuid,
+ self.tls_context,
+ api_version=self.ensure_registrar_version(),
+ )
if not agent_info:
logger.info(
@@ -945,7 +1115,10 @@ class Tenant:
self.set_full_id_str()
response = registrar_client.doRegistrarList(
- self.registrar_ip, self.registrar_port, tls_context=self.tls_context
+ self.registrar_ip,
+ self.registrar_port,
+ tls_context=self.tls_context,
+ api_version=self.ensure_registrar_version(),
)
# Marked for deletion (need to modify the code on CI tests)
@@ -964,7 +1137,11 @@ class Tenant:
raise UserError("registrar_ip and registrar_port have both to be set in the configuration")
response = registrar_client.doRegistrarDelete(
- self.registrar_ip, self.registrar_port, self.agent_uuid, tls_context=self.tls_context
+ self.registrar_ip,
+ self.registrar_port,
+ self.agent_uuid,
+ tls_context=self.tls_context,
+ api_version=self.ensure_registrar_version(),
)
return response
@@ -1014,7 +1191,7 @@ class Tenant:
do_cvreactivate = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = do_cvreactivate.put(
- f"/v{self.api_version}/agents/{self.agent_uuid}/reactivate",
+ f"/v{self.ensure_verifier_version()}/agents/{self.agent_uuid}/reactivate",
data=b"",
timeout=self.request_timeout,
)
@@ -1039,7 +1216,7 @@ class Tenant:
def do_cvstop(self) -> None:
"""Stop declared active agent"""
- params = f"/v{self.api_version}/agents/{self.agent_uuid}/stop"
+ params = f"/v{self.ensure_verifier_version()}/agents/{self.agent_uuid}/stop"
do_cvstop = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = do_cvstop.put(params, data=b"", timeout=self.request_timeout)
@@ -1073,7 +1250,7 @@ class Tenant:
# Note: We need a specific retry handler (perhaps in common), no point having localised unless we have too.
while True:
try:
- params = f"/v{self.supported_version}/quotes/identity?nonce=%s" % (self.nonce)
+ params = f"/v{self.agent_api_version}/quotes/identity?nonce=%s" % (self.nonce)
cloudagent_base_url = f"{bracketize_ipv6(self.agent_ip)}:{self.agent_port}"
if self.enable_agent_mtls and self.registrar_data and self.registrar_data["mtls_cert"]:
@@ -1165,7 +1342,7 @@ class Tenant:
data["payload"] = self.payload.decode("utf-8")
# post encrypted U back to CloudAgent
- params = f"/v{self.supported_version}/keys/ukey"
+ params = f"/v{self.agent_api_version}/keys/ukey"
cloudagent_base_url = f"{bracketize_ipv6(self.agent_ip)}:{self.agent_port}"
if self.enable_agent_mtls and self.registrar_data and self.registrar_data["mtls_cert"]:
@@ -1209,14 +1386,14 @@ class Tenant:
tls_context=self.agent_tls_context,
) as do_verify:
response = do_verify.get(
- f"/v{self.supported_version}/keys/verify?challenge={challenge}",
+ f"/v{self.agent_api_version}/keys/verify?challenge={challenge}",
timeout=self.request_timeout,
)
else:
logger.warning("Connecting to %s without using mTLS!", self.agent_fid_str)
do_verify = RequestsClient(cloudagent_base_url, tls_enabled=False)
response = do_verify.get(
- f"/v{self.supported_version}/keys/verify?challenge={challenge}", timeout=self.request_timeout
+ f"/v{self.agent_api_version}/keys/verify?challenge={challenge}", timeout=self.request_timeout
)
response_json = Tenant._jsonify_response(response, print_response=False, raise_except=True)
@@ -1320,7 +1497,7 @@ class Tenant:
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = cv_client.post(
- f"/v{self.api_version}/allowlists/{self.runtime_policy_name}", data=body, timeout=self.request_timeout
+ f"/v{self.ensure_verifier_version()}/allowlists/{self.runtime_policy_name}", data=body, timeout=self.request_timeout
)
response_json = Tenant._jsonify_response(response)
@@ -1332,7 +1509,7 @@ class Tenant:
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = cv_client.put(
- f"/v{self.api_version}/allowlists/{self.runtime_policy_name}", data=body, timeout=self.request_timeout
+ f"/v{self.ensure_verifier_version()}/allowlists/{self.runtime_policy_name}", data=body, timeout=self.request_timeout
)
response_json = Tenant._jsonify_response(response)
@@ -1343,7 +1520,7 @@ class Tenant:
if not name:
raise UserError("--allowlist_name or --runtime_policy_name is required to delete a runtime policy")
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.delete(f"/v{self.api_version}/allowlists/{name}", timeout=self.request_timeout)
+ response = cv_client.delete(f"/v{self.ensure_verifier_version()}/allowlists/{name}", timeout=self.request_timeout)
response_json = Tenant._jsonify_response(response)
if response.status_code >= 400:
@@ -1353,7 +1530,7 @@ class Tenant:
if not name:
raise UserError("--allowlist_name or --runtime_policy_name is required to show a runtime policy")
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.get(f"/v{self.api_version}/allowlists/{name}", timeout=self.request_timeout)
+ response = cv_client.get(f"/v{self.ensure_verifier_version()}/allowlists/{name}", timeout=self.request_timeout)
print(f"Show allowlist command response: {response.status_code}.")
response_json = Tenant._jsonify_response(response)
@@ -1362,7 +1539,7 @@ class Tenant:
def do_list_runtime_policy(self) -> None:
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.get(f"/v{self.api_version}/allowlists/", timeout=self.request_timeout)
+ response = cv_client.get(f"/v{self.ensure_verifier_version()}/allowlists/", timeout=self.request_timeout)
print(f"list command response: {response.status_code}.")
response_json = Tenant._jsonify_response(response)
@@ -1393,7 +1570,7 @@ class Tenant:
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = cv_client.post(
- f"/v{self.api_version}/mbpolicies/{self.mb_policy_name}", data=body, timeout=self.request_timeout
+ f"/v{self.ensure_verifier_version()}/mbpolicies/{self.mb_policy_name}", data=body, timeout=self.request_timeout
)
response_json = Tenant._jsonify_response(response)
@@ -1405,7 +1582,7 @@ class Tenant:
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
response = cv_client.put(
- f"/v{self.api_version}/mbpolicies/{self.mb_policy_name}", data=body, timeout=self.request_timeout
+ f"/v{self.ensure_verifier_version()}/mbpolicies/{self.mb_policy_name}", data=body, timeout=self.request_timeout
)
response_json = Tenant._jsonify_response(response)
@@ -1416,7 +1593,7 @@ class Tenant:
if not name:
raise UserError("--mb_policy_name is required to delete a runtime policy")
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.delete(f"/v{self.api_version}/mbpolicies/{name}", timeout=self.request_timeout)
+ response = cv_client.delete(f"/v{self.ensure_verifier_version()}/mbpolicies/{name}", timeout=self.request_timeout)
response_json = Tenant._jsonify_response(response)
if response.status_code >= 400:
@@ -1426,7 +1603,7 @@ class Tenant:
if not name:
raise UserError("--mb_policy_name is required to show a runtime policy")
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.get(f"/v{self.api_version}/mbpolicies/{name}", timeout=self.request_timeout)
+ response = cv_client.get(f"/v{self.ensure_verifier_version()}/mbpolicies/{name}", timeout=self.request_timeout)
print(f"showmbpolicy command response: {response.status_code}.")
response_json = Tenant._jsonify_response(response)
@@ -1435,7 +1612,7 @@ class Tenant:
def do_list_mb_policy(self) -> None: # pylint: disable=unused-argument
cv_client = RequestsClient(self.verifier_base_url, True, tls_context=self.tls_context)
- response = cv_client.get(f"/v{self.api_version}/mbpolicies/", timeout=self.request_timeout)
+ response = cv_client.get(f"/v{self.ensure_verifier_version()}/mbpolicies/", timeout=self.request_timeout)
print(f"listmbpolicy command response: {response.status_code}.")
response_json = Tenant._jsonify_response(response)
@@ -1688,16 +1865,29 @@ def main() -> None:
default=None,
help="The name of the measure boot policy to operate with",
)
+ parser.add_argument(
+ "--agent-api-version",
+ default=None,
+ action="store",
+ dest="agent_api_version",
+ help="API version to use for agent communication. Detected automatically by default",
+ )
parser.add_argument(
"--supported-version",
default=None,
action="store",
dest="supported_version",
- help="API version that is supported by the agent. Detected automatically by default",
+ help="DEPRECATED: Use --agent-api-version instead. API version to use for agent communication",
)
args = parser.parse_args()
+ # Handle deprecated --supported-version argument
+ if args.supported_version is not None:
+ logger.warning("--supported-version is deprecated. Use --agent-api-version instead.")
+ if args.agent_api_version is None:
+ args.agent_api_version = args.supported_version
+
argerr, argerrmsg = options.get_opts_error(args)
if argerr:
parser.error(argerrmsg)
diff --git a/test/test_api_version.py b/test/test_api_version.py
index 5389aaa..e1d834a 100644
--- a/test/test_api_version.py
+++ b/test/test_api_version.py
@@ -56,6 +56,71 @@ class APIVersion_Test(unittest.TestCase):
with self.subTest(description):
self.assertEqual(api_version.is_supported_version(version), supported, description)
+ def test_negotiate_version_with_string(self):
+ """Test negotiate_version with a single version string."""
+ result = api_version.negotiate_version("2.0")
+ self.assertEqual(result, "2.0")
+
+ result = api_version.negotiate_version("99.0")
+ self.assertIsNone(result)
+
+ def test_negotiate_version_with_list(self):
+ """Test negotiate_version with a list of versions."""
+ result = api_version.negotiate_version(["1.0", "2.0", "2.1"])
+ self.assertEqual(result, "2.1")
+
+ result = api_version.negotiate_version(["1.0", "2.0", "99.0"])
+ self.assertEqual(result, "2.0")
+
+ result = api_version.negotiate_version(["98.0", "99.0"])
+ self.assertIsNone(result)
+
+ def test_negotiate_version_returns_highest(self):
+ """Test that negotiate_version returns the highest common version."""
+ result = api_version.negotiate_version(["1.0", "2.0", "2.1", "2.2", "2.3"])
+ self.assertEqual(result, "2.3")
+
+ result = api_version.negotiate_version(["1.0", "2.0"])
+ self.assertEqual(result, "2.0")
+
+ def test_negotiate_version_with_custom_local_versions(self):
+ """Test negotiate_version with custom local_versions."""
+ local_versions = ["1.0", "2.0", "2.1", "2.2", "2.3"]
+ result = api_version.negotiate_version(["2.3", "3.0"], local_versions)
+ self.assertEqual(result, "2.3")
+
+ result = api_version.negotiate_version(["3.0"], local_versions)
+ self.assertIsNone(result)
+
+ def test_negotiate_version_raise_on_error(self):
+ """Test negotiate_version with raise_on_error=True."""
+ with self.assertRaises(ValueError) as context:
+ api_version.negotiate_version(["99.0"], raise_on_error=True)
+ self.assertIn("No compatible API version", str(context.exception))
+
+ result = api_version.negotiate_version(["2.0"], raise_on_error=True)
+ self.assertEqual(result, "2.0")
+
+ def test_negotiate_version_empty_list(self):
+ """Test negotiate_version with empty list."""
+ result = api_version.negotiate_version([])
+ self.assertIsNone(result)
+
+ with self.assertRaises(ValueError):
+ api_version.negotiate_version([], raise_on_error=True)
+
+ def test_negotiate_version_proper_version_comparison(self):
+ """Test that version comparison is numeric, not string-based (2.10 > 2.2)."""
+ local_versions = ["2.2", "2.10"]
+ remote_versions = ["2.2", "2.10"]
+ result = api_version.negotiate_version(remote_versions, local_versions)
+ self.assertEqual(result, "2.10", "2.10 should be greater than 2.2")
+
+ local_versions = ["1.0", "2.1", "2.2", "2.9", "2.10", "2.11"]
+ remote_versions = ["2.2", "2.9", "2.10"]
+ result = api_version.negotiate_version(remote_versions, local_versions)
+ self.assertEqual(result, "2.10", "2.10 should be the highest common version")
+
if __name__ == "__main__":
unittest.main()
--
2.52.0

View File

@ -1,145 +0,0 @@
From 6466fcd8071c6332db4c695797ca6a0573e8df23 Mon Sep 17 00:00:00 2001
From: Sergio Correia <scorreia@redhat.com>
Date: Sun, 24 May 2026 20:28:42 +0000
Subject: [PATCH 24/24] Apply limit on keylime-policy workers
Upstream backport: https://github.com/keylime/keylime/pull/1811
Signed-off-by: Sergio Correia <scorreia@redhat.com>
---
keylime/policy/create_runtime_policy.py | 24 ++++++++++++++++++++++--
keylime/policy/rpm_repo.py | 17 +++++++++++++----
2 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/keylime/policy/create_runtime_policy.py b/keylime/policy/create_runtime_policy.py
index 8e1c687..2db9b73 100644
--- a/keylime/policy/create_runtime_policy.py
+++ b/keylime/policy/create_runtime_policy.py
@@ -434,6 +434,13 @@ def get_arg_parser(create_parser: _SubparserType, parent_parser: argparse.Argume
dest="remote_rpm_repo",
help="Remote RPM repo URL",
)
+ repo_group.add_argument(
+ "--max-workers",
+ dest="max_workers",
+ type=_positive_int,
+ default=rpm_repo.MAX_WORKERS_DEFAULT,
+ help=f"The maximum number of workers used for RPM processing (must be a positive integer; default {rpm_repo.MAX_WORKERS_DEFAULT}).",
+ )
fs_group.add_argument(
"--algo",
@@ -839,6 +846,19 @@ def _get_digest_algorithm_from_map_list(maplist: Dict[str, List[str]]) -> str:
return algo
+def _positive_int(value: str) -> int:
+ """
+ A custom type function for argparse that checks for a positive integer.
+ """
+ try:
+ ivalue = int(value)
+ if ivalue <= 0:
+ raise argparse.ArgumentTypeError(f"'{value}' is not a positive integer.")
+ return ivalue
+ except ValueError as exc:
+ raise argparse.ArgumentTypeError(f"'{value}' is not a positive integer.") from exc
+
+
def create_runtime_policy(args: argparse.Namespace) -> Optional[RuntimePolicyType]:
"""Create a runtime policy from the input arguments."""
policy = None
@@ -924,7 +944,7 @@ def create_runtime_policy(args: argparse.Namespace) -> Optional[RuntimePolicyTyp
# FIXME: pass the IMA sigs as well.
local_rpm_digests = {}
local_rpm_digests, _imasigs, ok = rpm_repo.analyze_local_repo(
- args.local_rpm_repo, digests=local_rpm_digests
+ args.local_rpm_repo, digests=local_rpm_digests, max_workers=args.max_workers
)
if not ok:
return None
@@ -932,7 +952,7 @@ def create_runtime_policy(args: argparse.Namespace) -> Optional[RuntimePolicyTyp
# FIXME: pass the IMA sigs as well.
remote_rpm_digests = {}
remote_rpm_digests, _imasigs, ok = rpm_repo.analyze_remote_repo(
- args.remote_rpm_repo, digests=remote_rpm_digests
+ args.remote_rpm_repo, digests=remote_rpm_digests, max_workers=args.max_workers
)
if not ok:
return None
diff --git a/keylime/policy/rpm_repo.py b/keylime/policy/rpm_repo.py
index d6a8bda..ab55484 100644
--- a/keylime/policy/rpm_repo.py
+++ b/keylime/policy/rpm_repo.py
@@ -25,6 +25,8 @@ from keylime.types import PathLike_str
logger = Logger().logger()
+MAX_WORKERS_DEFAULT = 8
+
def _parse_rpm_header(hdr: rpm.hdr) -> Tuple[Dict[str, List[str]], Dict[str, List[bytes]]]:
# First, the file digests.
@@ -110,6 +112,7 @@ def analyze_local_repo(
digests: Optional[Dict[str, List[str]]] = None,
imasigs: Optional[Dict[str, List[bytes]]] = None,
jobs: Optional[int] = None,
+ max_workers: int = MAX_WORKERS_DEFAULT,
) -> Tuple[Dict[str, List[str]], Dict[str, List[bytes]], bool]:
"""
Analyze a local repository.
@@ -121,6 +124,7 @@ def analyze_local_repo(
:param imasigs: dict of str and a list of bytes, to store the files and
their associated IMA signatures
:param jobs: integer, the number of jobs to use when processing the rpms
+ :param max_workers: integer, the maximum number of workers used for RPM processing
:return: tuple with the dict of digests, the dict of IMA signatures and a
boolean indicating the success of this method
"""
@@ -163,7 +167,7 @@ def analyze_local_repo(
else:
logger.warning("Warning. Unsigned repository. Continuing the RPM scanning")
- jobs = jobs if jobs else multiprocessing.cpu_count()
+ jobs = jobs if jobs else min(multiprocessing.cpu_count(), max_workers)
if not digests:
digests = {}
@@ -255,7 +259,11 @@ def _parse_xml_file(filepath: str) -> ET.ElementTree:
def _analyze_remote_repo(
- repo: str, digests: Optional[Dict[str, List[str]]], imasigs: Optional[Dict[str, List[bytes]]], jobs: Optional[int]
+ repo: str,
+ digests: Optional[Dict[str, List[str]]],
+ imasigs: Optional[Dict[str, List[bytes]]],
+ jobs: Optional[int],
+ max_workers: int = MAX_WORKERS_DEFAULT,
) -> Tuple[Dict[str, List[str]], Dict[str, List[bytes]], bool]:
# Make the repo ends with "/", so we can be considered as a base URL
repo = repo if (repo).endswith("/") else f"{repo}/"
@@ -319,7 +327,7 @@ def _analyze_remote_repo(
# single thread (asyncio) or multiple process. To avoid change
# all the stack, I go for synchronous functions but with many
# process. In the future we can move all to asyncio.
- jobs = jobs if jobs else (multiprocessing.cpu_count() * 8)
+ jobs = jobs if jobs else max_workers
# Analyze all the RPMs in parallel
with multiprocessing.Pool(jobs) as pool:
@@ -335,10 +343,11 @@ def analyze_remote_repo(
digests: Optional[Dict[str, List[str]]] = None,
imasigs: Optional[Dict[str, List[bytes]]] = None,
jobs: Optional[int] = None,
+ max_workers: int = MAX_WORKERS_DEFAULT,
) -> Tuple[Dict[str, List[str]], Dict[str, List[bytes]], bool]:
"""Analyze a remote repository."""
try:
- return _analyze_remote_repo(str(*repourl), digests, imasigs, jobs)
+ return _analyze_remote_repo(str(*repourl), digests, imasigs, jobs, max_workers)
except Exception as exc:
logger.error(exc)
return {}, {}, False
--
2.47.3

View File

@ -1,5 +1,5 @@
%global srcname keylime
%global policy_version 42.1.2
%global policy_version 44.1.0
%global with_selinux 1
%global selinuxtype targeted
@ -8,8 +8,8 @@
%global debug_package %{nil}
Name: keylime
Version: 7.12.1
Release: 17%{?dist}
Version: 7.14.3
Release: 1%{?dist}
Summary: Open source TPM software for Bootstrapping and Maintaining Trust
URL: https://github.com/keylime/keylime
@ -18,68 +18,28 @@ Source1: https://github.com/RedHat-SP-Security/%{name}-selinux/archive/v%
Source2: %{srcname}.sysusers
Source3: %{srcname}.tmpfiles
# Update keylime to work with python 3.9.
Patch: 0001-Make-keylime-compatible-with-python-3.9.patch
Patch: 0002-tests-fix-rpm-repo-tests-from-create-runtime-policy.patch
Patch: 0003-tests-skip-measured-boot-related-tests-for-s390x-and.patch
Patch: 0004-templates-duplicate-str_to_version-in-the-adjust-scr.patch
# RHEL-9 ships a slightly modified version of create_allowlist.sh and
# also a "default" server_key_password for the registrar and verifier.
# DO NOT REMOVE THE FOLLOWING TWO PATCHES IN FOLLOWING RHEL-9.x REBASES.
Patch: 0005-Restore-RHEL-9-version-of-create_allowlist.sh.patch
Patch: 0006-Revert-default-server_key_password-for-verifier-regi.patch
# Backported from https://github.com/keylime/keylime/pull/1782
Patch: 0007-fix_db_connection_leaks.patch
# We also disable push model in RHEL-9.
# DO NOT REMOVE THE FOLLOWING THREE PATCHES IN FOLLOWING RHEL-9.x REBASES.
Patch: 0002-Restore-RHEL-9-version-of-create_allowlist.sh.patch
Patch: 0003-Revert-default-server_key_password-for-verifier-regi.patch
Patch: 0004-Disable-push-model-agent-driven-attestation-for-RHEL.patch
# Backported from https://github.com/keylime/keylime/pull/1791
Patch: 0008-mb-support-EV_EFI_HANDOFF_TABLES-events-on-PCR1.patch
Patch: 0009-mb-support-vendor_db-as-logged-by-newer-shim-version.patch
# Backported from https://github.com/keylime/keylime/pull/1784
# and https://github.com/keylime/keylime/pull/1785.
Patch: 0010-verifier-Gracefully-shutdown-on-signal.patch
Patch: 0011-revocations-Try-to-send-notifications-on-shutdown.patch
Patch: 0012-requests_client-close-the-session-at-the-end-of-the-.patch
Patch: 0013-fix-malformed-certs-workaround.patch
# CVE-2025-13609
# Backports from:
# - https://github.com/keylime/keylime/pull/1817/commits/1024e19d
# - https://github.com/keylime/keylime/pull/1825
Patch: 0014-Add-shared-memory-infrastructure-for-multiprocess-co.patch
Patch: 0015-Fix-registrar-duplicate-UUID-vulnerability.patch
# Backported from:
# - https://github.com/keylime/keylime/pull/1746
# - https://github.com/keylime/keylime/pull/1803
# - https://github.com/keylime/keylime/pull/1808
# ECC attestation support.
Patch: 0016-algorithms-add-support-for-specific-ECC-curve-algori.patch
Patch: 0017-algorithms-add-support-for-specific-RSA-algorithms.patch
Patch: 0018-tpm_util-fix-quote-signature-extraction-for-ECDSA.patch
Patch: 0019-tpm-fix-ECC-P-521-coordinate-validation.patch
Patch: 0020-tpm-fix-ECC-P-521-credential-activation-with-consist.patch
Patch: 0021-tpm-fix-ECC-signature-parsing-to-support-variable-le.patch
# CVE-2026-1709
# Fix registrar authentication bypass
Patch: 0022-CVE-2026-1709.patch
# Tenant version negotiation.
# Backport from:
# - https://github.com/keylime/keylime/pull/1838
# - https://github.com/keylime/keylime/pull/1845
Patch: 0023-Backport-tenant-version-negotiation-mechanism.patch
# Backport from:
# - https://github.com/keylime/keylime/pull/1811
Patch: 0024-Apply-limit-on-keylime-policy-workers.patch
# Backport: https://github.com/keylime/keylime/pull/1930
Patch: 0005-fix-handle-invalid-config-values-for-typed-server-op.patch
License: ASL 2.0 and MIT
BuildRequires: git-core
BuildRequires: openssl
BuildRequires: openssl-devel
BuildRequires: python3-devel
BuildRequires: python3-dbus
BuildRequires: python3-gpg
BuildRequires: python3-jinja2
BuildRequires: python3-cryptography
BuildRequires: python3-pyasn1
@ -89,6 +49,7 @@ BuildRequires: python3-sqlalchemy
BuildRequires: python3-lark-parser
BuildRequires: python3-psutil
BuildRequires: python3-pyyaml
BuildRequires: python3-requests
BuildRequires: python3-jsonschema
BuildRequires: python3-setuptools
BuildRequires: systemd-rpm-macros
@ -468,6 +429,10 @@ fi
%license LICENSE
%changelog
* Mon Jun 01 2026 Sergio Correia <scorreia@redhat.com> - 7.14.1-1
- Update to 7.14.2
Resolves: RHEL-180618
* Fri May 22 2026 Sergio Correia <scorreia@redhat.com> 7.12.1-17
- Apply limit on keylime-policy workers
Resolves: RHEL-114482

View File

@ -1,2 +1,2 @@
SHA512 (v7.12.1.tar.gz) = c1297ebfc659102d73283255cfda4a977dfbff9bdd3748e05de405dadb70f752ad39aa5848edda9143d8ec620d07c21f1551fa4a914c99397620ab1682e58458
SHA512 (keylime-selinux-42.1.2.tar.gz) = cb7b7b10d1d81af628a7ffdadc1be5af6d75851a44f58cff04edc575cbba1613447e56bfa1fb86660ec7c15e5fcf16ba51f2984094550ba3e08f8095b800b741
SHA512 (v7.14.3.tar.gz) = 254c8b100bd2cc5b793d1266451e1f6ad8ac05ace2c8cc0c62a78a6faba487b45d1050857b39ffed8b6c8bb41bfa6d0d9e56f840d50ff56876eb54006cc9bc67
SHA512 (keylime-selinux-44.1.0.tar.gz) = 3ba9869cc07c3bd64ef29f572a13a3c3ea92eec50f5cfb3b882835e6db6b835cb1a4c272ab92273da6912bed36abe54eafdac7c8e5b103dd3f761b1d38e7edc6