diff options
-rw-r--r-- | gemato/openpgp.py | 66 | ||||
-rw-r--r-- | tests/keydata.py | 24 | ||||
-rw-r--r-- | tests/test_openpgp.py | 54 |
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 |