Update to 7.14.3
Resolves: RHEL-180618 Signed-off-by: Sergio Correia <scorreia@redhat.com>
This commit is contained in:
parent
86d98c9da8
commit
a498566a20
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
192
0004-Disable-push-model-agent-driven-attestation-for-RHEL.patch
Normal file
192
0004-Disable-push-model-agent-driven-attestation-for-RHEL.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
102
0005-fix-handle-invalid-config-values-for-typed-server-op.patch
Normal file
102
0005-fix-handle-invalid-config-values-for-typed-server-op.patch
Normal 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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
73
keylime.spec
73
keylime.spec
@ -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
|
||||
|
||||
4
sources
4
sources
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user