Skip to content

Commit 9827fcc

Browse files
committed
Remove python-gnupg dependency
Use pysequoia instead, or shell out to gpg on the command line, which is what python-gnupg does anyway. It's not much additional code to just ditch the dependency everywhere. Assisted-By: claude-opus-4.6
1 parent 829fd36 commit 9827fcc

7 files changed

Lines changed: 117 additions & 40 deletions

File tree

functest_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pytest<10
22
pytest-custom_exit_code
33
pytest-xdist
4-
python-gnupg
4+
pysequoia
55
proxy.py~=2.4.10
66
trustme~=1.2.1
77

pulpcore/app/management/commands/add-signing-service.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
2-
3-
import gnupg
2+
import subprocess
43

54
from pathlib import Path
65

@@ -72,13 +71,33 @@ def handle(self, *args, **options):
7271
)
7372
)
7473

75-
gpg = gnupg.GPG(gnupghome=options["gnupghome"], keyring=options["keyring"])
74+
gpg_cmd = ["gpg"]
75+
if options["gnupghome"]:
76+
gpg_cmd += ["--homedir", options["gnupghome"]]
77+
if options["keyring"]:
78+
gpg_cmd += ["--keyring", options["keyring"]]
7679

77-
key_list = gpg.list_keys(keys=[key_id])
78-
if not len(key_list) == 1:
79-
raise CommandError(_("There are {} keys matching the key id.").format(len(key_list)))
80-
fingerprint = key_list[0]["fingerprint"]
81-
public_key = gpg.export_keys(key_id)
80+
result = subprocess.run(
81+
gpg_cmd + ["--with-colons", "--fingerprint", key_id],
82+
capture_output=True,
83+
text=True,
84+
)
85+
if result.returncode != 0:
86+
raise CommandError(result.stderr.strip())
87+
88+
fpr_lines = [line for line in result.stdout.splitlines() if line.startswith("fpr:")]
89+
if len(fpr_lines) != 1:
90+
raise CommandError(_("There are {} keys matching the key id.").format(len(fpr_lines)))
91+
fingerprint = fpr_lines[0].split(":")[9]
92+
93+
result = subprocess.run(
94+
gpg_cmd + ["--armor", "--export", key_id],
95+
capture_output=True,
96+
text=True,
97+
)
98+
if result.returncode != 0:
99+
raise CommandError(result.stderr.strip())
100+
public_key = result.stdout
82101

83102
try:
84103
script_path = Path(script).resolve(strict=True)

pulpcore/app/models/openpgp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def represent(self, repository_version=None):
4848
else:
4949
content_filter = {}
5050
data = self.packet()
51+
# note: because these queries aren't ordered, the result may be nondetermininistic
5152
for signature in self.openpgp_signatures.filter(**content_filter):
5253
data += signature.packet()
5354
for user_id in self.user_ids.filter(**content_filter):

pulpcore/app/util.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
import zlib
33
import os
44
import socket
5-
import tempfile
65
from io import RawIOBase
76
from pathlib import Path
87
from types import TracebackType
98
from typing import Self, IO, Any
109

11-
import gnupg
12-
1310
from functools import lru_cache
1411
from gettext import gettext as _
1512
from urllib.parse import urlparse
@@ -384,9 +381,45 @@ def get_request_without_query_params(context):
384381
return request
385382

386383

384+
class VerifyResult:
385+
"""
386+
Verification result mimicking the interface of gnupg.Verify for compatibility.
387+
388+
Attributes:
389+
valid (bool): Always True; invalid signatures raise InvalidSignatureError instead.
390+
fingerprint (str): Fingerprint of the signing subkey (uppercase hex).
391+
pubkey_fingerprint (str): Fingerprint of the signing certificate (uppercase hex).
392+
key_id (str): Short (16-char) key ID derived from the fingerprint.
393+
username (str): Empty string; primary UID not available without extending pysequoia.
394+
data (bytes or None): The verified plaintext content for inline signatures, None for
395+
detached signatures.
396+
"""
397+
398+
def __init__(self, decrypted):
399+
self.valid = True
400+
self.data = bytes(decrypted.bytes) if decrypted.bytes is not None else None
401+
if decrypted.valid_sigs:
402+
vs = decrypted.valid_sigs[0]
403+
self.fingerprint = vs.signing_key.upper()
404+
self.pubkey_fingerprint = vs.certificate.upper()
405+
self.key_id = vs.signing_key[-16:].upper()
406+
self.username = ""
407+
else:
408+
self.fingerprint = None
409+
self.pubkey_fingerprint = None
410+
self.key_id = None
411+
self.username = ""
412+
413+
def __repr__(self):
414+
return (
415+
f"<VerifyResult valid={self.valid} fingerprint={self.fingerprint}"
416+
f" key_id={self.key_id}>"
417+
)
418+
419+
387420
def gpg_verify(public_keys, signature, detached_data=None):
388421
"""
389-
Check whether the provided gnupg signature is valid for one of the provided public keys.
422+
Check whether the provided signature is valid for one of the provided public keys.
390423
391424
Args:
392425
public_keys (str): Ascii armored public key data
@@ -395,25 +428,37 @@ def gpg_verify(public_keys, signature, detached_data=None):
395428
signature
396429
397430
Returns:
398-
gnupg.Verify: The result of the verification
431+
VerifyResult: The verification result with `valid`, `fingerprint`, `pubkey_fingerprint`,
432+
`key_id`, `username`, and `data` attributes, mimicking the gnupg.Verify interface.
399433
400434
Raises:
401435
pulpcore.exceptions.validation.InvalidSignatureError: In case the signature is invalid.
402436
"""
403-
with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_directory_name:
404-
gpg = gnupg.GPG(gnupghome=temp_directory_name)
405-
gpg.import_keys(public_keys)
406-
407-
with ExitStack() as stack:
408-
if isinstance(signature, str):
409-
signature = stack.enter_context(open(signature, "rb"))
410-
elif isinstance(signature, models.Artifact):
411-
signature = stack.enter_context(signature.file)
412-
413-
verified = gpg.verify_file(signature, detached_data)
414-
if not verified.valid:
415-
raise InvalidSignatureError(_("The signature is not valid."), verified=verified)
416-
return verified
437+
from pysequoia import Cert, Sig, verify
438+
439+
certs = Cert.split_bytes(public_keys.encode("utf8"))
440+
441+
def store(key_ids):
442+
return certs
443+
444+
with ExitStack() as stack:
445+
if isinstance(signature, str):
446+
sig_data = stack.enter_context(open(signature, "rb")).read()
447+
elif isinstance(signature, models.Artifact):
448+
sig_data = stack.enter_context(signature.file).read()
449+
else:
450+
sig_data = signature.read()
451+
452+
try:
453+
sig = Sig.from_bytes(sig_data)
454+
if detached_data is not None:
455+
result = verify(file=detached_data, store=store, signature=sig)
456+
else:
457+
result = verify(bytes=sig_data, store=store)
458+
except Exception:
459+
raise InvalidSignatureError(_("The signature is not valid."))
460+
461+
return VerifyResult(result)
417462

