From cf7956f0c20e0df0298f7b566b670b1d0f7fc1ac Mon Sep 17 00:00:00 2001 From: guillermodotn Date: Fri, 15 May 2026 15:41:58 +0000 Subject: [PATCH 1/3] feat: add sigkeys field for multiple RPM signature support in v2 Assisted-by: Claude Opus --- productmd/rpms.py | 57 ++++- tests/compose-v2/metadata/rpms.json | 1 + tests/test_rpms_v2.py | 362 ++++++++++++++++++++++++++++ 3 files changed, 419 insertions(+), 1 deletion(-) diff --git a/productmd/rpms.py b/productmd/rpms.py index caa086b..e21d8a9 100644 --- a/productmd/rpms.py +++ b/productmd/rpms.py @@ -163,6 +163,11 @@ def _serialize_v2(self, data): v2_entry["sigkey"] = rpm_data.get("sigkey") v2_entry["category"] = rpm_data.get("category") + # Include sigkeys if present (RPM v6 multiple signatures) + sigkeys = rpm_data.get("sigkeys") + if sigkeys: + v2_entry["sigkeys"] = sigkeys + v2_rpms[variant][arch][srpm_nevra][rpm_nevra] = v2_entry data["payload"]["rpms"] = v2_rpms @@ -235,12 +240,17 @@ def _deserialize_v2(self, data): "category": rpm_data.get("category"), } + # Preserve sigkeys for RPM v6 multiple signatures + sigkeys = rpm_data.get("sigkeys") + if sigkeys: + entry["sigkeys"] = sigkeys + # Preserve full location for round-trip fidelity entry["_location"] = loc self.rpms[variant][arch][srpm_nevra][rpm_nevra] = entry - def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, location=None): + def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, location=None, sigkeys=None): """ Map RPM to to variant and arch. @@ -265,6 +275,10 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc and *path* is not explicitly set, *path* defaults to ``location.local_path``. :type location: :class:`~productmd.location.Location` or None + :param sigkeys: list of signing key identifiers for RPM v6 + packages with multiple signatures. Each value must be a hex + string. Only included in v2.0 serialization. + :type sigkeys: list of str or None """ # When location is provided without an explicit path, derive path @@ -304,6 +318,24 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc if sigkey is not None: sigkey = sigkey.lower() + if sigkeys is not None: + if not isinstance(sigkeys, list): + raise TypeError("'sigkeys' must be a list, got: %s" % type(sigkeys)) + validated = [] + for key in sigkeys: + if not isinstance(key, str) or not key: + raise ValueError("Each sigkey must be a non-empty string, got: %r" % key) + try: + int(key, 16) + except ValueError: + raise ValueError("Each sigkey must be a hex string, got: %r" % key) + validated.append(key.lower()) + sigkeys = validated + + # Ensure sigkey is included in sigkeys for completeness + if sigkeys is not None and sigkey is not None and sigkey not in sigkeys: + sigkeys.insert(0, sigkey) + if srpm_nevra: srpm_nevra, _ = self._check_nevra(srpm_nevra) else: @@ -313,6 +345,8 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc srpms = arches.setdefault(arch, {}) rpms = srpms.setdefault(srpm_nevra, {}) entry = {"sigkey": sigkey, "path": path, "category": category} + if sigkeys: + entry["sigkeys"] = sigkeys if location is not None: entry["_location"] = location rpms[nevra] = entry @@ -337,3 +371,24 @@ def get_location(self, variant, arch, srpm_nevra, rpm_nevra): except KeyError: return None return entry.get("_location") + + def get_sigkeys(self, variant, arch, srpm_nevra, rpm_nevra): + """ + Return the list of signing key identifiers for an RPM entry. + + :param variant: compose variant UID + :type variant: str + :param arch: compose architecture + :type arch: str + :param srpm_nevra: name-epoch:version-release.arch of the SRPM + :type srpm_nevra: str + :param rpm_nevra: name-epoch:version-release.arch of the RPM + :type rpm_nevra: str + :return: list of signing key identifiers, empty if not found or not set + :rtype: list of str + """ + try: + entry = self.rpms[variant][arch][srpm_nevra][rpm_nevra] + except KeyError: + return [] + return entry.get("sigkeys", []) diff --git a/tests/compose-v2/metadata/rpms.json b/tests/compose-v2/metadata/rpms.json index 18c8e03..650c25d 100644 --- a/tests/compose-v2/metadata/rpms.json +++ b/tests/compose-v2/metadata/rpms.json @@ -22,6 +22,7 @@ "local_path": "Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm" }, "sigkey": "a15b79cc", + "sigkeys": ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"], "category": "binary" }, "bash-doc-0:5.2.26-3.fc41.x86_64": { diff --git a/tests/test_rpms_v2.py b/tests/test_rpms_v2.py index 84eb034..33befd3 100644 --- a/tests/test_rpms_v2.py +++ b/tests/test_rpms_v2.py @@ -787,3 +787,365 @@ def test_get_location_missing_rpm_returns_none(self): result = rpms.get_location("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "no-such-0:1.0-1.fc41.x86_64") assert result is None + + +class TestRpmsSigkeys: + """Tests for multiple RPM signature support (sigkeys field).""" + + def test_add_with_sigkeys(self): + """add() stores sigkeys in the entry dict.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["a15b79cc", "1234567890abcdef1234567890abcdef12345678"], + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert entry["sigkeys"] == ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] + + def test_add_without_sigkeys_has_no_key(self): + """add() without sigkeys does not add the key to the entry.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert "sigkeys" not in entry + + def test_add_sigkeys_normalized_to_lowercase(self): + """sigkeys values are normalized to lowercase.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["A15B79CC", "ABCDEF1234567890ABCDEF1234567890ABCDEF12"], + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert entry["sigkeys"] == ["a15b79cc", "abcdef1234567890abcdef1234567890abcdef12"] + + def test_add_sigkeys_invalid_hex_raises(self): + """add() raises ValueError for non-hex sigkeys.""" + rpms = _create_rpms() + with pytest.raises(ValueError, match="hex string"): + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["not-hex-value"], + ) + + def test_add_sigkeys_empty_string_raises(self): + """add() raises ValueError for empty string in sigkeys.""" + rpms = _create_rpms() + with pytest.raises(ValueError, match="non-empty string"): + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=[""], + ) + + def test_add_sigkeys_not_list_raises(self): + """add() raises TypeError when sigkeys is not a list.""" + rpms = _create_rpms() + with pytest.raises(TypeError, match="must be a list"): + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys="a15b79cc", + ) + + def test_serialize_v2_includes_sigkeys(self): + """v2.0 serialization includes sigkeys when present.""" + rpms = _create_rpms() + rpms.output_version = VERSION_2_0 + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["a15b79cc", "1234567890abcdef1234567890abcdef12345678"], + ) + + data = rpms.serialize({}) + rpm = data["payload"]["rpms"]["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert rpm["sigkeys"] == ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] + assert rpm["sigkey"] == "a15b79cc" + + def test_serialize_v2_omits_empty_sigkeys(self): + """v2.0 serialization does not include sigkeys when not set.""" + rpms = _create_rpms() + rpms.output_version = VERSION_2_0 + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + ) + + data = rpms.serialize({}) + rpm = data["payload"]["rpms"]["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert "sigkeys" not in rpm + + def test_serialize_v1_excludes_sigkeys(self): + """v1.x serialization does not include sigkeys.""" + rpms = _create_rpms() + rpms.output_version = VERSION_1_2 + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["a15b79cc", "1234567890abcdef1234567890abcdef12345678"], + ) + + data = rpms.serialize({}) + rpm = data["payload"]["rpms"]["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert "sigkeys" not in rpm + + def test_deserialize_v2_with_sigkeys(self): + """v2.0 deserialization preserves sigkeys.""" + data = { + "header": {"type": "productmd.rpms", "version": "2.0"}, + "payload": { + "compose": { + "id": "Test-1.0-20240101.0", + "date": "20240101", + "type": "production", + "respin": 0, + }, + "rpms": { + "Server": { + "x86_64": { + "bash-0:5.2.26-3.fc41.src": { + "bash-0:5.2.26-3.fc41.x86_64": { + "location": { + "url": "https://cdn.example.com/bash.rpm", + "size": 1849356, + "checksum": "sha256:" + "a" * 64, + "local_path": "Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + }, + "sigkey": "a15b79cc", + "sigkeys": ["a15b79cc", "deadbeef12345678"], + "category": "binary", + } + } + } + } + }, + }, + } + + rpms = Rpms() + rpms.deserialize(data) + + result = rpms.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.x86_64") + assert result == ["a15b79cc", "deadbeef12345678"] + + def test_deserialize_v2_without_sigkeys(self): + """v2.0 deserialization without sigkeys returns empty list.""" + data = { + "header": {"type": "productmd.rpms", "version": "2.0"}, + "payload": { + "compose": { + "id": "Test-1.0-20240101.0", + "date": "20240101", + "type": "production", + "respin": 0, + }, + "rpms": { + "Server": { + "x86_64": { + "bash-0:5.2.26-3.fc41.src": { + "bash-0:5.2.26-3.fc41.x86_64": { + "location": { + "url": "https://cdn.example.com/bash.rpm", + "size": 1849356, + "checksum": "sha256:" + "a" * 64, + "local_path": "Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + }, + "sigkey": "a15b79cc", + "category": "binary", + } + } + } + } + }, + }, + } + + rpms = Rpms() + rpms.deserialize(data) + + result = rpms.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.x86_64") + assert result == [] + + def test_get_sigkeys_after_add(self): + """get_sigkeys() returns the list passed to add().""" + rpms = _create_rpms() + keys = ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=keys, + ) + + result = rpms.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.x86_64") + assert result == keys + + def test_get_sigkeys_no_sigkeys_returns_empty(self): + """get_sigkeys() returns empty list when RPM has no sigkeys.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + ) + + result = rpms.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.x86_64") + assert result == [] + + def test_get_sigkeys_missing_entry_returns_empty(self): + """get_sigkeys() returns empty list for non-existent entry.""" + rpms = _create_rpms() + result = rpms.get_sigkeys("NoVariant", "x86_64", "foo-0:1.0-1.src", "foo-0:1.0-1.x86_64") + assert result == [] + + def test_sigkey_auto_included_in_sigkeys(self): + """sigkey is automatically prepended to sigkeys if not present.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["deadbeef12345678"], + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert entry["sigkey"] == "a15b79cc" + assert entry["sigkeys"] == ["a15b79cc", "deadbeef12345678"] + + def test_sigkey_not_duplicated_in_sigkeys(self): + """sigkey is not duplicated if already in sigkeys.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["a15b79cc", "deadbeef12345678"], + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert entry["sigkeys"] == ["a15b79cc", "deadbeef12345678"] + + def test_sigkey_null_not_added_to_sigkeys(self): + """sigkey=None is not added to sigkeys.""" + rpms = _create_rpms() + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey=None, + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=["deadbeef12345678"], + ) + + entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] + assert entry["sigkey"] is None + assert entry["sigkeys"] == ["deadbeef12345678"] + + def test_v2_roundtrip_with_sigkeys(self): + """v2.0 serialize-deserialize preserves sigkeys.""" + rpms = _create_rpms() + rpms.output_version = VERSION_2_0 + keys = ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.x86_64", + path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", + sigkey="a15b79cc", + category="binary", + srpm_nevra="bash-0:5.2.26-3.fc41.src", + sigkeys=keys, + ) + rpms.add( + "Server", + "x86_64", + "bash-0:5.2.26-3.fc41.src", + path="Server/source/SRPMS/b/bash-5.2.26-3.fc41.src.rpm", + sigkey="a15b79cc", + category="source", + ) + + data = rpms.serialize({}) + + rpms2 = Rpms() + rpms2.deserialize(data) + + result = rpms2.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.x86_64") + assert result == keys + + # Source RPM without sigkeys should return empty + result2 = rpms2.get_sigkeys("Server", "x86_64", "bash-0:5.2.26-3.fc41.src", "bash-0:5.2.26-3.fc41.src") + assert result2 == [] From 53cd5251a650cd79890893384f014ecea715c156 Mon Sep 17 00:00:00 2001 From: guillermodotn Date: Fri, 15 May 2026 15:42:05 +0000 Subject: [PATCH 2/3] doc: document sigkeys field in rpms-2.0 schema Assisted-by: Claude Opus --- doc/rpms-2.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/rpms-2.0.rst b/doc/rpms-2.0.rst index 73fac05..55065fd 100644 --- a/doc/rpms-2.0.rst +++ b/doc/rpms-2.0.rst @@ -11,6 +11,7 @@ Changes from 1.1 * ``path`` field replaced by a ``location`` object * Location objects include ``url``, ``size``, ``checksum``, and ``local_path`` fields * ``sigkey`` and ``category`` fields are preserved alongside the location +* Optional ``sigkeys`` list for RPM v6 packages with multiple signatures * URLs may be HTTPS URLs, OCI registry references, or relative paths * Checksums use ``algorithm:hexdigest`` format (e.g., ``sha256:abc123...``) * ``force_version`` parameter added to ``serialize()`` for version control @@ -60,6 +61,7 @@ in order to read and diff rpms.json files easily. "local_path": # relative path for v1.x filesystem layout }, "sigkey": , # sigkey ID; null for unsigned RPMs + "sigkeys": [, ...],# (optional) list of signing key IDs; for RPM v6 multiple signatures "category": # binary, debug, source } } @@ -99,6 +101,7 @@ Bash and kernel RPMs in Fedora 41:: "local_path": "Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm" }, "sigkey": "a15b79cc", + "sigkeys": ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"], "category": "binary" } }, From 976991068319e8d5999e96d8897a55cde0ba4ac0 Mon Sep 17 00:00:00 2001 From: guillermodotn Date: Tue, 19 May 2026 15:06:47 +0000 Subject: [PATCH 3/3] refactor: sigkeys always a list, sigkey derived from sigkeys[0] Assisted-by: Claude Opus --- doc/rpms-2.0.rst | 5 +++- productmd/rpms.py | 35 +++++++++++++--------------- tests/test_rpms_v2.py | 53 +++++++++++++++---------------------------- 3 files changed, 38 insertions(+), 55 deletions(-) diff --git a/doc/rpms-2.0.rst b/doc/rpms-2.0.rst index 55065fd..4e3912d 100644 --- a/doc/rpms-2.0.rst +++ b/doc/rpms-2.0.rst @@ -11,7 +11,10 @@ Changes from 1.1 * ``path`` field replaced by a ``location`` object * Location objects include ``url``, ``size``, ``checksum``, and ``local_path`` fields * ``sigkey`` and ``category`` fields are preserved alongside the location -* Optional ``sigkeys`` list for RPM v6 packages with multiple signatures +* Optional ``sigkeys`` list for RPM v6 packages with multiple signatures. + When ``sigkeys`` is provided to ``Rpms.add()``, ``sigkey`` is derived from + ``sigkeys[0]``. This derivation only happens at ``add()`` time; modifying + ``sigkeys`` directly on the entry dict afterwards will not update ``sigkey``. * URLs may be HTTPS URLs, OCI registry references, or relative paths * Checksums use ``algorithm:hexdigest`` format (e.g., ``sha256:abc123...``) * ``force_version`` parameter added to ``serialize()`` for version control diff --git a/productmd/rpms.py b/productmd/rpms.py index e21d8a9..2583a81 100644 --- a/productmd/rpms.py +++ b/productmd/rpms.py @@ -163,10 +163,8 @@ def _serialize_v2(self, data): v2_entry["sigkey"] = rpm_data.get("sigkey") v2_entry["category"] = rpm_data.get("category") - # Include sigkeys if present (RPM v6 multiple signatures) - sigkeys = rpm_data.get("sigkeys") - if sigkeys: - v2_entry["sigkeys"] = sigkeys + # Include sigkeys (RPM v6 multiple signatures) + v2_entry["sigkeys"] = rpm_data.get("sigkeys", []) v2_rpms[variant][arch][srpm_nevra][rpm_nevra] = v2_entry @@ -241,9 +239,7 @@ def _deserialize_v2(self, data): } # Preserve sigkeys for RPM v6 multiple signatures - sigkeys = rpm_data.get("sigkeys") - if sigkeys: - entry["sigkeys"] = sigkeys + entry["sigkeys"] = rpm_data.get("sigkeys", []) # Preserve full location for round-trip fidelity entry["_location"] = loc @@ -262,7 +258,8 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc :type nevra: str :param path: relative path to the RPM file :type path: str - :param sigkey: sigkey hash + :param sigkey: sigkey hash. When *sigkeys* is provided, this is + derived from ``sigkeys[0]`` and should not be set explicitly. :type sigkey: str or None :param category: RPM category, one of binary, debug, source :type category: str @@ -277,7 +274,10 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc :type location: :class:`~productmd.location.Location` or None :param sigkeys: list of signing key identifiers for RPM v6 packages with multiple signatures. Each value must be a hex - string. Only included in v2.0 serialization. + string. Only included in v2.0 serialization. When provided, + *sigkey* is derived from ``sigkeys[0]``. This derivation + only happens at ``add()`` time; modifying *sigkeys* directly + on the entry dict afterwards will not update *sigkey*. :type sigkeys: list of str or None """ @@ -315,9 +315,6 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc if (category == "source") != (nevra_dict["arch"] in ("src", "nosrc")): raise ValueError("Invalid category/arch combination: %s/%s" % (category, nevra)) - if sigkey is not None: - sigkey = sigkey.lower() - if sigkeys is not None: if not isinstance(sigkeys, list): raise TypeError("'sigkeys' must be a list, got: %s" % type(sigkeys)) @@ -331,10 +328,12 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc raise ValueError("Each sigkey must be a hex string, got: %r" % key) validated.append(key.lower()) sigkeys = validated - - # Ensure sigkey is included in sigkeys for completeness - if sigkeys is not None and sigkey is not None and sigkey not in sigkeys: - sigkeys.insert(0, sigkey) + # Derive sigkey from sigkeys[0] for v1.x backward compatibility + sigkey = sigkeys[0] if sigkeys else None + else: + sigkeys = [] + if sigkey is not None: + sigkey = sigkey.lower() if srpm_nevra: srpm_nevra, _ = self._check_nevra(srpm_nevra) @@ -344,9 +343,7 @@ def add(self, variant, arch, nevra, path, sigkey, category, srpm_nevra=None, loc arches = self.rpms.setdefault(variant, {}) srpms = arches.setdefault(arch, {}) rpms = srpms.setdefault(srpm_nevra, {}) - entry = {"sigkey": sigkey, "path": path, "category": category} - if sigkeys: - entry["sigkeys"] = sigkeys + entry = {"sigkey": sigkey, "path": path, "category": category, "sigkeys": sigkeys} if location is not None: entry["_location"] = location rpms[nevra] = entry diff --git a/tests/test_rpms_v2.py b/tests/test_rpms_v2.py index 33befd3..7c6b491 100644 --- a/tests/test_rpms_v2.py +++ b/tests/test_rpms_v2.py @@ -809,8 +809,8 @@ def test_add_with_sigkeys(self): entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] assert entry["sigkeys"] == ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] - def test_add_without_sigkeys_has_no_key(self): - """add() without sigkeys does not add the key to the entry.""" + def test_add_without_sigkeys_defaults_to_empty_list(self): + """add() without sigkeys stores an empty list.""" rpms = _create_rpms() rpms.add( "Server", @@ -823,7 +823,7 @@ def test_add_without_sigkeys_has_no_key(self): ) entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] - assert "sigkeys" not in entry + assert entry["sigkeys"] == [] def test_add_sigkeys_normalized_to_lowercase(self): """sigkeys values are normalized to lowercase.""" @@ -907,8 +907,8 @@ def test_serialize_v2_includes_sigkeys(self): assert rpm["sigkeys"] == ["a15b79cc", "1234567890abcdef1234567890abcdef12345678"] assert rpm["sigkey"] == "a15b79cc" - def test_serialize_v2_omits_empty_sigkeys(self): - """v2.0 serialization does not include sigkeys when not set.""" + def test_serialize_v2_emits_empty_sigkeys(self): + """v2.0 serialization always includes sigkeys, even when empty.""" rpms = _create_rpms() rpms.output_version = VERSION_2_0 rpms.add( @@ -923,7 +923,7 @@ def test_serialize_v2_omits_empty_sigkeys(self): data = rpms.serialize({}) rpm = data["payload"]["rpms"]["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] - assert "sigkeys" not in rpm + assert rpm["sigkeys"] == [] def test_serialize_v1_excludes_sigkeys(self): """v1.x serialization does not include sigkeys.""" @@ -1061,58 +1061,41 @@ def test_get_sigkeys_missing_entry_returns_empty(self): result = rpms.get_sigkeys("NoVariant", "x86_64", "foo-0:1.0-1.src", "foo-0:1.0-1.x86_64") assert result == [] - def test_sigkey_auto_included_in_sigkeys(self): - """sigkey is automatically prepended to sigkeys if not present.""" - rpms = _create_rpms() - rpms.add( - "Server", - "x86_64", - "bash-0:5.2.26-3.fc41.x86_64", - path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", - sigkey="a15b79cc", - category="binary", - srpm_nevra="bash-0:5.2.26-3.fc41.src", - sigkeys=["deadbeef12345678"], - ) - - entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] - assert entry["sigkey"] == "a15b79cc" - assert entry["sigkeys"] == ["a15b79cc", "deadbeef12345678"] - - def test_sigkey_not_duplicated_in_sigkeys(self): - """sigkey is not duplicated if already in sigkeys.""" + def test_sigkey_derived_from_sigkeys(self): + """sigkey is derived from sigkeys[0] when sigkeys is provided.""" rpms = _create_rpms() rpms.add( "Server", "x86_64", "bash-0:5.2.26-3.fc41.x86_64", path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", - sigkey="a15b79cc", + sigkey="ignored", category="binary", srpm_nevra="bash-0:5.2.26-3.fc41.src", - sigkeys=["a15b79cc", "deadbeef12345678"], + sigkeys=["deadbeef12345678", "a15b79cc"], ) entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] - assert entry["sigkeys"] == ["a15b79cc", "deadbeef12345678"] + assert entry["sigkey"] == "deadbeef12345678" + assert entry["sigkeys"] == ["deadbeef12345678", "a15b79cc"] - def test_sigkey_null_not_added_to_sigkeys(self): - """sigkey=None is not added to sigkeys.""" + def test_sigkey_none_when_sigkeys_empty(self): + """sigkey is None when sigkeys is an empty list.""" rpms = _create_rpms() rpms.add( "Server", "x86_64", "bash-0:5.2.26-3.fc41.x86_64", path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", - sigkey=None, + sigkey="ignored", category="binary", srpm_nevra="bash-0:5.2.26-3.fc41.src", - sigkeys=["deadbeef12345678"], + sigkeys=[], ) entry = rpms.rpms["Server"]["x86_64"]["bash-0:5.2.26-3.fc41.src"]["bash-0:5.2.26-3.fc41.x86_64"] assert entry["sigkey"] is None - assert entry["sigkeys"] == ["deadbeef12345678"] + assert entry["sigkeys"] == [] def test_v2_roundtrip_with_sigkeys(self): """v2.0 serialize-deserialize preserves sigkeys.""" @@ -1124,7 +1107,7 @@ def test_v2_roundtrip_with_sigkeys(self): "x86_64", "bash-0:5.2.26-3.fc41.x86_64", path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm", - sigkey="a15b79cc", + sigkey=None, category="binary", srpm_nevra="bash-0:5.2.26-3.fc41.src", sigkeys=keys,