From 20b29577db7953ff93e0186f326d6eda49a24f63 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Fri, 16 Oct 2026 13:39:04 +0200 Subject: [PATCH] Support a per-call key override in signing services Metadata signing services bind one key per service, so signing repositories with different keys needs a separate service for each: an on-disk script, an importable key, and a full validate() run. In a containerized deployment that is a deployment change per key. Package signing already selects its key per call. An absent fingerprint falls back to the service's own key. Refs: #7942 Co-Authored-By: Claude Opus 5 --- CHANGES/plugin_api/7942.feature | 1 + pulpcore/app/models/content.py | 12 +++++-- pulpcore/tests/unit/test_signing_service.py | 39 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 CHANGES/plugin_api/7942.feature create mode 100644 pulpcore/tests/unit/test_signing_service.py diff --git a/CHANGES/plugin_api/7942.feature b/CHANGES/plugin_api/7942.feature new file mode 100644 index 00000000000..6b505a38b53 --- /dev/null +++ b/CHANGES/plugin_api/7942.feature @@ -0,0 +1 @@ +`SigningService.sign()` and `asign()` now accept an optional `pubkey_fingerprint` to sign with a key other than the one the signing service was created with. diff --git a/pulpcore/app/models/content.py b/pulpcore/app/models/content.py index c2666833fd5..dc9ef3fecd0 100644 --- a/pulpcore/app/models/content.py +++ b/pulpcore/app/models/content.py @@ -814,7 +814,7 @@ def _env_variables(self, env_vars=None): env.update(env_vars) return {**os.environ, **env} - def sign(self, filename, env_vars=None): + def sign(self, filename, env_vars=None, pubkey_fingerprint=None): """ Signs the file provided via 'filename' by invoking an external script (or executable). @@ -825,6 +825,8 @@ def sign(self, filename, env_vars=None): Args: filename (str): A relative path to a file which is intended to be signed. env_vars (dict): dictionary of environment variables + pubkey_fingerprint (str): fingerprint of the key to sign with. Defaults to the + signing service's own key. Raises: RuntimeError: If the return code of the script is not equal to 0. @@ -832,6 +834,9 @@ def sign(self, filename, env_vars=None): Returns: A dictionary as validated by the validate() method. """ + if pubkey_fingerprint: + env_vars = {**(env_vars or {}), "PULP_SIGNING_KEY_FINGERPRINT": pubkey_fingerprint} + completed_process = subprocess.run( [self.script, filename], env=self._env_variables(env_vars), @@ -849,8 +854,11 @@ def sign(self, filename, env_vars=None): return return_value - async def asign(self, filename, env_vars=None): + async def asign(self, filename, env_vars=None, pubkey_fingerprint=None): """Async version of sign.""" + if pubkey_fingerprint: + env_vars = {**(env_vars or {}), "PULP_SIGNING_KEY_FINGERPRINT": pubkey_fingerprint} + process = await asyncio.create_subprocess_exec( self.script, filename, diff --git a/pulpcore/tests/unit/test_signing_service.py b/pulpcore/tests/unit/test_signing_service.py new file mode 100644 index 00000000000..f916323252d --- /dev/null +++ b/pulpcore/tests/unit/test_signing_service.py @@ -0,0 +1,39 @@ +import os + +import pytest + +from pulpcore.app.models.content import AsciiArmoredDetachedSigningService + + +@pytest.fixture +def signing_service(tmp_path): + """A signing service whose script reports the fingerprint and env vars it was invoked with.""" + script = tmp_path / "echo_env.sh" + script.write_text( + '#!/bin/sh\nprintf \'{"fingerprint": "%s", "extra": "%s"}\'' + ' "$PULP_SIGNING_KEY_FINGERPRINT" "$EXTRA"\n' + ) + script.chmod(0o755) + return AsciiArmoredDetachedSigningService( + name="echo-env", + public_key="public-key", + pubkey_fingerprint="SERVICE_KEY", + script=str(script), + ) + + +def test_sign_defaults_to_service_key(signing_service): + assert signing_service.sign(os.devnull)["fingerprint"] == "SERVICE_KEY" + + +def test_sign_override_replaces_key_and_keeps_env_vars(signing_service): + return_value = signing_service.sign( + os.devnull, env_vars={"EXTRA": "kept"}, pubkey_fingerprint="OTHER_KEY" + ) + assert return_value == {"fingerprint": "OTHER_KEY", "extra": "kept"} + + +@pytest.mark.asyncio +async def test_asign_override_replaces_key(signing_service): + return_value = await signing_service.asign(os.devnull, pubkey_fingerprint="OTHER_KEY") + assert return_value["fingerprint"] == "OTHER_KEY"