Skip to content

Commit 52ef8da

Browse files
committed
Extend tests with PQC coverage
Assisted-By: Codex 5.6 Luna
1 parent bc2a862 commit 52ef8da

4 files changed

Lines changed: 94 additions & 18 deletions

File tree

pulp_deb/tests/functional/api/test_crud_remotes.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@
44
from uuid import uuid4
55

66
import pytest
7+
import requests
78

89
from pulpcore.client.pulp_deb.exceptions import ApiException
10+
from pulpcore.pytest_plugin import KEY_V6_ED25519_PUBLIC, KEY_V6_MLDSA65_ED25519_PUBLIC
911

1012
from pulp_deb.tests.functional.constants import DOWNLOAD_POLICIES
1113
from pulp_deb.tests.functional.utils import gen_deb_remote_verbose
1214

1315

16+
@pytest.mark.parametrize("key_url", [KEY_V6_ED25519_PUBLIC, KEY_V6_MLDSA65_ED25519_PUBLIC])
17+
def test_create_remote_with_openpgp_gpgkey(
18+
apt_remote_api, deb_remote_custom_data_factory, deb_get_fixture_server_url, key_url
19+
):
20+
"""Verify that classical and PQC OpenPGP keys can be stored on an AptRemote."""
21+
key = requests.get(key_url)
22+
key.raise_for_status()
23+
remote = deb_remote_custom_data_factory(
24+
gen_deb_remote_verbose(deb_get_fixture_server_url()) | {"gpgkey": key.text}
25+
)
26+
27+
assert remote.gpgkey == key.text
28+
29+
1430
@pytest.fixture
1531
def deb_init_verbose_remote(deb_get_fixture_server_url, deb_remote_custom_data_factory):
1632
"""A fixture that initializes are deb remote with verbose data."""

pulp_deb/tests/functional/api/test_publish.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import os
22
import re
3+
import shutil
34
from random import choice
45

56
import pytest
7+
import requests
68
from debian import deb822
79
from django.conf import settings
810

911
from pulpcore.client.pulp_deb.exceptions import ApiException
12+
from pulpcore.pytest_plugin import KEY_V6_MLDSA65_ED25519_PRIVATE, KEY_V6_MLDSA65_ED25519_PUBLIC
1013

