summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2023-01-21 21:04:34 +0100
committerMichał Górny <mgorny@gentoo.org>2023-01-21 21:04:34 +0100
commit11f2afd6c15daaf20571c819482147986bd9c464 (patch)
tree7ee8bb43fc60c1984050de491a15f4c9195149ad
parentee4f947258fbaddddfbaecc3141abd5c59608818 (diff)
downloadgemato-11f2afd6c15daaf20571c819482147986bd9c464.tar.gz
openpgp: Initial support for multiple signatures
Signed-off-by: Michał Górny <mgorny@gentoo.org>
-rw-r--r--gemato/openpgp.py66
-rw-r--r--tests/keydata.py24
-rw-r--r--tests/test_openpgp.py54
3 files changed, 123 insertions, 21 deletions
diff --git a/gemato/openpgp.py b/gemato/openpgp.py
index e85dfd8..57e84fa 100644
--- a/gemato/openpgp.py
+++ b/gemato/openpgp.py
@@ -46,13 +46,36 @@ GNUPG = os.environ.get('GNUPG', 'gpg')
GNUPGCONF = os.environ.get('GNUPGCONF', 'gpgconf')
-@dataclasses.dataclass
+@dataclasses.dataclass(order=True)
class OpenPGPSignatureData:
fingerprint: str = ""
timestamp: typing.Optional[datetime.datetime] = None
expire_timestamp: typing.Optional[datetime.datetime] = None
primary_key_fingerprint: str = ""
+ good_sig: bool = False
+ trusted_sig: bool = False
+
+
+class OpenPGPSignatureList(list[OpenPGPSignatureData]):
+ # backwards compatibility with OpenPGPSignatureData
+
+ @property
+ def fingerprint(self) -> str:
+ return self[0].fingerprint
+
+ @property
+ def timestamp(self) -> typing.Optional[datetime.datetime]:
+ return self[0].timestamp
+
+ @property
+ def expire_timestamp(self) -> typing.Optional[datetime.datetime]:
+ return self[0].expire_timestamp
+
+ @property
+ def primary_key_fingerprint(self) -> str:
+ return self[0].primary_key_fingerprint
+
ZBASE32_TRANSLATE = bytes.maketrans(
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
@@ -131,7 +154,9 @@ class SystemGPGEnvironment:
else:
return datetime.datetime.utcfromtimestamp(int(ts))
- def verify_file(self, f):
+ def verify_file(self,
+ f: typing.IO[str],
+ ) -> OpenPGPSignatureList:
"""
Perform an OpenPGP verification of Manifest data in open file @f.
The file should be open in text mode and set at the beginning
@@ -144,44 +169,49 @@ class SystemGPGEnvironment:
f.read().encode('utf8'),
raise_on_error=OpenPGPVerificationFailure)
- is_good = False
- is_trusted = False
- sig_data = None
-
# process the output of gpg to find the exact result
+ print(out.decode("iso-8859-1"))
+ sig_list = OpenPGPSignatureList()
for line in out.splitlines():
- if line.startswith(b'[GNUPG:] GOODSIG'):
- is_good = True
+ if line.startswith(b'[GNUPG:] NEWSIG'):
+ sig_list.append(OpenPGPSignatureData())
+ elif line.startswith(b'[GNUPG:] GOODSIG'):
+ assert sig_list
+ sig_list[-1].good_sig = True
elif line.startswith(b'[GNUPG:] EXPKEYSIG'):
+ assert sig_list
raise OpenPGPExpiredKeyFailure(
err.decode('utf8', errors='backslashreplace'))
elif line.startswith(b'[GNUPG:] REVKEYSIG'):
+ assert sig_list
raise OpenPGPRevokedKeyFailure(
err.decode('utf8', errors='backslashreplace'))
elif line.startswith(b'[GNUPG:] VALIDSIG'):
+ assert sig_list
spl = line.split(b' ')
assert len(spl) >= 12
- fp = spl[2].decode('utf8')
- ts = self._parse_gpg_ts(spl[4].decode('utf8'))
- expts = self._parse_gpg_ts(spl[5].decode('utf8'))
- pkfp = spl[11].decode('utf8')
-
- sig_data = OpenPGPSignatureData(fp, ts, expts, pkfp)
+ sig_list[-1].fingerprint = spl[2].decode('utf8')
+ sig_list[-1].timestamp = (
+ self._parse_gpg_ts(spl[4].decode('utf8')))
+ sig_list[-1].expiration_timestamp = (
+ self._parse_gpg_ts(spl[5].decode('utf8')))
+ sig_list[-1].primary_key_fingerprint = spl[11].decode('utf8')
elif line.startswith(b'[GNUPG:] TRUST_'):
+ assert sig_list
spl = line.split(b' ', 2)
if spl[1] in (b'TRUST_MARGINAL',
b'TRUST_FULL',
b'TRUST_ULTIMATE'):
- is_trusted = True
+ sig_list[-1].trusted_sig = True
# require both GOODSIG and VALIDSIG
- if not is_good or sig_data is None:
+ if not sig_list or not all(x.good_sig for x in sig_list):
raise OpenPGPUnknownSigFailure(
err.decode('utf8', errors='backslashreplace'))
- if not is_trusted:
+ if not all(x.trusted_sig for x in sig_list):
raise OpenPGPUntrustedSigFailure(
err.decode('utf8', errors='backslashreplace'))
- return sig_data
+ return sig_list
def clear_sign_file(self, f, outf, keyid=None):
"""
diff --git a/tests/keydata.py b/tests/keydata.py
index 844c912..6384c97 100644
--- a/tests/keydata.py
+++ b/tests/keydata.py
@@ -155,6 +155,25 @@ Wq7iapS3DqitGoDRtKyPXeSFDpWsgcAYzghFMI265fqeBebTeKtz7mtYUw4DrBlYXSBPpRte
T1oNst52zSr1Wzuc9w==
''')
+SECOND_SECRET_KEY = base64.b64decode(b"""
+lFgEY8wUwRYJKwYBBAHaRw8BAQdAQ9Y36mOHda8FHRNM/sXEpvzGKJiC733H2OgQtvVrYNsA
+AQCS5w1GsElAdtNFCbpDq5LWp8hNq2jVSH3foz3+CYo1+hCV
+""")
+
+SECOND_PUBLIC_KEY = base64.b64decode(b"""
+mDMEY8wUwRYJKwYBBAHaRw8BAQdAQ9Y36mOHda8FHRNM/sXEpvzGKJiC733H2OgQtvVrYNs=
+""")
+
+SECOND_UID = base64.b64decode(b"""
+tDBTZWNvbmQgZ2VtYXRvIHRlc3QgaWRlbnRpdHkgPHNlY29uZEBleGFtcGxlLmNvbT4=
+""")
+
+SECOND_KEY_SIG = base64.b64decode(b"""
+iJMEExYKADsWIQR1jj6cjPscaH2bJCVTcI9ps0i0zAUCY8wUwQIbAwULCQgHAgIiAgYVCgkI
+CwIEFgIDAQIeBwIXgAAKCRBTcI9ps0i0zEWCAQDEpFQFHMubpdSIdtrFPztMM64Xg4Vkdk+k
+30HoYvFwKwD/aNSymTkZS4R8Ld0mxEJhFml7EAPUf//LjQYEIbe83gQ=
+""")
+
VALID_PUBLIC_KEY = PUBLIC_KEY + UID + PUBLIC_KEY_SIG
EXPIRED_PUBLIC_KEY = PUBLIC_KEY + UID + EXPIRED_KEY_SIG
REVOKED_PUBLIC_KEY = PUBLIC_KEY + REVOCATION_SIG + UID + PUBLIC_KEY_SIG
@@ -190,6 +209,11 @@ UNSIGNED_SUBKEY = PUBLIC_KEY + UID + PUBLIC_KEY_SIG + PUBLIC_SUBKEY
COMBINED_PUBLIC_KEYS = OTHER_VALID_PUBLIC_KEY + VALID_PUBLIC_KEY
+SECOND_VALID_PUBLIC_KEY = SECOND_PUBLIC_KEY + SECOND_UID + SECOND_KEY_SIG
+SECOND_KEY_FINGERPRINT = "758E3E9C8CFB1C687D9B242553708F69B348B4CC"
+
+TWO_SIGNATURE_PUBLIC_KEYS = VALID_PUBLIC_KEY + SECOND_VALID_PUBLIC_KEY
+
if __name__ == "__main__":
import argparse
diff --git a/tests/test_openpgp.py b/tests/test_openpgp.py
index a88ccd9..2b14dcf 100644
--- a/tests/test_openpgp.py
+++ b/tests/test_openpgp.py
@@ -3,6 +3,7 @@
# Licensed under the terms of 2-clause BSD license
import contextlib
+import datetime
import io
import logging
import os
@@ -33,6 +34,8 @@ from gemato.openpgp import (
IsolatedGPGEnvironment,
PGPyEnvironment,
get_wkd_url,
+ OpenPGPSignatureList,
+ OpenPGPSignatureData,
)
from gemato.recursiveloader import ManifestRecursiveLoader
@@ -43,7 +46,7 @@ from tests.keydata import (
OTHER_VALID_PUBLIC_KEY, UNSIGNED_PUBLIC_KEY, FORGED_PUBLIC_KEY,
UNSIGNED_SUBKEY, FORGED_SUBKEY, SIG_TIMESTAMP, SUBKEY_FINGERPRINT,
SUBKEY_SIG_TIMESTAMP, UNEXPIRE_PUBLIC_KEY, OLD_UNEXPIRE_PUBLIC_KEY,
- FORGED_UNEXPIRE_KEY,
+ FORGED_UNEXPIRE_KEY, TWO_SIGNATURE_PUBLIC_KEYS, SECOND_KEY_FINGERPRINT,
)
from tests.test_recursiveloader import INSECURE_HASH_TESTS
from tests.testutil import HKPServer
@@ -186,6 +189,27 @@ n4XmpdPvu+UdAHpQIGzKoNOEDJpZ5CzPLhYa5KgZiJhpYsDXgg==
-----END PGP SIGNATURE-----
"""
+TWO_SIGNATURE_MANIFEST = f"""
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA256
+
+{COMMON_MANIFEST_TEXT}
+-----BEGIN PGP SIGNATURE-----
+
+iQFHBAABCAAxFiEEgeEsFr2NzWC+GAhFE2iA5yp7E4QFAmPMHYQTHGdlbWF0b0Bl
+eGFtcGxlLmNvbQAKCRATaIDnKnsThCDWB/95B9njv423M94uRdpPqSNqTpAokNhy
+V0hjnhpiqnY85iFdL1Zc/rvhuxYbZezrig3dqctLseWYcx2mINBTLZqWHk5/NKEm
+rd8iCdXZU1B7yo/HCfzUYR4HX5wISCiRjKimFFgkWKOg7KYGOqqrwLjAjaYJKmL5
+L7R5joHpGbp87jix7c0ruSIMslQg5PbJ6/YAQWyOPTcZvqMFieJ8tqE/G2FabQcs
+YRHEGu1x8wNY40rFzWd90ICR/hPjXZlCdCN2qk7hs+Coasb29n6pXjmt5L8/ICcL
+zApRg8cetid6/SIzUSwiVqBt7i8noYWbgaazNt3HDlGq55v21dkOhmrXiIkEABYI
+ADEWIQR1jj6cjPscaH2bJCVTcI9ps0i0zAUCY8wd6BMcc2Vjb25kQGV4YW1wbGUu
+Y29tAAoJEFNwj2mzSLTMHKcA/0QbVl3PafYp45PFFo2e/knGKJKrm8D4bUH9wS5h
+dchVAP0RSzkUQPP7Zs+2uHQItkqbXJyrBBHOqjGzeh39sWVuAw==
+=wG4b
+-----END PGP SIGNATURE-----
+"""
+
def strip_openpgp(text):
lines = text.lstrip().splitlines()
@@ -213,6 +237,7 @@ _ = FORGED_SUBKEY
_ = FORGED_UNEXPIRE_KEY
_ = OLD_UNEXPIRE_PUBLIC_KEY
_ = OTHER_VALID_PUBLIC_KEY
+_ = TWO_SIGNATURE_PUBLIC_KEYS
_ = UNEXPIRE_PUBLIC_KEY
_ = UNSIGNED_PUBLIC_KEY
_ = UNSIGNED_SUBKEY
@@ -356,6 +381,8 @@ MANIFEST_VARIANTS = [
('SIGNED_MANIFEST', 'COMBINED_PUBLIC_KEYS', None),
('DASH_ESCAPED_SIGNED_MANIFEST', 'VALID_PUBLIC_KEY', None),
('SUBKEY_SIGNED_MANIFEST', 'VALID_KEY_SUBKEY', None),
+ # == Manifest with two signatures ==
+ ("TWO_SIGNATURE_MANIFEST", "TWO_SIGNATURE_PUBLIC_KEYS", None),
# == using private key ==
('SIGNED_MANIFEST', 'PRIVATE_KEY', None),
# == bad manifests ==
@@ -383,14 +410,35 @@ MANIFEST_VARIANTS = [
]
-def assert_signature(sig, manifest_var):
+def assert_signature(sig: OpenPGPSignatureList,
+ manifest_var: str,
+ ) -> None:
"""Make assertions about the signature"""
- if manifest_var == 'SUBKEY_SIGNED_MANIFEST':
+ if manifest_var == "TWO_SIGNATURE_MANIFEST":
+ assert sorted(sig) == [
+ OpenPGPSignatureData(
+ fingerprint=SECOND_KEY_FINGERPRINT,
+ timestamp=datetime.datetime(2023, 1, 21, 17, 16, 24),
+ primary_key_fingerprint=SECOND_KEY_FINGERPRINT,
+ good_sig=True,
+ trusted_sig=True,
+ ),
+ OpenPGPSignatureData(
+ fingerprint=KEY_FINGERPRINT,
+ timestamp=datetime.datetime(2023, 1, 21, 17, 14, 44),
+ primary_key_fingerprint=KEY_FINGERPRINT,
+ good_sig=True,
+ trusted_sig=True,
+ ),
+ ]
+ elif manifest_var == 'SUBKEY_SIGNED_MANIFEST':
+ assert len(sig) == 1
assert sig.fingerprint == SUBKEY_FINGERPRINT
assert sig.timestamp == SUBKEY_SIG_TIMESTAMP
assert sig.expire_timestamp is None
assert sig.primary_key_fingerprint == KEY_FINGERPRINT
else:
+ assert len(sig) == 1
assert sig.fingerprint == KEY_FINGERPRINT
assert sig.timestamp == SIG_TIMESTAMP
assert sig.expire_timestamp is None