418463

419464
def compute_file_hash(filename, hasher=None, cumulative_hash=None, blocksize=8192):

pulpcore/exceptions/validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def __str__(self):
108108

109109
class InvalidSignatureError(ValidationError):
110110
"""
111-
Raised when a signature could not be verified by the GnuPG utility.
111+
Raised when a signature could not be verified.
112112
"""
113113

114114
error_code = "PLP0021"

pulpcore/pytest_plugin.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import aiohttp
22
import asyncio
3-
import gnupg
43
import json
54
import os
65
import pathlib
@@ -1149,22 +1148,36 @@ def signing_gpg_metadata(signing_gpg_homedir_path):
11491148
with suppress(FileNotFoundError, PermissionError):
11501149
key_file.write_text(private_key_data)
11511150

1152-
gpg = gnupg.GPG(gnupghome=signing_gpg_homedir_path)
1153-
gpg.import_keys(private_key_data)
1151+
from pysequoia import Cert
11541152

1155-
fingerprint = gpg.list_keys()[0]["fingerprint"]
1156-
keyid = gpg.list_keys()[0]["keyid"]
1153+
cert = Cert.from_bytes(private_key_data.encode())
1154+
fingerprint = cert.fingerprint.upper()
1155+
keyid = fingerprint[-16:]
11571156

1158-
gpg.trust_keys(fingerprint, "TRUST_ULTIMATE")
1157+
gpg_cmd = ["gpg", "--homedir", str(signing_gpg_homedir_path)]
1158+
subprocess.run(
1159+
gpg_cmd + ["--import"],
1160+
input=private_key_data,
1161+
capture_output=True,
1162+
text=True,
1163+
check=True,
1164+
)
1165+
subprocess.run(
1166+
gpg_cmd + ["--import-ownertrust"],
1167+
input=f"{fingerprint}:6:\n",
1168+
capture_output=True,
1169+
text=True,
1170+
check=True,
1171+
)
11591172

1160-
return gpg, fingerprint, keyid
1173+
return cert, fingerprint, keyid
11611174

11621175

11631176
@pytest.fixture(scope="session")
11641177
def pulp_trusted_public_key(signing_gpg_metadata):
11651178
"""Fixture to extract the ascii armored trusted public test key."""
1166-
gpg, _, keyid = signing_gpg_metadata
1167-
return gpg.export_keys([keyid])
1179+
cert, _, keyid = signing_gpg_metadata
1180+
return str(cert)
11681181

11691182

11701183
@pytest.fixture(scope="session")
@@ -1180,7 +1193,7 @@ def _ascii_armored_detached_signing_service_name(
11801193
signing_gpg_homedir_path,
11811194
):
11821195
service_name = str(uuid.uuid4())
1183-
gpg, fingerprint, keyid = signing_gpg_metadata
1196+
_, fingerprint, keyid = signing_gpg_metadata
11841197

11851198
cmd = (
11861199
"pulpcore-manager",

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ dependencies = [
5757
"pygtrie>=2.5,<=2.5.0",
5858
"psycopg[binary]>=3.1.8,<3.4", # SemVer, not explicitely stated, but mentioned on multiple changes.
5959
"pyparsing>=3.1.0,<3.4", # Looks like only bugfixes in z-Stream.
60-
"python-gnupg>=0.5.0,<0.6", # Looks like only bugfixes in z-Stream [changelog only in git]
6160
"pysequoia==0.1.32",
6261
"PyYAML>=5.1.1,<6.1", # Looks like only bugfixes in z-Stream.
6362
"redis>=4.3.0,<7.2", # Looks like only bugfixes in z-Stream.

0 commit comments

Comments
 (0)