14+
from pulp_deb.app.models import AptReleaseSigningService
15+
from pulp_deb.tests.functional.conftest import import_signing_key
1116
from pulp_deb.tests.functional.constants import (
1217
DEB_FIXTURE_ALT_SINGLE_DIST,
1318
DEB_FIXTURE_ARCH,
@@ -382,6 +387,34 @@ def test_publish_signing_services(
382387
deb_delete_repository(repo)
383388

384389

390+
@pytest.mark.skipif(shutil.which("sq") is None, reason="Sequoia sq is required")
391+
def test_pqc_release_signing_service(tmp_path):
392+
"""Verify detached and inline Release signatures made with an ML-DSA key."""
393+
public_key = requests.get(KEY_V6_MLDSA65_ED25519_PUBLIC)
394+
public_key.raise_for_status()
395+
home = tmp_path / "sq-home"
396+
home.mkdir()
397+
_, fingerprint, _ = import_signing_key(KEY_V6_MLDSA65_ED25519_PRIVATE, home, backend="sq")
398+
script = tmp_path / "sign.sh"
399+
script.write_text(
400+
"#!/bin/sh\n"
401+
f'sq --home "{home}" sign --signer {fingerprint} '
402+
'--signature-file="$PULP_TEMP_WORKING_DIR/Release.gpg" "$1"\n'
403+
f'sq --home "{home}" sign --signer {fingerprint} --clear-sign '
404+
'--output="$PULP_TEMP_WORKING_DIR/InRelease" "$1"\n'
405+
'python -c \'import json, os; print(json.dumps({"signatures": '
406+
'{"detached": os.environ["PULP_TEMP_WORKING_DIR"] + "/Release.gpg", '
407+
'"inline": os.environ["PULP_TEMP_WORKING_DIR"] + "/InRelease"}}))\'\n'
408+
)
409+
script.chmod(0o755)
410+
AptReleaseSigningService(
411+
name="pqc-test",
412+
pubkey_fingerprint=fingerprint,
413+
public_key=public_key.text,
414+
script=str(script),
415+
).validate()
416+
417+
385418
@pytest.mark.parallel
386419
def test_publish_repository_version_verbatim(
387420
create_publication_and_verify_repo_version,

pulp_deb/tests/functional/conftest.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -539,29 +539,56 @@ def _deb_copy_content_domain(
539539
return _deb_copy_content_domain
540540

541541

542-
def import_signing_key(key_url, gpg_home):
543-
"""Import a PGP key into a GPG home directory and trust it.
542+
def import_signing_key(key_url, home, *, backend="gpg"):
543+
"""Import a PGP key into a keyring and return metadata.
544544
545-
Returns ``(gpg, fingerprint, keyid)``.
545+
Returns `(gpg_instance_or_none, fingerprint, keyid)`. The first element
546+
is a `gnupg.GPG` instance when `backend` is `"gpg"`, or `None` when
547+
`backend` is `"sq"`.
546548
"""
547-
try:
548-
import gnupg
549-
except ImportError:
550-
pytest.skip("python-gnupg not installed")
551-
552-
gpg = gnupg.GPG(gnupghome=gpg_home)
553-
554549
response = requests.get(key_url)
555550
response.raise_for_status()
556-
result = gpg.import_keys(response.content)
557-
assert result.count >= 1, f"Failed to import key from {key_url}"
558551

559-
key_info = gpg.list_keys()[0]
560-
fingerprint = key_info["fingerprint"]
561-
keyid = key_info["keyid"]
562-
gpg.trust_keys(fingerprint, "TRUST_ULTIMATE")
552+
if backend == "sq":
553+
from pysequoia import Cert
554+
555+
def openpgp_key_id(fingerprint):
556+
"""Return the OpenPGP key ID for a hexadecimal fingerprint.
557+
558+
OpenPGP v4 key IDs use the low-order 64 bits, while v6 key IDs use the
559+
high-order 64 bits. The fingerprint length distinguishes these versions.
560+
"""
561+
return (fingerprint[:16] if len(fingerprint) == 64 else fingerprint[-16:]).upper()
562+
563+
completed = subprocess.run(
564+
("sq", "--home", str(home), "key", "import"),
565+
input=response.content,
566+
capture_output=True,
567+
)
568+
assert completed.returncode == 0, completed.stderr.decode()
569+
570+
cert = Cert.from_bytes(response.content)
571+
fingerprint = cert.fingerprint.upper()
572+
keyid = openpgp_key_id(fingerprint)
573+
574+
return None, fingerprint, keyid
575+
else:
576+
try:
577+
import gnupg
578+
except ImportError:
579+
pytest.skip("python-gnupg not installed")
580+
581+
gpg = gnupg.GPG(gnupghome=home)
582+
583+
result = gpg.import_keys(response.content)
584+
assert result.count >= 1, f"Failed to import key from {key_url}"
585+
586+
key_info = gpg.list_keys()[0]
587+
fingerprint = key_info["fingerprint"]
588+
keyid = key_info["keyid"]
589+
gpg.trust_keys(fingerprint, "TRUST_ULTIMATE")
563590

564-
return gpg, fingerprint, keyid
591+
return gpg, fingerprint, keyid
565592

566593

567594
def create_signing_service(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ requires-python = ">=3.11"
2828
dependencies = [
2929
# All things django and asyncio are deliberately left to pulpcore
3030
# Example transitive requirements: asgiref, asyncio, aiohttp
31-
"pulpcore>=3.113.0,<3.130",
31+
"pulpcore>=3.117.2,<3.130",
3232
"python-debian>=0.1.44,<0.2.0",
3333
"jsonschema>=4.6,<5.0",
3434
]

0 commit comments

Comments
 (0)