From cbd2fa14bd06b2e1de68106715205f99de191009 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 3 Nov 2022 14:52:22 +0300 Subject: [PATCH 001/287] Remove unneeded inheritance on `object` --- pgpy/_curves.py | 10 +++++----- pgpy/decorators.py | 4 ++-- pgpy/packet/types.py | 2 +- pgpy/pgp.py | 2 +- pgpy/types.py | 4 ++-- tests/test_01_packetfields.py | 8 ++++---- tests/test_01_types.py | 2 +- tests/test_02_packets.py | 2 +- tests/test_03_armor.py | 4 ++-- tests/test_04_PGP_objects.py | 8 ++++---- tests/test_05_actions.py | 6 +++--- tests/test_10_exceptions.py | 18 +++++++++--------- 12 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pgpy/_curves.py b/pgpy/_curves.py index 14f25284..69f546f7 100644 --- a/pgpy/_curves.py +++ b/pgpy/_curves.py @@ -44,31 +44,31 @@ def use_legacy_cryptography_decorator(): if use_legacy_cryptography_decorator(): @utils.register_interface(ec.EllipticCurve) - class BrainpoolP256R1(object): + class BrainpoolP256R1: name = 'brainpoolP256r1' key_size = 256 @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class BrainpoolP384R1(object): + class BrainpoolP384R1: name = 'brainpoolP384r1' key_size = 384 @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class BrainpoolP512R1(object): + class BrainpoolP512R1: name = 'brainpoolP512r1' key_size = 512 @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class X25519(object): + class X25519: name = 'X25519' key_size = 256 @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class Ed25519(object): + class Ed25519: name = 'ed25519' key_size = 256 else: diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 8a050c15..52de8d46 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -20,7 +20,7 @@ def classproperty(fget): - class ClassProperty(object): + class ClassProperty: def __init__(self, fget): self.fget = fget self.__doc__ = fget.__doc__ @@ -69,7 +69,7 @@ def setter(self, fset): return SDProperty(fget, sdmethod(defset)) -class KeyAction(object): +class KeyAction: def __init__(self, *usage, **conditions): super(KeyAction, self).__init__() self.flags = set(usage) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 969783da..36b76a4a 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -214,7 +214,7 @@ def parse(self, packet): # pragma: no cover # key marker classes for convenience -class Key(object): +class Key: pass diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..e0610d48 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2619,7 +2619,7 @@ def _getpkt(d): getpkt = filter(lambda p: p.header.tag != PacketTag.Trust, iter(functools.partial(_getpkt, data), None)) def pktgrouper(): - class PktGrouper(object): + class PktGrouper: def __init__(self): self.last = None diff --git a/pgpy/types.py b/pgpy/types.py index 187d3984..efc5a349 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -224,7 +224,7 @@ def __copy__(self): return obj -class ParentRef(object): +class ParentRef: # mixin class to handle weak-referencing a parent object @property def _parent(self): @@ -560,7 +560,7 @@ def __typeid__(self): # pragma: no cover __ver__ = None -class SignatureVerification(object): +class SignatureVerification: __slots__ = ("_subjects",) _sigsubj = collections.namedtuple('sigsubj', ['issues', 'by', 'signature', 'subject']) diff --git a/tests/test_01_packetfields.py b/tests/test_01_packetfields.py index fcd9a411..0803596e 100644 --- a/tests/test_01_packetfields.py +++ b/tests/test_01_packetfields.py @@ -51,7 +51,7 @@ ] -class TestHeaders(object): +class TestHeaders: @pytest.mark.parametrize('pheader', pkt_headers) def test_packet_header(self, pheader): b = pheader[:] @@ -197,7 +197,7 @@ def test_subpacket_header(self, spheader): sig_subpkts = [bytearray(sp) + _trailer for sp in _ssps] -class TestSignatureSubPackets(object): +class TestSignatureSubPackets: @pytest.mark.parametrize('sigsubpacket', sig_subpkts) def test_load(self, sigsubpacket): spb = sigsubpacket[:] @@ -267,7 +267,7 @@ def test_load(self, sigsubpacket): ua_subpkts = [bytearray(sp) + _trailer for sp in _uassps] -class TestUserAttributeSubPackets(object): +class TestUserAttributeSubPackets: @pytest.mark.parametrize('uasubpacket', ua_subpkts) def test_load(self, uasubpacket): spb = uasubpacket[:] @@ -317,7 +317,7 @@ def test_load(self, uasubpacket): gnus2ks = [bytearray(b'\xff\x00\x65\x00GNU' + i) for i in ([b'\x01'] + [b'\x02' + bytearray([len(s)]) + s for s in _gnu_scserials])] -class TestString2Key(object): +class TestString2Key: @pytest.mark.parametrize('sis2k', sis2ks) def test_simple_string2key(self, sis2k): b = sis2k[:] diff --git a/tests/test_01_types.py b/tests/test_01_types.py index c425c96f..1f24ce8a 100644 --- a/tests/test_01_types.py +++ b/tests/test_01_types.py @@ -46,7 +46,7 @@ def parse(self, packet): self.data = packet -class TestPGPObject(object): +class TestPGPObject: @pytest.mark.regression(issue=154) @pytest.mark.parametrize('text', [v for _, v in sorted(text.items())], ids=sorted(text.keys())) def test_text_to_bytes(self, text): diff --git a/tests/test_02_packets.py b/tests/test_02_packets.py index 0e31d641..61e82aa4 100644 --- a/tests/test_02_packets.py +++ b/tests/test_02_packets.py @@ -43,7 +43,7 @@ def binload(f): pktfiles = sorted(glob.glob('tests/testdata/packets/[0-9]*')) -class TestPacket(object): +class TestPacket: @pytest.mark.parametrize('packet', pktfiles, ids=[os.path.basename(f) for f in pktfiles]) def test_load(self, packet): b = binload(packet) + _trailer diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index e32ee1e9..9f4f1bde 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -297,7 +297,7 @@ # generic block tests -class TestBlocks(object): +class TestBlocks: @pytest.mark.parametrize('block', blocks, ids=[os.path.basename(f) for f in blocks]) def test_load_blob(self, block): with open(block) as bf: @@ -338,7 +338,7 @@ def test_load_blob(self, block): raw = txt + binary # armor matching test -class TestMatching(object): +class TestMatching: @pytest.mark.parametrize('armored', armored, ids=[os.path.basename(f) for f in armored]) def test_is_armor(self, armored): with open(armored) as af: diff --git a/tests/test_04_PGP_objects.py b/tests/test_04_PGP_objects.py index b8d5b392..04216eb5 100644 --- a/tests/test_04_PGP_objects.py +++ b/tests/test_04_PGP_objects.py @@ -24,7 +24,7 @@ def abe_image(): _msgfiles = sorted(glob.glob('tests/testdata/messages/*.asc')) -class TestPGPMessage(object): +class TestPGPMessage: @pytest.mark.parametrize('msgfile', _msgfiles, ids=[os.path.basename(f) for f in _msgfiles]) def test_load_from_file(self, msgfile): # TODO: figure out a good way to verify that all went well here, because @@ -63,7 +63,7 @@ def abe(): return PGPUID.new('Abraham Lincoln', comment='Honest Abe', email='abraham.lincoln@whitehouse.gov') -class TestPGPUID(object): +class TestPGPUID: def test_userid(self, abe): assert abe.name == 'Abraham Lincoln' assert abe.comment == 'Honest Abe' @@ -97,7 +97,7 @@ def test_format(self, un, unc, une, unce): 'rsaseckey.asc': 'F4294BC8094A7E0585C85E8637473B3758C44F36',} -class TestPGPKey(object): +class TestPGPKey: @pytest.mark.parametrize('kf', _keyfiles, ids=[os.path.basename(f) for f in _keyfiles]) def test_load_from_file(self, kf): key, _ = PGPKey.from_file(kf) @@ -147,7 +147,7 @@ def keyring(): return PGPKeyring() -class TestPGPKeyring(object): +class TestPGPKeyring: def test_load(self, keyring): # load from filenames keys = keyring.load(glob.glob('tests/testdata/*test.asc'), glob.glob('tests/testdata/signatures/*.key.asc')) diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 8a801c9c..d1716e2a 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -40,7 +40,7 @@ enc_msgs = [ PGPMessage.from_file(f) for f in sorted(glob.glob('tests/testdata/messages/message*.pass*.asc')) ] -class TestPGPMessage(object): +class TestPGPMessage: @staticmethod def gpg_message(msg): ret = None @@ -237,7 +237,7 @@ def userphoto(): (PubKeyAlgorithm.ECDH, EllipticCurveOID.Curve25519),) -class TestPGPKey_Management(object): +class TestPGPKey_Management: # test PGPKey management actions, e.g.: # - key/subkey generation # - adding/removing UIDs @@ -622,7 +622,7 @@ def targette_sec(): pubkeys = [ PGPKey.from_file(f)[0] for f in sorted(glob.glob('tests/testdata/keys/*.pub.asc')) ] -class TestPGPKey_Actions(object): +class TestPGPKey_Actions: sigs = {} msgs = {} diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 299f3456..8bfc0cc7 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -85,7 +85,7 @@ def temp_key(): badkeyspec = [ (alg, size) for alg in key_algs_badsizes.keys() for size in key_algs_badsizes[alg] ] -class TestArmorable(object): +class TestArmorable: # some basic test cases specific to the Armorable mixin class def test_malformed_base64(self): # 'asdf' base64-encoded becomes 'YXNkZg==' @@ -99,7 +99,7 @@ def test_malformed_base64(self): Armorable.ascii_unarmor(data) -class TestMetaDispatchable(object): +class TestMetaDispatchable: # test a couple of error cases in MetaDispatchable that affect all packet classes def test_parse_bytes_typeerror(self): # use a marker packet, but don't wrap it in a bytearray to get a TypeError @@ -146,7 +146,7 @@ class WhatException(Exception): pass fuzz_pkt(5, 0x64, None) -class TestPGPKey(object): +class TestPGPKey: def test_unlock_pubkey(self, rsa_pub, recwarn): with rsa_pub.unlock("QwertyUiop") as _unlocked: assert _unlocked is rsa_pub @@ -343,7 +343,7 @@ def test_add_key_with_subkeys_as_subkey(self, rsa_sec, temp_key): rsa_sec.add_subkey(temp_key) -class TestPGPKeyring(object): +class TestPGPKeyring: kr = PGPKeyring(_read('tests/testdata/pubtest.asc')) def test_key_keyerror(self): @@ -352,7 +352,7 @@ def test_key_keyerror(self): pass -class TestPGPMessage(object): +class TestPGPMessage: def test_decrypt_unsupported_algorithm(self): msg = PGPMessage.from_file('tests/testdata/message.enc.twofish.asc') with pytest.raises(PGPDecryptionError): @@ -390,7 +390,7 @@ def test_parse_wrong_magic(self): msg.parse(msgtext) -class TestPGPSignature(object): +class TestPGPSignature: @pytest.mark.parametrize('inp', [12, None]) def test_or_typeerror(self, inp): with pytest.raises(TypeError): @@ -409,20 +409,20 @@ def test_parse_wrong_contents(self): sig.parse(notsigtext) -class TestPGPUID(object): +class TestPGPUID: def test_or_typeerror(self): u = PGPUID.new("Asdf Qwert") with pytest.raises(TypeError): u |= 12 -class TestSignatureVerification(object): +class TestSignatureVerification: def test_and_typeerror(self): with pytest.raises(TypeError): sv = SignatureVerification() & 12 -class TestFingerprint(object): +class TestFingerprint: def test_bad_input(self): with pytest.raises(ValueError): Fingerprint("ABCDEFG") From 1497a55b8ddc09667d03a8bde619072310c610d8 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 3 Nov 2022 14:52:22 +0300 Subject: [PATCH 002/287] Replaced old-style `super` with the modern one. --- pgpy/decorators.py | 2 +- pgpy/packet/fields.py | 60 ++++++------- pgpy/packet/packets.py | 82 +++++++++--------- pgpy/packet/subpackets/signature.py | 108 ++++++++++++------------ pgpy/packet/subpackets/types.py | 12 +-- pgpy/packet/subpackets/userattribute.py | 6 +- pgpy/packet/types.py | 20 ++--- pgpy/pgp.py | 20 ++--- pgpy/types.py | 10 +-- 9 files changed, 160 insertions(+), 160 deletions(-) diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 52de8d46..5a494094 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -71,7 +71,7 @@ def setter(self, fset): class KeyAction: def __init__(self, *usage, **conditions): - super(KeyAction, self).__init__() + super().__init__() self.flags = set(usage) self.conditions = conditions diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c94021c6..cd34ed7e 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -107,7 +107,7 @@ class SubPackets(collections_abc.MutableMapping, Field): _spmodule = signature def __init__(self): - super(SubPackets, self).__init__() + super().__init__() self._hashed_sp = collections.OrderedDict() self._unhashed_sp = collections.OrderedDict() @@ -267,7 +267,7 @@ def from_signer(self, sig): class OpaqueSignature(Signature): def __init__(self): - super(OpaqueSignature, self).__init__() + super().__init__() self.data = bytearray() def __bytearray__(self): @@ -384,7 +384,7 @@ def __mpis__(self): yield i def __init__(self): - super(PubKey, self).__init__() + super().__init__() for field in self.__pubfields__: if isinstance(field, tuple): # pragma: no cover field, val = field @@ -415,7 +415,7 @@ def verify(self, subj, sigbytes, hash_alg): class OpaquePubKey(PubKey): # pragma: no cover def __init__(self): - super(OpaquePubKey, self).__init__() + super().__init__() self.data = bytearray() def __iter__(self): @@ -553,7 +553,7 @@ class ECDSAPub(PubKey): __pubfields__ = ('p',) def __init__(self): - super(ECDSAPub, self).__init__() + super().__init__() self.oid = None def __len__(self): @@ -569,7 +569,7 @@ def __bytearray__(self): return _b def __copy__(self): - pkt = super(ECDSAPub, self).__copy__() + pkt = super().__copy__() pkt.oid = self.oid return pkt @@ -599,7 +599,7 @@ class EdDSAPub(PubKey): __pubfields__ = ('p', ) def __init__(self): - super(EdDSAPub, self).__init__() + super().__init__() self.oid = None def __len__(self): @@ -615,7 +615,7 @@ def __pubkey__(self): return ed25519.Ed25519PublicKey.from_public_bytes(self.p.x) def __copy__(self): - pkt = super(EdDSAPub, self).__copy__() + pkt = super().__copy__() pkt.oid = self.oid return pkt @@ -650,7 +650,7 @@ class ECDHPub(PubKey): __pubfields__ = ('p',) def __init__(self): - super(ECDHPub, self).__init__() + super().__init__() self.oid = None self.kdf = ECKDF() @@ -671,7 +671,7 @@ def __bytearray__(self): return _b def __copy__(self): - pkt = super(ECDHPub, self).__copy__() + pkt = super().__copy__() pkt.oid = self.oid pkt.kdf = copy.copy(self.kdf) return pkt @@ -876,7 +876,7 @@ def count_int(self, val): self._count = val def __init__(self): - super(String2Key, self).__init__() + super().__init__() self.usage = 0 self.encalg = 0 self.specifier = 0 @@ -1092,7 +1092,7 @@ def encalg_int(self, val): self._encalg = SymmetricKeyAlgorithm(val) def __init__(self): - super(ECKDF, self).__init__() + super().__init__() self.halg = 0 self.encalg = 0 @@ -1143,14 +1143,14 @@ class PrivKey(PubKey): @property def __mpis__(self): - for i in super(PrivKey, self).__mpis__: + for i in super().__mpis__: yield i for i in self.__privfields__: yield i def __init__(self): - super(PrivKey, self).__init__() + super().__init__() self.s2k = String2Key() self.encbytes = bytearray() @@ -1161,7 +1161,7 @@ def __init__(self): def __bytearray__(self): _bytes = bytearray() - _bytes += super(PrivKey, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.s2k.__bytearray__() if self.s2k: @@ -1177,7 +1177,7 @@ def __bytearray__(self): return _bytes def __len__(self): - nbytes = super(PrivKey, self).__len__() + len(self.s2k) + len(self.chksum) + nbytes = super().__len__() + len(self.s2k) + len(self.chksum) if self.s2k: nbytes += len(self.encbytes) @@ -1187,7 +1187,7 @@ def __len__(self): return nbytes def __copy__(self): - pk = super(PrivKey, self).__copy__() + pk = super().__copy__() pk.s2k = copy.copy(self.s2k) pk.encbytes = copy.copy(self.encbytes) pk.chksum = copy.copy(self.chksum) @@ -1205,7 +1205,7 @@ def _compute_chksum(self): "Calculate the key checksum" def publen(self): - return super(PrivKey, self).__len__() + return super().__len__() def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): # PGPy will only ever use iterated and salted S2k mode @@ -1332,7 +1332,7 @@ def _generate(self, key_size): self._compute_chksum() def parse(self, packet): - super(RSAPriv, self).parse(packet) + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1350,7 +1350,7 @@ def parse(self, packet): self.encbytes = packet def decrypt_keyblob(self, passphrase): - kb = super(RSAPriv, self).decrypt_keyblob(passphrase) + kb = super().decrypt_keyblob(passphrase) del passphrase self.d = MPI(kb) @@ -1398,7 +1398,7 @@ def _generate(self, key_size): self._compute_chksum() def parse(self, packet): - super(DSAPriv, self).parse(packet) + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1412,7 +1412,7 @@ def parse(self, packet): del packet[:2] def decrypt_keyblob(self, passphrase): - kb = super(DSAPriv, self).decrypt_keyblob(passphrase) + kb = super().decrypt_keyblob(passphrase) del passphrase self.x = MPI(kb) @@ -1439,7 +1439,7 @@ def _generate(self, key_size): raise NotImplementedError(PubKeyAlgorithm.ElGamal) def parse(self, packet): - super(ElGPriv, self).parse(packet) + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1453,7 +1453,7 @@ def parse(self, packet): del packet[:2] def decrypt_keyblob(self, passphrase): - kb = super(ElGPriv, self).decrypt_keyblob(passphrase) + kb = super().decrypt_keyblob(passphrase) del passphrase self.x = MPI(kb) @@ -1490,7 +1490,7 @@ def _generate(self, oid): self._compute_chksum() def parse(self, packet): - super(ECDSAPriv, self).parse(packet) + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1504,7 +1504,7 @@ def parse(self, packet): self.encbytes = packet def decrypt_keyblob(self, passphrase): - kb = super(ECDSAPriv, self).decrypt_keyblob(passphrase) + kb = super().decrypt_keyblob(passphrase) del passphrase self.s = MPI(kb) @@ -1543,7 +1543,7 @@ def _generate(self, oid): self._compute_chksum() def parse(self, packet): - super(EdDSAPriv, self).parse(packet) + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1556,7 +1556,7 @@ def parse(self, packet): self.encbytes = packet def decrypt_keyblob(self, passphrase): - kb = super(EdDSAPriv, self).decrypt_keyblob(passphrase) + kb = super().decrypt_keyblob(passphrase) del passphrase self.s = MPI(kb) @@ -1640,7 +1640,7 @@ def sign(self, sigdata, hash_alg): class CipherText(MPIs): def __init__(self): - super(CipherText, self).__init__() + super().__init__() for i in self.__mpis__: setattr(self, i, MPI(0)) @@ -1773,7 +1773,7 @@ def decrypt(self, pk, *args): return padder.update(_m) + padder.finalize() def __init__(self): - super(ECDHCipherText, self).__init__() + super().__init__() self.c = bytearray(0) def __bytearray__(self): diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index cd494137..517ce8b1 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -183,14 +183,14 @@ def pkalg_int(self, val): self.ct = ct() if ct is not None else ct def __init__(self): - super(PKESessionKeyV3, self).__init__() + super().__init__() self.encrypter = bytearray(8) self.pkalg = 0 self.ct = None def __bytearray__(self): _bytes = bytearray() - _bytes += super(PKESessionKeyV3, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += binascii.unhexlify(self.encrypter.encode()) _bytes += bytearray([self.pkalg]) _bytes += self.ct.__bytearray__() if self.ct is not None else b'\x00' * (self.header.length - 10) @@ -270,7 +270,7 @@ def encrypt_sk(self, pk, symalg, symkey): self.update_hlen() def parse(self, packet): - super(PKESessionKeyV3, self).parse(packet) + super().parse(packet) self.encrypter = packet[:8] del packet[:8] @@ -459,7 +459,7 @@ def __copy__(self): def update_hlen(self): self.subpackets.update_hlen() - super(SignatureV4, self).update_hlen() + super().update_hlen() def parse(self, packet): super(Signature, self).parse(packet) @@ -551,13 +551,13 @@ def symalg(self): return self.s2k.encalg def __init__(self): - super(SKESessionKeyV4, self).__init__() + super().__init__() self.s2k = String2Key() self.ct = bytearray() def __bytearray__(self): _bytes = bytearray() - _bytes += super(SKESessionKeyV4, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.s2k.__bytearray__()[1:] _bytes += self.ct return _bytes @@ -571,7 +571,7 @@ def __copy__(self): return sk def parse(self, packet): - super(SKESessionKeyV4, self).parse(packet) + super().parse(packet) # prepend a valid usage identifier so this parses correctly packet.insert(0, 255) self.s2k.parse(packet, iv=False) @@ -704,7 +704,7 @@ def signer_bin(self, val): self._signer = binascii.hexlify(val).upper().decode('latin-1') def __init__(self): - super(OnePassSignatureV3, self).__init__() + super().__init__() self._sigtype = None self._halg = None self._pubalg = None @@ -713,7 +713,7 @@ def __init__(self): def __bytearray__(self): _bytes = bytearray() - _bytes += super(OnePassSignatureV3, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += bytearray([self.sigtype]) _bytes += bytearray([self.halg]) _bytes += bytearray([self.pubalg]) @@ -722,7 +722,7 @@ def __bytearray__(self): return _bytes def parse(self, packet): - super(OnePassSignatureV3, self).parse(packet) + super().parse(packet) self.sigtype = packet[0] del packet[0] @@ -846,14 +846,14 @@ def fingerprint(self): return Fingerprint(fp.hexdigest().upper()) def __init__(self): - super(PubKeyV4, self).__init__() + super().__init__() self.created = datetime.now(timezone.utc) self.pkalg = 0 self.keymaterial = None def __bytearray__(self): _bytes = bytearray() - _bytes += super(PubKeyV4, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4) _bytes += self.int_to_bytes(self.pkalg) _bytes += self.keymaterial.__bytearray__() @@ -872,7 +872,7 @@ def verify(self, subj, sigbytes, hash_alg): return self.keymaterial.verify(subj, sigbytes, hash_alg) def parse(self, packet): - super(PubKeyV4, self).parse(packet) + super().parse(packet) self.created = packet[:4] del packet[:4] @@ -996,13 +996,13 @@ def calg_int(self, val): self._calg = CompressionAlgorithm(val) def __init__(self): - super(CompressedData, self).__init__() + super().__init__() self._calg = None self.packets = [] def __bytearray__(self): _bytes = bytearray() - _bytes += super(CompressedData, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += bytearray([self.calg]) _pb = bytearray() @@ -1013,7 +1013,7 @@ def __bytearray__(self): return _bytes def parse(self, packet): - super(CompressedData, self).parse(packet) + super().parse(packet) self.calg = packet[0] del packet[0] @@ -1071,12 +1071,12 @@ class SKEData(Packet): __typeid__ = 0x09 def __init__(self): - super(SKEData, self).__init__() + super().__init__() self.ct = bytearray() def __bytearray__(self): _bytes = bytearray() - _bytes += super(SKEData, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.ct return _bytes @@ -1086,7 +1086,7 @@ def __copy__(self): return skd def parse(self, packet): - super(SKEData, self).parse(packet) + super().parse(packet) self.ct = packet[:self.header.length] del packet[:self.header.length] @@ -1115,17 +1115,17 @@ class Marker(Packet): __typeid__ = 0x0a def __init__(self): - super(Marker, self).__init__() + super().__init__() self.data = b'PGP' def __bytearray__(self): _bytes = bytearray() - _bytes += super(Marker, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.data return _bytes def parse(self, packet): - super(Marker, self).parse(packet) + super().parse(packet) self.data = packet[:self.header.length] del packet[:self.header.length] @@ -1208,7 +1208,7 @@ def contents(self): return self._contents def __init__(self): - super(LiteralData, self).__init__() + super().__init__() self.format = 'b' self.filename = '' self.mtime = datetime.now(timezone.utc) @@ -1216,7 +1216,7 @@ def __init__(self): def __bytearray__(self): _bytes = bytearray() - _bytes += super(LiteralData, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.format.encode('latin-1') _bytes += bytearray([len(self.filename)]) _bytes += self.filename.encode('latin-1') @@ -1235,7 +1235,7 @@ def __copy__(self): return pkt def parse(self, packet): - super(LiteralData, self).parse(packet) + super().parse(packet) self.format = chr(packet[0]) del packet[0] @@ -1291,18 +1291,18 @@ def trustflags_int(self, val): self._trustflags = TrustFlags & val def __init__(self): - super(Trust, self).__init__() + super().__init__() self.trustlevel = TrustLevel.Unknown self.trustflags = [] def __bytearray__(self): _bytes = bytearray() - _bytes += super(Trust, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.int_to_bytes(self.trustlevel + sum(self.trustflags), 2) return _bytes def parse(self, packet): - super(Trust, self).parse(packet) + super().parse(packet) # self.trustlevel = packet[0] & 0x1f t = self.bytes_to_int(packet[:2]) del packet[:2] @@ -1324,13 +1324,13 @@ class UserID(Packet): __typeid__ = 0x0D def __init__(self, uid=""): - super(UserID, self).__init__() + super().__init__() self.uid = uid self._encoding_fallback = False def __bytearray__(self): _bytes = bytearray() - _bytes += super(UserID, self).__bytearray__() + _bytes += super().__bytearray__() textenc = 'utf-8' if not self._encoding_fallback else 'charmap' _bytes += self.uid.encode(textenc) @@ -1343,7 +1343,7 @@ def __copy__(self): return uid def parse(self, packet): - super(UserID, self).parse(packet) + super().parse(packet) uid_bytes = packet[:self.header.length] # uid_text = packet[:self.header.length].decode('utf-8') @@ -1406,17 +1406,17 @@ def image(self): return next(iter(self.subpackets['Image'])) def __init__(self): - super(UserAttribute, self).__init__() + super().__init__() self.subpackets = UserAttributeSubPackets() def __bytearray__(self): _bytes = bytearray() - _bytes += super(UserAttribute, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.subpackets.__bytearray__() return _bytes def parse(self, packet): - super(UserAttribute, self).parse(packet) + super().parse(packet) plen = len(packet) while self.header.length > (plen - len(packet)): @@ -1424,7 +1424,7 @@ def parse(self, packet): def update_hlen(self): self.subpackets.update_hlen() - super(UserAttribute, self).update_hlen() + super().update_hlen() class IntegrityProtectedSKEData(VersionedPacket): @@ -1535,12 +1535,12 @@ class IntegrityProtectedSKEDataV1(IntegrityProtectedSKEData): __ver__ = 1 def __init__(self): - super(IntegrityProtectedSKEDataV1, self).__init__() + super().__init__() self.ct = bytearray() def __bytearray__(self): _bytes = bytearray() - _bytes += super(IntegrityProtectedSKEDataV1, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.ct return _bytes @@ -1550,7 +1550,7 @@ def __copy__(self): return skd def parse(self, packet): - super(IntegrityProtectedSKEDataV1, self).parse(packet) + super().parse(packet) self.ct = packet[:self.header.length - 1] del packet[:self.header.length - 1] @@ -1617,13 +1617,13 @@ class MDC(Packet): __typeid__ = 0x13 def __init__(self): - super(MDC, self).__init__() + super().__init__() self.mdc = '' def __bytearray__(self): - return super(MDC, self).__bytearray__() + binascii.unhexlify(self.mdc) + return super().__bytearray__() + binascii.unhexlify(self.mdc) def parse(self, packet): - super(MDC, self).parse(packet) + super().parse(packet) self.mdc = binascii.hexlify(packet[:20]) del packet[:20] diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index a68a0bd9..50540a21 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -76,16 +76,16 @@ def uri_bytearray(self, val): self.uri = val.decode('latin-1') def __init__(self): - super(URI, self).__init__() + super().__init__() self.uri = "" def __bytearray__(self): - _bytes = super(URI, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.uri.encode() return _bytes def parse(self, packet): - super(URI, self).parse(packet) + super().parse(packet) self.uri = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] @@ -118,16 +118,16 @@ def flags_bytearray(self, val): self.flags = self.bytes_to_int(val) def __init__(self): - super(FlagList, self).__init__() + super().__init__() self.flags = [] def __bytearray__(self): - _bytes = super(FlagList, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += b''.join(self.int_to_bytes(b) for b in self.flags) return _bytes def parse(self, packet): - super(FlagList, self).parse(packet) + super().parse(packet) for i in range(0, self.header.length - 1): self.flags = packet[:1] del packet[:1] @@ -159,11 +159,11 @@ def flags_bytearray(self, val): self.flags = self.bytes_to_int(val) def __init__(self): - super(ByteFlag, self).__init__() + super().__init__() self.flags = [] def __bytearray__(self): - _bytes = super(ByteFlag, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(sum(self.flags)) # null-pad _bytes if they are not up to the end now if len(_bytes) < len(self): @@ -171,7 +171,7 @@ def __bytearray__(self): return _bytes def parse(self, packet): - super(ByteFlag, self).parse(packet) + super().parse(packet) for i in range(0, self.header.length - 1): self.flags = packet[:1] del packet[:1] @@ -191,11 +191,11 @@ def bflag_bytearray(self, val): self.bool = bool(self.bytes_to_int(val)) def __init__(self): - super(Boolean, self).__init__() + super().__init__() self.bflag = False def __bytearray__(self): - _bytes = super(Boolean, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(int(self.bflag)) return _bytes @@ -206,7 +206,7 @@ def __nonzero__(self): return self.__bool__() def parse(self, packet): - super(Boolean, self).parse(packet) + super().parse(packet) self.bflag = packet[:1] del packet[:1] @@ -242,16 +242,16 @@ def created_bytearray(self, val): self.created = self.bytes_to_int(val) def __init__(self): - super(CreationTime, self).__init__() + super().__init__() self.created = datetime.now(timezone.utc) def __bytearray__(self): - _bytes = super(CreationTime, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(calendar.timegm(self.created.utctimetuple()), 4) return _bytes def parse(self, packet): - super(CreationTime, self).parse(packet) + super().parse(packet) self.created = packet[:4] del packet[:4] @@ -285,16 +285,16 @@ def expires_bytearray(self, val): self.expires = self.bytes_to_int(val) def __init__(self): - super(SignatureExpirationTime, self).__init__() + super().__init__() self.expires = 0 def __bytearray__(self): - _bytes = super(SignatureExpirationTime, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(int(self.expires.total_seconds()), 4) return _bytes def parse(self, packet): - super(SignatureExpirationTime, self).parse(packet) + super().parse(packet) self.expires = packet[:4] del packet[:4] @@ -379,18 +379,18 @@ def amount_bytearray(self, val): self.amount = self.bytes_to_int(val) def __init__(self): - super(TrustSignature, self).__init__() + super().__init__() self.level = 0 self.amount = 0 def __bytearray__(self): - _bytes = super(TrustSignature, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(self.level) _bytes += self.int_to_bytes(self.amount) return _bytes def parse(self, packet): - super(TrustSignature, self).parse(packet) + super().parse(packet) self.level = packet[:1] del packet[:1] self.amount = packet[:1] @@ -427,16 +427,16 @@ def regex_bytearray(self, val): self.regex = val.decode('latin-1') def __init__(self): - super(RegularExpression, self).__init__() + super().__init__() self.regex = r'' def __bytearray__(self): - _bytes = super(RegularExpression, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.regex.encode() return _bytes def parse(self, packet): - super(RegularExpression, self).parse(packet) + super().parse(packet) self.regex = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] @@ -558,20 +558,20 @@ def fingerprint_bytearray(self, val): self.fingerprint = ''.join('{:02x}'.format(c) for c in val).upper() def __init__(self): - super(RevocationKey, self).__init__() + super().__init__() self.keyclass = [] self.algorithm = PubKeyAlgorithm.Invalid self._fingerprint = "" def __bytearray__(self): - _bytes = super(RevocationKey, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(sum(self.keyclass)) _bytes += self.int_to_bytes(self.algorithm.value) _bytes += self.fingerprint.__bytes__() return _bytes def parse(self, packet): - super(RevocationKey, self).parse(packet) + super().parse(packet) self.keyclass = packet[:1] del packet[:1] self.algorithm = packet[:1] @@ -592,16 +592,16 @@ def issuer_bytearray(self, val): self._issuer = binascii.hexlify(val).upper().decode('latin-1') def __init__(self): - super(Issuer, self).__init__() + super().__init__() self.issuer = bytearray() def __bytearray__(self): - _bytes = super(Issuer, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += binascii.unhexlify(self._issuer.encode()) return _bytes def parse(self, packet): - super(Issuer, self).parse(packet) + super().parse(packet) self.issuer = packet[:8] del packet[:8] @@ -657,13 +657,13 @@ def value_bytearray(self, val): self._value = val def __init__(self): - super(NotationData, self).__init__() + super().__init__() self.flags = [0, 0, 0, 0] self.name = "" self.value = "" def __bytearray__(self): - _bytes = super(NotationData, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(sum(self.flags)) + b'\x00\x00\x00' _bytes += self.int_to_bytes(len(self.name), 2) _bytes += self.int_to_bytes(len(self.value), 2) @@ -672,7 +672,7 @@ def __bytearray__(self): return bytes(_bytes) def parse(self, packet): - super(NotationData, self).parse(packet) + super().parse(packet) self.flags = packet[:1] del packet[:4] nlen = self.bytes_to_int(packet[:2]) @@ -724,11 +724,11 @@ def primary_byrearray(self, val): self.primary = bool(self.bytes_to_int(val)) def __init__(self): - super(PrimaryUserID, self).__init__() + super().__init__() self.primary = True def __bytearray__(self): - _bytes = super(PrimaryUserID, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(int(self.primary)) return _bytes @@ -739,7 +739,7 @@ def __nonzero__(self): return self.__bool__() def parse(self, packet): - super(PrimaryUserID, self).parse(packet) + super().parse(packet) self.primary = packet[:1] del packet[:1] @@ -770,16 +770,16 @@ def userid_bytearray(self, val): self.userid = val.decode('latin-1') def __init__(self): - super(SignersUserID, self).__init__() + super().__init__() self.userid = "" def __bytearray__(self): - _bytes = super(SignersUserID, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.userid.encode() return _bytes def parse(self, packet): - super(SignersUserID, self).parse(packet) + super().parse(packet) self.userid = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] @@ -814,18 +814,18 @@ def string_bytearray(self, val): self.string = val.decode('latin-1') def __init__(self): - super(ReasonForRevocation, self).__init__() + super().__init__() self.code = 0x00 self.string = "" def __bytearray__(self): - _bytes = super(ReasonForRevocation, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(self.code) _bytes += self.string.encode() return _bytes def parse(self, packet): - super(ReasonForRevocation, self).parse(packet) + super().parse(packet) self.code = packet[:1] del packet[:1] self.string = packet[:(self.header.length - 2)] @@ -884,16 +884,16 @@ def signer(self): return self._sig.signer def __init__(self): - super(EmbeddedSignature, self).__init__() + super().__init__() from ..packets import SignatureV4 self._sigpkt = SignatureV4() self._sigpkt.header = EmbeddedSignatureHeader() def __bytearray__(self): - return super(EmbeddedSignature, self).__bytearray__() + self._sigpkt.__bytearray__() + return super().__bytearray__() + self._sigpkt.__bytearray__() def parse(self, packet): - super(EmbeddedSignature, self).parse(packet) + super().parse(packet) self._sig.parse(packet) @@ -942,18 +942,18 @@ def issuer_fingerprint_bytearray(self, val): self.issuer_fingerprint = ''.join('{:02x}'.format(c) for c in val).upper() def __init__(self): - super(IssuerFingerprint, self).__init__() + super().__init__() self.version = 4 self._issuer_fpr = "" def __bytearray__(self): - _bytes = super(IssuerFingerprint, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(self.version) _bytes += self.issuer_fingerprint.__bytes__() return _bytes def parse(self, packet): - super(IssuerFingerprint, self).parse(packet) + super().parse(packet) self.version = packet[:1] del packet[:1] @@ -1014,18 +1014,18 @@ def intended_recipient_bytearray(self, val): self.intended_recipient = ''.join('{:02x}'.format(c) for c in val).upper() def __init__(self): - super(IntendedRecipient, self).__init__() + super().__init__() self.version = 4 self._intended_recipient = "" def __bytearray__(self): - _bytes = super(IntendedRecipient, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.int_to_bytes(self.version) _bytes += self.intended_recipient.__bytes__() return _bytes def parse(self, packet): - super(IntendedRecipient, self).parse(packet) + super().parse(packet) self.version = packet[:1] del packet[:1] @@ -1126,15 +1126,15 @@ def attested_certifications_bytearray(self, val): self._attested_certifications = val def __init__(self): - super(AttestedCertifications, self).__init__() + super().__init__() self._attested_certifications = bytearray() def __bytearray__(self): - _bytes = super(AttestedCertifications, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self._attested_certifications return _bytes def parse(self, packet): - super(AttestedCertifications, self).parse(packet) + super().parse(packet) self.attested_certifications = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 1765b2bd..8a55a8f2 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -42,7 +42,7 @@ def typeid_bin(self, val): self.critical = bool(v & 0x80) def __init__(self): - super(Header, self).__init__() + super().__init__() self._typeid = -1 self.critical = False @@ -67,14 +67,14 @@ def __bytearray__(self): def parse(self, packet): self.tag = 2 - super(EmbeddedSignatureHeader, self).parse(packet) + super().parse(packet) class SubPacket(Dispatchable): __headercls__ = Header def __init__(self): - super(SubPacket, self).__init__() + super().__init__() self.header = Header() if ( @@ -123,15 +123,15 @@ def payload_bin(self, val): self._payload = bytearray(val) def __init__(self): - super(Opaque, self).__init__() + super().__init__() self.payload = b'' def __bytearray__(self): - _bytes = super(Opaque, self).__bytearray__() + _bytes = super().__bytearray__() _bytes += self.payload return _bytes def parse(self, packet): - super(Opaque, self).parse(packet) + super().parse(packet) self.payload = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] diff --git a/pgpy/packet/subpackets/userattribute.py b/pgpy/packet/subpackets/userattribute.py index 407deb92..dec7150f 100644 --- a/pgpy/packet/subpackets/userattribute.py +++ b/pgpy/packet/subpackets/userattribute.py @@ -79,13 +79,13 @@ def image_bin(self, val): self._image = bytearray(val) def __init__(self): - super(Image, self).__init__() + super().__init__() self.version = 1 self.iencoding = 1 self.image = bytearray() def __bytearray__(self): - _bytes = super(Image, self).__bytearray__() + _bytes = super().__bytearray__() if self.version == 1: # v1 image header length is always 16 bytes @@ -96,7 +96,7 @@ def __bytearray__(self): return _bytes def parse(self, packet): - super(Image, self).parse(packet) + super().parse(packet) with memoryview(packet) as _head: _, self.version, self.iencoding, _, _, _ = struct.unpack_from('', leaving out any comment or email fields that are not present. """ - super(PGPUID, self).__init__() + super().__init__() self._uid = None self._signatures = SorteDeque() @@ -965,7 +965,7 @@ def __init__(self): Any signatures within the PGPMessage that are marked as being non-exportable will not be included in the output of either of those methods. """ - super(PGPMessage, self).__init__() + super().__init__() self._compression = CompressionAlgorithm.Uncompressed self._message = None self._mdc = None @@ -998,9 +998,9 @@ def __str__(self): return tmpl.format(hhdr=hhdr, cleartext=self.dash_escape(self.bytes_to_text(self._message)), - signature=super(PGPMessage, self).__str__()) + signature=super().__str__()) - return super(PGPMessage, self).__str__() + return super().__str__() def __iter__(self): if self.type == 'cleartext': @@ -1080,7 +1080,7 @@ def __or__(self, other): raise NotImplementedError(str(type(other))) def __copy__(self): - msg = super(PGPMessage, self).__copy__() + msg = super().__copy__() msg._compression = self._compression msg._message = copy.copy(self._message) msg._mdc = copy.copy(self._mdc) @@ -1624,7 +1624,7 @@ def __init__(self): Any signatures within the PGPKey that are marked as being non-exportable will not be included in the output of either of those methods. """ - super(PGPKey, self).__init__() + super().__init__() self._key = None self._children = collections.OrderedDict() self._signatures = SorteDeque() @@ -1713,7 +1713,7 @@ def __or__(self, other, from_sib=False): return self def __copy__(self): - key = super(PGPKey, self).__copy__() + key = super().__copy__() key._key = copy.copy(self._key) for uid in self._uids: @@ -2682,7 +2682,7 @@ def __init__(self, *args): PGPKeyring objects represent in-memory keyrings that can contain any combination of supported private and public keys. It can not currently be conveniently exported to a format that can be understood by GnuPG. """ - super(PGPKeyring, self).__init__() + super().__init__() self._keys = {} self._pubkeys = collections.deque() self._privkeys = collections.deque() diff --git a/pgpy/types.py b/pgpy/types.py index efc5a349..d8b7989a 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -203,7 +203,7 @@ def from_blob(cls, blob): return obj # pragma: no cover def __init__(self): - super(Armorable, self).__init__() + super().__init__() self.ascii_headers = collections.OrderedDict() def __str__(self): @@ -245,7 +245,7 @@ def parent(self): return self._parent def __init__(self): - super(ParentRef, self).__init__() + super().__init__() self._parent = None @@ -414,7 +414,7 @@ def llen_int(self, val): self._llen = {0: 1, 1: 2, 2: 4, 3: 0}[val] def __init__(self): - super(Header, self).__init__() + super().__init__() self._len = 1 self._llen = 1 self._lenfmt = 1 @@ -467,7 +467,7 @@ class MetaDispatchable(abc.ABCMeta): """ def __new__(mcs, name, bases, attrs): # NOQA - ncls = super(MetaDispatchable, mcs).__new__(mcs, name, bases, attrs) + ncls = super().__new__(mcs, name, bases, attrs) if not hasattr(ncls.__typeid__, '__isabstractmethod__'): if ncls.__typeid__ == -1 and not issubclass(ncls, tuple(MetaDispatchable._roots)): @@ -611,7 +611,7 @@ def __init__(self): Can be compared directly as a boolean to determine whether or not the specified signature verified. """ - super(SignatureVerification, self).__init__() + super().__init__() self._subjects = [] def __contains__(self, item): From 1aafeb89d7521ed0c472869330d5b7de88d060f1 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 3 Nov 2022 14:52:22 +0300 Subject: [PATCH 003/287] Modernized yields into `yield from` --- pgpy/decorators.py | 3 +-- pgpy/packet/fields.py | 13 ++++--------- pgpy/pgp.py | 19 +++++++------------ tests/test_04_copy.py | 3 +-- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 5a494094..f22f0bb4 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -79,8 +79,7 @@ def __init__(self, *usage, **conditions): def usage(self, key, user): def _preiter(first, iterable): yield first - for item in iterable: - yield item + yield from iterable em = {} em['keyid'] = key.fingerprint.keyid diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index cd34ed7e..78f621ce 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -135,8 +135,7 @@ def __len__(self): # pragma: no cover return sum(sp.header.length for sp in itertools.chain(self._hashed_sp.values(), self._unhashed_sp.values())) + 4 def __iter__(self): - for sp in itertools.chain(self._hashed_sp.values(), self._unhashed_sp.values()): - yield sp + yield from itertools.chain(self._hashed_sp.values(), self._unhashed_sp.values()) def __setitem__(self, key, val): # the key provided should always be the classname for the subpacket @@ -380,8 +379,7 @@ class PubKey(MPIs): @property def __mpis__(self): - for i in self.__pubfields__: - yield i + yield from self.__pubfields__ def __init__(self): super().__init__() @@ -1143,11 +1141,8 @@ class PrivKey(PubKey): @property def __mpis__(self): - for i in super().__mpis__: - yield i - - for i in self.__privfields__: - yield i + yield from super().__mpis__ + yield from self.__privfields__ def __init__(self): super().__init__() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 68b7bb13..cfe1c6ee 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1010,8 +1010,7 @@ def __iter__(self): elif self.is_encrypted: for sig in self._signatures: yield sig - for pkt in self._sessionkeys: - yield pkt + yield from self._sessionkeys yield self.message else: @@ -1541,9 +1540,8 @@ def self_signatures(self): else (self.parent.fingerprint.keyid, SignatureType.Subkey_Binding) ##TODO: filter out revoked signatures as well - for sig in iter(sig for sig in self._signatures - if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])): - yield sig + yield from iter(sig for sig in self._signatures + if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])) @property def signers(self): @@ -1555,9 +1553,8 @@ def revocation_signatures(self): keyid, keytype = (self.fingerprint.keyid, SignatureType.KeyRevocation) if self.is_primary \ else (self.parent.fingerprint.keyid, SignatureType.SubkeyRevocation) - for sig in iter(sig for sig in self._signatures - if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])): - yield sig + yield from iter(sig for sig in self._signatures + if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])) @property def subkeys(self): @@ -2701,8 +2698,7 @@ def __len__(self): return len(self._keys) def __iter__(self): # pragma: no cover - for pgpkey in itertools.chain(self._pubkeys, self._privkeys): - yield pgpkey + yield from itertools.chain(self._pubkeys, self._privkeys) def _get_key(self, alias): for m in self._aliases: @@ -2796,8 +2792,7 @@ def load(self, *args): """ def _preiter(first, iterable): yield first - for item in iterable: - yield item + yield from iterable loaded = set() for key in iter(item for ilist in iter(ilist if isinstance(ilist, (tuple, list)) else [ilist] for ilist in args) diff --git a/tests/test_04_copy.py b/tests/test_04_copy.py index 366ff743..669950aa 100644 --- a/tests/test_04_copy.py +++ b/tests/test_04_copy.py @@ -40,8 +40,7 @@ def walk_obj(obj, prefix=""): yield '{}{}'.format(prefix, name), val if not isinstance(val, Enum): - for n, v in walk_obj(val, prefix="{}{}.".format(prefix, name)): - yield n, v + yield from walk_obj(val, prefix="{}{}.".format(prefix, name)) def check_id(obj): From f66009e594936f0a88f5d4cba13b313826b95c25 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 3 Nov 2022 14:52:22 +0300 Subject: [PATCH 004/287] Modernized collections to literal comprehensions syntax --- pgpy/packet/fields.py | 2 +- pgpy/pgp.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 78f621ce..4f0fd3ef 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -173,7 +173,7 @@ def __delitem__(self, key): raise NotImplementedError def __contains__(self, key): - return key in set(k for k, _ in itertools.chain(self._hashed_sp, self._unhashed_sp)) + return key in {k for k, _ in itertools.chain(self._hashed_sp, self._unhashed_sp)} def __copy__(self): sp = SubPackets() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index cfe1c6ee..fa514295 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -237,7 +237,7 @@ def notation(self): """ A ``dict`` of notation data in this signature, if any. Otherwise, an empty ``dict``. """ - return dict((nd.name, nd.value) for nd in self._signature.subpackets['NotationData']) + return {nd.name: nd.value for nd in self._signature.subpackets['NotationData']} @property def policy_uri(self): @@ -696,7 +696,7 @@ def signers(self): """ This will be a set of all of the key ids which have signed this User ID or Attribute. """ - return set(s.signer for s in self.__sig__) + return {s.signer for s in self.__sig__} @property def hashdata(self): @@ -873,7 +873,7 @@ def dash_escape(text): @property def encrypters(self): """A ``set`` containing all key ids (if any) to which this message was encrypted.""" - return set(m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey)) + return {m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey)} @property def filename(self): @@ -936,7 +936,7 @@ def signatures(self): @property def signers(self): """A ``set`` containing all key ids (if any) which have signed this message.""" - return set(m.signer for m in self._signatures) + return {m.signer for m in self._signatures} @property def type(self): @@ -993,7 +993,7 @@ def __str__(self): u"{signature:s}" # only add a Hash: header if we actually have at least one signature - hashes = set(s.hash_algorithm.name for s in self.signatures) + hashes = {s.hash_algorithm.name for s in self.signatures} hhdr = 'Hash: {hashes:s}\n'.format(hashes=','.join(sorted(hashes))) if hashes else '' return tmpl.format(hhdr=hhdr, @@ -2738,7 +2738,7 @@ def _add_alias(self, alias, pkid): self._aliases[-1][alias] = pkid # this is a duplicate alias->key link; ignore it - elif alias in self and pkid in set(m[alias] for m in self._aliases if alias in m): + elif alias in self and pkid in {m[alias] for m in self._aliases if alias in m}: pass # pragma: no cover # this is an alias that already exists, but points to a key that is not already referenced by it From d342d3f4f4b500fb16995a9a3e773dd14f64542e Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 24 Nov 2022 20:18:08 +0300 Subject: [PATCH 005/287] Got rid of Unicode literals --- pgpy/pgp.py | 8 ++++---- tests/test_01_types.py | 16 ++++++++-------- tests/test_05_actions.py | 20 ++++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index fa514295..58725012 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -987,10 +987,10 @@ def __bytearray__(self): def __str__(self): if self.type == 'cleartext': - tmpl = u"-----BEGIN PGP SIGNED MESSAGE-----\n" \ - u"{hhdr:s}\n" \ - u"{cleartext:s}\n" \ - u"{signature:s}" + tmpl = "-----BEGIN PGP SIGNED MESSAGE-----\n" \ + "{hhdr:s}\n" \ + "{cleartext:s}\n" \ + "{signature:s}" # only add a Hash: header if we actually have at least one signature hashes = {s.hash_algorithm.name for s in self.signatures} diff --git a/tests/test_01_types.py b/tests/test_01_types.py index 1f24ce8a..e3a4c679 100644 --- a/tests/test_01_types.py +++ b/tests/test_01_types.py @@ -7,14 +7,14 @@ text = { # some basic utf-8 test strings - these should all pass - 'english': u'The quick brown fox jumped over the lazy dog', + 'english': 'The quick brown fox jumped over the lazy dog', # this hiragana pangram comes from http://www.columbia.edu/~fdc/utf8/ - 'hiragana': u'いろはにほへど ちりぬるを\n' - u'わがよたれぞ つねならむ\n' - u'うゐのおくやま けふこえて\n' - u'あさきゆめみじ ゑひもせず', + 'hiragana': 'いろはにほへど ちりぬるを\n' + 'わがよたれぞ つねならむ\n' + 'うゐのおくやま けふこえて\n' + 'あさきゆめみじ ゑひもせず', - 'poo': u'Hello, \U0001F4A9!', + 'poo': 'Hello, \U0001F4A9!', } # some alternate encodings to try @@ -22,9 +22,9 @@ encoded_text = { # try some alternate encodings as well # 'crunch the granite of science' - 'cyrillic': u'грызть гранит науки'.encode('iso8859_5'), + 'cyrillic': 'грызть гранит науки'.encode('iso8859_5'), # 'My hovercraft is full of eels' - 'cp865': u'Mit luftpudefartøj er fyldt med ål'.encode('cp865'), + 'cp865': 'Mit luftpudefartøj er fyldt med ål'.encode('cp865'), } diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index d1716e2a..a9afd8c3 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -71,7 +71,7 @@ def gpg_decrypt(msg, passphrase): @pytest.mark.parametrize('comp_alg,sensitive', itertools.product(CompressionAlgorithm, [False, True])) def test_new(self, comp_alg, sensitive): - mtxt = u"This is a new message!" + mtxt = "This is a new message!" msg = PGPMessage.new(mtxt, compression=comp_alg, sensitive=sensitive) assert isinstance(msg, PGPMessage) @@ -106,10 +106,10 @@ def test_new_from_file(self, comp_alg, sensitive, path): # @pytest.mark.parametrize('cleartext', [False, True]) def test_new_non_unicode(self): # this message text comes from http://www.columbia.edu/~fdc/utf8/ - text = u'色は匂へど 散りぬるを\n' \ - u'我が世誰ぞ 常ならむ\n' \ - u'有為の奥山 今日越えて\n' \ - u'浅き夢見じ 酔ひもせず' + text = '色は匂へど 散りぬるを\n' \ + '我が世誰ぞ 常ならむ\n' \ + '有為の奥山 今日越えて\n' \ + '浅き夢見じ 酔ひもせず' msg = PGPMessage.new(text.encode('jisx0213'), encoding='jisx0213') assert msg.type == 'literal' @@ -122,17 +122,17 @@ def test_new_non_unicode(self): @pytest.mark.regression(issue=154) def test_new_non_unicode_cleartext(self): # this message text comes from http://www.columbia.edu/~fdc/utf8/ - text = u'色は匂へど 散りぬるを\n' \ - u'我が世誰ぞ 常ならむ\n' \ - u'有為の奥山 今日越えて\n' \ - u'浅き夢見じ 酔ひもせず' + text = '色は匂へど 散りぬるを\n' \ + '我が世誰ぞ 常ならむ\n' \ + '有為の奥山 今日越えて\n' \ + '浅き夢見じ 酔ひもせず' msg = PGPMessage.new(text.encode('jisx0213'), cleartext=True, encoding='jisx0213') assert msg.type == 'cleartext' assert msg.message == text def test_add_marker(self): - msg = PGPMessage.new(u"This is a new message") + msg = PGPMessage.new("This is a new message") marker = Packet(bytearray(b'\xa8\x03\x50\x47\x50')) msg |= marker From 2b409f4c6e37d2a65b9bfbc3e918720f1967113b Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 24 Nov 2022 20:24:53 +0300 Subject: [PATCH 006/287] Removed unneeded traced of Python 2 related to division --- pgpy/packet/fields.py | 3 +-- pgpy/packet/types.py | 1 - pgpy/types.py | 1 - tests/test_04_copy.py | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 4f0fd3ef..f5b989a7 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1,6 +1,5 @@ """ fields.py """ -from __future__ import absolute_import, division import abc import binascii @@ -1017,7 +1016,7 @@ def derive_key(self, passphrase): keylen = self.encalg.key_size hashlen = self.halg.digest_size * 8 - ctx = int(math.ceil((keylen / hashlen))) + ctx = int(math.ceil(keylen / hashlen)) # Simple S2K - always done hsalt = b'' diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 18855e91..d8e7cd40 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -1,6 +1,5 @@ """ types.py """ -from __future__ import division import abc import copy diff --git a/pgpy/types.py b/pgpy/types.py index d8b7989a..61250541 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -1,6 +1,5 @@ """ types.py """ -from __future__ import division import abc import base64 diff --git a/tests/test_04_copy.py b/tests/test_04_copy.py index 669950aa..2cf8fba0 100644 --- a/tests/test_04_copy.py +++ b/tests/test_04_copy.py @@ -1,6 +1,5 @@ """ test copying PGP objects """ -from __future__ import print_function import pytest import copy From 9e3110d829144756ad8c5fb7f67ac23764d2c223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Moj=C5=BE=C3=AD=C5=A1?= Date: Wed, 18 Jan 2023 18:21:54 +0100 Subject: [PATCH 007/287] define more subpacket features --- pgpy/constants.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index 28a4561a..999dd42f 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -564,6 +564,13 @@ class KeyFlags(FlagEnum): class Features(FlagEnum): ModificationDetection = 0x01 + UnknownFeature02 = 0x02 + UnknownFeature04 = 0x04 + UnknownFeature08 = 0x08 + UnknownFeature10 = 0x10 + UnknownFeature20 = 0x20 + UnknownFeature40 = 0x40 + UnknownFeature80 = 0x80 @classproperty def pgpy_features(cls): From 2a37a53df622b1b265db54c1fcdccd60e7bafc52 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 2 Feb 2023 12:42:17 -0500 Subject: [PATCH 008/287] Drop unneccessary duplicate string registrations (these were introduced accidentally when KOLANICH removed the six module in a23dccef97711f37de8b461b9bbbb4e68c2fbe38) --- pgpy/packet/packets.py | 1 - pgpy/packet/subpackets/signature.py | 9 --------- 2 files changed, 10 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index cd494137..aa0f5f29 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -694,7 +694,6 @@ def halg_int(self, val): def signer(self): return self._signer - @signer.register(str) @signer.register(str) def signer_str(self, val): self._signer = val diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index a68a0bd9..09f21687 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -66,7 +66,6 @@ class URI(Signature): def uri(self): return self._uri - @uri.register(str) @uri.register(str) def uri_str(self, val): self._uri = val @@ -417,7 +416,6 @@ class RegularExpression(Signature): def regex(self): return self._regex - @regex.register(str) @regex.register(str) def regex_str(self, val): self._regex = val @@ -547,7 +545,6 @@ def algorithm_bytearray(self, val): def fingerprint(self): return self._fingerprint - @fingerprint.register(str) @fingerprint.register(str) @fingerprint.register(Fingerprint) def fingerprint_str(self, val): @@ -630,7 +627,6 @@ def flags_bytearray(self, val): def name(self): return self._name - @name.register(str) @name.register(str) def name_str(self, val): self._name = val @@ -643,7 +639,6 @@ def name_bytearray(self, val): def value(self): return self._value - @value.register(str) @value.register(str) def value_str(self, val): self._value = val @@ -760,7 +755,6 @@ class SignersUserID(Signature): def userid(self): return self._userid - @userid.register(str) @userid.register(str) def userid_str(self, val): self._userid = val @@ -804,7 +798,6 @@ def code_bytearray(self, val): def string(self): return self._string - @string.register(str) @string.register(str) def string_str(self, val): self._string = val @@ -931,7 +924,6 @@ def version_bytearray(self, val): def issuer_fingerprint(self): return self._issuer_fpr - @issuer_fingerprint.register(str) @issuer_fingerprint.register(str) @issuer_fingerprint.register(Fingerprint) def issuer_fingerprint_str(self, val): @@ -1003,7 +995,6 @@ def version_bytearray(self, val): def intended_recipient(self): return self._intended_recipient - @intended_recipient.register(str) @intended_recipient.register(str) @intended_recipient.register(Fingerprint) def intended_recipient_str(self, val): From 609cb8a53d44162fad0a982366d025c2892c9985 Mon Sep 17 00:00:00 2001 From: Ken Dreyer Date: Fri, 26 May 2023 15:14:42 -0400 Subject: [PATCH 009/287] remove more `six` references Commit a23dccef97711f37de8b461b9bbbb4e68c2fbe38 removed the six dependency. Remove the other references in the docs and CI. --- .coveragerc | 5 ----- README.rst | 2 -- gentoo/pgpy-0.4.0.ebuild | 1 - tox.ini | 1 - 4 files changed, 9 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8e7ef503..ad0916ad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,11 +9,6 @@ exclude_lines = # skip abstract methods @(abc\.)?abstract - # Python 2.x compatibility stuff - if six.PY2: - if six.PY3: - def __nonzero__ - # debug-only code def __repr__ diff --git a/README.rst b/README.rst index d800c14f..774ff856 100644 --- a/README.rst +++ b/README.rst @@ -58,8 +58,6 @@ Requirements - `pyasn1 `_ -- `six `_ - License ------- diff --git a/gentoo/pgpy-0.4.0.ebuild b/gentoo/pgpy-0.4.0.ebuild index 8338e8ab..67888a4e 100644 --- a/gentoo/pgpy-0.4.0.ebuild +++ b/gentoo/pgpy-0.4.0.ebuild @@ -19,7 +19,6 @@ IUSE="" DEPEND="dev-python/setuptools[${PYTHON_USEDEP}]" RDEPEND="dev-python/singledispatch[${PYTHON_USEDEP}] dev-python/pyasn1[${PYTHON_USEDEP}] - >=dev-python/six-1.9.0[${PYTHON_USEDEP}] >=dev-python/cryptography-1.1.0[${PYTHON_USEDEP}] $(python_gen_cond_dep 'dev-python/enum34[${PYTHON_USEDEP}]' python2_7 python3_3)" DOCS=( README.rst ) diff --git a/tox.ini b/tox.ini index 0956986e..a7fd8433 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,6 @@ deps = cryptography>=2.6 gpg==1.10.0 pyasn1 - six>=1.9.0 pytest pytest-cov # We need a patched version of pytest-order to run on 3.5 and handle parameterized tests From a3941e07580e1d350b882a69e767617a759d3bda Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 10 Jun 2023 23:37:58 -0400 Subject: [PATCH 010/287] Tests: Check on forward-compatibility failures These tests are pulled from the OpenPGP Interoperability Test Suite, in particular from those tests tagged "forward-compat": https://tests.sequoia-pgp.org/?q=forward-compat These failures make it difficult to use PGPy in an OpenPGP ecosystem that can evolve, because PGPy will complain about OpenPGP objects that contain something it doesn't understand, even though the rest of the message is otherwise comprehensible and usable. See Justus Winter's message to openpgp@ietf.org describing his concerns: https://mailarchive.ietf.org/arch/msg/openpgp/QUiEKx3PQeJOXnkcvvnuHpv739M I've selected specific examples that PGPy is known to currently fail with. --- tests/test_06_compatibility.py | 58 +++++++++++++ tests/testdata/compatibility/bob-key.pgp | 82 ++++++++++++++++++ tests/testdata/compatibility/bob.pgp | 42 +++++++++ .../bob_with_unknown_alg_certification.pgp | 50 +++++++++++ .../bob_with_unknown_ecdh_curve.pgp | 40 +++++++++ .../bob_with_unknown_ecdsa_curve.pgp | 40 +++++++++ .../bob_with_unknown_eddsa_curve.pgp | 40 +++++++++ .../bob_with_unknown_subkey_algorithm.pgp | 45 ++++++++++ ...ncrypted_signed_with_unknown_algorithm.msg | 23 +++++ ..._signed_with_unknown_signature_version.msg | 25 ++++++ .../compatibility/pkesk_unknown_pkalg.msg | 20 +++++ .../compatibility/pkesk_unknown_version.msg | 16 ++++ tests/testdata/compatibility/ricarda.pgp | 86 +++++++++++++++++++ tests/testdata/compatibility/sig23_sig4.sig | 26 ++++++ .../testdata/compatibility/sig4_b-sig4_r.sig | 26 ++++++ .../testdata/compatibility/sig4_r-sig4_b.sig | 26 ++++++ tests/testdata/compatibility/sig4_sig23.sig | 26 ++++++ .../compatibility/skesk_unknown_s2k_algo.msg | 16 ++++ .../compatibility/skesk_unknown_version.msg | 16 ++++ 19 files changed, 703 insertions(+) create mode 100644 tests/test_06_compatibility.py create mode 100644 tests/testdata/compatibility/bob-key.pgp create mode 100644 tests/testdata/compatibility/bob.pgp create mode 100644 tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp create mode 100644 tests/testdata/compatibility/bob_with_unknown_ecdh_curve.pgp create mode 100644 tests/testdata/compatibility/bob_with_unknown_ecdsa_curve.pgp create mode 100644 tests/testdata/compatibility/bob_with_unknown_eddsa_curve.pgp create mode 100644 tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp create mode 100644 tests/testdata/compatibility/encrypted_signed_with_unknown_algorithm.msg create mode 100644 tests/testdata/compatibility/encrypted_signed_with_unknown_signature_version.msg create mode 100644 tests/testdata/compatibility/pkesk_unknown_pkalg.msg create mode 100644 tests/testdata/compatibility/pkesk_unknown_version.msg create mode 100644 tests/testdata/compatibility/ricarda.pgp create mode 100644 tests/testdata/compatibility/sig23_sig4.sig create mode 100644 tests/testdata/compatibility/sig4_b-sig4_r.sig create mode 100644 tests/testdata/compatibility/sig4_r-sig4_b.sig create mode 100644 tests/testdata/compatibility/sig4_sig23.sig create mode 100644 tests/testdata/compatibility/skesk_unknown_s2k_algo.msg create mode 100644 tests/testdata/compatibility/skesk_unknown_version.msg diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py new file mode 100644 index 00000000..e6e9aaee --- /dev/null +++ b/tests/test_06_compatibility.py @@ -0,0 +1,58 @@ +# coding=utf-8 +""" ensure that we don't crash on surprising messages +""" +import pytest + +from pgpy import PGPKey, PGPMessage +from pgpy.constants import SecurityIssues +import glob + +class TestPGP_Compatibility(object): + # test compatibility: + # - Armored object with (non-ASCII) UTF-8 comments + # - signatures with unknown pubkey algorithms + # - certs with certifications from unknown algorithms + # - certs with subkeys with unknown algorithms + # - certs with ECC subkeys with unknown curves + # - encrypted messages with surprising parts + def test_import_unicode_armored_cert(self) -> None: + k:PGPKey + (k, _) = PGPKey.from_file('tests/testdata/compatibility/ricarda.pgp') + pytest.xfail('Cannot handle UTF-8 (non-ASCII) comments in armored certificate') + assert k.check_soundness() == SecurityIssues.OK + + @pytest.mark.parametrize('sig', glob.glob('*.sig', root_dir='tests/testdata/compatibility')) + def test_bob_sig_from_multisig(self, sig:str)-> None: + k:PGPKey + (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob.pgp') + msg = 'Hello World :)' + pytest.xfail(f'Cannot handle detached signature objects with more than one signature present (see https://github.com/SecurityInnovation/PGPy/issues/197)') + + def test_cert_unknown_algo(self) -> None: + k:PGPKey + pytest.xfail('cannot handle certificates containing certifications made using unknown pubkey algorithms') + (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp') + assert k.check_soundness() == SecurityIssues.OK + + def test_cert_unknown_subkey_algo(self) -> None: + k:PGPKey + pytest.xfail('cannot handle certificates containing subkeys with unknown pubkey algorithms') + (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp') + assert k.check_soundness() == SecurityIssues.OK + + @pytest.mark.parametrize('flavor', ['ecdsa', 'eddsa', 'ecdh']) + def test_cert_unknown_curve(self, flavor:str) -> None: + k:PGPKey + pytest.xfail(f'cannot handle certificates containing subkeys with unknown OIDs for {flavor}') + (k, _) = PGPKey.from_file(f'tests/testdata/compatibility/bob_with_unknown_{flavor}_curve.pgp') + assert k.check_soundness() == SecurityIssues.OK + + @pytest.mark.parametrize('msg', glob.glob('*.msg', root_dir='tests/testdata/compatibility')) + def test_unknown_message(self, msg:str)-> None: + k:PGPKey + (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob-key.pgp') + pytest.xfail('cannot handle unknowns in message formats') + msg:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') + cleartext:PGPMessage = k.decrypt(msg) + assert not cleartext.is_encrypted + assert cleartext.message.startswith(b'Encrypted') diff --git a/tests/testdata/compatibility/bob-key.pgp b/tests/testdata/compatibility/bob-key.pgp new file mode 100644 index 00000000..59e981b7 --- /dev/null +++ b/tests/testdata/compatibility/bob-key.pgp @@ -0,0 +1,82 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: Bob's OpenPGP Transferable Secret Key + +lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM +cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK +3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z +Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs +hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ +bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4 +i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI +1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP +fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6 +fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E +LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx ++akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL +hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN +WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/ +MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC +mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC +YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E +he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8 +zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P +NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT +t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w +ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC +F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U +2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX +yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe +doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3 +BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl +sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN +4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+ +L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG +ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad +BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD +bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar +29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2 +WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB +leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te +g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj +Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn +JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx +IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp +SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h +OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np +Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c ++EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0 +tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o +BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny +zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK +clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl +zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr +gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ +aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5 +fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/ +ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5 +HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf +SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd +5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ +E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM +GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY +vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ +26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP +eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX +c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief +rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0 +JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg +71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH +s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd +NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91 +6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7 +xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE= +=miES +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob.pgp b/tests/testdata/compatibility/bob.pgp new file mode 100644 index 00000000..b58b8c3a --- /dev/null +++ b/tests/testdata/compatibility/bob.pgp @@ -0,0 +1,42 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Bob's OpenPGP certificate + +mQGNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAbQhQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+iQHOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGuQGNBF2lnPIBDADW +ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI +DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+ +Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO +baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT +86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh +827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6 +vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U +qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A +EQEAAYkBtgQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ +EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS +KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx +cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i +tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV +dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w +qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy +jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj +zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV +NEJd3XZRzaXZE2aAMQ== +=NXei +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp b/tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp new file mode 100644 index 00000000..3707582d --- /dev/null +++ b/tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp @@ -0,0 +1,50 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGwsDzBBNjCgAnBQJd +pZ76CRCqqru7zMzd3RYhBKqqu7vMzN3dqqq7u8zM3d2qqru7AABvbAv/VNk90a6h +G8Od9xTzXxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOh +Q5Esm6DOZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad +75BrZ+3g9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42b +g8lpmdXFDcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQ +NZ5Jix7cZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEP +c0fHp5G16rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+eg +LjsIbPJZZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiAC +szNU+RRozAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsDNBF2l +nPIBDADWML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamX +nn9sSXvIDEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMX +SO4uImA+Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6 +rrd5y2AObaifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA +0YwIMgIT86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/ +wGlQ01rh827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+pa +LNDdVPL6vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV +8rUnR76UqVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwz +j8sxH48AEQEAAcLA9gQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzy +AhsMAAoJEPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+4 +1IL4rVcSKhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZ +QanYmtSxcVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zp +f3u0k14itcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn +3OWjCPHVdTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK +2b0vk/+wqMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFA +ExaEK6VyjP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWi +f9RSK4xjzRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj +5KjhX2PVNEJd3XZRzaXZE2aAMQ== +=WSi3 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob_with_unknown_ecdh_curve.pgp b/tests/testdata/compatibility/bob_with_unknown_ecdh_curve.pgp new file mode 100644 index 00000000..4d61f469 --- /dev/null +++ b/tests/testdata/compatibility/bob_with_unknown_ecdh_curve.pgp @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsBYBF2lnPISCwYJ +KwYBBAGColpjB/0RWUyaRh7Ih2+VvD4TXboI3UCdkWKxvQyzS+KHL9RSaPhTlYKd +Oxgj/9VGMAcK31+ORMQrJNlKEL1bhqIZ5KhxmjLjPZnSZCWQ3V3WRcALyvbr/v5q +IGIEsnZMifZZDYQflsu83Fiy22Vv2VKq90SCCqa8eSU1Asm9tyvC2oBEiqZsgkpi +1Gjcq+KtWQNFTGEKKEWvcLSWhCXt1xG4rVa7QTRKDQsw1VGKPoh90OE2eWTMp2bm +A71oPa+LvaTiTXPsgZ7ZZ0cMywqid9U+n6xom+7sLc+izxtQe5TSV782RT4h5anO +gN08ywp4M2lhJzGgy4bXyR2LyU5u+vAEny5cAwEKCcLBPgQYAQoAcgWCXaWc8gkQ ++/zIKgFeczBHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn +m7NaW4RUAfNUvXdbu3BRAJRPAmXTT4F0aeigqAsW/jkCmwwWIQTRpm4aI7GCyZgP +eIz7/MgqAV5zMAAA3jIL/jfotBQnvXa788J4eud8S9vnXP01SJ0yz/4Ws8Jal05T +pD11fsE8qZosMk6mIVm1XxYKGDhYRx4E6HCd+qPzKhy5g7vqLzZ7zZzJ5VcDLnDE +RZVLmZOrl2rEMRDmYbZWhWesdbvnQnOO52F2tcHGwhW1RG6o48WDvHReV8iiejT8 +G5TtfnyyluzyFqpQ9QAJoGI2rDCemExRfpV3vp25tBTFzQVLYEdimVAstaraIn+Z +ulVSJP3JkNhlHvJK/rgMv4yT5u2Mh4Cl9348g0sJYfFN7/h4PH+G1J+lMUZKv4Co +qUv0/HFPS8RJFUfwJoP0G9QQ1QQbinvqFfRnCV/TgOfoFUOY0LAgJMhbzvQs0B78 +gucKrYA5m95aT1uL/NuePQtTPLu5d9/yQG5EE0wb66mL01HO886PYclYzplZpsMs +UOg8w5og9anHSBl9CV2lXI5qTm3Y4DkF8rqfaZeZPgh/r5I5i6DhH4ZD3mPI9i9O +MF4/9Ikup96bHiodjzRzjg== +=4wyr +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob_with_unknown_ecdsa_curve.pgp b/tests/testdata/compatibility/bob_with_unknown_ecdsa_curve.pgp new file mode 100644 index 00000000..baabb928 --- /dev/null +++ b/tests/testdata/compatibility/bob_with_unknown_ecdsa_curve.pgp @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsBUBF2lnPITCwYJ +KwYBBAGColpjB/0RWUyaRh7Ih2+VvD4TXboI3UCdkWKxvQyzS+KHL9RSaPhTlYKd +Oxgj/9VGMAcK31+ORMQrJNlKEL1bhqIZ5KhxmjLjPZnSZCWQ3V3WRcALyvbr/v5q +IGIEsnZMifZZDYQflsu83Fiy22Vv2VKq90SCCqa8eSU1Asm9tyvC2oBEiqZsgkpi +1Gjcq+KtWQNFTGEKKEWvcLSWhCXt1xG4rVa7QTRKDQsw1VGKPoh90OE2eWTMp2bm +A71oPa+LvaTiTXPsgZ7ZZ0cMywqid9U+n6xom+7sLc+izxtQe5TSV782RT4h5anO +gN08ywp4M2lhJzGgy4bXyR2LyU5u+vAEny5cwsE+BBgBCgByBYJdpZzyCRD7/Mgq +AV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdO3gn/ +QXHPMqz235LzkiIW9P08otpOP/Hp+TCW1FfiXAKbIBYhBNGmbhojsYLJmA94jPv8 +yCoBXnMwAABsxgv+MITO4NbSkHROZV4gg5rxuFxOZWUkyYkD6TA68WsfRKlLcsNb +XWSaBBSV+313bE9IYWyQUX1WFiVskB0jZZzDjuDwX0VwMNuwKt8sRs0p6BQKD+9p +u9GrUY9UUqnHEWZWRv6HLphHt9d5REgeq3YybER9cDijoOaEhZlLuy+GWYAK6NXd +MBG7srVRpnkgr3jaJYaFLXebpzHMNdUIDpU+3Rrhci4I78ZzRDptGVN+ppzZLhpM +W+TjJeCePncFeU9rzY0sT3Tk0dY4y8GZDVTp+Sp3SlQXFioF3ihXv7iNPfBkmZgZ +E+W4nQK6jfOTvwhkItnDi2In3FUx5Wxak3lYUulwCKWbNgl8rObJLQ14PC1MgBkp +VYtgCL3pL0lNGb7RNVPV0VN3i9d/SGMtvlY3y/2HgS8dMY6SjebjFIH/sRRPzmE9 +l3DW8pnrAp6+vfQn+u70UZBhFzDtUlNf4O7yQOnQL3twfP9QDskfhVrLfFHXpCQS +8U1sStGbL6xCqoC7 +=F3Jy +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob_with_unknown_eddsa_curve.pgp b/tests/testdata/compatibility/bob_with_unknown_eddsa_curve.pgp new file mode 100644 index 00000000..1885531b --- /dev/null +++ b/tests/testdata/compatibility/bob_with_unknown_eddsa_curve.pgp @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsBUBF2lnPIWCwYJ +KwYBBAGColpjB/0RWUyaRh7Ih2+VvD4TXboI3UCdkWKxvQyzS+KHL9RSaPhTlYKd +Oxgj/9VGMAcK31+ORMQrJNlKEL1bhqIZ5KhxmjLjPZnSZCWQ3V3WRcALyvbr/v5q +IGIEsnZMifZZDYQflsu83Fiy22Vv2VKq90SCCqa8eSU1Asm9tyvC2oBEiqZsgkpi +1Gjcq+KtWQNFTGEKKEWvcLSWhCXt1xG4rVa7QTRKDQsw1VGKPoh90OE2eWTMp2bm +A71oPa+LvaTiTXPsgZ7ZZ0cMywqid9U+n6xom+7sLc+izxtQe5TSV782RT4h5anO +gN08ywp4M2lhJzGgy4bXyR2LyU5u+vAEny5cwsE+BBgBCgByBYJdpZzyCRD7/Mgq +AV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfKHYzE +8uay50LpLy+PQFxb/Shyo2hSCVpHN5wJwZiPkAKbIBYhBNGmbhojsYLJmA94jPv8 +yCoBXnMwAADTkwv/aTr8lQnQRyqrpc9fUaxXvEeM5teTKmTFNzaWihTxYxLPTsrZ +tKUTR3nT2c19VKwZVY0fjamZyiabd2OYSVJQiC9BYoLcYan5NKnqJNzHikd/AwzZ +2Pxco8pUsLXu0sHmsldJz+WpPXTEdVauiai6tSMCN7tY977m4XDa0pbPpSF5CjGh +ZyAV+P79NTTRj92NryWCdkfoDw3JNKc4u6/YsjIg93BMjb4iDseb/NzluMgg/WVv +Cxnlfu/DqBbhHR9pYxBAe8knes+B2F2W+LH3nJEhJWaQvafU0EobK3fi77JDluHS +0m6NoO4mhddcWOZ88xyqpyLa79uJAVK7zBfEV6mE29tqrNRtc948gpbkCFQaS0Tu +RSOeb6SZRp1Je9cSuRKtZaSakoAYfwmvQ5+EK7PKH5UNnGQHZMiY/+xV9x/C9Tlf +eg/OYJnjZwzqDLCRjmNJCEoipvq2+ecuoQRWHeb9B2k7zkzvvdczZJHNHrDzbMUT +rGbptmnWtUt/BkSu +=rsWG +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp b/tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp new file mode 100644 index 00000000..4802a8ce --- /dev/null +++ b/tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp @@ -0,0 +1,45 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv +/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz +/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/ +5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3 +X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv +9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0 +qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb +SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb +vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w +bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx +gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz +XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO +ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g +9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF +DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c +ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1 +6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ +ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo +zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsFKBF2lnPJjCACB +m5BjCKhMy20KKep12fBXw0CCxe+osNXh9lPPmJdAn0WWRcK/b4ww/Moe98GrwLFl +OG8mwGUeVjhsRPXnKSlmUIevSpFxr3sI0yAuq4RD7DdDCI8ZII4zrgP9Xpa9jHil +DO6AUJTO0emlvUwvikgGmmjfN1fJZZLWOxerutjZLeBHz8/AFg6x+fGwEzkyUVPV +aYHCIU0lH343FDDsZiK7uXj3F/L72r7mPIj86B0dscNXt+S2YZgGiP/c7cWikc4a +kqUNODGDJGbkJB3xYCijZHf5taYuPapeNrdrGqEEBZjovnXQpHVGa5wCcnrfGAUq +2CyPNBazC74WP+58TOjtCACaq/3edRsa/QRRG9lY0ncusyBkOIAopcDt5IBGXFJv +gw2yWfQEwjtqsa9MKEHiXaejoE6TlgIX46LFLOi45ZeUHPzBJPyH4mAeOlUhl5qD +bl2xA8ehwxE3TiMTzZ455lvvtga4CcsJv0qC6eTSh+YnPZdXgb/rqylZ+WuYrCvB +5Kj1W0PhlbfY7nibg8+j1ttRKCrITIjYIhPSkTogw7WcvGWN3siDpbOGZmk0VhA0 +V4ilbPe5FFZqJM73k6u53m3/IlpVxVkp2gZecZHBVPRDoOUeGu0GLiksWNxqla9X +EckzZHbqprGCOjiyYRpGPjA7+T5Sm/2aG040iWHhPI1NwsE+BBgBCgByBYJdpZzy +CRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v +cmeqtZJm7YPPF1E7Cb1y+Oc6QAh3LMINygkBSwBZEWeBWwKbDBYhBNGmbhojsYLJ +mA94jPv8yCoBXnMwAABLZwv6AnkokACTFudwuaB4mWo/PdIWQZzHnsZK/aGtjTEH +XtEnPAlD+atgMWpe+pF1qWSBQgZWYlI7evgueToh30SKiXiSMrmfOy0yq8/JLqZx +Axt+66Famne/Ry8g9oIQEzCytx3NHQ3qzb1cB9Qft8Rpyrjktb7dV7+ruXGJLO/f +7Cj7UTcJ6QwoGz/sFjTJDfyrP8xfyIQWKUcHqJmPW2z18uQxoHp0WzgoxsCdaxuQ +eCRZJt/yeERnvyjoiw1IHOr1NuaQYE24pCopTnlUXdPWjsiNaRNx0a8v4VHDvwRQ +BGS9iFi8SSjSAtO2rAn3oMJi7FQB2gWRbUQ2eZEM0YNJaPlHc9oWwtRQz709Wukj +gRCwMtltQUArRqi3O5V+sjxphe3jcwgFJjBkToFfFdLFj1SpLp8N5Sue+UXFhz++ +5loyUjaRySCQB1M5LgDtU4m7O5NO3OG1Crc6szo9L60f7zqSh5cSxQSNzSaliVAY +GayI21FEA+d9JQ+3rFmecxxZ +=z8mg +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/encrypted_signed_with_unknown_algorithm.msg b/tests/testdata/compatibility/encrypted_signed_with_unknown_algorithm.msg new file mode 100644 index 00000000..0c789821 --- /dev/null +++ b/tests/testdata/compatibility/encrypted_signed_with_unknown_algorithm.msg @@ -0,0 +1,23 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+OGc6ftXom/H7P7G/pg6XUaDdAJ4aWqvKprWyoJ+nlqC2 +ZTjhRab15GGKDPNgcCVVB8X+Q45fsWx5QojlcTlshwB9pSd0/JlbBsYQQYNIJZCb +YiiNky+TA/7o4k2QXJI11qj4Vk9AAj7g2QxmmQFvL0CwGCpFMLsYVewbZySrLLA8 +AJ09iwDt/vldyPHeLc0rU3H+kAO+VbFgV97uOnR/8zymjoI42oWxfs7GIhEkq/Md +2x+ssvFvBejxXWS3dJ8DH3HW1hc0gTp2aBjoPb9oJP8UCGjw0vVG7PUWsnpKuKOX +6x6xFXiqgJZzDVj/lgJjUopvlRb0iXa0K7aDb2dClFV8pyOM+YBr7cmVZrD/cMya +aTqmgVBN4bIF9B89Bguma9hMpIeODeI8ZmImhNy60QE767UteosIakh+HhdpmVMz +9ZkeALj+MjXZLZnCCRcMcxsCLvX0rkGQFPAEzxbli+POj2Kln6mIntcu5d0IZjMi +NQwnKHm4IurwOuYsfXw00sEWAWIMiWBTtRsBrjQT5WZkTHFLAObXDPJJ9tKC/znX +ooTA1IKzpyoejDMZ1BC5899UaZR/WhoKHAKrLz9SnDC2fy1JhQFRXJsv+XdFSaSl +bzG2nQoCyZ+BFeSIJSdUxeARLDH/w4lgPeMTaqfs4P0V2Han483sjlVeJQ9BkCrx +2VwaxzdTvpvqb7BbZLXm4Fi+AlXZNHGjXZVf5PxfTBnQAqor1DWT0YVs1W3MAGHe +mtEdxex0kdZeSvI2JQJGuWdQR+q/hiiv9P0tymMPlEYhJ3oLsKoBrMZCvFvMXen3 +LpGEzUuYBH9A8PpjLYF7LeUDYCpQlM+2tnw3q68S0L6fEAt2kwI9XvLZmbOeLwDs +rrjyZ8tDb43xeBxMF3f2YRGUmuDEQyXgLWMBkUlN8C+MIJaenVwWUt7okt9Bzrrs +HoUozWdL7/f61utrkLrEcvuyBdm8f756+Tp9Pgwk3x8v69BET8d6ExYzq84sxLq/ +YR4YOzAuYzxwThjZo9kKD+C19vq05nz0BLblDK370y0sx4dJfCQiW/XDcTSEdTuh +1y3tFvtbOhBe7W7WxyBM8NrnbGyT169Ynd8MHZcjfT4rdg3vEMK8iJ5wUHiXYO3q +r6OGxt5iiVg= +=tj4l +-----END PGP MESSAGE----- diff --git a/tests/testdata/compatibility/encrypted_signed_with_unknown_signature_version.msg b/tests/testdata/compatibility/encrypted_signed_with_unknown_signature_version.msg new file mode 100644 index 00000000..4f8ee703 --- /dev/null +++ b/tests/testdata/compatibility/encrypted_signed_with_unknown_signature_version.msg @@ -0,0 +1,25 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv9FLXg7nalbJA17Pbd4otaD1beLSJ1tf9zWv6aKGoMclPS +sWJMvfW4ojBH2jJuH7HUenyPWMsnJIzeP8YC8jWF+vnVn6CVB0Hnef8+YNOF0K5T +jj+/lIKOxGPgUUo9hL7h+UI0a/W1noggmwgG2D5SgfQ7enyDk0uw/vpk5wRwqRzF +765O97izaUyBhkx7snls9dwrFPduwuvAYTnt2NbeaSJeivj+0N+YYPGqiXL5pWqn +WdPkCY0EPMIRDgco1rHOP2qUDKXN224OeJiCWE/sHlpmwyPE6P2L9WvazPD/pmBo +AI27TjejVvcmSDx45XNVu5ONRZ7pHerrURMKXb27nscfx4dj6MO4nSFqXiNN4uqy +mt1vF4ELEhu6OtQBXg3AlSj8EYQPkHI0nzEkOmWZrWMJU+at7Ph1BblhEQFiidOy +528BciZIvrLdvcWhgNiJ2eYxH3MXrnl/LKmeRtr1Zj1oJMxlpo47owq2vXc1fBIW +z88dDoN/PJgImGGzeHCY0sGYAUTo/Bd+MIiH/lTJUxUU4UfmYRTsD+R18CLLVhB9 +cIysmxlXbbvQAiSTY2t409rELyfJQU1A7D9Y0JwIou/njkIsrufOUGF936ZFN9T1 +WQUzbnyHqEeOsA6No/qk2SNVNgdGPv9emE5TX/rUQkonC0/zpPIedEY2fiBVNxkj +mvALMs97sIWcle/Vt3qPHZCV9xNtajg0qOZvmPtS+dnipqoe+1T7GEjO9xOwEHjZ +LGrSfbGAmvzNEJnfiX9rl2qAe7NaXiIDHVHQkY31DLihrjlX+6mJtHeqKVOQW1Ru +yvl4yBAuu0mxOQahRGmNRdYlUHnyYXJHLoXljJvNUCsH43r2KGeaDk9i4kDQA4pG +qqEyqOCUk94nKA/FhSf8247nubHRGyf3Sa0OXa7h8QvEtVqS+cBV3vjDhFVkFTLu +/pEDimiy267XDF5nLwwVHrLavT4VeGc9w7SoACBYvDspvt8RgCT3gnG2S3vtxgvw +8OzoDR8TATk/wKwEav5WI35ElfrnM60Erde5XZH5R1YiWFKOMqDe16YU4c4tZa7Y +dWcfaSFfOUm7ekAuCkOQx024L91c2wVdJY18ZGvR6I3XhaAbj0+a7ksT4UU1eNLX +qfGJjafpwNqPyKdSSVc7jxxsY4v8B24rtrWDOgBbRp1j8JSmtmSVB3rz39Gq4aLn +yuy/kZPgsNuaLzDvZ06/iWdGgHPcLAhz7orU++epxLK6xKgs/mjSanp0UYWZjx2d +AO1qJiiABKSJn1krOid7pezsOLxkN4rKNJ2rpuFZKuUeChVzDqZF7S/f +=V8p/ +-----END PGP MESSAGE----- diff --git a/tests/testdata/compatibility/pkesk_unknown_pkalg.msg b/tests/testdata/compatibility/pkesk_unknown_pkalg.msg new file mode 100644 index 00000000..04d70f8b --- /dev/null +++ b/tests/testdata/compatibility/pkesk_unknown_pkalg.msg @@ -0,0 +1,20 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+PkvYwtAPmEyGugGzZ3FvqGzxQ9kVm0N2+VjJ0VBMoWpz +jE6UB6rXXnBbaCCeiXiTKGTNOueyXB09xZVUxa1Kub4yTA90d+NxU7YFhUvcEzt+ +LgP9oWe3MrKB+WOFA7z9RAwCgfvBzwYZ9AFO/v4UHQVrDL7KLLnglPtcXnrvMf7G +ecqcoHAbyj/Jn4ZQqL1/fVF3Dqkfrv+M307IJtfh/SebMxAnYNDi/5U9xnZo/Bvf +JVGa2UtxrvyMkIbZZuy82ZzkDK1HI+NHQhBlksS00AKA/vIwTHKNsdITE+ToY7Pi +jjRU92Sl1KyKdt8e00DTPoDRWxxUFSVMJB4eSP/39HBcAVGW5tHYkMGYXhwRoxPD +qbxWawGfrLxCNga0n5cM3Z1I4m8jel1Xj0RvjWptgHYGB64oWte9hfh3GHT7o3X6 +40zJnsCrp6BFyY+lp7DHTsyOGCh6qxA+jvcu0UBMn+mSEkWmyP318KEN2ohJZzWt +rWzU709sTOgZDI1qqMbLwcBKAwEjRWeJq83vYxoaGhoaGhoaGhoaGhoaGhoaGhoa +GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa +GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa +GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa +GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa +GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrSTAHK +7+Hdple30q0bt73f41f61wdFRUYmyREN1zmOt8NWLUIdQFkeY1fw25Nil2yH3h+/ +spp41fwEYgEStCN2jv7rb4AvPlNcCb929/c= +=Gt9v +-----END PGP MESSAGE----- diff --git a/tests/testdata/compatibility/pkesk_unknown_version.msg b/tests/testdata/compatibility/pkesk_unknown_version.msg new file mode 100644 index 00000000..75b9f7fe --- /dev/null +++ b/tests/testdata/compatibility/pkesk_unknown_version.msg @@ -0,0 +1,16 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+PkvYwtAPmEyGugGzZ3FvqGzxQ9kVm0N2+VjJ0VBMoWpz +jE6UB6rXXnBbaCCeiXiTKGTNOueyXB09xZVUxa1Kub4yTA90d+NxU7YFhUvcEzt+ +LgP9oWe3MrKB+WOFA7z9RAwCgfvBzwYZ9AFO/v4UHQVrDL7KLLnglPtcXnrvMf7G +ecqcoHAbyj/Jn4ZQqL1/fVF3Dqkfrv+M307IJtfh/SebMxAnYNDi/5U9xnZo/Bvf +JVGa2UtxrvyMkIbZZuy82ZzkDK1HI+NHQhBlksS00AKA/vIwTHKNsdITE+ToY7Pi +jjRU92Sl1KyKdt8e00DTPoDRWxxUFSVMJB4eSP/39HBcAVGW5tHYkMGYXhwRoxPD +qbxWawGfrLxCNga0n5cM3Z1I4m8jel1Xj0RvjWptgHYGB64oWte9hfh3GHT7o3X6 +40zJnsCrp6BFyY+lp7DHTsyOGCh6qxA+jvcu0UBMn+mSEkWmyP318KEN2ohJZzWt +rWzU709sTOgZDI1qqMbLwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAcrv +4d2mV7fSrRu3vd/jV/rXB0VFRibJEQ3XOY63w1YtQh1AWR5jV/Dbk2KXbIfeH7+y +mnjV/ARiARK0I3aO/utvgC8+U1wJv3b39w== +=DN9X +-----END PGP MESSAGE----- diff --git a/tests/testdata/compatibility/ricarda.pgp b/tests/testdata/compatibility/ricarda.pgp new file mode 100644 index 00000000..e0a5fb25 --- /dev/null +++ b/tests/testdata/compatibility/ricarda.pgp @@ -0,0 +1,86 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: 2ADE 0F8A A059 6BC9 4E50 D2AD 9162 53AB 652E F195 +Comment: Ricarda S. Álvarez + +xsDNBGJvrDABDACrZUY7VU1uZ/uzntlAKEHVF4mb3eYSdW1rE3hVke0HoDvtQbzq +KQ9qgfPaNwdkxRexgrNGSeKkQJcgR7gMWxFxM/FwddQIXfVL43nRlN+iGvFDYR+9 +dn5gSOD9EvZUYLN9p6yR3Umyglt4NpdjYM0J+Rn2DVyfGHCtS+z1fdym1h1zdImo +rArBpWMEdvNN/6dR8BN67WSBs5pVsvnDPdjbeU+GPJVoRH4CWe/LdJnJDICPmlva +gAyeJeK+KitkxD8IIc9d18x5dV1Z/LL2o1B0Psort8+az2Z+NbkP2cUv0DDRyZIM +Ww1A6KMSSNuvewXGCU+gEFlGIA83zfq7XE3WNp9EPy9xCs610+KxSS9cftKdrMmU +cl37Gb1lZyLFIBUCwPdIyTvhs6r6rMgu4GQ46sPl8qeLbtaoXnPWj9V1Fn9RoMgz +Kq8ZItFCJfCbf+gamx/0S/0mE47giuAtymw8Hf53e3jsSANK28GpcsEUNqWT4djb +YgcWl2iXR7n0i9cAEQEAAcLBSQQfAQoAfQWCYm+sMAMLCQcJEJFiU6tlLvGVRxQA +AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6O6vv9v4H1aAR9Z +aEksrL/2rQGm4Omv2SCubCC1pk/DAxUKCAKbAQIeARYhBCreD4qgWWvJTlDSrZFi +U6tlLvGVAACp1wv+MEkIs8x2Wk6e+i4rbclnkBjR2tSwv3N66jcz09QeDt5xwX1Q +jq1TP+XGFJfFA+LUugbLWNzWqwh7CypSc6IAB82+Ha+lCMjY3SZLfTe6crUgDWOZ +wDprVND6z2g9UlgJ6rfFf329LEUziknjKFBbreONH12tOzvCxrxipp84J0DZEqO3 +0VB2b2B7uO1X5V1aQMFj4JEafQ6Or6uXtR3Fwcs/JALwhqydy3LI4Y0s/y91qdPP +69DgugVDbG/Kkp5W2CNtGyd51cnEoFEbh9nV0hHR+rERU2ZcyoNfZQCyPuHMIT1L +PndcWLsT6Ga3TvXLUEhOOl6CPpox0aV7bsGpip0u54yGkhgR5YYLchcTVt15q5+/ +M5Z03et1LcSZKk1ijSFXNsOWdD2ojneN3vhlKbJoDosT10WyCsXKL8JNP4F4q4nU +22+/N1sttbgzSatwPxyiC7kKymQmDtg8NCyyCtpDoRkEIzaMv+4DqoMVkvLRcF1K +vX+rNEg1OxneqMSEzS1SaWNhcmRhIFMuIMOBbHZhcmV6IDxyaWNhcmRhQG9wZW5w +Z3AuZXhhbXBsZT7CwUwEEwEKAIAFgmJvrDADCwkHCRCRYlOrZS7xlUcUAAAAAAAe +ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfo22n7d9n+Nv3PxzMP93f7 +rejDV4BFsobH6T5cZGvj8AMVCggCmQECmwECHgEWIQQq3g+KoFlryU5Q0q2RYlOr +ZS7xlQAAu6EL/iJ91h1dwcQMZpbKYnBbH/1U/ncbIlhkQf8YzjrpzVprU0sr4cKu +mx42EylcteODVoDN8ITGROUC8Prw5tu+NS2mjkKWUlj56eLkpS3A3D6NqPIFEzU3 +KYztp+Y0hq2ZmsGsPgL1AbVPc2+ngUSL4pXgQ5hmVoD3B6VGqBReyKXhZNzH42S5 +fMuOsY8+/xgQ0WDwSCVeUW7Otql24iFji9U4eL036AzOO1hXkbOesrw4E7GmU2mR +kkX/aDZ9bGYT3yVZ/CJkU4wrUHJWW1IJl/bl//becL8Vnqr92vQkYQnUh19zIi2n +6ualROjcoITjjRemvkBfM9zXTd6kVFKD+ySDnGtNq62Ukz0/COg81tAnnnXeNhlP +1M8yA6zfrY17tAFYLmALUrPYjVy4ZJuaHScnIcM5lHIKYn1ijetGQSjI4sT/mSi4 +8fwNHFRufR7ta9XWv5b4+m8SAyrJ/FqnRoOwtoph0ZxSItjpv+qLPX2N4M8/JOqh +3QhI5ZDd+ScGJc7AzQRib6wwAQwArMv2IdO1f8a/brBc/+LeCqyqR+qLKzZRLAvF ++c6+qG7D9OzZCJfbPltnQ5BgRzIiSKgzDugDDt0m3fdWBxOQG5Ojo53Xu1ZTgRUD +0KWI4kH5Cs0gp3Kl62TdNAxNUlHdWqJ6G6WYVMlMhLmBVPjo65pT1+OON2v/O/qA +8ZJRlK3RyKym4Kcr8JbtX1roSRtVPRpi++Y1CGuhnK3UM68putqfgZOnTZxln2m2 +BUrL8fWgme2rTJyrrN8fSM61dZZ+5E0SWDMBxEp5bimMrRjJc7V0JMUx2HiCDe/l +panzleAugfEVUeAYsEe9C+7k3p2r00roL0dWnrMgfhGnJy5ccmPyzEQpoA6ARq1a +EBk2Syr/QNO93GfDFcK9pTcDcQdUgq93x5s9LShLQ7fuvVXXxkl4QkdKJn+Vb2yP +e2Bw6VmMy40JUVyMvUY99YUK+y8iZmA+ro6naeJn+P/uCiURL+dMoWmlGsCbZjz0 +sVh1t5pH+gPRI531U7BfNX6OmRk9ABEBAAHCwT4EGAEKAHIFgmJvrDAJEJFiU6tl +LvGVRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+GZb8lQ +10FTIysD5BKY6IJRAGxevRcudbWb9ItkymXvApsMFiEEKt4PiqBZa8lOUNKtkWJT +q2Uu8ZUAAFl5C/wLIh2UcurP0mpNaaSiRGgNVrFCPceF4rFlY/1/yJbNE8yWIEQt +rI4Dh2jdP7mxlfSH8SMsAPkSl9mA8xIGXHUiWkN+tEh4v3BurccaSUMA81+FveC3 +jSR9AtXECk/Bk6l4gAz2qRRwq9uErxZD+IuZN/W6uue6z4nnSET7qc9q7vu6tDYR +g8J6vXef4RdIq7pRsdSMSNFTIHSDEgXpGV+ru+7Y98ipHhwKqYHUlMgTX56m8HQ3 +1uJvgFFGkKVwQQORiu48mgqdAFHgHrree32BxDpxAJstvsdGcvNraaFqAgkikFHV +DiScG1yKZnYgzeJhI6eNwxpDDNl5FkHub8YOXftr936Is4jmKVg7H430502dh5ko +A695dMCpCo8uaoGduWx/7Mh+SmV9WbqjHQlWKZbnQ0eCAyn2TZD7VyI1/QCz8dsb +YC5wLe0xqQxx0fEPqHZS57QerBlKQ8eaxEIUpTx63LrPvu15XsBMCsGPik4gTpqR +ZTWc+9wHu+IMtobOwM0EYm+sMAEMALpkWQP4+YULtR+qeJX0OlJkWIk77o5/7TwZ +n/Ho+fz9hnXmp6YtrSUFIofJ9LpcKWyx8OB9G/8FBG1TZXHdgndAkKpdzN9fVKay +86+p9+F3ExoioFazjYiXNJFwgtIFcHXkibnOUJvtftvJlFoupQBPAih89Jlg1OBt +80wRW44zC6IBxWWfMIKVuMPIOw6sMxKh8vz2bBfa8S3N9Mxi8t2ncKQuTi4Hy4Hr +o6duFuUBUVTSqzrxvq3uS+CrWSae4xpjgSOf/gWjKfRqWip5fT/DtKkmarQ7dqLP +C8IncBpLo4riREDu3lub/AFjDLyvZ1rg/F4CeNYK6xsdgcZvF00+a4nKZaW4KAS/ +MDBHYaK0WFbMebtHO2veCQ/+DUqyaRf7Trrr56h11ffGGDuGR3m9XLXaQJ07Ct87 +Kpywly5ZaSRs8vRtyrxCfmAX7QWnjio/FeI2JzJdyMlGfzDV+VjCBUVXin8DlJ8v +MV8n9/Uzu6LSMOJKbVX8VZSg28YWuwARAQABwsM8BBgBCgJwBYJib6wwCRCRYlOr +ZS7xlUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcHvNZ+ +NAzY3Y0vZZy2BB5vTzG2mB28DjINb6OcDwAWbgKbAsE8oAQZAQoAbwWCYm+sMAkQ +s/nxXZcRZK5HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn +tYbTPBH4zs1yFsUSK50UToSSLpDbHSCIVT0CZFQSOywWIQTfVkurgqxmNp++Bc+z ++fFdlxFkrgAAJOsL/0in5Hj6I+/bDavsLhg/atB4UUP3EBO6x0rW5TUuW3UxYrlT +yjza4aVYPHbcm7DNmkHPoYoU4l374kOozn7cXX2hB1xOMINd8MI/cfoKD9oL3hGF +BdhuVgyJZAUVfQKIvoAcaYUjivRCIbrUkgIkqFSYTPwJ792mrkQXecRdHLbP/OcP +tBgLB+lfnFdNh0KU5HIN5E/Ohse3it+HyRUAcNdkYH/VxTYTOTXYUt8kO7Rpe6uI +YcfPzPnXqGub6lbF4pXvQQRyuj/lPOPcPtBrpZgZFCXu0nl8EIJRdAZOb2eclBft +rrYf7z/jwi/z9rPNvDMyuKotgrmppiYdgraTNh9v6cLRQiKSjit5sK4FsJeXiP21 +xbwb22j5fJyZqksbgq1zBGarmbdIbJ07oGHkaVFkO2/rXoseWaUKkQM0VDw9aDa/ +Qe0vuMiHa5B04HkzxvJdI3XcJ9vLpqCKNoFbksGlSuc5N6euAYRjMFbMaPl2f+k5 +4xny8TGYmncul26DSBYhBCreD4qgWWvJTlDSrZFiU6tlLvGVAAC/VgwAnczy13qB +4bVGkcjGGGjw7coqUZwVihwXqf8uhh9iSJcTocslYYnoB/K/4nHab6Xor92lCRJO +iw2LByr+YhzRwkxog//PvvAjAvGCoidpIfU8FMkUd4X57e95MvOpD/ePojOVmFCE +gW/77VDIZcpJ3VYNx+VBv6FxnbnXO3Kd62p7SKiHOnAZgH/U+tq8qIFUv0QLJtzG +4BbppPTjIH+gZc2nrXp0sZ2Ov296qwl62ZprW2S17ljFTmQv6zcnicC0J25F5Zro +GWgMAJcpdb77V3PlPCy4QhttrMjGpGPtzVGjm13YxaEkGuPCvWmoM/6nqAVOZFmy +zTgQjuQpJElwQGI+pi8DvzkKmLo5N7+0iOjBF4hTLKQ6AP7+9WxLCgHDtjRgFPtp +Vo1sVqH0l596O4qSgs81JLtS7SwXfihyRAxXqrlvseAEaPNjK/FgQym3q17UqElm +u1Fr1U9mB1v6s2rXN2USut8xQECpA+OIiUItczMBd/OYOnu0Y3eImkYc +=i+jK +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/compatibility/sig23_sig4.sig b/tests/testdata/compatibility/sig23_sig4.sig new file mode 100644 index 00000000..4710b31b --- /dev/null +++ b/tests/testdata/compatibility/sig23_sig4.sig @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNATURE----- + +wsE7FwABCgBvBYJkf2S3CRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmeLydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WP +XxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUq +kKkeyOdTGYZEw4Wr9q3bW+Ihl8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihD +b/LN/9K35qqaJgp0Vw1w9KC0NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13o +eKoteXc+bSYa7eA6JJpO2cBxq/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/ +tk3OdnodsST5c2bwPkThnYJazEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJ +haiBrRjQ9D5ePdpzNLPtemG79vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2 +AvmeI254FJqrd2ORdWoMFK1raAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXW +WadP9HQqgld8H3UG8cLDM7XdZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQ +m6ow6tOrgzvG7bsTbgvsWeOjlc55TrCONOK1+qpIwsE7BAABCgBvBYJkf2S3CRD7 +/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeL +ydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WPXxYhBNGmbhojsYLJmA94jPv8 +yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUqkKkeyOdTGYZEw4Wr9q3bW+Ih +l8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihDb/LN/9K35qqaJgp0Vw1w9KC0 +NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13oeKoteXc+bSYa7eA6JJpO2cBx +q/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/tk3OdnodsST5c2bwPkThnYJa +zEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJhaiBrRjQ9D5ePdpzNLPtemG7 +9vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2AvmeI254FJqrd2ORdWoMFK1r +aAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXWWadP9HQqgld8H3UG8cLDM7Xd +ZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQm6ow6tOrgzvG7bsTbgvsWeOj +lc55TrCONOK1+qpI +=idy8 +-----END PGP SIGNATURE----- diff --git a/tests/testdata/compatibility/sig4_b-sig4_r.sig b/tests/testdata/compatibility/sig4_b-sig4_r.sig new file mode 100644 index 00000000..281dc097 --- /dev/null +++ b/tests/testdata/compatibility/sig4_b-sig4_r.sig @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNATURE----- + +wsE7BAABCgBvBYJkf2S3CRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmeLydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WP +XxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUq +kKkeyOdTGYZEw4Wr9q3bW+Ihl8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihD +b/LN/9K35qqaJgp0Vw1w9KC0NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13o +eKoteXc+bSYa7eA6JJpO2cBxq/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/ +tk3OdnodsST5c2bwPkThnYJazEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJ +haiBrRjQ9D5ePdpzNLPtemG79vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2 +AvmeI254FJqrd2ORdWoMFK1raAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXW +WadP9HQqgld8H3UG8cLDM7XdZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQ +m6ow6tOrgzvG7bsTbgvsWeOjlc55TrCONOK1+qpIwsE7BAABCgBvBYJkf2S3CRCz ++fFdlxFkrkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfZ +kjsbm6GUzqohKl3WhKgzzGl51pJBsREOkFBHTNhpeBYhBN9WS6uCrGY2n74Fz7P5 +8V2XEWSuAABShwwAo41bVBEuZPowTEL5uvvBL8eEPuS/+11ygXF/hfWayEAvIW+y +tUSLdQ2xF/SJLDGPgkwRcStpX2qIHK0qB4of3fG9OX95upcldKgmdrq38OWuti95 +IuZnuWQ75w0Ka8E/er/lvLmI6fW8istAvUkX1fSthQ1IYeEAPH4CEy0m5lwJfzK+ +XEF6Ne1rEBig6+LC+5/VyBI0jPWhW99g7kH5PiusSFdllO2Ewsfe7GXbWpqwJwCs +DqrdyCgh9z9lt7rTKwpbg2aSPKqKk8DGz2FPjmFgz5WyYWiCG28oTmBDHXhN5i5X ++pPXOlwSvUiGmrvZswewbA4qfBX8jLwqmH/5VaNAiegS05MjTsYEtgAjogCAW3kv +9ka2bOnw6r6GWBiKebE2JU8my+B0H07o9NJ561r7qfyrclF/610z5b6Jh/v6jbCj +2ZCkDTCxyXu4z7IbMY2bPW09QfaW4bYlsfehcwJRuDtxekFbS73mmo9nhVXPhG9s +5I3wawEahPoXGOVn +=QY9Z +-----END PGP SIGNATURE----- diff --git a/tests/testdata/compatibility/sig4_r-sig4_b.sig b/tests/testdata/compatibility/sig4_r-sig4_b.sig new file mode 100644 index 00000000..b8240c63 --- /dev/null +++ b/tests/testdata/compatibility/sig4_r-sig4_b.sig @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNATURE----- + +wsE7BAABCgBvBYJkf2S3CRCz+fFdlxFkrkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmfZkjsbm6GUzqohKl3WhKgzzGl51pJBsREOkFBHTNhp +eBYhBN9WS6uCrGY2n74Fz7P58V2XEWSuAABShwwAo41bVBEuZPowTEL5uvvBL8eE +PuS/+11ygXF/hfWayEAvIW+ytUSLdQ2xF/SJLDGPgkwRcStpX2qIHK0qB4of3fG9 +OX95upcldKgmdrq38OWuti95IuZnuWQ75w0Ka8E/er/lvLmI6fW8istAvUkX1fSt +hQ1IYeEAPH4CEy0m5lwJfzK+XEF6Ne1rEBig6+LC+5/VyBI0jPWhW99g7kH5Pius +SFdllO2Ewsfe7GXbWpqwJwCsDqrdyCgh9z9lt7rTKwpbg2aSPKqKk8DGz2FPjmFg +z5WyYWiCG28oTmBDHXhN5i5X+pPXOlwSvUiGmrvZswewbA4qfBX8jLwqmH/5VaNA +iegS05MjTsYEtgAjogCAW3kv9ka2bOnw6r6GWBiKebE2JU8my+B0H07o9NJ561r7 +qfyrclF/610z5b6Jh/v6jbCj2ZCkDTCxyXu4z7IbMY2bPW09QfaW4bYlsfehcwJR +uDtxekFbS73mmo9nhVXPhG9s5I3wawEahPoXGOVnwsE7BAABCgBvBYJkf2S3CRD7 +/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeL +ydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WPXxYhBNGmbhojsYLJmA94jPv8 +yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUqkKkeyOdTGYZEw4Wr9q3bW+Ih +l8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihDb/LN/9K35qqaJgp0Vw1w9KC0 +NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13oeKoteXc+bSYa7eA6JJpO2cBx +q/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/tk3OdnodsST5c2bwPkThnYJa +zEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJhaiBrRjQ9D5ePdpzNLPtemG7 +9vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2AvmeI254FJqrd2ORdWoMFK1r +aAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXWWadP9HQqgld8H3UG8cLDM7Xd +ZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQm6ow6tOrgzvG7bsTbgvsWeOj +lc55TrCONOK1+qpI +=peEs +-----END PGP SIGNATURE----- diff --git a/tests/testdata/compatibility/sig4_sig23.sig b/tests/testdata/compatibility/sig4_sig23.sig new file mode 100644 index 00000000..90d7282e --- /dev/null +++ b/tests/testdata/compatibility/sig4_sig23.sig @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNATURE----- + +wsE7BAABCgBvBYJkf2S3CRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u +cy5zZXF1b2lhLXBncC5vcmeLydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WP +XxYhBNGmbhojsYLJmA94jPv8yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUq +kKkeyOdTGYZEw4Wr9q3bW+Ihl8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihD +b/LN/9K35qqaJgp0Vw1w9KC0NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13o +eKoteXc+bSYa7eA6JJpO2cBxq/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/ +tk3OdnodsST5c2bwPkThnYJazEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJ +haiBrRjQ9D5ePdpzNLPtemG79vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2 +AvmeI254FJqrd2ORdWoMFK1raAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXW +WadP9HQqgld8H3UG8cLDM7XdZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQ +m6ow6tOrgzvG7bsTbgvsWeOjlc55TrCONOK1+qpIwsE7FwABCgBvBYJkf2S3CRD7 +/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeL +ydr/KwuIg2juO15I6oucMhsgNCcuFLmwzmRjN7WPXxYhBNGmbhojsYLJmA94jPv8 +yCoBXnMwAACzwgwAsa09M/8lZf3EfRfcDzzwYKUqkKkeyOdTGYZEw4Wr9q3bW+Ih +l8EECzntzPkTObJjNfRjlB8VW+XGgSY4CoNVbihDb/LN/9K35qqaJgp0Vw1w9KC0 +NcmWqykn+b3mmWcbHnxE9uEnG4QhCXIM9/u+O13oeKoteXc+bSYa7eA6JJpO2cBx +q/ZqG1CHp5x8+0QC2kiZllsmkZqgqCleF9M70Al/tk3OdnodsST5c2bwPkThnYJa +zEMBhHv2YPGLhwj6j0N+I+HTqloEZxd7gTcFsIDJhaiBrRjQ9D5ePdpzNLPtemG7 +9vxUeVpHniQAgkXHZqCct0S788kbMJC0hAOYWD/2AvmeI254FJqrd2ORdWoMFK1r +aAzRLWetj3ts4dcU/6Y36/AjMhN+zqPlxDrd3hXWWadP9HQqgld8H3UG8cLDM7Xd +ZRBqr5v2zwH9dgJTg/bVysKsg2LWm0m1k6rbd6EQm6ow6tOrgzvG7bsTbgvsWeOj +lc55TrCONOK1+qpI +=FIFU +-----END PGP SIGNATURE----- diff --git a/tests/testdata/compatibility/skesk_unknown_s2k_algo.msg b/tests/testdata/compatibility/skesk_unknown_s2k_algo.msg new file mode 100644 index 00000000..c359159a --- /dev/null +++ b/tests/testdata/compatibility/skesk_unknown_s2k_algo.msg @@ -0,0 +1,16 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+PkvYwtAPmEyGugGzZ3FvqGzxQ9kVm0N2+VjJ0VBMoWpz +jE6UB6rXXnBbaCCeiXiTKGTNOueyXB09xZVUxa1Kub4yTA90d+NxU7YFhUvcEzt+ +LgP9oWe3MrKB+WOFA7z9RAwCgfvBzwYZ9AFO/v4UHQVrDL7KLLnglPtcXnrvMf7G +ecqcoHAbyj/Jn4ZQqL1/fVF3Dqkfrv+M307IJtfh/SebMxAnYNDi/5U9xnZo/Bvf +JVGa2UtxrvyMkIbZZuy82ZzkDK1HI+NHQhBlksS00AKA/vIwTHKNsdITE+ToY7Pi +jjRU92Sl1KyKdt8e00DTPoDRWxxUFSVMJB4eSP/39HBcAVGW5tHYkMGYXhwRoxPD +qbxWawGfrLxCNga0n5cM3Z1I4m8jel1Xj0RvjWptgHYGB64oWte9hfh3GHT7o3X6 +40zJnsCrp6BFyY+lp7DHTsyOGCh6qxA+jvcu0UBMn+mSEkWmyP318KEN2ohJZzWt +rWzU709sTOgZDI1qqMbLw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YdJMAcrv4d2mV7fSrRu3vd/jV/rXB0VFRibJEQ3XOY63w1YtQh1AWR5jV/Dbk2KX +bIfeH7+ymnjV/ARiARK0I3aO/utvgC8+U1wJv3b39w== +=74qf +-----END PGP MESSAGE----- diff --git a/tests/testdata/compatibility/skesk_unknown_version.msg b/tests/testdata/compatibility/skesk_unknown_version.msg new file mode 100644 index 00000000..490b1a91 --- /dev/null +++ b/tests/testdata/compatibility/skesk_unknown_version.msg @@ -0,0 +1,16 @@ +-----BEGIN PGP MESSAGE----- + +wcDMA3wvqk35PDeyAQv+PkvYwtAPmEyGugGzZ3FvqGzxQ9kVm0N2+VjJ0VBMoWpz +jE6UB6rXXnBbaCCeiXiTKGTNOueyXB09xZVUxa1Kub4yTA90d+NxU7YFhUvcEzt+ +LgP9oWe3MrKB+WOFA7z9RAwCgfvBzwYZ9AFO/v4UHQVrDL7KLLnglPtcXnrvMf7G +ecqcoHAbyj/Jn4ZQqL1/fVF3Dqkfrv+M307IJtfh/SebMxAnYNDi/5U9xnZo/Bvf +JVGa2UtxrvyMkIbZZuy82ZzkDK1HI+NHQhBlksS00AKA/vIwTHKNsdITE+ToY7Pi +jjRU92Sl1KyKdt8e00DTPoDRWxxUFSVMJB4eSP/39HBcAVGW5tHYkMGYXhwRoxPD +qbxWawGfrLxCNga0n5cM3Z1I4m8jel1Xj0RvjWptgHYGB64oWte9hfh3GHT7o3X6 +40zJnsCrp6BFyY+lp7DHTsyOGCh6qxA+jvcu0UBMn+mSEkWmyP318KEN2ohJZzWt +rWzU709sTOgZDI1qqMbLw00XCQMIMiFs6BH34GH/YWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM +Acrv4d2mV7fSrRu3vd/jV/rXB0VFRibJEQ3XOY63w1YtQh1AWR5jV/Dbk2KXbIfe +H7+ymnjV/ARiARK0I3aO/utvgC8+U1wJv3b39w== +=e3i1 +-----END PGP MESSAGE----- From fb4bea1bd7871a53fca1b8374a650b226c028e10 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 11 Feb 2023 12:40:03 -0500 Subject: [PATCH 011/287] Drop unused test_load_ask_bench.py pgpy.types.Exportable was renamed to Armorable before version 0.3.0 was released (in d00010ef04c0fe1353756b04cba257d2a75e5db9, back in 2014), which indicates that this test program has not been run in over 8 years. Given that the asyncio stuff has also changed in python since then, it is simpler to just drop this file than try to fix it. --- test_load_asc_bench.py | 159 ----------------------------------------- 1 file changed, 159 deletions(-) delete mode 100644 test_load_asc_bench.py diff --git a/test_load_asc_bench.py b/test_load_asc_bench.py deleted file mode 100644 index 1d9cf59f..00000000 --- a/test_load_asc_bench.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python -import asyncio -import bisect -import collections -import itertools -import os -import sys - -from progressbar import ProgressBar, AnimatedMarker, Timer, Bar, Percentage, Widget - -import pgpy -from pgpy.packet import Packet -from pgpy.types import Exportable - - -ascfiles = [ os.path.abspath(os.path.expanduser(f)) for f in sys.argv[1:] if os.path.exists(os.path.abspath(os.path.expanduser(f))) ] - -if len(ascfiles) == 0: - sys.stderr.write("Please specify one or more ASCII-armored files to load\n") - sys.exit(-1) - -for a in [ os.path.abspath(os.path.expanduser(a)) for a in sys.argv[1:] if a not in ascfiles ]: - sys.stderr.write("Error: {} does not exist\n".write()) - -class Mebibyte(int): - iec = {1: 'B', - 1024: 'KiB', - 1024**2: 'MiB', - 1024**3: 'GiB', - 1024**4: 'TiB', - 1024**5: 'PiB', - 1024**6: 'EiB', - 1024**7: 'ZiB', - 1024**8: 'YiB'} - iecl = [1, 1024, 1024**2, 1024**3, 1024**4, 1024**5, 1024**6, 1024**7, 1024**8] - - # custom format class for human readable IEC byte formatting - def __format__(self, spec): - # automatically format based on size - - iiec = max(0, min(bisect.bisect_right(self.iecl, int(self)), len(self.iecl))) - ieck = self.iecl[iiec - 1] - return '{:,.2f} {:s}'.format(int(self) / ieck, self.iec[ieck]) - - -@asyncio.coroutine -def _dospinner(pbar): - for i in pbar(itertools.cycle(range(100))): - try: - yield from asyncio.shield(asyncio.sleep(0.005)) - - except asyncio.CancelledError: - print("") - break - -@asyncio.coroutine -def _load_pubring(ascfile, future): - with open(ascfile, 'r') as ppr: - a = yield from asyncio.get_event_loop().run_in_executor(None, ppr.read) - future.set_result(a) - -@asyncio.coroutine -def _unarmor(a, future): - b = yield from asyncio.get_event_loop().run_in_executor(None, pgpy.types.Exportable.ascii_unarmor, a) - future.set_result(b) - -_b = bytearray() - - -loop = asyncio.get_event_loop() -for ascfile in ascfiles: - ascfile = os.path.abspath(ascfile) - if not os.path.isfile(ascfile): - sys.stderr.write('Error: {} does not exist'.format(ascfile)) - continue - - load_bar = ProgressBar(widgets=["Reading {} ({}): ".format(ascfile, Mebibyte(os.path.getsize(ascfile))), AnimatedMarker()]) - unarmor_bar = ProgressBar(widgets=["Unarmoring data: ", AnimatedMarker()]) - - - a = asyncio.Future() - b = asyncio.Future() - - lbp = asyncio.Task(_dospinner(load_bar)) - asyncio.Task(_load_pubring(ascfile, a)) - loop.run_until_complete(a) - _a = a.result() - lbp.cancel() - - uap = asyncio.Task(_dospinner(unarmor_bar)) - asyncio.Task(_unarmor(_a, b)) - loop.run_until_complete(b) - _b += b.result()['body'] - uap.cancel() - -loop.stop() -print("\n") - -packets = [] -_mv = len(_b) - - -class BetterCounter(Widget): - def __init__(self, pktlist, iec=False, format='{:,}'): - self.list = pktlist - self.iec = iec - self.format = format - - def update(self, pbar): - if self.iec: - return self.format.format(Mebibyte(len(self.list))) - - return self.format.format(len(self.list)) - - -pb3w = [BetterCounter(packets, False, '{:,} pkts'), '|', BetterCounter(_b, True, '{:,} rem.'), '|', Timer("%s"), '|', Percentage(), Bar()] - -pbar3 = ProgressBar(maxval=_mv, widgets=pb3w).start() -while len(_b) > 0: - olen = len(_b) - pkt = Packet(_b) - # if len(packets) == 10132: - # a=0 - # try: - # pkt = Packet(_b) - # - # except: - # print("\n\tSomething went wrong!") - # print("\tBad packet followed packet #{:,d}".format(len(packets))) - # print("\tLast packet was: {:s} (tag {:d}) ({:,d} bytes)".format(packets[-1].__class__.__name__, packets[-1].header.tag, packets[-1].header.length)) - # print("\t{:,d} bytes left unparsed".format(len(_b))) - # print("\tFailed packet consumed {:,d} bytes".format(olen - len(_b))) - # raise - # - # if (olen - len(_b)) != len(pkt.header) + pkt.header.length: - # print("Incorrect number of bytes consumed. Got: {:,}. Expected: {:,}".format((olen - len(_b)), (len(pkt.header) + pkt.header.length))) - # print("Bad packet was: {cls:s}, {id:d}, {ver:s}".format(cls=pkt.__class__.__name__, id=pkt.header.typeid, ver=str(pkt.header.version) if hasattr(pkt.header, 'version') else '')) - # print("loaded: " + str(len(packets))) - packets.append(pkt) - pbar3.update(_mv - len(_b)) -pbar3.finish() - -print("\n\n") -print('Parsed Packet Stats\n') - -pcnts = collections.Counter(['{cls:s} v{v:d}'.format(cls=c.__class__.__name__, v=c.version) if hasattr(c, 'version') else c.__class__.__name__ - for c in packets if not isinstance(c, pgpy.packet.Opaque)] + - ['Opaque [{:02d}]{:s}'.format(c.header.tag, '[v{:d}]'.format(c.header.version) if hasattr(c.header, 'version') else '') for c in packets if isinstance(c, pgpy.packet.Opaque)]) - -ml = max(5, max([len(s) for s in pcnts.keys()])) -mcl = max(5, max([len("{:,}".format(c)) for c in pcnts.values()])) - -print('Class{0: <{pad1}} Count\n' \ - '====={0:=<{pad1}} ====={0:=<{pad2}}'.format('', pad1=(ml - 5), pad2=(mcl - 5))) - -for pc, cnt in sorted(pcnts.items(), key=lambda x: x[1], reverse=True): - print('{cls:{pad1}} {count: <{pad2},}'.format(pad1=ml, pad2=mcl, cls=pc, count=cnt)) - -print("") From d5155d28d3c5d6a9c1d539255f75476490c22f7c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 15:58:38 -0400 Subject: [PATCH 012/287] tests: Avoid passing naive datetime objects --- tests/test_99_regressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index da12d079..fac50591 100644 --- a/tests/test_99_regressions.py +++ b/tests/test_99_regressions.py @@ -19,7 +19,7 @@ def test_reg_bug_56(): # some imports only used by this regression test import hashlib - from datetime import datetime + from datetime import datetime, timezone from pgpy.pgp import PGPSignature @@ -135,7 +135,7 @@ def test_reg_bug_56(): sig = PGPSignature.new(SignatureType.BinaryDocument, PubKeyAlgorithm.RSAEncryptOrSign, HashAlgorithm.SHA512, sk.fingerprint.keyid) - sig._signature.subpackets['h_CreationTime'][-1].created = datetime(2014, 8, 6, 23, 28, 51) + sig._signature.subpackets['h_CreationTime'][-1].created = datetime(2014, 8, 6, 23, 28, 51, tzinfo=timezone.utc) sig._signature.subpackets.update_hlen() hdata = sig.hashdata(sigsubject) sig._signature.hash2 = hashlib.new('sha512', hdata).digest()[:2] From 17cb140b2a3b4acd2d9b62c66ca2d96757677cd0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 14 Jun 2023 17:33:17 -0400 Subject: [PATCH 013/287] Avoid looking for encrypted messages with "Plaintext" encoding This reduces the number of SKIPPED and XFAIL messages from the test suite. Given where the cleanup is happening, also make the remaining XFAIL messages a little clearer. --- tests/test_05_actions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 8a801c9c..55b088cc 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -621,6 +621,7 @@ def targette_sec(): seckeys = [ PGPKey.from_file(f)[0] for f in sorted(glob.glob('tests/testdata/keys/*.sec.asc')) ] pubkeys = [ PGPKey.from_file(f)[0] for f in sorted(glob.glob('tests/testdata/keys/*.pub.asc')) ] +symalgos = sorted(filter(lambda x: x is not SymmetricKeyAlgorithm.Plaintext, sorted(SymmetricKeyAlgorithm))) class TestPGPKey_Actions(object): sigs = {} @@ -906,14 +907,14 @@ def test_gpg_import_abe(self, abe): self.gpg_verify(abe) @pytest.mark.parametrize('pub,cipher', - itertools.product(pubkeys, sorted(SymmetricKeyAlgorithm)), - ids=['{}:{}-{}'.format(pk.key_algorithm.name, pk.key_size, c.name) for pk, c in itertools.product(pubkeys, sorted(SymmetricKeyAlgorithm))]) + itertools.product(pubkeys, symalgos), + ids=['{}:{}-{}'.format(pk.key_algorithm.name, pk.key_size, c.name) for pk, c in itertools.product(pubkeys, symalgos)]) def test_encrypt_message(self, pub, cipher): if pub.key_algorithm in {PubKeyAlgorithm.DSA}: pytest.skip('Asymmetric encryption only implemented for RSA/ECDH currently') - if cipher in {SymmetricKeyAlgorithm.Plaintext, SymmetricKeyAlgorithm.Twofish256, SymmetricKeyAlgorithm.IDEA}: - pytest.xfail('Symmetric cipher {} not supported for encryption'.format(cipher)) + if cipher in {SymmetricKeyAlgorithm.Twofish256, SymmetricKeyAlgorithm.IDEA}: + pytest.xfail(f'{cipher!r} not supported for encryption') # test encrypting a message mtxt = "This message will have been encrypted" @@ -925,7 +926,7 @@ def test_encrypt_message(self, pub, cipher): @pytest.mark.order(after='test_encrypt_message') @pytest.mark.parametrize('sf,cipher', - itertools.product(sorted(glob.glob('tests/testdata/keys/*.sec.asc')), sorted(SymmetricKeyAlgorithm))) + itertools.product(sorted(glob.glob('tests/testdata/keys/*.sec.asc')), symalgos)) def test_decrypt_message(self, sf, cipher): # test decrypting a message sec, _ = PGPKey.from_file(sf) @@ -949,7 +950,7 @@ def test_decrypt_message(self, sf, cipher): @pytest.mark.order(after='test_encrypt_message') @pytest.mark.parametrize('sf,cipher', - itertools.product(sorted(glob.glob('tests/testdata/keys/*.sec.asc')), sorted(SymmetricKeyAlgorithm))) + itertools.product(sorted(glob.glob('tests/testdata/keys/*.sec.asc')), symalgos)) def test_sign_encrypted_message(self, sf, cipher): # test decrypting a message sec, _ = PGPKey.from_file(sf) From 8e4f7ca3d4a344ea0fac94c2d467d1f8cacba30c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 16:38:46 -0400 Subject: [PATCH 014/287] rename setter for EmbeddedSignature._sig (otherwise definition was aliased) --- pgpy/packet/subpackets/signature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index a68a0bd9..ace8fd02 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -848,7 +848,7 @@ def _sig(self): return self._sigpkt @_sig.setter - def _sig(self, val): + def _sig_set(self, val): esh = EmbeddedSignatureHeader() esh.version = val.header.version val.header = esh From 0f8c67d1c1ca9e1bd5574cf583642bf93aefc059 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 11 May 2023 15:22:32 -0400 Subject: [PATCH 015/287] Drop erroneous SubkeyBindingSignature This is not a signature subpacket. Rather, it is a signature type. --- pgpy/packet/subpackets/signature.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index a68a0bd9..423c9fc3 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -52,7 +52,6 @@ 'Policy', 'KeyFlags', 'SignersUserID', - 'SubkeyBindingSignature', 'ReasonForRevocation', 'Features', 'EmbeddedSignature', @@ -704,11 +703,7 @@ class PreferredKeyServer(URI): __typeid__ = 0x18 -class SubkeyBindingSignature(Signature): - __typeid__ = 0x18 - - -class PrimaryUserID(SubkeyBindingSignature): +class PrimaryUserID(Signature): __typeid__ = 0x19 @sdproperty From edd7e4fa3b8c49053c4cb8845a75dd6618d75955 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 10 Jun 2023 23:41:57 -0400 Subject: [PATCH 016/287] handle Armored text with UTF-8 comments --- docs/source/changelog.rst | 13 +++++++++++++ pgpy/pgp.py | 7 ++----- pgpy/types.py | 31 ++++++++++++++++++++++++------- tests/test_06_compatibility.py | 1 - 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6454614e..a0573cba 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,19 @@ Changelog ********* +v0.7.0 +====== + +(not yet released) + +API changes +----------- + +Armorable.is_ascii() is deprecated. You probably want +Armorable.is_utf8() instead, since OpenPGP assumes that all text is +UTF-8. + + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..b45c4b5f 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1153,9 +1153,9 @@ def new(cls, message, **kwargs): # message is definitely UTF-8 already format = 'u' - elif cls.is_ascii(message): + elif cls.is_utf8(message): # message is probably text - format = 't' + format = 'u' else: # message is probably binary @@ -1176,9 +1176,6 @@ def new(cls, message, **kwargs): lit.mtime = mtime lit.format = format - # if cls.is_ascii(message): - # lit.format = 't' - lit.update_hlen() msg |= lit diff --git a/pgpy/types.py b/pgpy/types.py index 187d3984..49c89ed6 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -17,6 +17,8 @@ from enum import EnumMeta from enum import IntEnum +from typing import Union, Optional, Dict + from .decorators import sdproperty from .errors import PGPError @@ -69,15 +71,27 @@ class Armorable(metaclass=abc.ABCMeta): """, flags=re.MULTILINE | re.VERBOSE) @property - def charset(self): + def charset(self) -> str: return self.ascii_headers.get('Charset', 'utf-8') @charset.setter - def charset(self, encoding): + def charset(self, encoding: str) -> None: self.ascii_headers['Charset'] = codecs.lookup(encoding).name @staticmethod - def is_ascii(text): + def is_utf8(text: Union[str, bytes, bytearray]) -> bool: + if isinstance(text, str): + return True + else: + try: + text.decode('utf-8') + return True + except UnicodeDecodeError: + return False + + @staticmethod + def is_ascii(text: Union[str, bytes, bytearray]) -> bool: + '''This is a deprecated, pointless method''' if isinstance(text, str): return bool(re.match(r'^[ -~\r\n\t]*$', text, flags=re.ASCII)) @@ -87,7 +101,7 @@ def is_ascii(text): raise TypeError("Expected: ASCII input of type str, bytes, or bytearray") # pragma: no cover @staticmethod - def is_armor(text): + def is_armor(text: Union[str, bytes, bytearray]) -> bool: """ Whether the ``text`` provided is an ASCII-armored PGP block. :param text: A possible ASCII-armored PGP block. @@ -95,12 +109,15 @@ def is_armor(text): :returns: Whether the text is ASCII-armored. """ if isinstance(text, (bytes, bytearray)): # pragma: no cover - text = text.decode('latin-1') + try: + text = text.decode('utf-8') + except UnicodeDecodeError: + return False return Armorable.__armor_regex.search(text) is not None @staticmethod - def ascii_unarmor(text): + def ascii_unarmor(text: Union[str, bytes, bytearray]) -> Dict[str, Optional[Union[str, bytes, bytearray]]]: """ Takes an ASCII-armored PGP block and returns the decoded byte value. @@ -111,7 +128,7 @@ def ascii_unarmor(text): It can contain the following keys: ``magic``, ``headers``, ``hashes``, ``cleartext``, ``body``, ``crc``. """ m = {'magic': None, 'headers': None, 'body': bytearray(), 'crc': None} - if not Armorable.is_ascii(text): + if not Armorable.is_utf8(text): m['body'] = bytearray(text) return m diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index e6e9aaee..a2f1d429 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -18,7 +18,6 @@ class TestPGP_Compatibility(object): def test_import_unicode_armored_cert(self) -> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/ricarda.pgp') - pytest.xfail('Cannot handle UTF-8 (non-ASCII) comments in armored certificate') assert k.check_soundness() == SecurityIssues.OK @pytest.mark.parametrize('sig', glob.glob('*.sig', root_dir='tests/testdata/compatibility')) From 26e6c37b5d1ef5c39956ba0395606a3e6a4de259 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 25 Oct 2019 09:49:07 -0400 Subject: [PATCH 017/287] initial PGPy-backed implementation of sop It works in the basic mode, but we still need to handle the args for encrypt/decrypt. --- pgpy/sopgpy.py | 308 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100755 pgpy/sopgpy.py diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py new file mode 100755 index 00000000..b5cf0898 --- /dev/null +++ b/pgpy/sopgpy.py @@ -0,0 +1,308 @@ +#!/usr/bin/python3 +'''OpenPGP Interoperability Test Suite Generic Functionality using PGPy + +Author: Daniel Kahn Gillmor +Date: 2019-10-24 +License: MIT (see below) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +''' + +import io +import os +import sop +import pgpy +import logging + +from typing import List, Union, Optional, Set + +class SOPGPy(sop.StatelessOpenPGP): + def __init__(self): + super().__init__(prog='SOPGPy', version=pgpy.__version__, + description=f'Stateless OpenPGP using PGPy {pgpy.__version__}') + + # implemented ciphers, in the order we prefer them: + _cipherprefs = [pgpy.constants.SymmetricKeyAlgorithm.AES256, + pgpy.constants.SymmetricKeyAlgorithm.AES192, + pgpy.constants.SymmetricKeyAlgorithm.AES128, + pgpy.constants.SymmetricKeyAlgorithm.Camellia256, + pgpy.constants.SymmetricKeyAlgorithm.Camellia192, + pgpy.constants.SymmetricKeyAlgorithm.Camellia128, + pgpy.constants.SymmetricKeyAlgorithm.CAST5, + pgpy.constants.SymmetricKeyAlgorithm.TripleDES, + pgpy.constants.SymmetricKeyAlgorithm.Blowfish, + pgpy.constants.SymmetricKeyAlgorithm.IDEA] + + + def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage,pgpy.PGPKey]): + if (armor): + return str(data).encode('ascii') + else: + return bytes(data) + + def _get_pgp_signature(self, fname:str) -> pgpy.PGPSignature: + sig:Optional[pgpy.PGPSignature] = None + if fname.startswith('@FD:'): + fd = int(fname.split(':', maxsplit=1)[1]) + with open(fd, 'rb') as filed: + data:bytes = filed.read() + sig = pgpy.PGPSignature.from_blob(data) + elif fname.startswith('@ENV:'): + sig = pgpy.PGPSignature.from_blob(os.environ[fname.split(':', maxsplit=1)[1]]) + else: + sig = pgpy.PGPSignature.from_file(fname) + return sig + + def _get_pgp_key(self, fname:str, secret:bool) -> pgpy.PGPKey: + # handle @FD: and @ENV: here + key:Optional[pgpy.PGPKey] = None + if fname.startswith('@FD:'): + fd = int(fname.split(':', maxsplit=1)[1]) + with open(fd, 'rb') as filed: + data:bytes = filed.read() + key, _ = pgpy.PGPKey.from_blob(data) + elif fname.startswith('@ENV:'): + key, _ = pgpy.PGPKey.from_blob(os.environ[fname.split(':', maxsplit=1)[1]]) + else: + key, _ = pgpy.PGPKey.from_file(fname) + if secret: + if key.is_public: + raise Exception(f'file "{fname}" does not contain OpenPGP secret key material (probably a certificate)') + logging.info(f'loaded secret key {key.fingerprint} from {fname}') + + if not secret: + if not key.is_public: + raise Exception(f'file "{fname}" does not contain an OpenPGP certificate (probably a secret key)') + logging.info(f'loaded certificate {key.fingerprint} from {fname}') + return key + + def generate(self, + inp:io.BufferedReader, + armor:bool, + uids:List[str]) -> bytes: + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, + pgpy.constants.EllipticCurveOID.Ed25519) + primaryflags: Set[int] = set() + primaryflags.add(pgpy.constants.KeyFlags.Certify) + primaryflags.add(pgpy.constants.KeyFlags.Sign) + first: bool = True + uidoptions = { + 'usage': primaryflags, + 'primary': True, + 'hashes': [pgpy.constants.HashAlgorithm.SHA512, + pgpy.constants.HashAlgorithm.SHA384, + pgpy.constants.HashAlgorithm.SHA256, + pgpy.constants.HashAlgorithm.SHA224], + 'ciphers': [pgpy.constants.SymmetricKeyAlgorithm.AES256, + pgpy.constants.SymmetricKeyAlgorithm.AES192, + pgpy.constants.SymmetricKeyAlgorithm.AES128], + 'compression': [pgpy.constants.CompressionAlgorithm.Uncompressed], + 'keyserver_flags': [pgpy.constants.KeyServerPreferences.NoModify] + } + + for uid in uids: + primary.add_uid(pgpy.PGPUID.new(uid), **uidoptions) + if 'primary' in uidoptions: # only first User ID is Primary + del uidoptions['primary'] + + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH, + pgpy.constants.EllipticCurveOID.Curve25519) + subflags = pgpy.constants.KeyFlags.EncryptCommunications + subflags |= pgpy.constants.KeyFlags.EncryptStorage + primary.add_subkey(subkey, usage=subflags) + return self._maybe_armor(armor, primary) + + + def convert(self, + inp:io.BufferedReader, + armor:bool) -> bytes: + data: bytes = inp.read() + seckey, _ = pgpy.PGPKey.from_blob(data) + return self._maybe_armor(armor, seckey.pubkey) + + + def sign(self, + inp:io.BufferedReader, + armor:bool, + sigtype:str, + signers:List[str]) -> bytes: + if not signers: + raise Exception("Need at least one OpenPGP Secret Key file as an argument") + + seckeys = [] + for keyfile in signers: + seckey = self._get_pgp_key(keyfile, True) + seckeys.append(seckey) + + data:bytes = inp.read() + msg:Optional[pgpy.PGPMessage] = None + if sigtype == 'text': + msg = pgpy.PGPMessage.new(data.decode('utf8'), cleartext=True, format='u') + elif sigtype == 'binary': + msg = pgpy.PGPMessage.new(data, format='b') + else: + raise Exception(f'unknown signature type {sigtype}') + signatures:List[pgpy.PGPSignature] = [] + for seckey in seckeys: + signatures.append(seckey.sign(msg)) + + # hack to assemble multiple signature packets! FIXME: need to report to PGPy + sigdata:bytes = b'' + for signature in signatures: + sigdata += bytes(seckey.sign(msg)) + class _multisig(pgpy.types.Armorable): + @property + def magic(self): + return 'SIGNATURE' + def parse(self, x): + self._bytes = x + def __bytes__(self): + return self._bytes + return self._maybe_armor(armor, _multisig.from_blob(sigdata)) + + + def verify(self, + inp:io.BufferedReader, + start:Optional[str], + end:Optional[str], + sig:str, + signers:List[str]) -> bytes: + signature = self._get_pgp_signature(sig) + certs: List[pgpy.PGPKey] = [] + for fname in signers: + cert = self._get_pgp_key(fname, False) + certs.append(cert) + + if not certs: + raise Exception('needs at least one OpenPGP certificate') + + if start is not None or end is not None: + raise Exception('have not implemented --not-before and --not-after') + + data:bytes = inp.read() + good:bool = False + ret:bytes = b'' + for cert in certs: + try: + verif = cert.verify(data, signature=signature) + for sig in verif.good_signatures: + if sig.verified: + ts = sig.signature.created.strftime('%Y-%m-%dT%H:%M:%SZ\n') + good = True + ret += ts.encode('ascii') + except: + pass + if not good: + raise Exception("No good signature found") + return ret + + + def encrypt(self, + inp:io.BufferedReader, + literaltype:str, + armor:bool, + mode:str, + passwords:List[str], + sessionkey:Optional[str], + signers:List[str], + recipients:List[str]) -> bytes: + # FIXME! + if literaltype != 'binary' or mode != 'any' or passwords or sessionkey or signers: + raise Exception('sopgpy does not support any arguments to encrypt yet, sorry') + + certs: List[pgpy.PGPKey] = [] + for fname in recipients: + cert = self._get_pgp_key(fname, False) + certs.append(cert) + + if not certs: + raise Exception('needs at least one OpenPGP certificate') + + ciphers = set(self._cipherprefs) + for cert in certs: + keyciphers=set() + for uid in cert.userids: + if uid.selfsig and uid.selfsig.cipherprefs: + for cipher in uid.selfsig.cipherprefs: + keyciphers.add(cipher) + ciphers = ciphers.intersection(keyciphers) + cipher = None + for c in self._cipherprefs: + if c in ciphers: + cipher = c + break + # AES128 is MTI in RFC4880: + if cipher is None: + cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 + data: bytes = inp.read() + sessionkey = cipher.gen_key() + msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + for cert in certs: + msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) + del sessionkey + return self._maybe_armor(armor, msg) + + + def decrypt(self, + inp:io.BufferedReader, + sessionkey:Optional[str], + passwords:List[str], + verifications:Optional[str], + signers:List[str], + start:Optional[str], + end:Optional[str], + secretkeys:List[str]) -> bytes: + # FIXME!!! + if sessionkey or passwords or verifications or signers or start or end: + raise Exception('sopgpy does not support any arguments to decrypt yet, sorry') + + seckeys: List[pgpy.PGPKey] = [] + for fname in secretkeys: + seckey = self._get_pgp_key(fname, True) + seckeys.append(seckey) + + if not seckeys: + raise Exception('needs at least one OpenPGP secret key') + data: bytes = inp.read() + encmsg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + ret:Optional[bytes] = None + for seckey in seckeys: + try: + msg: pgpy.PGPMessage = seckey.decrypt(encmsg) + out:Union[str,bytes] = msg.message + if isinstance(out, str): + ret = out.encode('utf8') + else: + ret = out + break + except pgpy.errors.PGPDecryptionError as e: + logging.warning(f'could not decrypt with {seckey.fingerprint}') + if ret is None: + raise Exception(f'could not find anything capable of decryption') + return ret + + +def main(): + sop = SOPGPy() + sop.dispatch() + +if __name__ == '__main__': + main() From 8dabb3ac9e4fc31cccc682b407c42a3eaf4d6d5a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 25 Oct 2019 13:15:09 -0400 Subject: [PATCH 018/287] sop.py: allow instances to augment the interface; provide autocomplete --- pgpy/sopgpy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index b5cf0898..ac6335b9 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +# PYTHON_ARGCOMPLETE_OK '''OpenPGP Interoperability Test Suite Generic Functionality using PGPy Author: Daniel Kahn Gillmor From 1d5e06e63e65377d814aa266c070e84ab1e39d12 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 26 Oct 2019 17:37:50 -0400 Subject: [PATCH 019/287] treat keyflags as a set, as expected --- pgpy/sopgpy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index ac6335b9..5193cefd 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -126,8 +126,9 @@ def generate(self, subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH, pgpy.constants.EllipticCurveOID.Curve25519) - subflags = pgpy.constants.KeyFlags.EncryptCommunications - subflags |= pgpy.constants.KeyFlags.EncryptStorage + subflags: Set[int] = set() + subflags.add(pgpy.constants.KeyFlags.EncryptCommunications) + subflags.add(pgpy.constants.KeyFlags.EncryptStorage) primary.add_subkey(subkey, usage=subflags) return self._maybe_armor(armor, primary) From 814a65e0b0a0b29e3873b67924f24794a0fc01bc Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 26 Oct 2019 17:38:32 -0400 Subject: [PATCH 020/287] use specialized exception for missing options --- pgpy/sopgpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 5193cefd..e466c428 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -228,7 +228,7 @@ def encrypt(self, recipients:List[str]) -> bytes: # FIXME! if literaltype != 'binary' or mode != 'any' or passwords or sessionkey or signers: - raise Exception('sopgpy does not support any arguments to encrypt yet, sorry') + raise sop.SOPUnsupportedOption('sopgpy does not support any arguments to encrypt yet, sorry') certs: List[pgpy.PGPKey] = [] for fname in recipients: @@ -274,7 +274,7 @@ def decrypt(self, secretkeys:List[str]) -> bytes: # FIXME!!! if sessionkey or passwords or verifications or signers or start or end: - raise Exception('sopgpy does not support any arguments to decrypt yet, sorry') + raise sop.SOPUnsupportedOption('sopgpy does not support any arguments to decrypt yet, sorry') seckeys: List[pgpy.PGPKey] = [] for fname in secretkeys: From adbd3bd766421e549274804a9d2fd481e9443e68 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 26 Oct 2019 21:08:25 -0400 Subject: [PATCH 021/287] sopgpy: add armor and dearmor subcommands --- pgpy/sopgpy.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index e466c428..e470ce5c 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -301,7 +301,57 @@ def decrypt(self, raise Exception(f'could not find anything capable of decryption') return ret - + def armor(self, + inp:io.BufferedReader, + label:Optional[str]) -> bytes: + data:bytes = inp.read() + obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None + try: + if label == 'message': + obj = pgpy.PGPMessage.from_blob(data) + elif label == 'key': + obj, _ = pgpy.PGPKey.from_blob(data) + if obj.is_public or not obj.is_primary: + raise sop.SOPInvalidDataType('not an OpenPGP secret key') + elif label == 'cert': + obj, _ = pgpy.PGPKey.from_blob(data) + if not obj.is_public: + raise sop.SOPInvalidDataType('not an OpenPGP certificate') + elif label == 'sig': + obj = pgpy.PGPSignature.from_blob(data) + elif label is None: # try to guess + try: + obj, _ = pgpy.PGPKey.from_blob(data) + except: + try: + obj = pgpy.PGPSignature.from_blob(data) + except: + try: + obj = pgpy.PGPMessage.from_blob(data) + except: + obj = pgpy.PGPMessage.new(data) + else: + raise sop.SOPInvalidDataType(f'unknown armor type {label}') + except (ValueError,TypeError) as e: + raise sop.SOPInvalidDataType(f'{e}') + return str(obj).encode('ascii') + + def dearmor(self, + inp:io.BufferedReader) -> bytes: + data:bytes = inp.read() + try: + key, _ = pgpy.PGPKey.from_blob(data) + return bytes(key) + except: + pass + for cls in [ pgpy.PGPSignature, pgpy.PGPMessage ]: + try: + obj:cls = cls.from_blob(data) + return bytes(obj) + except: + pass + raise sop.SOPInvalidDataType() + def main(): sop = SOPGPy() sop.dispatch() From 820a9fc388bd94894f9cc55f01d54e5e50b06cdd Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 26 Oct 2019 21:19:13 -0400 Subject: [PATCH 022/287] indicate each unsupported feature separately for cleaner diffs --- pgpy/sopgpy.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index e470ce5c..454687be 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -227,8 +227,16 @@ def encrypt(self, signers:List[str], recipients:List[str]) -> bytes: # FIXME! - if literaltype != 'binary' or mode != 'any' or passwords or sessionkey or signers: - raise sop.SOPUnsupportedOption('sopgpy does not support any arguments to encrypt yet, sorry') + if literaltype != 'binary' + raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') + if or mode != 'any': + raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --mode yet') + if passwords: + raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') + if signers: + raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --sign-with yet') + if sessionkey: + raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --session-key yet') certs: List[pgpy.PGPKey] = [] for fname in recipients: @@ -273,8 +281,16 @@ def decrypt(self, end:Optional[str], secretkeys:List[str]) -> bytes: # FIXME!!! - if sessionkey or passwords or verifications or signers or start or end: - raise sop.SOPUnsupportedOption('sopgpy does not support any arguments to decrypt yet, sorry') + if sessionkey: + raise sop.SOPUnsupportedOption('sopgpy does not support --session-key yet') + if passwords: + raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') + if verifications: + raise sop.SOPUnsupportedOption('sopgpy does not support --verify-out yet') + if signers: + raise sop.SOPUnsupportedOption('sopgpy does not support --verify-with yet') + if start or end: + raise sop.SOPUnsupportedOption('sopgpy does not support --verify-not-before or --verify-not-after yet') seckeys: List[pgpy.PGPKey] = [] for fname in secretkeys: From 19c7f7e6a788343c0e7dc7d31edd4d37c5e15ffb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 26 Oct 2019 23:43:38 -0400 Subject: [PATCH 023/287] sopgpy: added --sessionkey to encrypt subcommand --- pgpy/sopgpy.py | 70 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 454687be..da8b17f8 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -31,9 +31,10 @@ import os import sop import pgpy +import codecs import logging -from typing import List, Union, Optional, Set +from typing import List, Union, Optional, Set, Tuple class SOPGPy(sop.StatelessOpenPGP): def __init__(self): @@ -59,6 +60,27 @@ def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage, else: return bytes(data) + def _get_session_key(self, fname:str) -> Tuple[pgpy.constants.SymmetricKeyAlgorithm,Optional[bytes]]: + data:str = '' + if fname.startswith('@FD:'): + fd = int(fname.split(':', maxsplit=1)[1]) + with open(fd, 'r') as filed: + data = filed.read() + elif fname.startswith('@ENV:'): + data = os.environ[fname.split(':', maxsplit=1)[1]] + else: + with open(fname, 'r') as f: + data = f.read() + data = data.strip() + algostr, keystr = data.split(':', maxsplit=2) + algo:pgpy.constants.SymmetricKeyAlgorithm = pgpy.constants.SymmetricKeyAlgorithm(int(algostr)) + key:Optional[bytes] = None + if keystr != '': + key = codecs.decode(keystr, 'hex') + if len(key) * 8 != algo.key_size: + raise sop.SOPInvalidDataType(f'session key {fname} has wrong size ({len(key)*8} bits) for cipher {algo} (expected: {algo.key_size} bits)') + return algo, key + def _get_pgp_signature(self, fname:str) -> pgpy.PGPSignature: sig:Optional[pgpy.PGPSignature] = None if fname.startswith('@FD:'): @@ -227,16 +249,14 @@ def encrypt(self, signers:List[str], recipients:List[str]) -> bytes: # FIXME! - if literaltype != 'binary' + if literaltype != 'binary': raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') - if or mode != 'any': + if mode != 'any': raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --mode yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') if signers: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --sign-with yet') - if sessionkey: - raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --session-key yet') certs: List[pgpy.PGPKey] = [] for fname in recipients: @@ -246,28 +266,36 @@ def encrypt(self, if not certs: raise Exception('needs at least one OpenPGP certificate') - ciphers = set(self._cipherprefs) - for cert in certs: - keyciphers=set() - for uid in cert.userids: - if uid.selfsig and uid.selfsig.cipherprefs: - for cipher in uid.selfsig.cipherprefs: - keyciphers.add(cipher) - ciphers = ciphers.intersection(keyciphers) - cipher = None - for c in self._cipherprefs: - if c in ciphers: - cipher = c - break + cipher:Optional[pgpy.constants.SymmetricKeyAlgorithm] = None + symmetrickey:Optional[bytes] = None + + if sessionkey: + (cipher, symmetrickey) = self._get_session_key(sessionkey) + del sessionkey + else: + ciphers = set(self._cipherprefs) + for cert in certs: + keyciphers=set() + for uid in cert.userids: + if uid.selfsig and uid.selfsig.cipherprefs: + for cipher in uid.selfsig.cipherprefs: + keyciphers.add(cipher) + ciphers = ciphers.intersection(keyciphers) + for c in self._cipherprefs: + if c in ciphers: + cipher = c + break # AES128 is MTI in RFC4880: if cipher is None: cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 + if symmetrickey is None: + symmetrickey = cipher.gen_key() + data: bytes = inp.read() - sessionkey = cipher.gen_key() msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) for cert in certs: - msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) - del sessionkey + msg = cert.encrypt(msg, cipher=cipher, sessionkey=symmetrickey) + del symmetrickey return self._maybe_armor(armor, msg) From 699f4b08094399584105ad8e3c6bc8660fa4d7c6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 27 Oct 2019 21:48:47 -0400 Subject: [PATCH 024/287] cleanup and overhaul python sop.py framework - add enums for the flags passed into the sop interface - make member functions of StatelessOpenPGP well-typed - adjust docstrings so that help(sop) provides useful guidance - handle sessionkey and timestamp parsing in sop.py - handle all indirect access directly in sop.py - complete strict typing ("mypy --strict sop.py" passes) --- pgpy/sopgpy.py | 312 ++++++++++++++++++++----------------------------- 1 file changed, 128 insertions(+), 184 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index da8b17f8..2ef98254 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -30,97 +30,63 @@ import io import os import sop -import pgpy +import pgpy #type: ignore import codecs import logging -from typing import List, Union, Optional, Set, Tuple +from datetime import datetime +from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict class SOPGPy(sop.StatelessOpenPGP): - def __init__(self): - super().__init__(prog='SOPGPy', version=pgpy.__version__, + def __init__(self) -> None: + super().__init__(name='SOPGPy', version=pgpy.__version__, description=f'Stateless OpenPGP using PGPy {pgpy.__version__}') # implemented ciphers, in the order we prefer them: - _cipherprefs = [pgpy.constants.SymmetricKeyAlgorithm.AES256, - pgpy.constants.SymmetricKeyAlgorithm.AES192, - pgpy.constants.SymmetricKeyAlgorithm.AES128, - pgpy.constants.SymmetricKeyAlgorithm.Camellia256, - pgpy.constants.SymmetricKeyAlgorithm.Camellia192, - pgpy.constants.SymmetricKeyAlgorithm.Camellia128, - pgpy.constants.SymmetricKeyAlgorithm.CAST5, - pgpy.constants.SymmetricKeyAlgorithm.TripleDES, - pgpy.constants.SymmetricKeyAlgorithm.Blowfish, - pgpy.constants.SymmetricKeyAlgorithm.IDEA] - + _cipherprefs:List[pgpy.constants.SymmetricKeyAlgorithm] = \ + [pgpy.constants.SymmetricKeyAlgorithm.AES256, + pgpy.constants.SymmetricKeyAlgorithm.AES192, + pgpy.constants.SymmetricKeyAlgorithm.AES128, + pgpy.constants.SymmetricKeyAlgorithm.Camellia256, + pgpy.constants.SymmetricKeyAlgorithm.Camellia192, + pgpy.constants.SymmetricKeyAlgorithm.Camellia128, + pgpy.constants.SymmetricKeyAlgorithm.CAST5, + pgpy.constants.SymmetricKeyAlgorithm.TripleDES, + pgpy.constants.SymmetricKeyAlgorithm.Blowfish, + pgpy.constants.SymmetricKeyAlgorithm.IDEA] - def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage,pgpy.PGPKey]): + def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage,pgpy.PGPKey]) -> bytes: if (armor): return str(data).encode('ascii') else: return bytes(data) - def _get_session_key(self, fname:str) -> Tuple[pgpy.constants.SymmetricKeyAlgorithm,Optional[bytes]]: - data:str = '' - if fname.startswith('@FD:'): - fd = int(fname.split(':', maxsplit=1)[1]) - with open(fd, 'r') as filed: - data = filed.read() - elif fname.startswith('@ENV:'): - data = os.environ[fname.split(':', maxsplit=1)[1]] - else: - with open(fname, 'r') as f: - data = f.read() - data = data.strip() - algostr, keystr = data.split(':', maxsplit=2) - algo:pgpy.constants.SymmetricKeyAlgorithm = pgpy.constants.SymmetricKeyAlgorithm(int(algostr)) - key:Optional[bytes] = None - if keystr != '': - key = codecs.decode(keystr, 'hex') - if len(key) * 8 != algo.key_size: - raise sop.SOPInvalidDataType(f'session key {fname} has wrong size ({len(key)*8} bits) for cipher {algo} (expected: {algo.key_size} bits)') - return algo, key - - def _get_pgp_signature(self, fname:str) -> pgpy.PGPSignature: + def _get_pgp_signature(self, data:bytes) -> Optional[pgpy.PGPSignature]: sig:Optional[pgpy.PGPSignature] = None - if fname.startswith('@FD:'): - fd = int(fname.split(':', maxsplit=1)[1]) - with open(fd, 'rb') as filed: - data:bytes = filed.read() - sig = pgpy.PGPSignature.from_blob(data) - elif fname.startswith('@ENV:'): - sig = pgpy.PGPSignature.from_blob(os.environ[fname.split(':', maxsplit=1)[1]]) - else: - sig = pgpy.PGPSignature.from_file(fname) + sig = pgpy.PGPSignature.from_blob(data) return sig - - def _get_pgp_key(self, fname:str, secret:bool) -> pgpy.PGPKey: - # handle @FD: and @ENV: here - key:Optional[pgpy.PGPKey] = None - if fname.startswith('@FD:'): - fd = int(fname.split(':', maxsplit=1)[1]) - with open(fd, 'rb') as filed: - data:bytes = filed.read() - key, _ = pgpy.PGPKey.from_blob(data) - elif fname.startswith('@ENV:'): - key, _ = pgpy.PGPKey.from_blob(os.environ[fname.split(':', maxsplit=1)[1]]) - else: - key, _ = pgpy.PGPKey.from_file(fname) - if secret: + + def _get_certs(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.PGPKey]: + certs:Dict[str,pgpy.PGPKey] = {} + for handle, data in vals.items(): + cert:pgpy.PGPKey + cert, _ = pgpy.PGPKey.from_blob(data) + if not cert.is_public: + raise sop.SOPInvalidDataType('cert {handle} is not an OpenPGP certificate (maybe secret key?)') + certs[handle] = cert + return certs + + def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.PGPKey]: + keys:Dict[str,pgpy.PGPKey] = {} + for handle, data in vals.items(): + key:pgpy.PGPKey + key, _ = pgpy.PGPKey.from_blob(data) if key.is_public: - raise Exception(f'file "{fname}" does not contain OpenPGP secret key material (probably a certificate)') - logging.info(f'loaded secret key {key.fingerprint} from {fname}') - - if not secret: - if not key.is_public: - raise Exception(f'file "{fname}" does not contain an OpenPGP certificate (probably a secret key)') - logging.info(f'loaded certificate {key.fingerprint} from {fname}') - return key - - def generate(self, - inp:io.BufferedReader, - armor:bool, - uids:List[str]) -> bytes: + raise sop.SOPInvalidDataType('cert {handle} is not an OpenPGP transferable secret key (maybe certificate?)') + keys[handle] = key + return keys + + def generate(self, armor:bool, uids:List[str]) -> bytes: primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, pgpy.constants.EllipticCurveOID.Ed25519) primaryflags: Set[int] = set() @@ -156,125 +122,110 @@ def generate(self, def convert(self, - inp:io.BufferedReader, + key:bytes, armor:bool) -> bytes: - data: bytes = inp.read() - seckey, _ = pgpy.PGPKey.from_blob(data) + seckey, _ = pgpy.PGPKey.from_blob(key) return self._maybe_armor(armor, seckey.pubkey) def sign(self, - inp:io.BufferedReader, + data:bytes, armor:bool, - sigtype:str, - signers:List[str]) -> bytes: + sigtype:sop.SOPSigType, + signers:MutableMapping[str, bytes]) -> bytes: if not signers: - raise Exception("Need at least one OpenPGP Secret Key file as an argument") - - seckeys = [] - for keyfile in signers: - seckey = self._get_pgp_key(keyfile, True) - seckeys.append(seckey) - - data:bytes = inp.read() - msg:Optional[pgpy.PGPMessage] = None - if sigtype == 'text': + raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") + seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) + msg:pgpy.PGPMessage + if sigtype is sop.SOPSigType.text: msg = pgpy.PGPMessage.new(data.decode('utf8'), cleartext=True, format='u') - elif sigtype == 'binary': + elif sigtype == sop.SOPSigType.binary: msg = pgpy.PGPMessage.new(data, format='b') else: - raise Exception(f'unknown signature type {sigtype}') + raise sop.SOPUnsupportedOption(f'unknown signature type {sigtype}') signatures:List[pgpy.PGPSignature] = [] - for seckey in seckeys: + for handle,seckey in seckeys.items(): signatures.append(seckey.sign(msg)) # hack to assemble multiple signature packets! FIXME: need to report to PGPy sigdata:bytes = b'' for signature in signatures: sigdata += bytes(seckey.sign(msg)) - class _multisig(pgpy.types.Armorable): + class _multisig(pgpy.types.Armorable): #type: ignore @property - def magic(self): + def magic(self) -> str: return 'SIGNATURE' - def parse(self, x): - self._bytes = x - def __bytes__(self): + def parse(self, x:bytes) -> None: + self._bytes:bytes = x + def __bytes__(self) -> bytes: return self._bytes return self._maybe_armor(armor, _multisig.from_blob(sigdata)) def verify(self, - inp:io.BufferedReader, - start:Optional[str], - end:Optional[str], - sig:str, - signers:List[str]) -> bytes: + data:bytes, + start:Optional[datetime], + end:Optional[datetime], + sig:bytes, + signers:MutableMapping[str,bytes]) -> List[sop.SOPSigResult]: + if not signers: + raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') signature = self._get_pgp_signature(sig) - certs: List[pgpy.PGPKey] = [] - for fname in signers: - cert = self._get_pgp_key(fname, False) - certs.append(cert) - - if not certs: - raise Exception('needs at least one OpenPGP certificate') - + certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) if start is not None or end is not None: - raise Exception('have not implemented --not-before and --not-after') + raise sop.SOPUnsupportedOption('have not implemented --not-before and --not-after') - data:bytes = inp.read() - good:bool = False - ret:bytes = b'' - for cert in certs: + ret:List[sop.SOPSigResult] = [] + for (handle,cert) in certs.items(): try: - verif = cert.verify(data, signature=signature) - for sig in verif.good_signatures: - if sig.verified: - ts = sig.signature.created.strftime('%Y-%m-%dT%H:%M:%SZ\n') - good = True - ret += ts.encode('ascii') + verif:pgpy.types.SignatureVerification = cert.verify(data, signature=signature) + goodsig:pgpy.types.sigsubj + for goodsig in verif.good_signatures: + if goodsig.verified: + ret += [sop.SOPSigResult(goodsig.signature.created, cert.fingerprint, cert)] except: pass - if not good: - raise Exception("No good signature found") + if not ret: + raise sop.SOPNoSignature("No good signature found") return ret def encrypt(self, - inp:io.BufferedReader, - literaltype:str, + data:bytes, + literaltype:sop.SOPLiteralDataType, armor:bool, - mode:str, - passwords:List[str], - sessionkey:Optional[str], - signers:List[str], - recipients:List[str]) -> bytes: + mode:sop.SOPEncryptMode, + passwords:MutableMapping[str,bytes], + sessionkey:Optional[sop.SOPSessionKey], + signers:MutableMapping[str,bytes], + recipients:MutableMapping[str,bytes]) -> bytes: + handle:str # FIXME! - if literaltype != 'binary': + if literaltype is not sop.SOPLiteralDataType.binary: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') - if mode != 'any': + if mode is not sop.SOPEncryptMode.any: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --mode yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') if signers: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --sign-with yet') - - certs: List[pgpy.PGPKey] = [] - for fname in recipients: - cert = self._get_pgp_key(fname, False) - certs.append(cert) - if not certs: - raise Exception('needs at least one OpenPGP certificate') + if not recipients and not passwords: + raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate or password to encrypt to') + + certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(recipients) + cipher:Optional[pgpy.constants.SymmetricKeyAlgorithm] = None symmetrickey:Optional[bytes] = None if sessionkey: - (cipher, symmetrickey) = self._get_session_key(sessionkey) + cipher = pgpy.constants.SymmetricKeyAlgorithm(sessionkey.algo) + symmetrickey = sessionkey.key del sessionkey else: ciphers = set(self._cipherprefs) - for cert in certs: + for handle, cert in certs.items(): keyciphers=set() for uid in cert.userids: if uid.selfsig and uid.selfsig.cipherprefs: @@ -291,48 +242,41 @@ def encrypt(self, if symmetrickey is None: symmetrickey = cipher.gen_key() - data: bytes = inp.read() msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) - for cert in certs: + for handle, cert in certs.items(): msg = cert.encrypt(msg, cipher=cipher, sessionkey=symmetrickey) del symmetrickey return self._maybe_armor(armor, msg) def decrypt(self, - inp:io.BufferedReader, - sessionkey:Optional[str], - passwords:List[str], - verifications:Optional[str], - signers:List[str], - start:Optional[str], - end:Optional[str], - secretkeys:List[str]) -> bytes: + data:bytes, + sessionkey:Optional[sop.SOPSessionKey], + passwords:MutableMapping[str,bytes], + signers:MutableMapping[str,bytes], + start:Optional[datetime], + end:Optional[datetime], + secretkeys:MutableMapping[str,bytes]) -> Tuple[bytes, List[sop.SOPSigResult]]: # FIXME!!! if sessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') - if verifications: - raise sop.SOPUnsupportedOption('sopgpy does not support --verify-out yet') if signers: raise sop.SOPUnsupportedOption('sopgpy does not support --verify-with yet') if start or end: raise sop.SOPUnsupportedOption('sopgpy does not support --verify-not-before or --verify-not-after yet') - seckeys: List[pgpy.PGPKey] = [] - for fname in secretkeys: - seckey = self._get_pgp_key(fname, True) - seckeys.append(seckey) - - if not seckeys: - raise Exception('needs at least one OpenPGP secret key') - data: bytes = inp.read() + if not secretkeys and not passwords: + raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP secret key') + + seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(secretkeys) + encmsg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) ret:Optional[bytes] = None - for seckey in seckeys: + for handle,seckey in seckeys.items(): try: - msg: pgpy.PGPMessage = seckey.decrypt(encmsg) + msg:pgpy.PGPMessage = seckey.decrypt(encmsg) out:Union[str,bytes] = msg.message if isinstance(out, str): ret = out.encode('utf8') @@ -342,26 +286,23 @@ def decrypt(self, except pgpy.errors.PGPDecryptionError as e: logging.warning(f'could not decrypt with {seckey.fingerprint}') if ret is None: - raise Exception(f'could not find anything capable of decryption') - return ret + raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') + return (ret, []) - def armor(self, - inp:io.BufferedReader, - label:Optional[str]) -> bytes: - data:bytes = inp.read() + def armor(self, data:bytes, label:Optional[sop.SOPArmorLabel]) -> bytes: obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None try: - if label == 'message': + if label is sop.SOPArmorLabel.message: obj = pgpy.PGPMessage.from_blob(data) - elif label == 'key': + elif label is sop.SOPArmorLabel.key: obj, _ = pgpy.PGPKey.from_blob(data) if obj.is_public or not obj.is_primary: raise sop.SOPInvalidDataType('not an OpenPGP secret key') - elif label == 'cert': + elif label is sop.SOPArmorLabel.cert: obj, _ = pgpy.PGPKey.from_blob(data) if not obj.is_public: raise sop.SOPInvalidDataType('not an OpenPGP certificate') - elif label == 'sig': + elif label is sop.SOPArmorLabel.sig: obj = pgpy.PGPSignature.from_blob(data) elif label is None: # try to guess try: @@ -380,23 +321,26 @@ def armor(self, raise sop.SOPInvalidDataType(f'{e}') return str(obj).encode('ascii') - def dearmor(self, - inp:io.BufferedReader) -> bytes: - data:bytes = inp.read() + def dearmor(self, data:bytes) -> bytes: try: + key:pgpy.PGPKey key, _ = pgpy.PGPKey.from_blob(data) return bytes(key) except: pass - for cls in [ pgpy.PGPSignature, pgpy.PGPMessage ]: - try: - obj:cls = cls.from_blob(data) - return bytes(obj) - except: - pass + try: + sig:pgpy.PGPSignature = pgpy.PGPSignature.from_blob(data) + return bytes(sig) + except: + pass + try: + msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + return bytes(msg) + except: + pass raise sop.SOPInvalidDataType() -def main(): +def main() -> None: sop = SOPGPy() sop.dispatch() From dad83bc8b6b3661a8ae3f5719e463e812ed72a8e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 27 Oct 2019 22:29:31 -0400 Subject: [PATCH 025/287] clean up signature result printing --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 2ef98254..d3549b98 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -182,7 +182,7 @@ def verify(self, goodsig:pgpy.types.sigsubj for goodsig in verif.good_signatures: if goodsig.verified: - ret += [sop.SOPSigResult(goodsig.signature.created, cert.fingerprint, cert)] + ret += [sop.SOPSigResult(goodsig.signature.created, cert.fingerprint, goodsig.signature.__repr__())] except: pass if not ret: From 48f2cc17d99fba32a8ecafd08ac09d9bb7b795cc Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 27 Oct 2019 22:48:09 -0400 Subject: [PATCH 026/287] sopgpy: do not try to encrypt with IDEA --- pgpy/sopgpy.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d3549b98..cedfdb62 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -37,12 +37,15 @@ from datetime import datetime from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict +__version__ = '0.1.0' + class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: - super().__init__(name='SOPGPy', version=pgpy.__version__, + super().__init__(name='SOPGPy', version=f'{__version__}/{pgpy.__version__}', description=f'Stateless OpenPGP using PGPy {pgpy.__version__}') - # implemented ciphers, in the order we prefer them: + # implemented ciphers that we are willing to use to encrypt, in + # the order we prefer them: _cipherprefs:List[pgpy.constants.SymmetricKeyAlgorithm] = \ [pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.SymmetricKeyAlgorithm.AES192, @@ -52,8 +55,7 @@ def __init__(self) -> None: pgpy.constants.SymmetricKeyAlgorithm.Camellia128, pgpy.constants.SymmetricKeyAlgorithm.CAST5, pgpy.constants.SymmetricKeyAlgorithm.TripleDES, - pgpy.constants.SymmetricKeyAlgorithm.Blowfish, - pgpy.constants.SymmetricKeyAlgorithm.IDEA] + pgpy.constants.SymmetricKeyAlgorithm.Blowfish] def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage,pgpy.PGPKey]) -> bytes: if (armor): From 0a64e505fa4e60b19b999a1affe119549c6930be Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 27 Oct 2019 23:51:31 -0400 Subject: [PATCH 027/287] sop decrypt: handle session key and verification output cleanly Signed-off-by: Daniel Kahn Gillmor --- pgpy/sopgpy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index cedfdb62..041be835 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -253,15 +253,15 @@ def encrypt(self, def decrypt(self, data:bytes, - sessionkey:Optional[sop.SOPSessionKey], + wantsessionkey:bool, passwords:MutableMapping[str,bytes], signers:MutableMapping[str,bytes], start:Optional[datetime], end:Optional[datetime], - secretkeys:MutableMapping[str,bytes]) -> Tuple[bytes, List[sop.SOPSigResult]]: + secretkeys:MutableMapping[str,bytes]) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: # FIXME!!! - if sessionkey: - raise sop.SOPUnsupportedOption('sopgpy does not support --session-key yet') + if wantsessionkey: + raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') if signers: @@ -289,7 +289,7 @@ def decrypt(self, logging.warning(f'could not decrypt with {seckey.fingerprint}') if ret is None: raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') - return (ret, []) + return (ret, [], None) def armor(self, data:bytes, label:Optional[sop.SOPArmorLabel]) -> bytes: obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None From 95b635a9dca7916ec26f6140087250a0ab43ef5a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 28 Oct 2019 01:05:43 -0400 Subject: [PATCH 028/287] Enable easier extension to the sop interface By making all arguments to the functions keyword arguments, we can use **kwargs to receive any extended options. Signed-off-by: Daniel Kahn Gillmor --- pgpy/sopgpy.py | 70 +++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 041be835..d72420c8 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -36,6 +36,7 @@ from datetime import datetime from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict +from argparse import Namespace, _SubParsersAction, ArgumentParser __version__ = '0.1.0' @@ -87,8 +88,9 @@ def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.P raise sop.SOPInvalidDataType('cert {handle} is not an OpenPGP transferable secret key (maybe certificate?)') keys[handle] = key return keys - - def generate(self, armor:bool, uids:List[str]) -> bytes: + + def generate(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, pgpy.constants.EllipticCurveOID.Ed25519) primaryflags: Set[int] = set() @@ -124,17 +126,21 @@ def generate(self, armor:bool, uids:List[str]) -> bytes: def convert(self, - key:bytes, - armor:bool) -> bytes: + key:bytes=b'', + armor:bool=True, + **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) seckey, _ = pgpy.PGPKey.from_blob(key) return self._maybe_armor(armor, seckey.pubkey) def sign(self, - data:bytes, - armor:bool, - sigtype:sop.SOPSigType, - signers:MutableMapping[str, bytes]) -> bytes: + data:bytes=b'', + armor:bool=True, + sigtype:sop.SOPSigType=sop.SOPSigType.binary, + signers:MutableMapping[str, bytes]={}, + **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) @@ -166,10 +172,12 @@ def __bytes__(self) -> bytes: def verify(self, data:bytes, - start:Optional[datetime], - end:Optional[datetime], - sig:bytes, - signers:MutableMapping[str,bytes]) -> List[sop.SOPSigResult]: + start:Optional[datetime]=None, + end:Optional[datetime]=None, + sig:bytes=b'', + signers:MutableMapping[str,bytes]={}, + **kwargs:Namespace) -> List[sop.SOPSigResult]: + self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') signature = self._get_pgp_signature(sig) @@ -194,13 +202,15 @@ def verify(self, def encrypt(self, data:bytes, - literaltype:sop.SOPLiteralDataType, - armor:bool, - mode:sop.SOPEncryptMode, - passwords:MutableMapping[str,bytes], - sessionkey:Optional[sop.SOPSessionKey], - signers:MutableMapping[str,bytes], - recipients:MutableMapping[str,bytes]) -> bytes: + literaltype:sop.SOPLiteralDataType=sop.SOPLiteralDataType.binary, + armor:bool=True, + mode:sop.SOPEncryptMode=sop.SOPEncryptMode.any, + passwords:MutableMapping[str,bytes]={}, + sessionkey:Optional[sop.SOPSessionKey]=None, + signers:MutableMapping[str,bytes]={}, + recipients:MutableMapping[str,bytes]={}, + **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) handle:str # FIXME! if literaltype is not sop.SOPLiteralDataType.binary: @@ -253,12 +263,14 @@ def encrypt(self, def decrypt(self, data:bytes, - wantsessionkey:bool, - passwords:MutableMapping[str,bytes], - signers:MutableMapping[str,bytes], - start:Optional[datetime], - end:Optional[datetime], - secretkeys:MutableMapping[str,bytes]) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: + wantsessionkey:bool=False, + passwords:MutableMapping[str,bytes]={}, + signers:MutableMapping[str,bytes]={}, + start:Optional[datetime]=None, + end:Optional[datetime]=None, + secretkeys:MutableMapping[str,bytes]={}, + **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: + self.raise_on_unknown_options(**kwargs) # FIXME!!! if wantsessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') @@ -291,7 +303,10 @@ def decrypt(self, raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') return (ret, [], None) - def armor(self, data:bytes, label:Optional[sop.SOPArmorLabel]) -> bytes: + def armor(self, data:bytes, + label:Optional[sop.SOPArmorLabel]=None, + **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None try: if label is sop.SOPArmorLabel.message: @@ -323,7 +338,8 @@ def armor(self, data:bytes, label:Optional[sop.SOPArmorLabel]) -> bytes: raise sop.SOPInvalidDataType(f'{e}') return str(obj).encode('ascii') - def dearmor(self, data:bytes) -> bytes: + def dearmor(self, data:bytes, **kwargs:Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) try: key:pgpy.PGPKey key, _ = pgpy.PGPKey.from_blob(data) From d5aaa909c743dccb129902ecf7e92e11f6c81385 Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Tue, 29 Oct 2019 11:34:24 +0100 Subject: [PATCH 029/287] sopgpy: Improve shebang so that we can use virtualenv. --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d72420c8..5ca4a07a 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 # PYTHON_ARGCOMPLETE_OK '''OpenPGP Interoperability Test Suite Generic Functionality using PGPy From 6a5f4cd14e98c0197040b4e1d014575828991a40 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 8 Nov 2019 00:28:36 -0500 Subject: [PATCH 030/287] sopgpy: move to 0.2.0 of python-sop This reflects the changes to the subcommand names and additional arguments from draft-dkg-openpgp-stateless-cli-01 --- pgpy/sopgpy.py | 75 +++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 5ca4a07a..d1ee7e4c 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -89,7 +89,7 @@ def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.P keys[handle] = key return keys - def generate(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> bytes: + def generate_key(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, pgpy.constants.EllipticCurveOID.Ed25519) @@ -125,10 +125,10 @@ def generate(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> by return self._maybe_armor(armor, primary) - def convert(self, - key:bytes=b'', - armor:bool=True, - **kwargs:Namespace) -> bytes: + def extract_cert(self, + key:bytes=b'', + armor:bool=True, + **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) seckey, _ = pgpy.PGPKey.from_blob(key) return self._maybe_armor(armor, seckey.pubkey) @@ -182,8 +182,6 @@ def verify(self, raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') signature = self._get_pgp_signature(sig) certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) - if start is not None or end is not None: - raise sop.SOPUnsupportedOption('have not implemented --not-before and --not-after') ret:List[sop.SOPSigResult] = [] for (handle,cert) in certs.items(): @@ -192,7 +190,9 @@ def verify(self, goodsig:pgpy.types.sigsubj for goodsig in verif.good_signatures: if goodsig.verified: - ret += [sop.SOPSigResult(goodsig.signature.created, cert.fingerprint, goodsig.signature.__repr__())] + if start is None or goodsig.signature.created >= start: + if end is None or goodsig.signature.created <= end: + ret += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] except: pass if not ret: @@ -204,9 +204,7 @@ def encrypt(self, data:bytes, literaltype:sop.SOPLiteralDataType=sop.SOPLiteralDataType.binary, armor:bool=True, - mode:sop.SOPEncryptMode=sop.SOPEncryptMode.any, passwords:MutableMapping[str,bytes]={}, - sessionkey:Optional[sop.SOPSessionKey]=None, signers:MutableMapping[str,bytes]={}, recipients:MutableMapping[str,bytes]={}, **kwargs:Namespace) -> bytes: @@ -215,8 +213,6 @@ def encrypt(self, # FIXME! if literaltype is not sop.SOPLiteralDataType.binary: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') - if mode is not sop.SOPEncryptMode.any: - raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --mode yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') if signers: @@ -229,41 +225,35 @@ def encrypt(self, cipher:Optional[pgpy.constants.SymmetricKeyAlgorithm] = None - symmetrickey:Optional[bytes] = None - if sessionkey: - cipher = pgpy.constants.SymmetricKeyAlgorithm(sessionkey.algo) - symmetrickey = sessionkey.key - del sessionkey - else: - ciphers = set(self._cipherprefs) - for handle, cert in certs.items(): - keyciphers=set() - for uid in cert.userids: - if uid.selfsig and uid.selfsig.cipherprefs: - for cipher in uid.selfsig.cipherprefs: - keyciphers.add(cipher) - ciphers = ciphers.intersection(keyciphers) - for c in self._cipherprefs: - if c in ciphers: - cipher = c - break + ciphers = set(self._cipherprefs) + for handle, cert in certs.items(): + keyciphers=set() + for uid in cert.userids: + if uid.selfsig and uid.selfsig.cipherprefs: + for cipher in uid.selfsig.cipherprefs: + keyciphers.add(cipher) + ciphers = ciphers.intersection(keyciphers) + for c in self._cipherprefs: + if c in ciphers: + cipher = c + break # AES128 is MTI in RFC4880: if cipher is None: cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 - if symmetrickey is None: - symmetrickey = cipher.gen_key() + sessionkey = cipher.gen_key() msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) for handle, cert in certs.items(): - msg = cert.encrypt(msg, cipher=cipher, sessionkey=symmetrickey) - del symmetrickey + msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) + del sessionkey return self._maybe_armor(armor, msg) def decrypt(self, data:bytes, wantsessionkey:bool=False, + sessionkeys:MutableMapping[str,sop.SOPSessionKey]={}, passwords:MutableMapping[str,bytes]={}, signers:MutableMapping[str,bytes]={}, start:Optional[datetime]=None, @@ -276,13 +266,15 @@ def decrypt(self, raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') + if sessionkeys: + raise sop.SOPUnsupportedOption('sopgpy does not support --with-session-key yet') if signers: raise sop.SOPUnsupportedOption('sopgpy does not support --verify-with yet') - if start or end: - raise sop.SOPUnsupportedOption('sopgpy does not support --verify-not-before or --verify-not-after yet') + if start: + raise sop.SOPUnsupportedOption('sopgpy does not support --verify-not-before yet') - if not secretkeys and not passwords: - raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP secret key') + if not secretkeys and not passwords and not sessionkeys: + raise sop.SOPMissingRequiredArgument('needs something to decrypt with (at least an OpenPGP secret key, a session key, or a password)') seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(secretkeys) @@ -304,9 +296,12 @@ def decrypt(self, return (ret, [], None) def armor(self, data:bytes, - label:Optional[sop.SOPArmorLabel]=None, + label:sop.SOPArmorLabel=sop.SOPArmorLabel.auto, + allow_nested:bool=False, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) + if allow_nested: + raise sop.SOPUnsupportedOption('sopgpy does not support --allow-nested yet') obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None try: if label is sop.SOPArmorLabel.message: @@ -321,7 +316,7 @@ def armor(self, data:bytes, raise sop.SOPInvalidDataType('not an OpenPGP certificate') elif label is sop.SOPArmorLabel.sig: obj = pgpy.PGPSignature.from_blob(data) - elif label is None: # try to guess + elif label is sop.SOPArmorLabel.auto: # try to guess try: obj, _ = pgpy.PGPKey.from_blob(data) except: From b2b861ca9b0f6a254c03fa70c436107df83f3a3b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 20:14:12 -0500 Subject: [PATCH 031/287] =?UTF-8?q?PGPy=20removed=20=5F=5Fversion=5F=5F=20?= =?UTF-8?q?in=200.5.4.=20=20This=20fix=20relies=20on=20python=20=E2=89=A5?= =?UTF-8?q?=203.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pgpy/sopgpy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d1ee7e4c..c026ce2c 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -33,6 +33,7 @@ import pgpy #type: ignore import codecs import logging +from importlib import metadata from datetime import datetime from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict @@ -42,8 +43,9 @@ class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: - super().__init__(name='SOPGPy', version=f'{__version__}/{pgpy.__version__}', - description=f'Stateless OpenPGP using PGPy {pgpy.__version__}') + pgpy_version = metadata.version('pgpy') + super().__init__(name='SOPGPy', version=f'{__version__}/{pgpy_version}', + description=f'Stateless OpenPGP using PGPy {pgpy_version}') # implemented ciphers that we are willing to use to encrypt, in # the order we prefer them: From e0b6b9af312262e9b969dc923b189e581b6a8293 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 20:47:26 -0500 Subject: [PATCH 032/287] sopgpy: encrypt --sign-with This implements simple signatures inside encryption for sopgpy --- pgpy/sopgpy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index c026ce2c..36d5df01 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -212,14 +212,15 @@ def encrypt(self, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) handle:str + keys:MutableMapping[str,pgpy.PGPKey] = {} # FIXME! if literaltype is not sop.SOPLiteralDataType.binary: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') if passwords: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') - if signers: - raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --sign-with yet') + if signers: + keys = self._get_keys(signers) if not recipients and not passwords: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate or password to encrypt to') @@ -246,6 +247,10 @@ def encrypt(self, sessionkey = cipher.gen_key() msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + for signer in keys: + sig = keys[signer].sign(msg) + msg |= sig + for handle, cert in certs.items(): msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) del sessionkey From 0fbb2aa843bf648647e6c6f060676f16b861e35f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 20:51:29 -0500 Subject: [PATCH 033/287] sopgpy decrypt: handle --verify-with This should enable tests of signature verification concurrent with decryption. We do this by refactoring out the signature verification and relying on PGPY to know how to verify an "inline" message. --- pgpy/sopgpy.py | 52 +++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 36d5df01..aa522f23 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -185,18 +185,7 @@ def verify(self, signature = self._get_pgp_signature(sig) certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) - ret:List[sop.SOPSigResult] = [] - for (handle,cert) in certs.items(): - try: - verif:pgpy.types.SignatureVerification = cert.verify(data, signature=signature) - goodsig:pgpy.types.sigsubj - for goodsig in verif.good_signatures: - if goodsig.verified: - if start is None or goodsig.signature.created >= start: - if end is None or goodsig.signature.created <= end: - ret += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] - except: - pass + ret:List[sop.SOPSigResult] = self._check_sigs(certs, data, signature, start, end) if not ret: raise sop.SOPNoSignature("No good signature found") return ret @@ -247,8 +236,8 @@ def encrypt(self, sessionkey = cipher.gen_key() msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) - for signer in keys: - sig = keys[signer].sign(msg) + for signer, key in keys.items(): + sig = key.sign(msg) msg |= sig for handle, cert in certs.items(): @@ -257,6 +246,26 @@ def encrypt(self, return self._maybe_armor(armor, msg) + def _check_sigs(self, + certs:MutableMapping[str,pgpy.PGPKey], + msg:pgpy.PGPMessage, + sig:Optional[pgpy.PGPSignature]=None, + start:Optional[datetime]=None, + end:Optional[datetime]=None) -> List[sop.SOPSigResult]: + sigs:List[sop.SOPSigResult] = [] + for signer, cert in certs.items(): + try: + verif:pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) + goodsig:pgpy.types.sigsubj + for goodsig in verif.good_signatures: + if goodsig.verified: + if start is None or goodsig.signature.created >= start: + if end is None or goodsig.signature.created <= end: + sigs += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] + except: + pass + return sigs + def decrypt(self, data:bytes, wantsessionkey:bool=False, @@ -268,6 +277,7 @@ def decrypt(self, secretkeys:MutableMapping[str,bytes]={}, **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: self.raise_on_unknown_options(**kwargs) + certs:MutableMapping[str,pgpy.PGPKey] = {} # FIXME!!! if wantsessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') @@ -275,21 +285,23 @@ def decrypt(self, raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') if sessionkeys: raise sop.SOPUnsupportedOption('sopgpy does not support --with-session-key yet') - if signers: - raise sop.SOPUnsupportedOption('sopgpy does not support --verify-with yet') - if start: - raise sop.SOPUnsupportedOption('sopgpy does not support --verify-not-before yet') + if signers: + certs = self._get_certs(signers) if not secretkeys and not passwords and not sessionkeys: raise sop.SOPMissingRequiredArgument('needs something to decrypt with (at least an OpenPGP secret key, a session key, or a password)') + sigs:List[sop.SOPSigResult] = [] seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(secretkeys) encmsg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + msg:pgpy.PGPMessage ret:Optional[bytes] = None for handle,seckey in seckeys.items(): try: - msg:pgpy.PGPMessage = seckey.decrypt(encmsg) + msg = seckey.decrypt(encmsg) + if certs: + sigs = self._check_sigs(certs, msg, None, start, end) out:Union[str,bytes] = msg.message if isinstance(out, str): ret = out.encode('utf8') @@ -300,7 +312,7 @@ def decrypt(self, logging.warning(f'could not decrypt with {seckey.fingerprint}') if ret is None: raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') - return (ret, [], None) + return (ret, sigs, None) def armor(self, data:bytes, label:sop.SOPArmorLabel=sop.SOPArmorLabel.auto, From 65fe73609daa6bdce05db9609c554411251a4f1e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 22:11:37 -0500 Subject: [PATCH 034/287] sopgpy encrypt: implement --with-password --- pgpy/sopgpy.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index aa522f23..f8f2a418 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -202,12 +202,18 @@ def encrypt(self, self.raise_on_unknown_options(**kwargs) handle:str keys:MutableMapping[str,pgpy.PGPKey] = {} + pws:MutableMapping[str,str] = {} # FIXME! if literaltype is not sop.SOPLiteralDataType.binary: raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') - if passwords: - raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --with-password yet') + if passwords: + for p, pwdata in passwords.items(): + try: + pstring = pwdata.decode(encoding='utf-8') + except UnicodeDecodeError: + raise sop.SOPPasswordNotHumanReadable(f'Password in {p} was not UTF-8') + pws[p] = pstring.strip() if signers: keys = self._get_keys(signers) if not recipients and not passwords: @@ -242,6 +248,8 @@ def encrypt(self, for handle, cert in certs.items(): msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) + for p, pw in pws.items(): + msg = msg.encrypt(passphrase=pw, sessionkey=sessionkey) del sessionkey return self._maybe_armor(armor, msg) From 03c9eae8e27a26410d0f8e9195dc732be4d5c8a1 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 23:01:36 -0500 Subject: [PATCH 035/287] sopgpy decrypt: implement --with-password --- pgpy/sopgpy.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index f8f2a418..2ef841fa 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -289,8 +289,6 @@ def decrypt(self, # FIXME!!! if wantsessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') - if passwords: - raise sop.SOPUnsupportedOption('sopgpy does not support --with-password yet') if sessionkeys: raise sop.SOPUnsupportedOption('sopgpy does not support --with-session-key yet') @@ -305,12 +303,13 @@ def decrypt(self, encmsg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) msg:pgpy.PGPMessage ret:Optional[bytes] = None + out:Union[str,bytes] for handle,seckey in seckeys.items(): try: msg = seckey.decrypt(encmsg) if certs: sigs = self._check_sigs(certs, msg, None, start, end) - out:Union[str,bytes] = msg.message + out = msg.message if isinstance(out, str): ret = out.encode('utf8') else: @@ -318,6 +317,37 @@ def decrypt(self, break except pgpy.errors.PGPDecryptionError as e: logging.warning(f'could not decrypt with {seckey.fingerprint}') + if ret is None: + for p, password in passwords.items(): + attempts:List[bytes] = [ password ] + extratext = '' + try: + trimmed = password.decode(encoding='utf-8').strip().encode('utf-8') + if trimmed != password: + # try the version with the trailing whitespace trimmed off first, + # as it is more likely to match the user's intent + attempts.insert(0, trimmed) + extratext = ' (also tried trimming trailing whitespace)' + except UnicodeDecodeError: + pass + for attempt in attempts: + if ret is None: + try: + # note: PGPy 0.5.4 and earlier don't accept bytes here: + # https://github.com/SecurityInnovation/PGPy/pull/388 + msg = encmsg.decrypt(passphrase=attempt) + if certs: + sigs = self._check_sigs(certs, msg, None, start, end) + out = msg.message + if isinstance(out, str): + ret = out.encode('utf8') + else: + ret = out + break + except pgpy.errors.PGPDecryptionError: + pass + if ret is None: + logging.warning(f'could not decrypt with password from {p}{extratext}') if ret is None: raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') return (ret, sigs, None) From a1fd82f865c788e62caaa7fdab7a9eeb5a121c45 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 1 Feb 2022 23:53:23 -0500 Subject: [PATCH 036/287] sopgpy sign: Raise the correct error when trying to sign non-UTF-8 text as text --- pgpy/sopgpy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 2ef841fa..dd1a577d 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -148,7 +148,11 @@ def sign(self, seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) msg:pgpy.PGPMessage if sigtype is sop.SOPSigType.text: - msg = pgpy.PGPMessage.new(data.decode('utf8'), cleartext=True, format='u') + try: + datastr:str = data.decode(encoding='utf-8') + except UnicodeDecodeError: + raise sop.SOPNotUTF8Text('Message was not encoded UTF-8 text') + msg = pgpy.PGPMessage.new(datastr, cleartext=True, format='u') elif sigtype == sop.SOPSigType.binary: msg = pgpy.PGPMessage.new(data, format='b') else: From 60a046e6b5436a280a5cea572bbbbb86d1485c57 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 2 Feb 2022 00:06:07 -0500 Subject: [PATCH 037/287] reported multiple signature problem to PGPy --- pgpy/sopgpy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index dd1a577d..9fdd31a6 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -161,7 +161,8 @@ def sign(self, for handle,seckey in seckeys.items(): signatures.append(seckey.sign(msg)) - # hack to assemble multiple signature packets! FIXME: need to report to PGPy + # hack to assemble multiple signature packets! reported to PGPy at + # https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 sigdata:bytes = b'' for signature in signatures: sigdata += bytes(seckey.sign(msg)) From e136dd71c2117f81a7d2c4382e920fd314bc14ac Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 2 Feb 2022 00:07:14 -0500 Subject: [PATCH 038/287] sopgpy encrypt: respect --as= argument --- pgpy/sopgpy.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 9fdd31a6..2b39d5de 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -208,9 +208,20 @@ def encrypt(self, handle:str keys:MutableMapping[str,pgpy.PGPKey] = {} pws:MutableMapping[str,str] = {} - # FIXME! - if literaltype is not sop.SOPLiteralDataType.binary: - raise sop.SOPUnsupportedOption('sopgpy encrypt does not support --as yet') + format_octet:str + + if literaltype is sop.SOPLiteralDataType.text: + format_octet = 'u' + try: + data.decode(encoding='utf-8') + except UnicodeDecodeError: + raise sop.SOPNotUTF8Text('Message was not encoded UTF-8 text') + elif literaltype is sop.SOPLiteralDataType.binary: + format_octet = 'b' + elif literaltype is sop.SOPLiteralDataType.mime: + format_octet = 'm' + else: + raise sop.SOPUnsupportedOption(f'sopgpy encrypt --as with value {literaltype}') if passwords: for p, pwdata in passwords.items(): @@ -246,7 +257,7 @@ def encrypt(self, cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 sessionkey = cipher.gen_key() - msg = pgpy.PGPMessage.new(data, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + msg = pgpy.PGPMessage.new(data, format=format_octet, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) for signer, key in keys.items(): sig = key.sign(msg) msg |= sig From 99b839bcc66bf123e1ccf950529a372871e506fe Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 3 Feb 2022 19:32:26 -0500 Subject: [PATCH 039/287] =?UTF-8?q?Avoid=20passing=20a=20bytes=20object=20?= =?UTF-8?q?as=20a=20passphrase=20to=20pgpy=20=E2=89=A4=200.5.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works around https://github.com/SecurityInnovation/PGPy/pull/388 --- pgpy/sopgpy.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 2b39d5de..579d2aaa 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -33,6 +33,7 @@ import pgpy #type: ignore import codecs import logging +import packaging.version from importlib import metadata from datetime import datetime @@ -43,9 +44,9 @@ class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: - pgpy_version = metadata.version('pgpy') - super().__init__(name='SOPGPy', version=f'{__version__}/{pgpy_version}', - description=f'Stateless OpenPGP using PGPy {pgpy_version}') + self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) + super().__init__(name='SOPGPy', version=f'{__version__}/{self.pgpy_version}', + description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') # implemented ciphers that we are willing to use to encrypt, in # the order we prefer them: @@ -335,7 +336,7 @@ def decrypt(self, logging.warning(f'could not decrypt with {seckey.fingerprint}') if ret is None: for p, password in passwords.items(): - attempts:List[bytes] = [ password ] + attempts:List[Union[bytes,str]] = [ password ] extratext = '' try: trimmed = password.decode(encoding='utf-8').strip().encode('utf-8') @@ -351,6 +352,9 @@ def decrypt(self, try: # note: PGPy 0.5.4 and earlier don't accept bytes here: # https://github.com/SecurityInnovation/PGPy/pull/388 + if isinstance (attempt, bytes) and \ + self.pgpy_version <= packaging.version.Version('0.5.4'): + attempt = attempt.decode(encoding='utf-8') msg = encmsg.decrypt(passphrase=attempt) if certs: sigs = self._check_sigs(certs, msg, None, start, end) From a6eb7dbca7015088d314831b6408bb1b2f276e15 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 3 Feb 2022 19:33:29 -0500 Subject: [PATCH 040/287] Bump sopgpy version to 0.2.0 there have been a bunch of changes (including implementations of previously-missing options) This just acknowledges those changes. --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 579d2aaa..6f56c831 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -40,7 +40,7 @@ from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict from argparse import Namespace, _SubParsersAction, ArgumentParser -__version__ = '0.1.0' +__version__ = '0.2.0' class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: From b5d9886c9c06472a3848966b5530988c77ff3ea0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:13:37 -0400 Subject: [PATCH 041/287] sopgpy: move _multisig bogus object to its own definition --- pgpy/sopgpy.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 6f56c831..f1b0ca7e 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -42,6 +42,25 @@ __version__ = '0.2.0' +# hack to assemble multiple signature packets! reported to PGPy at +# https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 +class _multisig(pgpy.types.Armorable): #type: ignore + @property + def magic(self) -> str: + return 'SIGNATURE' + def parse(self, x:bytes) -> None: + self._bytes:bytes = x + def __bytes__(self) -> bytes: + return self._bytes + @classmethod + def from_signatures(cls, signatures:List[pgpy.PGPSignature]) -> pgpy.types.Armorable: + obj = cls() + sigdata:bytes = b'' + for signature in signatures: + sigdata += bytes(signature) + obj.parse(sigdata) + return obj + class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) @@ -162,20 +181,7 @@ def sign(self, for handle,seckey in seckeys.items(): signatures.append(seckey.sign(msg)) - # hack to assemble multiple signature packets! reported to PGPy at - # https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 - sigdata:bytes = b'' - for signature in signatures: - sigdata += bytes(seckey.sign(msg)) - class _multisig(pgpy.types.Armorable): #type: ignore - @property - def magic(self) -> str: - return 'SIGNATURE' - def parse(self, x:bytes) -> None: - self._bytes:bytes = x - def __bytes__(self) -> bytes: - return self._bytes - return self._maybe_armor(armor, _multisig.from_blob(sigdata)) + return self._maybe_armor(armor, _multisig.from_signatures(signatures)) def verify(self, From 112f8f52a2d1c20afe6122b531429ede94245ce1 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:18:50 -0400 Subject: [PATCH 042/287] sopgpy: drop sop armor's --allow-nested option (deprecated in the spec) --- pgpy/sopgpy.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index f1b0ca7e..42b1b282 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -380,11 +380,8 @@ def decrypt(self, def armor(self, data:bytes, label:sop.SOPArmorLabel=sop.SOPArmorLabel.auto, - allow_nested:bool=False, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) - if allow_nested: - raise sop.SOPUnsupportedOption('sopgpy does not support --allow-nested yet') obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None try: if label is sop.SOPArmorLabel.message: From 40aa0173bd01a619333e2326cf6c7fdc201aa775 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:23:37 -0400 Subject: [PATCH 043/287] sopgpy: fix decrypt warnings when symmetric decryption fails --- pgpy/sopgpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 42b1b282..d911686d 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -372,8 +372,8 @@ def decrypt(self, break except pgpy.errors.PGPDecryptionError: pass - if ret is None: - logging.warning(f'could not decrypt with password from {p}{extratext}') + if ret is None: + logging.warning(f'could not decrypt with password from {p}{extratext}') if ret is None: raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') return (ret, sigs, None) From 6da28fc4802a7dafb4f978b8410547b3a8670bdb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:24:20 -0400 Subject: [PATCH 044/287] sopgpy: clean up _maybe_armor type definitions --- pgpy/sopgpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d911686d..877ef25b 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -79,8 +79,8 @@ def __init__(self) -> None: pgpy.constants.SymmetricKeyAlgorithm.CAST5, pgpy.constants.SymmetricKeyAlgorithm.TripleDES, pgpy.constants.SymmetricKeyAlgorithm.Blowfish] - - def _maybe_armor(self, armor:bool, data:Union[pgpy.PGPSignature,pgpy.PGPMessage,pgpy.PGPKey]) -> bytes: + + def _maybe_armor(self, armor:bool, data:pgpy.types.Armorable) -> bytes: if (armor): return str(data).encode('ascii') else: From a7271882f2b5654ee8a595ff429d97508a5b6a44 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:27:29 -0400 Subject: [PATCH 045/287] sopgpy: clean up tz-naive objects if pgpy emits them (it shouldn't) --- pgpy/sopgpy.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 877ef25b..7640533d 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -36,7 +36,7 @@ import packaging.version from importlib import metadata -from datetime import datetime +from datetime import datetime, timezone from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict from argparse import Namespace, _SubParsersAction, ArgumentParser @@ -289,9 +289,14 @@ def _check_sigs(self, verif:pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) goodsig:pgpy.types.sigsubj for goodsig in verif.good_signatures: + sigtime = goodsig.signature.created + # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: + # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects + if sigtime.tzinfo is None: + sigtime = sigtime.replace(tzinfo=timezone.utc) if goodsig.verified: - if start is None or goodsig.signature.created >= start: - if end is None or goodsig.signature.created <= end: + if start is None or sigtime >= start: + if end is None or sigtime <= end: sigs += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] except: pass From 28fc92f4b960539da4a8d755488420fa2a2a71c9 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:30:12 -0400 Subject: [PATCH 046/287] sopgpy: create wrapper that permits a closure to do operations with a locked secret key --- pgpy/sopgpy.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 7640533d..9d583e37 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -37,7 +37,7 @@ from importlib import metadata from datetime import datetime, timezone -from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict +from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable from argparse import Namespace, _SubParsersAction, ArgumentParser __version__ = '0.2.0' @@ -111,6 +111,34 @@ def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.P keys[handle] = key return keys + # FIXME: consider making the return type a generic instead of this clunky Union: + # https://docs.python.org/3/library/typing.html#generics + def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, + keypasswords:MutableMapping[str,bytes], + func:Callable[[pgpy.PGPKey], + Union[pgpy.PGPMessage,pgpy.PGPSignature]]) -> \ + Union[pgpy.PGPMessage,pgpy.PGPSignature]: + # try all passphrases in map: + for handle,pw in keypasswords.items(): + # FIXME: be cleverer about which password to try + # when multiple passwords and keys are + # present. see for example the discussion in: + # https://gitlab.com/dkg/openpgp-stateless-cli/-/issues/60 + try: + with seckey.unlock(pw): + return func(seckey) + except pgpy.errors.PGPDecryptionError: + pass + err:str + if len(keypasswords) == 0: + err = "; no passwords provided" + elif len(keypasswords) == 1: + err = "by the provided password" + else: + err = f"by any of the {len(keypasswords)} passwords provided" + raise sop.SOPKeyIsProtected(f"Key found at {keyhandle} could not be unlocked {err}.") + + def generate_key(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, From 6678af84ab9035d7b0d2c3278e7fdad4e0d21266 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:32:55 -0400 Subject: [PATCH 047/287] sopgpy: add sop sign --micalg-out --- pgpy/sopgpy.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 9d583e37..7c28f824 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -189,7 +189,8 @@ def sign(self, armor:bool=True, sigtype:sop.SOPSigType=sop.SOPSigType.binary, signers:MutableMapping[str, bytes]={}, - **kwargs:Namespace) -> bytes: + wantmicalg:bool=False, + **kwargs:Namespace) -> Tuple[bytes, Optional[str]]: self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") @@ -206,10 +207,19 @@ def sign(self, else: raise sop.SOPUnsupportedOption(f'unknown signature type {sigtype}') signatures:List[pgpy.PGPSignature] = [] + hashalgs:Set[pgpy.constants.HashAlgorithm] = set() for handle,seckey in seckeys.items(): - signatures.append(seckey.sign(msg)) + hashalgs.add(sig.hash_algorithm) + signatures.append(sig) - return self._maybe_armor(armor, _multisig.from_signatures(signatures)) + micalg:Optional[str] = None + if wantmicalg: + if len(hashalgs) != 1: + micalg = '' + else: + micalg = f'pgp-{hashalgs.pop().lower()}' + + return (self._maybe_armor(armor, _multisig.from_signatures(signatures)), micalg) def verify(self, From 6210476a8a239a3d981aa0f3d7712ac4dcac3752 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:38:16 -0400 Subject: [PATCH 048/287] sopgpy: announce backend distinctly from version of sopgpy --- pgpy/sopgpy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 7c28f824..0539cdbc 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -64,7 +64,8 @@ def from_signatures(cls, signatures:List[pgpy.PGPSignature]) -> pgpy.types.Armor class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) - super().__init__(name='SOPGPy', version=f'{__version__}/{self.pgpy_version}', + super().__init__(name='SOPGPy', version=f'{__version__}', + backend=f'PGPy {self.pgpy_version}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') # implemented ciphers that we are willing to use to encrypt, in From faf8f4ea54d8088d8ae0283294f9e3767dbca4a1 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:38:39 -0400 Subject: [PATCH 049/287] sopgpy: add sop generate-key --with-key-password --- pgpy/sopgpy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 0539cdbc..6cc28931 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -173,6 +173,10 @@ def generate_key(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) - subflags.add(pgpy.constants.KeyFlags.EncryptCommunications) subflags.add(pgpy.constants.KeyFlags.EncryptStorage) primary.add_subkey(subkey, usage=subflags) + if keypassword is not None: + primary.protect(keypassword, + pgpy.constants.SymmetricKeyAlgorithm.AES256, + pgpy.constants.HashAlgorithm.SHA512) return self._maybe_armor(armor, primary) From 14d94b70a6f40a7152099ed5290cc915c9a56035 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:39:50 -0400 Subject: [PATCH 050/287] sopgpy: add --with-key-password to decrypt, sign, and encrypt (when signing) --- pgpy/sopgpy.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 6cc28931..66eb52fc 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -140,7 +140,9 @@ def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, raise sop.SOPKeyIsProtected(f"Key found at {keyhandle} could not be unlocked {err}.") - def generate_key(self, armor:bool=True, uids:List[str]=[], **kwargs:Namespace) -> bytes: + def generate_key(self, armor:bool=True, uids:List[str]=[], + keypassword:Optional[str]=None, + **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, pgpy.constants.EllipticCurveOID.Ed25519) @@ -195,6 +197,7 @@ def sign(self, sigtype:sop.SOPSigType=sop.SOPSigType.binary, signers:MutableMapping[str, bytes]={}, wantmicalg:bool=False, + keypasswords:MutableMapping[str,bytes]={}, **kwargs:Namespace) -> Tuple[bytes, Optional[str]]: self.raise_on_unknown_options(**kwargs) if not signers: @@ -214,6 +217,15 @@ def sign(self, signatures:List[pgpy.PGPSignature] = [] hashalgs:Set[pgpy.constants.HashAlgorithm] = set() for handle,seckey in seckeys.items(): + sig:pgpy.PGPSignature + if seckey.is_protected: + res = self._op_with_locked_key(seckey, handle, keypasswords, lambda key: key.sign(msg)) + if isinstance(res, pgpy.PGPSignature): + sig = res + else: + raise TypeError("Expected signature to be produced") + else: + sig = seckey.sign(msg) hashalgs.add(sig.hash_algorithm) signatures.append(sig) @@ -252,6 +264,7 @@ def encrypt(self, armor:bool=True, passwords:MutableMapping[str,bytes]={}, signers:MutableMapping[str,bytes]={}, + keypasswords:MutableMapping[str,bytes]={}, recipients:MutableMapping[str,bytes]={}, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) @@ -309,7 +322,15 @@ def encrypt(self, msg = pgpy.PGPMessage.new(data, format=format_octet, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) for signer, key in keys.items(): - sig = key.sign(msg) + if key.is_protected: + res:Union[pgpy.PGPMessage,pgpy.PGPSignature] + res = self._op_with_locked_key(key, signer, keypasswords, lambda seckey: seckey.sign(msg)) + if isinstance(res, pgpy.PGPSignature): + sig = res + else: + raise TypeError("Expected signature to be produced") + else: + sig = key.sign(msg) msg |= sig for handle, cert in certs.items(): @@ -353,6 +374,7 @@ def decrypt(self, signers:MutableMapping[str,bytes]={}, start:Optional[datetime]=None, end:Optional[datetime]=None, + keypasswords:MutableMapping[str,bytes]={}, secretkeys:MutableMapping[str,bytes]={}, **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: self.raise_on_unknown_options(**kwargs) @@ -377,7 +399,15 @@ def decrypt(self, out:Union[str,bytes] for handle,seckey in seckeys.items(): try: - msg = seckey.decrypt(encmsg) + if seckey.is_protected: + res = self._op_with_locked_key(seckey, handle, keypasswords, + lambda key: key.decrypt(encmsg)) + if isinstance(res, pgpy.PGPMessage): + msg = res + else: + raise TypeError("Expected message to be produced") + else: + msg = seckey.decrypt(encmsg) if certs: sigs = self._check_sigs(certs, msg, None, start, end) out = msg.message @@ -388,6 +418,11 @@ def decrypt(self, break except pgpy.errors.PGPDecryptionError as e: logging.warning(f'could not decrypt with {seckey.fingerprint}') + except sop.SOPKeyIsProtected as e: + # FIXME: this means we couldn't unlock. should we + # propagate this forward if no eventual unlock is + # found? + logging.warning(e) if ret is None: for p, password in passwords.items(): attempts:List[Union[bytes,str]] = [ password ] From 323b48f85b0fc1a21184de8356a55e518e79a760 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 21:40:11 -0400 Subject: [PATCH 051/287] sopgpy: add inline-{detach,sign,verify} --- pgpy/sopgpy.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 66eb52fc..3e8fd660 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -515,7 +515,85 @@ def dearmor(self, data:bytes, **kwargs:Namespace) -> bytes: except: pass raise sop.SOPInvalidDataType() + + def inline_detach(self, + clearsigned:bytes, + armor:bool=True, + **kwargs:Namespace) -> Tuple[bytes,bytes]: + self.raise_on_unknown_options(**kwargs) + msg:pgpy.PGPMessage + msg = pgpy.PGPMessage.from_blob(clearsigned) + body:Union[bytes,bytearray,str] = msg.message + if isinstance(body, str): + body = body.encode('utf-8') + return (bytes(body), self._maybe_armor(armor, _multisig.from_signatures(msg.signatures))) + + def inline_sign(self, + data:bytes, + armor:bool=True, + sigtype:sop.SOPInlineSigType=sop.SOPInlineSigType.binary, + signers:MutableMapping[str,bytes]={}, + keypasswords:MutableMapping[str,bytes]={}, + **kwargs:Namespace + ) -> bytes: + self.raise_on_unknown_options(**kwargs) + if not signers: + raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") + seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) + msg:pgpy.PGPMessage + if sigtype in [sop.SOPInlineSigType.text, sop.SOPInlineSigType.clearsigned]: + try: + datastr:str = data.decode(encoding='utf-8') + except UnicodeDecodeError: + raise sop.SOPNotUTF8Text('Message was not encoded UTF-8 text') + msg = pgpy.PGPMessage.new(datastr, cleartext=(sigtype == sop.SOPInlineSigType.clearsigned), format='u', compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + elif sigtype == sop.SOPInlineSigType.binary: + msg = pgpy.PGPMessage.new(data, format='b', compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + else: + raise sop.SOPUnsupportedOption(f'unknown signature type {sigtype}') + signatures:List[pgpy.PGPSignature] = [] + for handle,seckey in seckeys.items(): + sig:pgpy.PGPSignature + if seckey.is_protected: + res = self._op_with_locked_key(seckey, handle, keypasswords, lambda key: key.sign(msg)) + if isinstance(res, pgpy.PGPSignature): + sig = res + else: + raise TypeError("Expected signature to be produced") + else: + sig = seckey.sign(msg) + signatures.append(sig) + + # FIXME: this creates one-pass signatures even for the + # non-clearsigned output. it would make more sense for the + # non-clearsigned output to create a normal signature series. + for sig in signatures: + msg |= sig + if armor or sigtype == sop.SOPInlineSigType.clearsigned: + return str(msg).encode('utf-8') + else: + return bytes(msg) + def inline_verify(self, data:bytes, + start:Optional[datetime]=None, + end:Optional[datetime]=None, + signers:MutableMapping[str,bytes]={}, + **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult]]: + self.raise_on_unknown_options(**kwargs) + if not signers: + raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') + msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) + + sigresults:List[sop.SOPSigResult] = self._check_sigs(certs, msg, None, start, end) + if not sigresults: + raise sop.SOPNoSignature("No good signature found") + outmsg:Union[bytes,str] = msg.message + if isinstance(outmsg, str): + outmsg = outmsg.encode("utf-8") + return (outmsg, sigresults) + + def main() -> None: sop = SOPGPy() sop.dispatch() From b7c319271235dbe53d1134f2af38eb559c82fea2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 23 Aug 2022 22:02:07 -0400 Subject: [PATCH 052/287] sopgpy: bump to version 0.3.0 (depends on sop 0.4.0) --- pgpy/sopgpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 3e8fd660..a2af3bf9 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -40,7 +40,7 @@ from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable from argparse import Namespace, _SubParsersAction, ArgumentParser -__version__ = '0.2.0' +__version__ = '0.3.0' # hack to assemble multiple signature packets! reported to PGPy at # https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 @@ -141,7 +141,7 @@ def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, def generate_key(self, armor:bool=True, uids:List[str]=[], - keypassword:Optional[str]=None, + keypassword:Optional[bytes]=None, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, From 114321b17861889bf685d5d777f3374648445325 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 7 Nov 2022 08:38:38 -0500 Subject: [PATCH 053/287] sopgpy generate-key: improve key password See discussion at https://gitlab.com/sequoia-pgp/sequoia-sop/-/issues/17 --- pgpy/sopgpy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index a2af3bf9..ff672b66 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -125,6 +125,8 @@ def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, # when multiple passwords and keys are # present. see for example the discussion in: # https://gitlab.com/dkg/openpgp-stateless-cli/-/issues/60 + # FIXME: if pw fails, retry with normalized form + # https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-04.html#name-consuming-password-protecte try: with seckey.unlock(pw): return func(seckey) @@ -176,6 +178,12 @@ def generate_key(self, armor:bool=True, uids:List[str]=[], subflags.add(pgpy.constants.KeyFlags.EncryptStorage) primary.add_subkey(subkey, usage=subflags) if keypassword is not None: + try: + pstring = keypassword.decode(encoding='utf-8') + except UnicodeDecodeError: + raise sop.SOPPasswordNotHumanReadable(f'Key password was not UTF-8') + keypassword = pstring.strip().encode(encoding='utf-8') + primary.protect(keypassword, pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.HashAlgorithm.SHA512) From 8272870f475c79da2a97d16cf4cb9a01e93f6fa0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 23 Jan 2023 21:34:38 -0500 Subject: [PATCH 054/287] sopgpy: move to PGPy 0.6.0 (sigsubj changes) sigsubj objects have an "issues" bitfield, which follows the "Anna Karenina principle" instead of "verified" boolean. --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index ff672b66..0741340c 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -366,7 +366,7 @@ def _check_sigs(self, # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects if sigtime.tzinfo is None: sigtime = sigtime.replace(tzinfo=timezone.utc) - if goodsig.verified: + if ('issues' in goodsig._fields and goodsig.issues == 0) or ('verified' in goodsig._fields and goodsig.verified): if start is None or sigtime >= start: if end is None or sigtime <= end: sigs += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] From 34a35b8f50d51752dafbf31040a8712e51b8f6cf Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 23 Jan 2023 21:52:29 -0500 Subject: [PATCH 055/287] sopgpy: move to PGPy 0.6.0 (from_blob() behavior changes) as of 0.6.0, from_blob() methods will return non-functioning objects rather than raising an error directly. --- pgpy/sopgpy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 0741340c..a1df10a5 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -490,12 +490,15 @@ def armor(self, data:bytes, elif label is sop.SOPArmorLabel.auto: # try to guess try: obj, _ = pgpy.PGPKey.from_blob(data) + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: try: obj = pgpy.PGPSignature.from_blob(data) + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: try: obj = pgpy.PGPMessage.from_blob(data) + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: obj = pgpy.PGPMessage.new(data) else: From be04b27a809e25372a04b0663ff434019abdb332 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 1 Jun 2023 18:50:27 -0400 Subject: [PATCH 056/287] sopgpy: change license to match PGPy license. I am the sole author so far. --- pgpy/sopgpy.py | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index a1df10a5..4aaf1475 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -3,28 +3,33 @@ '''OpenPGP Interoperability Test Suite Generic Functionality using PGPy Author: Daniel Kahn Gillmor -Date: 2019-10-24 -License: MIT (see below) - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Date: 2023-06-01 +License: 3-clause BSD, same as PGPy itself + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''' import io From 70601fcac7cd012757b6b9101a05fc2571069f69 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 08:51:02 -0400 Subject: [PATCH 057/287] sopgpy: use the PGPy version directly, rather than a distinct sopgpy version --- pgpy/sopgpy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 4aaf1475..488c3a10 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -34,8 +34,6 @@ import io import os -import sop -import pgpy #type: ignore import codecs import logging import packaging.version @@ -45,7 +43,8 @@ from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable from argparse import Namespace, _SubParsersAction, ArgumentParser -__version__ = '0.3.0' +import sop +import pgpy # hack to assemble multiple signature packets! reported to PGPy at # https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 @@ -69,7 +68,7 @@ def from_signatures(cls, signatures:List[pgpy.PGPSignature]) -> pgpy.types.Armor class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) - super().__init__(name='SOPGPy', version=f'{__version__}', + super().__init__(name='SOPGPy', version=f'{self.pgpy_version}', backend=f'PGPy {self.pgpy_version}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') From a2a0621767bf9d244917433e6920469ae7c2361c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 19:03:20 -0400 Subject: [PATCH 058/287] sopgpy version --extended: add version info about cryptography module and OpenSSL --- pgpy/sopgpy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 488c3a10..86e30530 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -43,6 +43,8 @@ from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable from argparse import Namespace, _SubParsersAction, ArgumentParser +from cryptography.hazmat.backends import openssl + import sop import pgpy @@ -68,8 +70,10 @@ def from_signatures(cls, signatures:List[pgpy.PGPSignature]) -> pgpy.types.Armor class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) + self.cryptography_version = packaging.version.Version(metadata.version('cryptography')) super().__init__(name='SOPGPy', version=f'{self.pgpy_version}', backend=f'PGPy {self.pgpy_version}', + extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') # implemented ciphers that we are willing to use to encrypt, in From 02bd731213f722c1f35740668c5da1e60d58e142 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 19:03:57 -0400 Subject: [PATCH 059/287] intentionally distribute sopgpy as part of the module --- README.rst | 9 +++++++++ setup.cfg | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/README.rst b/README.rst index d800c14f..1d154f88 100644 --- a/README.rst +++ b/README.rst @@ -35,6 +35,11 @@ To install PGPy, simply: $ pip install PGPy +Command-Line Interface +---------------------- + +This module will install `sopgpy`, an implementation of the `Stateless OpenPGP Command-line Interface `_. + Documentation ------------- @@ -60,6 +65,10 @@ Requirements - `six `_ +To use `sopgpy` you'll also need: + +- `sop `_ + License ------- diff --git a/setup.cfg b/setup.cfg index af26cfdd..6742149e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ packages = install_requires = cryptography>=3.3.2 pyasn1 + sop>=0.4.0 python_requires = >=3.6 # doc_requires = @@ -54,3 +55,7 @@ python_requires = >=3.6 source-dir = docs/source build-dir = docs/build all_files = 1 + +[options.entry_points] +console_scripts = + sopgpy = pgpy.sopgpy:main From 4127e47294de8e7d79d55887f32e850119a82803 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 19:06:52 -0400 Subject: [PATCH 060/287] sopgpy: drop trailing whitespace --- pgpy/sopgpy.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 86e30530..1fd86083 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -267,7 +267,7 @@ def verify(self, raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') signature = self._get_pgp_signature(sig) certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) - + ret:List[sop.SOPSigResult] = self._check_sigs(certs, data, signature, start, end) if not ret: raise sop.SOPNoSignature("No good signature found") @@ -348,7 +348,7 @@ def encrypt(self, else: sig = key.sign(msg) msg |= sig - + for handle, cert in certs.items(): msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) for p, pw in pws.items(): @@ -398,9 +398,9 @@ def decrypt(self, # FIXME!!! if wantsessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') - if sessionkeys: + if sessionkeys: raise sop.SOPUnsupportedOption('sopgpy does not support --with-session-key yet') - + if signers: certs = self._get_certs(signers) if not secretkeys and not passwords and not sessionkeys: @@ -592,12 +592,12 @@ def inline_sign(self, return str(msg).encode('utf-8') else: return bytes(msg) - - def inline_verify(self, data:bytes, - start:Optional[datetime]=None, - end:Optional[datetime]=None, - signers:MutableMapping[str,bytes]={}, - **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult]]: + + def inline_verify(self, data: bytes, + start: Optional[datetime] = None, + end: Optional[datetime] = None, + signers: MutableMapping[str, bytes] = {}, + **kwargs: Namespace) -> Tuple[bytes, List[sop.SOPSigResult]]: self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') From c3090f9c56034d64bd23e24946ec9ed9d722d998 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 19:58:25 -0400 Subject: [PATCH 061/287] sopgpy: return ArgumentParser for argparse-manpage --- pgpy/sopgpy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 1fd86083..fb9955b0 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -617,5 +617,11 @@ def main() -> None: sop = SOPGPy() sop.dispatch() + +def get_parser() -> ArgumentParser: + 'Return an ArgumentParser object for the sake of tools like argparse-manpage' + return SOPGPy()._parser + + if __name__ == '__main__': main() From 8d071c8b55760730bef058abafa7e03fd6dccfe0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 2 Jun 2023 20:02:03 -0400 Subject: [PATCH 062/287] sopgpy: lowercase name to match the command-line name --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index fb9955b0..fe262cf9 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -71,7 +71,7 @@ class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) self.cryptography_version = packaging.version.Version(metadata.version('cryptography')) - super().__init__(name='SOPGPy', version=f'{self.pgpy_version}', + super().__init__(name='sopgpy', version=f'{self.pgpy_version}', backend=f'PGPy {self.pgpy_version}', extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') From d21c5def5c0ad8523238837e629f5d6bdcb32b69 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 8 Jun 2023 17:07:06 -0400 Subject: [PATCH 063/287] use keyword-based arguments when creating SOPSigResult --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index fe262cf9..2f0df686 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -377,7 +377,7 @@ def _check_sigs(self, if ('issues' in goodsig._fields and goodsig.issues == 0) or ('verified' in goodsig._fields and goodsig.verified): if start is None or sigtime >= start: if end is None or sigtime <= end: - sigs += [sop.SOPSigResult(goodsig.signature.created, goodsig.by.fingerprint, cert.fingerprint, goodsig.signature.__repr__())] + sigs += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, primary_fpr=cert.fingerprint, moreinfo=goodsig.signature.__repr__())] except: pass return sigs From 773f39b48d2ea89e1d1f808739649ff6a7c20e4e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 19 May 2023 20:06:29 -0400 Subject: [PATCH 064/287] OPSv3: get the "nested" flag right "nested" semantically probably is meant to mean "another OPS packet follows". But the byte on the wire is defined as 0 means another OPS packet follows. Furthermore, the flags are set in the wrong way: before this commit, the code produced a series of OPS packets where the first packet had a different value for the flag than all subsequent ones. What we want is where all the flags *except* the last OPS packet (corresponding to the *first* Sig packet) are 0. See https://gitlab.com/sequoia-pgp/openpgp-interoperability-test-suite/-/issues/84 --- pgpy/packet/packets.py | 4 ++-- pgpy/pgp.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index cd494137..1dae50df 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -718,7 +718,7 @@ def __bytearray__(self): _bytes += bytearray([self.halg]) _bytes += bytearray([self.pubalg]) _bytes += binascii.unhexlify(self.signer.encode("latin-1")) - _bytes += bytearray([int(self.nested)]) + _bytes += bytearray([int(not self.nested)]) return _bytes def parse(self, packet): @@ -735,7 +735,7 @@ def parse(self, packet): self.signer = packet[:8] del packet[:8] - self.nested = (packet[0] == 1) + self.nested = (packet[0] == 0) del packet[0] diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..b3e47a11 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1018,7 +1018,7 @@ def __iter__(self): ##TODO: is it worth coming up with a way of disabling one-pass signing? for sig in reversed(self._signatures): ops = sig.make_onepass() - if sig is not self._signatures[-1]: + if sig is not self._signatures[0]: ops.nested = True yield ops From e0c2ba8efcfd33fe1c174d059e4b789c6b2930e0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 11 Jun 2023 09:37:02 -0400 Subject: [PATCH 065/287] Add PGPSignatures object, representing bundled detached signatures a PGPMessage object can contain more than one signature. Detached signatures should also be able to handle having more than one signature. https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-09.html#name-detached-signatures says: > These detached signatures are simply one or more Signature packets > stored separately from the data for which they are a signature. A PGPSignatures object makes the most sense to represent such a thing. Closes: #197 --- docs/source/changelog.rst | 12 ++++++ pgpy/__init__.py | 2 + pgpy/pgp.py | 55 ++++++++++++++++++++++++ pgpy/sopgpy.py | 89 ++++++++++++++++----------------------- 4 files changed, 106 insertions(+), 52 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6454614e..2d9d72b6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,18 @@ Changelog ********* +v0.7.0 +====== + +(not yet released) + +API additions +------------- + +PGPSignatures represents a detached signature, which can contain more +than a single signature. It is a simple sequence of individual +PGPSignature objects. + v0.6.0 ====== diff --git a/pgpy/__init__.py b/pgpy/__init__.py index b4c30d13..472cf75f 100644 --- a/pgpy/__init__.py +++ b/pgpy/__init__.py @@ -5,6 +5,7 @@ from .pgp import PGPKeyring from .pgp import PGPMessage from .pgp import PGPSignature +from .pgp import PGPSignatures from .pgp import PGPUID __all__ = ['constants', @@ -13,4 +14,5 @@ 'PGPKeyring', 'PGPMessage', 'PGPSignature', + 'PGPSignatures', 'PGPUID', ] diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..e85b5851 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -20,6 +20,8 @@ from datetime import datetime, timezone +from typing import Any, List + from cryptography.hazmat.primitives import hashes from .constants import CompressionAlgorithm @@ -79,6 +81,7 @@ from .types import SorteDeque __all__ = ['PGPSignature', + 'PGPSignatures', 'PGPUID', 'PGPMessage', 'PGPKey', @@ -592,6 +595,58 @@ def parse(self, packet): raise ValueError('Expected: Signature. Got: {:s}'.format(pkt.__class__.__name__)) +class PGPSignatures(collections_abc.Container, collections_abc.Iterable, collections_abc.Sized, Armorable, PGPObject): + '''OpenPGP detached signatures can often contain more than one signature in them.''' + + def __init__(self, signatures: List[PGPSignature] = []) -> None: + super().__init__() + self._sigs: List[PGPSignature] = signatures + + def __contains__(self, thing: Any) -> bool: + if not isinstance(thing, PGPSignature): + raise TypeError(f'PGPSignatures only contains a PGPSignature, not {type(thing)}') + return thing in self._sigs + + def __len__(self) -> int: + return len(self._sigs) + + def __iter__(self) -> collections_abc.Iterator[PGPSignature]: + for sig in self._sigs: + yield sig + + @property + def magic(self) -> str: + return "SIGNATURE" + + def __bytearray__(self) -> bytearray: + b = bytearray() + for sig in self._sigs: + b += sig.__bytearray__() + return b + + def parse(self, packet: bytes) -> None: + unarmored = self.ascii_unarmor(packet) + data = unarmored['body'] + + if unarmored['magic'] is not None and unarmored['magic'] != 'SIGNATURE': + raise ValueError(f"Expected: SIGNATURE. Got: {format(str(unarmored['magic']))}") + + if unarmored['headers'] is not None: + self.ascii_headers = unarmored['headers'] + + while data: + pkt = Packet(data) + if pkt.header.tag == PacketTag.Signature: + if isinstance(pkt, Opaque): + # skip unrecognized version. + pass + else: + sig = PGPSignature() + sig._signature = pkt + self._sigs.append(sig) + else: + raise ValueError(f"Expected: Signature. Got: {format(pkt.__class__.__name__)}") + class PGPUID(ParentRef): @property def __sig__(self): diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 2f0df686..751e5054 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -48,24 +48,6 @@ import sop import pgpy -# hack to assemble multiple signature packets! reported to PGPy at -# https://github.com/SecurityInnovation/PGPy/issues/197#issuecomment-1027582415 -class _multisig(pgpy.types.Armorable): #type: ignore - @property - def magic(self) -> str: - return 'SIGNATURE' - def parse(self, x:bytes) -> None: - self._bytes:bytes = x - def __bytes__(self) -> bytes: - return self._bytes - @classmethod - def from_signatures(cls, signatures:List[pgpy.PGPSignature]) -> pgpy.types.Armorable: - obj = cls() - sigdata:bytes = b'' - for signature in signatures: - sigdata += bytes(signature) - obj.parse(sigdata) - return obj class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: @@ -95,10 +77,10 @@ def _maybe_armor(self, armor:bool, data:pgpy.types.Armorable) -> bytes: else: return bytes(data) - def _get_pgp_signature(self, data:bytes) -> Optional[pgpy.PGPSignature]: - sig:Optional[pgpy.PGPSignature] = None - sig = pgpy.PGPSignature.from_blob(data) - return sig + def _get_pgp_signatures(self, data: bytes) -> Optional[pgpy.PGPSignatures]: + sigs: Optional[pgpy.PGPSignatures] = None + sigs = pgpy.PGPSignatures.from_blob(data) + return sigs def _get_certs(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.PGPKey]: certs:Dict[str,pgpy.PGPKey] = {} @@ -252,7 +234,7 @@ def sign(self, else: micalg = f'pgp-{hashalgs.pop().lower()}' - return (self._maybe_armor(armor, _multisig.from_signatures(signatures)), micalg) + return (self._maybe_armor(armor, pgpy.PGPSignatures(signatures)), micalg) def verify(self, @@ -265,10 +247,10 @@ def verify(self, self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') - signature = self._get_pgp_signature(sig) - certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) + signatures = self._get_pgp_signatures(sig) + certs: MutableMapping[str, pgpy.PGPKey] = self._get_certs(signers) - ret:List[sop.SOPSigResult] = self._check_sigs(certs, data, signature, start, end) + ret: List[sop.SOPSigResult] = self._check_sigs(certs, data, signatures, start, end) if not ret: raise sop.SOPNoSignature("No good signature found") return ret @@ -360,27 +342,30 @@ def encrypt(self, def _check_sigs(self, certs:MutableMapping[str,pgpy.PGPKey], msg:pgpy.PGPMessage, - sig:Optional[pgpy.PGPSignature]=None, + sigs:Optional[pgpy.PGPSignatures]=None, start:Optional[datetime]=None, end:Optional[datetime]=None) -> List[sop.SOPSigResult]: - sigs:List[sop.SOPSigResult] = [] - for signer, cert in certs.items(): - try: - verif:pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) - goodsig:pgpy.types.sigsubj - for goodsig in verif.good_signatures: - sigtime = goodsig.signature.created - # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: - # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects - if sigtime.tzinfo is None: - sigtime = sigtime.replace(tzinfo=timezone.utc) - if ('issues' in goodsig._fields and goodsig.issues == 0) or ('verified' in goodsig._fields and goodsig.verified): - if start is None or sigtime >= start: - if end is None or sigtime <= end: - sigs += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, primary_fpr=cert.fingerprint, moreinfo=goodsig.signature.__repr__())] - except: - pass - return sigs + results:List[sop.SOPSigResult] = [] + if sigs is not None: + for sig in sigs: + for signer, cert in certs.items(): + try: + verif:pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) + goodsig:pgpy.types.sigsubj + for goodsig in verif.good_signatures: + sigtime = goodsig.signature.created + # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: + # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects + if sigtime.tzinfo is None: + sigtime = sigtime.replace(tzinfo=timezone.utc) + if ('issues' in goodsig._fields and goodsig.issues == 0) or ('verified' in goodsig._fields and goodsig.verified): + if start is None or sigtime >= start: + if end is None or sigtime <= end: + results += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, + primary_fpr=cert.fingerprint, moreinfo=goodsig.signature.__repr__())] + except: + pass + return results def decrypt(self, data:bytes, @@ -481,7 +466,7 @@ def armor(self, data:bytes, label:sop.SOPArmorLabel=sop.SOPArmorLabel.auto, **kwargs:Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) - obj:Union[None,pgpy.PGPMessage,pgpy.PGPKey,pgpy.PGPSignature] = None + obj: Union[None, pgpy.PGPMessage, pgpy.PGPKey, pgpy.PGPSignatures] = None try: if label is sop.SOPArmorLabel.message: obj = pgpy.PGPMessage.from_blob(data) @@ -494,15 +479,15 @@ def armor(self, data:bytes, if not obj.is_public: raise sop.SOPInvalidDataType('not an OpenPGP certificate') elif label is sop.SOPArmorLabel.sig: - obj = pgpy.PGPSignature.from_blob(data) - elif label is sop.SOPArmorLabel.auto: # try to guess + obj = pgpy.PGPSignatures.from_blob(data) + elif label is sop.SOPArmorLabel.auto: # try to guess try: obj, _ = pgpy.PGPKey.from_blob(data) len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: try: - obj = pgpy.PGPSignature.from_blob(data) - len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset + obj = pgpy.PGPSignatures.from_blob(data) + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: try: obj = pgpy.PGPMessage.from_blob(data) @@ -524,7 +509,7 @@ def dearmor(self, data:bytes, **kwargs:Namespace) -> bytes: except: pass try: - sig:pgpy.PGPSignature = pgpy.PGPSignature.from_blob(data) + sig: pgpy.PGPSignatures = pgpy.PGPSignatures.from_blob(data) return bytes(sig) except: pass @@ -545,7 +530,7 @@ def inline_detach(self, body:Union[bytes,bytearray,str] = msg.message if isinstance(body, str): body = body.encode('utf-8') - return (bytes(body), self._maybe_armor(armor, _multisig.from_signatures(msg.signatures))) + return (bytes(body), self._maybe_armor(armor, pgpy.PGPSignatures(msg.signatures))) def inline_sign(self, data:bytes, From f087339495ae43cc15e6a04acd35bb873170cd3e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 17:05:01 -0400 Subject: [PATCH 066/287] sopgpy: clean up type annotations --- pgpy/sopgpy.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 751e5054..39d064e0 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -75,7 +75,10 @@ def _maybe_armor(self, armor:bool, data:pgpy.types.Armorable) -> bytes: if (armor): return str(data).encode('ascii') else: - return bytes(data) + if isinstance(data, pgpy.types.PGPObject): + return bytes(data) + else: + raise TypeError(f"got an object of type {type(data)} which is not a PGPObject") def _get_pgp_signatures(self, data: bytes) -> Optional[pgpy.PGPSignatures]: sigs: Optional[pgpy.PGPSignatures] = None @@ -232,7 +235,7 @@ def sign(self, if len(hashalgs) != 1: micalg = '' else: - micalg = f'pgp-{hashalgs.pop().lower()}' + micalg = f'pgp-{hashalgs.pop().name.lower()}' return (self._maybe_armor(armor, pgpy.PGPSignatures(signatures)), micalg) @@ -340,25 +343,27 @@ def encrypt(self, def _check_sigs(self, - certs:MutableMapping[str,pgpy.PGPKey], - msg:pgpy.PGPMessage, - sigs:Optional[pgpy.PGPSignatures]=None, - start:Optional[datetime]=None, - end:Optional[datetime]=None) -> List[sop.SOPSigResult]: - results:List[sop.SOPSigResult] = [] + certs: MutableMapping[str, pgpy.PGPKey], + msg: Union[pgpy.PGPMessage, bytes], + sigs: Optional[pgpy.PGPSignatures] = None, + start: Optional[datetime] = None, + end: Optional[datetime] = None) -> List[sop.SOPSigResult]: + results: List[sop.SOPSigResult] = [] if sigs is not None: for sig in sigs: for signer, cert in certs.items(): try: - verif:pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) - goodsig:pgpy.types.sigsubj + verif: pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) + goodsig: pgpy.types.SignatureVerification._sigsubj for goodsig in verif.good_signatures: sigtime = goodsig.signature.created # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects if sigtime.tzinfo is None: sigtime = sigtime.replace(tzinfo=timezone.utc) - if ('issues' in goodsig._fields and goodsig.issues == 0) or ('verified' in goodsig._fields and goodsig.verified): + # PGPy before 0.6.0 included a "verified" boolean in sigsubj: + if ('issues' in goodsig._fields and goodsig.issues == 0) or \ + ('verified' in goodsig._fields and goodsig.verified): # type: ignore[attr-defined] if start is None or sigtime >= start: if end is None or sigtime <= end: results += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, @@ -472,11 +477,11 @@ def armor(self, data:bytes, obj = pgpy.PGPMessage.from_blob(data) elif label is sop.SOPArmorLabel.key: obj, _ = pgpy.PGPKey.from_blob(data) - if obj.is_public or not obj.is_primary: + if not isinstance(obj, pgpy.PGPKey) or obj.is_public or not obj.is_primary: raise sop.SOPInvalidDataType('not an OpenPGP secret key') elif label is sop.SOPArmorLabel.cert: obj, _ = pgpy.PGPKey.from_blob(data) - if not obj.is_public: + if not isinstance(obj, pgpy.PGPKey) or not obj.is_public: raise sop.SOPInvalidDataType('not an OpenPGP certificate') elif label is sop.SOPArmorLabel.sig: obj = pgpy.PGPSignatures.from_blob(data) From 37a741b2232be567e27f523dccbada59e0aa600b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 17:15:36 -0400 Subject: [PATCH 067/287] sopgpy: use sop 0.5.1 (support profiles) --- README.rst | 2 +- pgpy/sopgpy.py | 34 ++++++++++++++++++++++------------ setup.cfg | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 1d154f88..aec81ae0 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ Requirements To use `sopgpy` you'll also need: -- `sop `_ +- `sop `_ >= 0.5.1 License ------- diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 39d064e0..b9b9fae0 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -58,6 +58,15 @@ def __init__(self) -> None: extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') + @property + def generate_key_profiles(self) -> List[sop.SOPProfile]: + return [sop.SOPProfile('draft-koch-eddsa-for-openpgp-00', 'EdDSA/ECDH with Curve25519 ')] + + @property + def encrypt_profiles(self) -> List[sop.SOPProfile]: + '''Override this to offer multiple encryption profiles''' + return [sop.SOPProfile('rfc4880', 'Algorithms from RFC 4880')] + # implemented ciphers that we are willing to use to encrypt, in # the order we prefer them: _cipherprefs:List[pgpy.constants.SymmetricKeyAlgorithm] = \ @@ -134,10 +143,10 @@ def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, err = f"by any of the {len(keypasswords)} passwords provided" raise sop.SOPKeyIsProtected(f"Key found at {keyhandle} could not be unlocked {err}.") - - def generate_key(self, armor:bool=True, uids:List[str]=[], - keypassword:Optional[bytes]=None, - **kwargs:Namespace) -> bytes: + def generate_key(self, armor: bool = True, uids: List[str] = [], + keypassword: Optional[bytes] = None, + profile: Optional[sop.SOPProfile] = None, + **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, pgpy.constants.EllipticCurveOID.Ed25519) @@ -260,14 +269,15 @@ def verify(self, def encrypt(self, - data:bytes, - literaltype:sop.SOPLiteralDataType=sop.SOPLiteralDataType.binary, - armor:bool=True, - passwords:MutableMapping[str,bytes]={}, - signers:MutableMapping[str,bytes]={}, - keypasswords:MutableMapping[str,bytes]={}, - recipients:MutableMapping[str,bytes]={}, - **kwargs:Namespace) -> bytes: + data: bytes, + literaltype: sop.SOPLiteralDataType = sop.SOPLiteralDataType.binary, + armor: bool = True, + passwords: MutableMapping[str, bytes] = {}, + signers: MutableMapping[str, bytes] = {}, + keypasswords: MutableMapping[str, bytes] = {}, + recipients: MutableMapping[str, bytes] = {}, + profile: Optional[sop.SOPProfile] = None, + **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) handle:str keys:MutableMapping[str,pgpy.PGPKey] = {} diff --git a/setup.cfg b/setup.cfg index 6742149e..02156dff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ packages = install_requires = cryptography>=3.3.2 pyasn1 - sop>=0.4.0 + sop>=0.5.1 python_requires = >=3.6 # doc_requires = From cbe16c5ebc5555ee511255b7a7abf740ab3a8963 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 17:24:00 -0400 Subject: [PATCH 068/287] sopgpy generate-key: Add "rfc4880" profile, for 3072-bit RSA keys --- pgpy/sopgpy.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index b9b9fae0..efcfa7c8 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -60,7 +60,8 @@ def __init__(self) -> None: @property def generate_key_profiles(self) -> List[sop.SOPProfile]: - return [sop.SOPProfile('draft-koch-eddsa-for-openpgp-00', 'EdDSA/ECDH with Curve25519 ')] + return [sop.SOPProfile('draft-koch-eddsa-for-openpgp-00', 'EdDSA/ECDH with Curve25519 '), + sop.SOPProfile('rfc4880', '3072-bit RSA'),] @property def encrypt_profiles(self) -> List[sop.SOPProfile]: @@ -148,8 +149,11 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], profile: Optional[sop.SOPProfile] = None, **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) - primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, - pgpy.constants.EllipticCurveOID.Ed25519) + if profile is not None and profile.name == 'rfc4880': + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 3072) + else: + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, + pgpy.constants.EllipticCurveOID.Ed25519) primaryflags: Set[int] = set() primaryflags.add(pgpy.constants.KeyFlags.Certify) primaryflags.add(pgpy.constants.KeyFlags.Sign) @@ -173,8 +177,11 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], if 'primary' in uidoptions: # only first User ID is Primary del uidoptions['primary'] - subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH, - pgpy.constants.EllipticCurveOID.Curve25519) + if profile is not None and profile.name == 'rfc4880': + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 3072) + else: + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH, + pgpy.constants.EllipticCurveOID.Curve25519) subflags: Set[int] = set() subflags.add(pgpy.constants.KeyFlags.EncryptCommunications) subflags.add(pgpy.constants.KeyFlags.EncryptStorage) From 0aa4d68ec79efeb2c5896069f0b3c3074bb6048c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 17:42:54 -0400 Subject: [PATCH 069/287] PEP-8 whitespace cleanup --- pgpy/sopgpy.py | 205 ++++++++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 105 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index efcfa7c8..094fee65 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -70,7 +70,7 @@ def encrypt_profiles(self) -> List[sop.SOPProfile]: # implemented ciphers that we are willing to use to encrypt, in # the order we prefer them: - _cipherprefs:List[pgpy.constants.SymmetricKeyAlgorithm] = \ + _cipherprefs: List[pgpy.constants.SymmetricKeyAlgorithm] = \ [pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.SymmetricKeyAlgorithm.AES192, pgpy.constants.SymmetricKeyAlgorithm.AES128, @@ -81,7 +81,7 @@ def encrypt_profiles(self) -> List[sop.SOPProfile]: pgpy.constants.SymmetricKeyAlgorithm.TripleDES, pgpy.constants.SymmetricKeyAlgorithm.Blowfish] - def _maybe_armor(self, armor:bool, data:pgpy.types.Armorable) -> bytes: + def _maybe_armor(self, armor: bool, data: pgpy.types.Armorable) -> bytes: if (armor): return str(data).encode('ascii') else: @@ -95,20 +95,20 @@ def _get_pgp_signatures(self, data: bytes) -> Optional[pgpy.PGPSignatures]: sigs = pgpy.PGPSignatures.from_blob(data) return sigs - def _get_certs(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.PGPKey]: - certs:Dict[str,pgpy.PGPKey] = {} + def _get_certs(self, vals: MutableMapping[str, bytes]) -> MutableMapping[str, pgpy.PGPKey]: + certs: Dict[str, pgpy.PGPKey] = {} for handle, data in vals.items(): - cert:pgpy.PGPKey + cert: pgpy.PGPKey cert, _ = pgpy.PGPKey.from_blob(data) if not cert.is_public: raise sop.SOPInvalidDataType('cert {handle} is not an OpenPGP certificate (maybe secret key?)') certs[handle] = cert return certs - def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.PGPKey]: - keys:Dict[str,pgpy.PGPKey] = {} + def _get_keys(self, vals: MutableMapping[str, bytes]) -> MutableMapping[str, pgpy.PGPKey]: + keys: Dict[str, pgpy.PGPKey] = {} for handle, data in vals.items(): - key:pgpy.PGPKey + key: pgpy.PGPKey key, _ = pgpy.PGPKey.from_blob(data) if key.is_public: raise sop.SOPInvalidDataType('cert {handle} is not an OpenPGP transferable secret key (maybe certificate?)') @@ -117,13 +117,13 @@ def _get_keys(self, vals:MutableMapping[str,bytes]) -> MutableMapping[str,pgpy.P # FIXME: consider making the return type a generic instead of this clunky Union: # https://docs.python.org/3/library/typing.html#generics - def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, - keypasswords:MutableMapping[str,bytes], - func:Callable[[pgpy.PGPKey], - Union[pgpy.PGPMessage,pgpy.PGPSignature]]) -> \ - Union[pgpy.PGPMessage,pgpy.PGPSignature]: + def _op_with_locked_key(self, seckey: pgpy.PGPKey, keyhandle: str, + keypasswords: MutableMapping[str, bytes], + func: Callable[[pgpy.PGPKey], + Union[pgpy.PGPMessage, pgpy.PGPSignature]]) -> \ + Union[pgpy.PGPMessage, pgpy.PGPSignature]: # try all passphrases in map: - for handle,pw in keypasswords.items(): + for handle, pw in keypasswords.items(): # FIXME: be cleverer about which password to try # when multiple passwords and keys are # present. see for example the discussion in: @@ -135,7 +135,7 @@ def _op_with_locked_key(self, seckey:pgpy.PGPKey, keyhandle:str, return func(seckey) except pgpy.errors.PGPDecryptionError: pass - err:str + err: str if len(keypasswords) == 0: err = "; no passwords provided" elif len(keypasswords) == 1: @@ -174,7 +174,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], for uid in uids: primary.add_uid(pgpy.PGPUID.new(uid), **uidoptions) - if 'primary' in uidoptions: # only first User ID is Primary + if 'primary' in uidoptions: # only first User ID is Primary del uidoptions['primary'] if profile is not None and profile.name == 'rfc4880': @@ -198,32 +198,30 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], pgpy.constants.HashAlgorithm.SHA512) return self._maybe_armor(armor, primary) - def extract_cert(self, - key:bytes=b'', - armor:bool=True, - **kwargs:Namespace) -> bytes: + key: bytes = b'', + armor: bool = True, + **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) seckey, _ = pgpy.PGPKey.from_blob(key) return self._maybe_armor(armor, seckey.pubkey) - def sign(self, - data:bytes=b'', - armor:bool=True, - sigtype:sop.SOPSigType=sop.SOPSigType.binary, - signers:MutableMapping[str, bytes]={}, - wantmicalg:bool=False, - keypasswords:MutableMapping[str,bytes]={}, - **kwargs:Namespace) -> Tuple[bytes, Optional[str]]: + data: bytes = b'', + armor: bool = True, + sigtype: sop.SOPSigType = sop.SOPSigType.binary, + signers: MutableMapping[str, bytes] = {}, + wantmicalg: bool = False, + keypasswords: MutableMapping[str, bytes] = {}, + **kwargs: Namespace) -> Tuple[bytes, Optional[str]]: self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") - seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) - msg:pgpy.PGPMessage + seckeys: MutableMapping[str, pgpy.PGPKey] = self._get_keys(signers) + msg: pgpy.PGPMessage if sigtype is sop.SOPSigType.text: try: - datastr:str = data.decode(encoding='utf-8') + datastr: str = data.decode(encoding='utf-8') except UnicodeDecodeError: raise sop.SOPNotUTF8Text('Message was not encoded UTF-8 text') msg = pgpy.PGPMessage.new(datastr, cleartext=True, format='u') @@ -231,10 +229,10 @@ def sign(self, msg = pgpy.PGPMessage.new(data, format='b') else: raise sop.SOPUnsupportedOption(f'unknown signature type {sigtype}') - signatures:List[pgpy.PGPSignature] = [] - hashalgs:Set[pgpy.constants.HashAlgorithm] = set() - for handle,seckey in seckeys.items(): - sig:pgpy.PGPSignature + signatures: List[pgpy.PGPSignature] = [] + hashalgs: Set[pgpy.constants.HashAlgorithm] = set() + for handle, seckey in seckeys.items(): + sig: pgpy.PGPSignature if seckey.is_protected: res = self._op_with_locked_key(seckey, handle, keypasswords, lambda key: key.sign(msg)) if isinstance(res, pgpy.PGPSignature): @@ -246,7 +244,7 @@ def sign(self, hashalgs.add(sig.hash_algorithm) signatures.append(sig) - micalg:Optional[str] = None + micalg: Optional[str] = None if wantmicalg: if len(hashalgs) != 1: micalg = '' @@ -255,14 +253,13 @@ def sign(self, return (self._maybe_armor(armor, pgpy.PGPSignatures(signatures)), micalg) - def verify(self, - data:bytes, - start:Optional[datetime]=None, - end:Optional[datetime]=None, - sig:bytes=b'', - signers:MutableMapping[str,bytes]={}, - **kwargs:Namespace) -> List[sop.SOPSigResult]: + data: bytes, + start: Optional[datetime] = None, + end: Optional[datetime] = None, + sig: bytes = b'', + signers: MutableMapping[str, bytes] = {}, + **kwargs: Namespace) -> List[sop.SOPSigResult]: self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') @@ -274,7 +271,6 @@ def verify(self, raise sop.SOPNoSignature("No good signature found") return ret - def encrypt(self, data: bytes, literaltype: sop.SOPLiteralDataType = sop.SOPLiteralDataType.binary, @@ -286,10 +282,10 @@ def encrypt(self, profile: Optional[sop.SOPProfile] = None, **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) - handle:str - keys:MutableMapping[str,pgpy.PGPKey] = {} - pws:MutableMapping[str,str] = {} - format_octet:str + handle: str + keys: MutableMapping[str, pgpy.PGPKey] = {} + pws: MutableMapping[str, str] = {} + format_octet: str if literaltype is sop.SOPLiteralDataType.text: format_octet = 'u' @@ -316,14 +312,13 @@ def encrypt(self, if not recipients and not passwords: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate or password to encrypt to') - certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(recipients) - + certs: MutableMapping[str, pgpy.PGPKey] = self._get_certs(recipients) - cipher:Optional[pgpy.constants.SymmetricKeyAlgorithm] = None + cipher: Optional[pgpy.constants.SymmetricKeyAlgorithm] = None ciphers = set(self._cipherprefs) for handle, cert in certs.items(): - keyciphers=set() + keyciphers = set() for uid in cert.userids: if uid.selfsig and uid.selfsig.cipherprefs: for cipher in uid.selfsig.cipherprefs: @@ -341,7 +336,7 @@ def encrypt(self, msg = pgpy.PGPMessage.new(data, format=format_octet, compression=pgpy.constants.CompressionAlgorithm.Uncompressed) for signer, key in keys.items(): if key.is_protected: - res:Union[pgpy.PGPMessage,pgpy.PGPSignature] + res: Union[pgpy.PGPMessage, pgpy.PGPSignature] res = self._op_with_locked_key(key, signer, keypasswords, lambda seckey: seckey.sign(msg)) if isinstance(res, pgpy.PGPSignature): sig = res @@ -358,7 +353,6 @@ def encrypt(self, del sessionkey return self._maybe_armor(armor, msg) - def _check_sigs(self, certs: MutableMapping[str, pgpy.PGPKey], msg: Union[pgpy.PGPMessage, bytes], @@ -390,18 +384,18 @@ def _check_sigs(self, return results def decrypt(self, - data:bytes, - wantsessionkey:bool=False, - sessionkeys:MutableMapping[str,sop.SOPSessionKey]={}, - passwords:MutableMapping[str,bytes]={}, - signers:MutableMapping[str,bytes]={}, - start:Optional[datetime]=None, - end:Optional[datetime]=None, - keypasswords:MutableMapping[str,bytes]={}, - secretkeys:MutableMapping[str,bytes]={}, - **kwargs:Namespace) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: + data: bytes, + wantsessionkey: bool = False, + sessionkeys: MutableMapping[str, sop.SOPSessionKey] = {}, + passwords: MutableMapping[str, bytes] = {}, + signers: MutableMapping[str, bytes] = {}, + start: Optional[datetime] = None, + end: Optional[datetime] = None, + keypasswords: MutableMapping[str, bytes] = {}, + secretkeys: MutableMapping[str, bytes] = {}, + **kwargs: Namespace) -> Tuple[bytes, List[sop.SOPSigResult], Optional[sop.SOPSessionKey]]: self.raise_on_unknown_options(**kwargs) - certs:MutableMapping[str,pgpy.PGPKey] = {} + certs: MutableMapping[str, pgpy.PGPKey] = {} # FIXME!!! if wantsessionkey: raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') @@ -413,14 +407,14 @@ def decrypt(self, if not secretkeys and not passwords and not sessionkeys: raise sop.SOPMissingRequiredArgument('needs something to decrypt with (at least an OpenPGP secret key, a session key, or a password)') - sigs:List[sop.SOPSigResult] = [] - seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(secretkeys) + sigs: List[sop.SOPSigResult] = [] + seckeys: MutableMapping[str, pgpy.PGPKey] = self._get_keys(secretkeys) - encmsg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) - msg:pgpy.PGPMessage - ret:Optional[bytes] = None - out:Union[str,bytes] - for handle,seckey in seckeys.items(): + encmsg: pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + msg: pgpy.PGPMessage + ret: Optional[bytes] = None + out: Union[str, bytes] + for handle, seckey in seckeys.items(): try: if seckey.is_protected: res = self._op_with_locked_key(seckey, handle, keypasswords, @@ -448,7 +442,7 @@ def decrypt(self, logging.warning(e) if ret is None: for p, password in passwords.items(): - attempts:List[Union[bytes,str]] = [ password ] + attempts: List[Union[bytes, str]] = [ password ] extratext = '' try: trimmed = password.decode(encoding='utf-8').strip().encode('utf-8') @@ -464,7 +458,7 @@ def decrypt(self, try: # note: PGPy 0.5.4 and earlier don't accept bytes here: # https://github.com/SecurityInnovation/PGPy/pull/388 - if isinstance (attempt, bytes) and \ + if isinstance(attempt, bytes) and \ self.pgpy_version <= packaging.version.Version('0.5.4'): attempt = attempt.decode(encoding='utf-8') msg = encmsg.decrypt(passphrase=attempt) @@ -484,9 +478,9 @@ def decrypt(self, raise sop.SOPCouldNotDecrypt(f'could not find anything capable of decryption') return (ret, sigs, None) - def armor(self, data:bytes, - label:sop.SOPArmorLabel=sop.SOPArmorLabel.auto, - **kwargs:Namespace) -> bytes: + def armor(self, data: bytes, + label: sop.SOPArmorLabel = sop.SOPArmorLabel.auto, + **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) obj: Union[None, pgpy.PGPMessage, pgpy.PGPKey, pgpy.PGPSignatures] = None try: @@ -505,7 +499,7 @@ def armor(self, data:bytes, elif label is sop.SOPArmorLabel.auto: # try to guess try: obj, _ = pgpy.PGPKey.from_blob(data) - len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: try: obj = pgpy.PGPSignatures.from_blob(data) @@ -513,19 +507,19 @@ def armor(self, data:bytes, except: try: obj = pgpy.PGPMessage.from_blob(data) - len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset + len(str(obj)) # try to get a string out of the supposed PGPKey, triggering an error if unset except: obj = pgpy.PGPMessage.new(data) else: raise sop.SOPInvalidDataType(f'unknown armor type {label}') - except (ValueError,TypeError) as e: + except (ValueError, TypeError) as e: raise sop.SOPInvalidDataType(f'{e}') return str(obj).encode('ascii') - def dearmor(self, data:bytes, **kwargs:Namespace) -> bytes: + def dearmor(self, data: bytes, **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) try: - key:pgpy.PGPKey + key: pgpy.PGPKey key, _ = pgpy.PGPKey.from_blob(data) return bytes(key) except: @@ -536,50 +530,51 @@ def dearmor(self, data:bytes, **kwargs:Namespace) -> bytes: except: pass try: - msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + msg: pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) return bytes(msg) except: pass raise sop.SOPInvalidDataType() def inline_detach(self, - clearsigned:bytes, - armor:bool=True, - **kwargs:Namespace) -> Tuple[bytes,bytes]: + clearsigned: bytes, + armor: bool = True, + **kwargs: Namespace) -> Tuple[bytes, bytes]: self.raise_on_unknown_options(**kwargs) - msg:pgpy.PGPMessage + msg: pgpy.PGPMessage msg = pgpy.PGPMessage.from_blob(clearsigned) - body:Union[bytes,bytearray,str] = msg.message + body: Union[bytes, bytearray, str] = msg.message if isinstance(body, str): body = body.encode('utf-8') return (bytes(body), self._maybe_armor(armor, pgpy.PGPSignatures(msg.signatures))) def inline_sign(self, - data:bytes, - armor:bool=True, - sigtype:sop.SOPInlineSigType=sop.SOPInlineSigType.binary, - signers:MutableMapping[str,bytes]={}, - keypasswords:MutableMapping[str,bytes]={}, - **kwargs:Namespace + data: bytes, + armor: bool = True, + sigtype: sop.SOPInlineSigType = sop.SOPInlineSigType.binary, + signers: MutableMapping[str, bytes] = {}, + keypasswords: MutableMapping[str, bytes] = {}, + **kwargs: Namespace ) -> bytes: self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument("Need at least one OpenPGP Secret Key file as an argument") - seckeys:MutableMapping[str,pgpy.PGPKey] = self._get_keys(signers) - msg:pgpy.PGPMessage + seckeys: MutableMapping[str, pgpy.PGPKey] = self._get_keys(signers) + msg: pgpy.PGPMessage if sigtype in [sop.SOPInlineSigType.text, sop.SOPInlineSigType.clearsigned]: try: - datastr:str = data.decode(encoding='utf-8') + datastr: str = data.decode(encoding='utf-8') except UnicodeDecodeError: raise sop.SOPNotUTF8Text('Message was not encoded UTF-8 text') - msg = pgpy.PGPMessage.new(datastr, cleartext=(sigtype == sop.SOPInlineSigType.clearsigned), format='u', compression=pgpy.constants.CompressionAlgorithm.Uncompressed) + msg = pgpy.PGPMessage.new(datastr, cleartext=(sigtype == sop.SOPInlineSigType.clearsigned), + format='u', compression=pgpy.constants.CompressionAlgorithm.Uncompressed) elif sigtype == sop.SOPInlineSigType.binary: msg = pgpy.PGPMessage.new(data, format='b', compression=pgpy.constants.CompressionAlgorithm.Uncompressed) else: raise sop.SOPUnsupportedOption(f'unknown signature type {sigtype}') - signatures:List[pgpy.PGPSignature] = [] - for handle,seckey in seckeys.items(): - sig:pgpy.PGPSignature + signatures: List[pgpy.PGPSignature] = [] + for handle, seckey in seckeys.items(): + sig: pgpy.PGPSignature if seckey.is_protected: res = self._op_with_locked_key(seckey, handle, keypasswords, lambda key: key.sign(msg)) if isinstance(res, pgpy.PGPSignature): @@ -608,13 +603,13 @@ def inline_verify(self, data: bytes, self.raise_on_unknown_options(**kwargs) if not signers: raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') - msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) - certs:MutableMapping[str,pgpy.PGPKey] = self._get_certs(signers) + msg: pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) + certs: MutableMapping[str, pgpy.PGPKey] = self._get_certs(signers) - sigresults:List[sop.SOPSigResult] = self._check_sigs(certs, msg, None, start, end) + sigresults: List[sop.SOPSigResult] = self._check_sigs(certs, msg, None, start, end) if not sigresults: raise sop.SOPNoSignature("No good signature found") - outmsg:Union[bytes,str] = msg.message + outmsg: Union[bytes, str] = msg.message if isinstance(outmsg, str): outmsg = outmsg.encode("utf-8") return (outmsg, sigresults) From a01c66c6f8726bf75294e7398d8c365e0aaa66f5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 17:35:28 -0400 Subject: [PATCH 070/287] Add PubKeyAlgorithm.Unknown --- pgpy/constants.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index 28a4561a..8fcc6933 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -247,6 +247,7 @@ def gen_key(self): class PubKeyAlgorithm(IntEnum): """Supported public key algorithms.""" + Unknown = -1 Invalid = 0x00 #: Signifies that a key is an RSA key. RSAEncryptOrSign = 0x01 @@ -264,6 +265,12 @@ class PubKeyAlgorithm(IntEnum): DiffieHellman = 0x15 # X9.42 EdDSA = 0x16 # https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04 + @classmethod + def _missing_(cls, val: object) -> 'PubKeyAlgorithm': + if not isinstance(val, int): + raise TypeError(f"cannot look up PubKeyAlgorithm by non-int {type(val)}") + return cls.Unknown + @property def can_gen(self): return self in {PubKeyAlgorithm.RSAEncryptOrSign, From d38c06938cce7699f644a6c89c8966aa353363ba Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 12 Jun 2023 17:21:24 -0400 Subject: [PATCH 071/287] Handle signatures that use an unknown public key algorithm --- pgpy/packet/packets.py | 26 +++++++++++++++++++------- tests/test_06_compatibility.py | 1 - tests/test_10_exceptions.py | 4 ++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index cd494137..93dbe70a 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -350,13 +350,17 @@ def sigtype_int(self, val): self._sigtype = SignatureType(val) @sdproperty - def pubalg(self): + def pubalg(self) -> PubKeyAlgorithm: return self._pubalg - @pubalg.register(int) - @pubalg.register(PubKeyAlgorithm) - def pubalg_int(self, val): - self._pubalg = PubKeyAlgorithm(val) + @pubalg.register + def pubalg_int(self, val: int) -> None: + if isinstance(val, PubKeyAlgorithm): + self._pubalg: PubKeyAlgorithm = val + else: + self._pubalg = PubKeyAlgorithm(val) + if self._pubalg is PubKeyAlgorithm.Unknown: + self._opaque_pubalg: int = val sigs = { PubKeyAlgorithm.RSAEncryptOrSign: RSASignature, @@ -407,7 +411,10 @@ def __bytearray__(self): _bytes = bytearray() _bytes += super(Signature, self).__bytearray__() _bytes += self.int_to_bytes(self.sigtype) - _bytes += self.int_to_bytes(self.pubalg) + if self.pubalg is PubKeyAlgorithm.Unknown: + _bytes.append(self._opaque_pubalg) + else: + _bytes.append(self.pubalg) _bytes += self.int_to_bytes(self.halg) _bytes += self.subpackets.__bytearray__() _bytes += self.hash2 @@ -432,7 +439,10 @@ def canonical_bytes(self): _body = bytearray() _body += self.int_to_bytes(self.header.version) _body += self.int_to_bytes(self.sigtype) - _body += self.int_to_bytes(self.pubalg) + if self.pubalg is PubKeyAlgorithm.Unknown: + _body.append(self._opaque_pubalg) + else: + _body.append(self.pubalg) _body += self.int_to_bytes(self.halg) _body += self.subpackets.__hashbytearray__() _body += self.int_to_bytes(0, minlen=2) # empty unhashed subpackets @@ -449,6 +459,8 @@ def __copy__(self): spkt.header = copy.copy(self.header) spkt._sigtype = self._sigtype spkt._pubalg = self._pubalg + if self._pubalg is PubKeyAlgorithm.Unknown: + spkt._opaque_pubalg = self._opaque_pubalg spkt._halg = self._halg spkt.subpackets = copy.copy(self.subpackets) diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index a2f1d429..b903c881 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -29,7 +29,6 @@ def test_bob_sig_from_multisig(self, sig:str)-> None: def test_cert_unknown_algo(self) -> None: k:PGPKey - pytest.xfail('cannot handle certificates containing certifications made using unknown pubkey algorithms') (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob_with_unknown_alg_certification.pgp') assert k.check_soundness() == SecurityIssues.OK diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 299f3456..0e6cf182 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -139,8 +139,8 @@ class WhatException(Exception): pass # unexpected signature type fuzz_pkt(3, 0x7f, PGPError) - # unexpected pubkey algorithm - fuzz_pkt(4, 0x64, PGPError) + # unexpected pubkey algorithm -- does not raise an exception during parsing + fuzz_pkt(4, 0x64, None) # unexpected hash algorithm - does not raise an exception during parsing fuzz_pkt(5, 0x64, None) From 0e298a950e91faf636957ff1bbe530292afbe4f9 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 12 Jun 2023 20:27:00 -0400 Subject: [PATCH 072/287] Handle subkeys with unknown public key algorithms cleanly --- pgpy/packet/packets.py | 29 +++++++++++++++++++++-------- tests/test_06_compatibility.py | 1 - 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 93dbe70a..258898eb 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -788,13 +788,17 @@ def created_bin(self, val): self.created = self.bytes_to_int(val) @sdproperty - def pkalg(self): + def pkalg(self) -> PubKeyAlgorithm: return self._pkalg - @pkalg.register(int) - @pkalg.register(PubKeyAlgorithm) - def pkalg_int(self, val): - self._pkalg = PubKeyAlgorithm(val) + @pkalg.register + def pkalg_int(self, val: int) -> None: + if isinstance(val, PubKeyAlgorithm): + self._pkalg: PubKeyAlgorithm = val + else: + self._pkalg = PubKeyAlgorithm(val) + if self._pkalg is PubKeyAlgorithm.Unknown: + self._opaque_pkalg: int = val _c = { # True means public @@ -850,7 +854,10 @@ def fingerprint(self): # c) timestamp of key creation (4 octets); fp.update(self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4)) # d) algorithm (1 octet): 17 = DSA (example); - fp.update(self.int_to_bytes(self.pkalg)) + if self.pkalg is PubKeyAlgorithm.Unknown: + fp.update(bytes([self._opaque_pkalg])) + else: + fp.update(self.int_to_bytes(self.pkalg)) # e) Algorithm-specific fields. fp.update(self.keymaterial.__bytearray__()[:plen]) @@ -867,7 +874,10 @@ def __bytearray__(self): _bytes = bytearray() _bytes += super(PubKeyV4, self).__bytearray__() _bytes += self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4) - _bytes += self.int_to_bytes(self.pkalg) + if self.pkalg is PubKeyAlgorithm.Unknown: + _bytes.append(self._opaque_pkalg) + else: + _bytes.append(self.pkalg) _bytes += self.keymaterial.__bytearray__() return _bytes @@ -875,7 +885,10 @@ def __copy__(self): pk = self.__class__() pk.header = copy.copy(self.header) pk.created = self.created - pk.pkalg = self.pkalg + if self.pkalg is PubKeyAlgorithm.Unknown: + pk.pkalg = self._opaque_pkalg + else: + pk.pkalg = self.pkalg pk.keymaterial = copy.copy(self.keymaterial) return pk diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index b903c881..b9d64c1f 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -34,7 +34,6 @@ def test_cert_unknown_algo(self) -> None: def test_cert_unknown_subkey_algo(self) -> None: k:PGPKey - pytest.xfail('cannot handle certificates containing subkeys with unknown pubkey algorithms') (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob_with_unknown_subkey_algorithm.pgp') assert k.check_soundness() == SecurityIssues.OK From 692eafd6f1dc6d607d3703f5d8890931441c7c54 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 11 Jun 2023 09:37:02 -0400 Subject: [PATCH 073/287] Add PGPSignatures object, representing bundled detached signatures a PGPMessage object can contain more than one signature. Detached signatures should also be able to handle having more than one signature. https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-09.html#name-detached-signatures says: > These detached signatures are simply one or more Signature packets > stored separately from the data for which they are a signature. A PGPSignatures object makes the most sense to represent such a thing. Closes: #197 --- docs/source/changelog.rst | 7 +++++ pgpy/__init__.py | 2 ++ pgpy/pgp.py | 55 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a0573cba..b39c2a82 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,13 @@ v0.7.0 (not yet released) +API additions +------------- + +PGPSignatures represents a detached signature, which can contain more +than a single signature. It is a simple sequence of individual +PGPSignature objects. + API changes ----------- diff --git a/pgpy/__init__.py b/pgpy/__init__.py index b4c30d13..472cf75f 100644 --- a/pgpy/__init__.py +++ b/pgpy/__init__.py @@ -5,6 +5,7 @@ from .pgp import PGPKeyring from .pgp import PGPMessage from .pgp import PGPSignature +from .pgp import PGPSignatures from .pgp import PGPUID __all__ = ['constants', @@ -13,4 +14,5 @@ 'PGPKeyring', 'PGPMessage', 'PGPSignature', + 'PGPSignatures', 'PGPUID', ] diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b45c4b5f..b77480a1 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -20,6 +20,8 @@ from datetime import datetime, timezone +from typing import Any, List + from cryptography.hazmat.primitives import hashes from .constants import CompressionAlgorithm @@ -79,6 +81,7 @@ from .types import SorteDeque __all__ = ['PGPSignature', + 'PGPSignatures', 'PGPUID', 'PGPMessage', 'PGPKey', @@ -592,6 +595,58 @@ def parse(self, packet): raise ValueError('Expected: Signature. Got: {:s}'.format(pkt.__class__.__name__)) +class PGPSignatures(collections_abc.Container, collections_abc.Iterable, collections_abc.Sized, Armorable, PGPObject): + '''OpenPGP detached signatures can often contain more than one signature in them.''' + + def __init__(self, signatures: List[PGPSignature] = []) -> None: + super().__init__() + self._sigs: List[PGPSignature] = signatures + + def __contains__(self, thing: Any) -> bool: + if not isinstance(thing, PGPSignature): + raise TypeError(f'PGPSignatures only contains a PGPSignature, not {type(thing)}') + return thing in self._sigs + + def __len__(self) -> int: + return len(self._sigs) + + def __iter__(self) -> collections_abc.Iterator[PGPSignature]: + for sig in self._sigs: + yield sig + + @property + def magic(self) -> str: + return "SIGNATURE" + + def __bytearray__(self) -> bytearray: + b = bytearray() + for sig in self._sigs: + b += sig.__bytearray__() + return b + + def parse(self, packet: bytes) -> None: + unarmored = self.ascii_unarmor(packet) + data = unarmored['body'] + + if unarmored['magic'] is not None and unarmored['magic'] != 'SIGNATURE': + raise ValueError(f"Expected: SIGNATURE. Got: {format(str(unarmored['magic']))}") + + if unarmored['headers'] is not None: + self.ascii_headers = unarmored['headers'] + + while data: + pkt = Packet(data) + if pkt.header.tag == PacketTag.Signature: + if isinstance(pkt, Opaque): + # skip unrecognized version. + pass + else: + sig = PGPSignature() + sig._signature = pkt + self._sigs.append(sig) + else: + raise ValueError(f"Expected: Signature. Got: {format(pkt.__class__.__name__)}") + class PGPUID(ParentRef): @property def __sig__(self): From d9e19412b4236925e5dc9ac199c0eec6ca0ea8e7 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 10:48:50 -0400 Subject: [PATCH 074/287] use PGPSignatures to handle multi-sig detached signatures --- tests/test_06_compatibility.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index b9d64c1f..4a3b99ba 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -3,7 +3,7 @@ """ import pytest -from pgpy import PGPKey, PGPMessage +from pgpy import PGPKey, PGPMessage, PGPSignatures from pgpy.constants import SecurityIssues import glob @@ -25,7 +25,12 @@ def test_bob_sig_from_multisig(self, sig:str)-> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob.pgp') msg = 'Hello World :)' - pytest.xfail(f'Cannot handle detached signature objects with more than one signature present (see https://github.com/SecurityInnovation/PGPy/issues/197)') + sigs = PGPSignatures.from_file(f'tests/testdata/compatibility/{sig}') + verif:Optional[pgpy.SignatureVerification] = None + for sig in sigs: + if sig.signer == k.fingerprint.keyid: + verif = k.verify(msg, sig) + assert verif is not None def test_cert_unknown_algo(self) -> None: k:PGPKey From eb9310a3f5785915905d0ef9618dad3592a5ac36 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 13:18:48 -0400 Subject: [PATCH 075/287] PGPMessage: ignore opaque packets when assembling --- pgpy/pgp.py | 4 ++++ tests/test_06_compatibility.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b77480a1..04929a7d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1132,6 +1132,10 @@ def __or__(self, other): self._signatures += other._signatures return self + if isinstance(other, Opaque): + # ignore opaque packets + return self + raise NotImplementedError(str(type(other))) def __copy__(self): diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index 4a3b99ba..03911100 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -53,7 +53,8 @@ def test_cert_unknown_curve(self, flavor:str) -> None: def test_unknown_message(self, msg:str)-> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob-key.pgp') - pytest.xfail('cannot handle unknowns in message formats') + if msg in {'pkesk_unknown_pkalg.msg', 'skesk_unknown_s2k_algo.msg'}: + pytest.xfail('cannot handle unknown algorithms in encrypted messages') msg:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') cleartext:PGPMessage = k.decrypt(msg) assert not cleartext.is_encrypted From 9d26b2e830338f78391681f10e52094dabfa6d43 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 14:23:18 -0400 Subject: [PATCH 076/287] PKESKv3: correct packet length consumption when pubkey algorithm is unknown --- pgpy/packet/packets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 258898eb..2a9a073d 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -281,7 +281,7 @@ def parse(self, packet): self.ct.parse(packet) else: # pragma: no cover - del packet[:(self.header.length - 18)] + del packet[:(self.header.length - 10)] class Signature(VersionedPacket): From cdb634727e2a2f686beac6cfdc4dc662459eaff6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 14:22:37 -0400 Subject: [PATCH 077/287] PKESKv3: handle unknown pubkey algorithms cleanly --- pgpy/packet/packets.py | 19 ++++++++++++++----- tests/test_06_compatibility.py | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 2a9a073d..ef9fba63 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -168,10 +168,14 @@ def encrypter_bin(self, val): def pkalg(self): return self._pkalg - @pkalg.register(int) - @pkalg.register(PubKeyAlgorithm) - def pkalg_int(self, val): - self._pkalg = PubKeyAlgorithm(val) + @pkalg.register + def pkalg_int(self, val: int) -> None: + if isinstance(val, PubKeyAlgorithm): + self._pkalg = val + else: + self._pkalg = PubKeyAlgorithm(val) + if self._pkalg is PubKeyAlgorithm.Invalid: + self._opaque_pkalg: int = val _c = {PubKeyAlgorithm.RSAEncryptOrSign: RSACipherText, PubKeyAlgorithm.RSAEncrypt: RSACipherText, @@ -192,7 +196,10 @@ def __bytearray__(self): _bytes = bytearray() _bytes += super(PKESessionKeyV3, self).__bytearray__() _bytes += binascii.unhexlify(self.encrypter.encode()) - _bytes += bytearray([self.pkalg]) + if self.pkalg == PubKeyAlgorithm.Invalid: + _bytes.append(self._opaque_pkalg) + else: + _bytes.append(self.pkalg) _bytes += self.ct.__bytearray__() if self.ct is not None else b'\x00' * (self.header.length - 10) return _bytes @@ -201,6 +208,8 @@ def __copy__(self): sk.header = copy.copy(self.header) sk._encrypter = self._encrypter sk.pkalg = self.pkalg + if self.pkalg == PubKeyAlgorithm.Invalid: + sk._opaque_pkalg = self._opaque_pkalg if self.ct is not None: sk.ct = copy.copy(self.ct) diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index 03911100..6e3eb983 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -53,8 +53,8 @@ def test_cert_unknown_curve(self, flavor:str) -> None: def test_unknown_message(self, msg:str)-> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob-key.pgp') - if msg in {'pkesk_unknown_pkalg.msg', 'skesk_unknown_s2k_algo.msg'}: - pytest.xfail('cannot handle unknown algorithms in encrypted messages') + if msg in {'skesk_unknown_s2k_algo.msg'}: + pytest.xfail('cannot handle unknown S2K algorithms in encrypted messages') msg:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') cleartext:PGPMessage = k.decrypt(msg) assert not cleartext.is_encrypted From 6d78d1baddb0ab1f604afacb42251efacd12ede8 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 15:07:51 -0400 Subject: [PATCH 078/287] String2KeyType: introduce "Unknown" option, and note opaque values when discovered --- pgpy/constants.py | 6 ++++++ pgpy/packet/fields.py | 24 +++++++++++++++++------- tests/test_06_compatibility.py | 2 -- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 8fcc6933..92cea473 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -528,12 +528,18 @@ class KeyServerPreferences(FlagEnum): class String2KeyType(IntEnum): + Unknown = -1 Simple = 0 Salted = 1 Reserved = 2 Iterated = 3 GNUExtension = 101 + @classmethod + def _missing_(cls, val: object) -> 'String2KeyType': + if not isinstance(val, int): + raise TypeError(f"cannot look up String2KeyType by non-int {type(val)}") + return cls.Unknown class S2KGNUExtension(IntEnum): NoSecret = 1 diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c94021c6..a3002291 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -839,13 +839,17 @@ def encalg_int(self, val): self._encalg = SymmetricKeyAlgorithm(val) @sdproperty - def specifier(self): + def specifier(self) -> String2KeyType: return self._specifier - @specifier.register(int) - @specifier.register(String2KeyType) - def specifier_int(self, val): - self._specifier = String2KeyType(val) + @specifier.register + def specifier_int(self, val: int) -> None: + if isinstance(val, String2KeyType): + self._specifier = val + else: + self._specifier = String2KeyType(val) + if self._specifier is String2KeyType.Unknown: + self._opaque_specifier: int = val @sdproperty def gnuext(self): @@ -903,7 +907,10 @@ def __bytearray__(self): _bytes.append(self.usage) if bool(self): _bytes.append(self.encalg) - _bytes.append(self.specifier) + if self.specifier is String2KeyType.Unknown: + _bytes.append(self._opaque_specifier) + else: + _bytes.append(self.specifier) if self.specifier == String2KeyType.GNUExtension: return self._experimental_bytearray(_bytes) if self.specifier >= String2KeyType.Simple: @@ -938,7 +945,10 @@ def __copy__(self): s2k = String2Key() s2k.usage = self.usage s2k.encalg = self.encalg - s2k.specifier = self.specifier + if bool(self) and self.specifier is String2KeyType.Unknown: + s2k.specifier = self._opaque_specifier + else: + s2k.specifier = self.specifier s2k.gnuext = self.gnuext s2k.iv = self.iv s2k.halg = self.halg diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index 6e3eb983..8a51fa3b 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -53,8 +53,6 @@ def test_cert_unknown_curve(self, flavor:str) -> None: def test_unknown_message(self, msg:str)-> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob-key.pgp') - if msg in {'skesk_unknown_s2k_algo.msg'}: - pytest.xfail('cannot handle unknown S2K algorithms in encrypted messages') msg:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') cleartext:PGPMessage = k.decrypt(msg) assert not cleartext.is_encrypted From feb450615abb1982ac8854b7eec935ccbf754e2e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 12 May 2023 13:36:50 -0400 Subject: [PATCH 079/287] Drop the use of pyasn1 for DSA/ECDSA signature translation The cryptography module supports encoding and decoding these signatures directly, so no need for pyasn1 or the custom ASN1 decoder to translate between the OpenPGP format and the RFC 3279 format. --- pgpy/packet/fields.py | 66 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 55 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index a3002291..dfffb93c 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -18,9 +18,6 @@ from pyasn1.codec.der import decoder from pyasn1.codec.der import encoder -from pyasn1.type.univ import Integer -from pyasn1.type.univ import Sequence -from pyasn1.type.namedtype import NamedTypes, NamedType from cryptography.exceptions import InvalidSignature @@ -35,6 +32,7 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives.asymmetric import utils from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash @@ -299,65 +297,23 @@ def from_signer(self, sig): class DSASignature(Signature): __mpis__ = ('r', 's') - def __sig__(self): - # return the signature data into an ASN.1 sequence of integers in DER format - seq = Sequence(componentType=NamedTypes(*[NamedType(n, Integer()) for n in self.__mpis__])) - for n in self.__mpis__: - seq.setComponentByName(n, getattr(self, n)) - - return encoder.encode(seq) - - def from_signer(self, sig): - ##TODO: just use pyasn1 for this - def _der_intf(_asn): - if _asn[0] != 0x02: # pragma: no cover - raise ValueError("Expected: Integer (0x02). Got: 0x{:02X}".format(_asn[0])) - del _asn[0] - - if _asn[0] & 0x80: # pragma: no cover - llen = _asn[0] & 0x7F - del _asn[0] - - flen = self.bytes_to_int(_asn[:llen]) - del _asn[:llen] - - else: - flen = _asn[0] & 0x7F - del _asn[0] + def __sig__(self) -> bytes: + # return the RFC 3279 encoding: + return utils.encode_dss_signature(self.r, self.s) - i = self.bytes_to_int(_asn[:flen]) - del _asn[:flen] - return i + def from_signer(self, sig: bytes) -> None: + # set up from the RFC 3279 encoding: + (r, s) = utils.decode_dss_signature(sig) + self.r = MPI(r) + self.s = MPI(s) - if isinstance(sig, bytes): - sig = bytearray(sig) - - # this is a very limited asn1 decoder - it is only intended to decode a DER encoded sequence of integers - if not sig[0] == 0x30: - raise NotImplementedError("Expected: Sequence (0x30). Got: 0x{:02X}".format(sig[0])) - del sig[0] - - # skip the sequence length field - if sig[0] & 0x80: # pragma: no cover - llen = sig[0] & 0x7F - del sig[:llen + 1] - - else: - del sig[0] - - self.r = MPI(_der_intf(sig)) - self.s = MPI(_der_intf(sig)) - - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.r = MPI(packet) self.s = MPI(packet) class ECDSASignature(DSASignature): - def from_signer(self, sig): - seq, _ = decoder.decode(sig) - self.r = MPI(seq[0]) - self.s = MPI(seq[1]) + pass class EdDSASignature(DSASignature): From 23ea1606d3f481fe387fb9bfa86d3cc6d149e054 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 13 May 2023 02:21:50 -0400 Subject: [PATCH 080/287] Overhaul EllipticCurveOID (reduce dependencies) OpenSSL 1.0.2 is ancient at this point -- Brainpool is part of the standard distribution. At any rate, we need 1.1.0 for X25519 and 1.1.1 for Ed25519. And python's cryptography module has supported Brainpool since version 2.2 (also ancient). Registering subclasses with the cryptography module is complicated across versions (see https://github.com/pyca/cryptography/pull/7234 which removed register_interface), but we don't need any of that functionality as long as we depend on non-ancient modules. At the same time, we don't need pyasn1 any longer if we just treat the OID as a bytestring label. As this also drops all the shenanigans around cryptography.utils.register_interface, we can also say it Closes: #402 --- README.rst | 2 - docs/source/changelog.rst | 11 ++ pgpy/_curves.py | 103 ------------------ pgpy/constants.py | 212 ++++++++++++++++++++++++-------------- pgpy/packet/fields.py | 87 +++++++--------- pgpy/pgp.py | 7 +- requirements.txt | 1 - setup.cfg | 1 - tests/test_05_actions.py | 5 +- tox.ini | 1 - 10 files changed, 190 insertions(+), 240 deletions(-) delete mode 100644 pgpy/_curves.py diff --git a/README.rst b/README.rst index d800c14f..7b3bc9b1 100644 --- a/README.rst +++ b/README.rst @@ -56,8 +56,6 @@ Requirements - `Cryptography `_ -- `pyasn1 `_ - - `six `_ License diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b39c2a82..2d3afce9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,14 @@ v0.7.0 (not yet released) +Dependency changes +------------------ + +pyasn1 is no longer needed + +Now depends transitively (via the cryptography module) on OpenSSL +1.1.1 or later for Brainpool, X25519, Ed25519. + API additions ------------- @@ -24,6 +32,9 @@ Armorable.is_utf8() instead, since OpenPGP assumes that all text is UTF-8. +EllipticCurveOID.Invalid was removed -- EllipticCurveOID only +enumerates supported curves now. + v0.6.0 ====== diff --git a/pgpy/_curves.py b/pgpy/_curves.py deleted file mode 100644 index 14f25284..00000000 --- a/pgpy/_curves.py +++ /dev/null @@ -1,103 +0,0 @@ -""" _curves.py -specify some additional curves that OpenSSL provides but cryptography doesn't explicitly expose -""" - -from cryptography import utils - -from cryptography.hazmat.primitives.asymmetric import ec - -from cryptography.hazmat.bindings.openssl.binding import Binding - -__all__ = tuple() - -# TODO: investigate defining additional curves using EC_GROUP_new_curve -# https://wiki.openssl.org/index.php/Elliptic_Curve_Cryptography#Defining_Curves - - -def _openssl_get_supported_curves(): - if hasattr(_openssl_get_supported_curves, '_curves'): - return _openssl_get_supported_curves._curves - - # use cryptography's cffi bindings to get an array of curve names - b = Binding() - cn = b.lib.EC_get_builtin_curves(b.ffi.NULL, 0) - cs = b.ffi.new('EC_builtin_curve[]', cn) - b.lib.EC_get_builtin_curves(cs, cn) - - # store the result so we don't have to do all of this every time - curves = { b.ffi.string(b.lib.OBJ_nid2sn(c.nid)).decode('utf-8') for c in cs } - # Ed25519 and X25519 are always present in cryptography>=2.6 - # The python cryptography lib provides a different interface for these curves, - # so they are handled differently in the ECDHPriv/Pub and EdDSAPriv/Pub classes - curves |= {'X25519', 'ed25519'} - _openssl_get_supported_curves._curves = curves - return curves - - -def use_legacy_cryptography_decorator(): - """ - The decorator utils.register_interface was removed in version 38.0.0. Keep using it - if the decorator exists, inherit from `ec.EllipticCurve` otherwise. - """ - return hasattr(utils, "register_interface") and callable(utils.register_interface) - - -if use_legacy_cryptography_decorator(): - @utils.register_interface(ec.EllipticCurve) - class BrainpoolP256R1(object): - name = 'brainpoolP256r1' - key_size = 256 - - - @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class BrainpoolP384R1(object): - name = 'brainpoolP384r1' - key_size = 384 - - - @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class BrainpoolP512R1(object): - name = 'brainpoolP512r1' - key_size = 512 - - - @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class X25519(object): - name = 'X25519' - key_size = 256 - - - @utils.register_interface(ec.EllipticCurve) # noqa: E303 - class Ed25519(object): - name = 'ed25519' - key_size = 256 -else: - class BrainpoolP256R1(ec.EllipticCurve): - name = 'brainpoolP256r1' - key_size = 256 - - - class BrainpoolP384R1(ec.EllipticCurve): # noqa: E303 - name = 'brainpoolP384r1' - key_size = 384 - - - class BrainpoolP512R1(ec.EllipticCurve): # noqa: E303 - name = 'brainpoolP512r1' - key_size = 512 - - - class X25519(ec.EllipticCurve): # noqa: E303 - name = 'X25519' - key_size = 256 - - - class Ed25519(ec.EllipticCurve): # noqa: E303 - name = 'ed25519' - key_size = 256 - - -# add these curves to the _CURVE_TYPES list -for curve in [BrainpoolP256R1, BrainpoolP384R1, BrainpoolP512R1, X25519, Ed25519]: - if curve.name not in ec._CURVE_TYPES and curve.name in _openssl_get_supported_curves(): - ec._CURVE_TYPES[curve.name] = curve diff --git a/pgpy/constants.py b/pgpy/constants.py index 92cea473..2d76e607 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -12,18 +12,18 @@ from enum import IntEnum from enum import IntFlag -from pyasn1.type.univ import ObjectIdentifier +from typing import NamedTuple, Optional, Type, Union from cryptography.hazmat.backends import openssl -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, x25519, ed25519 from cryptography.hazmat.primitives.ciphers import algorithms from .types import FlagEnum from .decorators import classproperty -from ._curves import BrainpoolP256R1, BrainpoolP384R1, BrainpoolP512R1, X25519, Ed25519 __all__ = [ 'Backend', + 'ECFields', 'EllipticCurveOID', 'ECPointFormat', 'PacketTag', @@ -55,83 +55,6 @@ class Backend(Enum): OpenSSL = openssl.backend -class EllipticCurveOID(Enum): - """OIDs for supported elliptic curves.""" - # these are specified as: - # id = (oid, curve) - Invalid = ('', ) - #: DJB's fast elliptic curve - Curve25519 = ('1.3.6.1.4.1.3029.1.5.1', X25519) - #: Twisted Edwards variant of Curve25519 - Ed25519 = ('1.3.6.1.4.1.11591.15.1', Ed25519) - #: NIST P-256, also known as SECG curve secp256r1 - NIST_P256 = ('1.2.840.10045.3.1.7', ec.SECP256R1) - #: NIST P-384, also known as SECG curve secp384r1 - NIST_P384 = ('1.3.132.0.34', ec.SECP384R1) - #: NIST P-521, also known as SECG curve secp521r1 - NIST_P521 = ('1.3.132.0.35', ec.SECP521R1) - #: Brainpool Standard Curve, 256-bit - #: - #: .. note:: - #: Requires OpenSSL >= 1.0.2 - Brainpool_P256 = ('1.3.36.3.3.2.8.1.1.7', BrainpoolP256R1) - #: Brainpool Standard Curve, 384-bit - #: - #: .. note:: - #: Requires OpenSSL >= 1.0.2 - Brainpool_P384 = ('1.3.36.3.3.2.8.1.1.11', BrainpoolP384R1) - #: Brainpool Standard Curve, 512-bit - #: - #: .. note:: - #: Requires OpenSSL >= 1.0.2 - Brainpool_P512 = ('1.3.36.3.3.2.8.1.1.13', BrainpoolP512R1) - #: SECG curve secp256k1 - SECP256K1 = ('1.3.132.0.10', ec.SECP256K1) - - def __new__(cls, oid, curve=None): - # preprocessing stage for enum members: - # - set enum_member.value to ObjectIdentifier(oid) - # - if curve is not None and curve.name is in ec._CURVE_TYPES, set enum_member.curve to curve - # - otherwise, set enum_member.curve to None - obj = object.__new__(cls) - obj._value_ = ObjectIdentifier(oid) - obj.curve = None - - if curve is not None and curve.name in ec._CURVE_TYPES: - obj.curve = curve - - return obj - - @property - def can_gen(self): - return self.curve is not None - - @property - def key_size(self): - if self.curve is not None: - return self.curve.key_size - - @property - def kdf_halg(self): - # return the hash algorithm to specify in the KDF fields when generating a key - algs = {256: HashAlgorithm.SHA256, - 384: HashAlgorithm.SHA384, - 512: HashAlgorithm.SHA512, - 521: HashAlgorithm.SHA512} - - return algs.get(self.key_size, None) - - @property - def kek_alg(self): - # return the AES algorithm to specify in the KDF fields when generating a key - algs = {256: SymmetricKeyAlgorithm.AES128, - 384: SymmetricKeyAlgorithm.AES192, - 512: SymmetricKeyAlgorithm.AES256, - 521: SymmetricKeyAlgorithm.AES256} - - return algs.get(self.key_size, None) - - class ECPointFormat(IntEnum): # https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-07#appendix-B Standard = 0x04 @@ -416,6 +339,135 @@ def is_considered_secure(self): return issues +class ECFields(NamedTuple): + name: str + OID: str + OID_der: bytes + key_size: int # in bits + kdf_halg: HashAlgorithm + kek_alg: SymmetricKeyAlgorithm + curve: Type + + def __repr__(self) -> str: + return f'' + + +class EllipticCurveOID(Enum): + """Supported elliptic curves.""" + + #: DJB's fast elliptic curve + Curve25519 = (x25519, '1.3.6.1.4.1.3029.1.5.1', + b'\x2b\x06\x01\x04\x01\x97\x55\x01\x05\x01', + 'X25519', 256) + #: Twisted Edwards variant of Curve25519 + Ed25519 = (ed25519, '1.3.6.1.4.1.11591.15.1', + b'\x2b\x06\x01\x04\x01\xda\x47\x0f\x01', + 'Ed25519', 256) + #: NIST P-256, also known as SECG curve secp256r1 + NIST_P256 = (ec.SECP256R1, '1.2.840.10045.3.1.7', + b'\x2a\x86\x48\xce\x3d\x03\x01\x07') + #: NIST P-384, also known as SECG curve secp384r1 + NIST_P384 = (ec.SECP384R1, '1.3.132.0.34', + b'\x2b\x81\x04\x00\x22') + #: NIST P-521, also known as SECG curve secp521r1 + NIST_P521 = (ec.SECP521R1, '1.3.132.0.35', + b'\x2b\x81\x04\x00\x23') + #: Brainpool Standard Curve, 256-bit + Brainpool_P256 = (ec.BrainpoolP256R1, '1.3.36.3.3.2.8.1.1.7', + b'\x2b\x24\x03\x03\x02\x08\x01\x01\x07') + #: Brainpool Standard Curve, 384-bit + Brainpool_P384 = (ec.BrainpoolP384R1, '1.3.36.3.3.2.8.1.1.11', + b'\x2b\x24\x03\x03\x02\x08\x01\x01\x0b') + #: Brainpool Standard Curve, 512-bit + Brainpool_P512 = (ec.BrainpoolP512R1, '1.3.36.3.3.2.8.1.1.13', + b'\x2b\x24\x03\x03\x02\x08\x01\x01\x0d') + #: SECG curve secp256k1 + SECP256K1 = (ec.SECP256K1, '1.3.132.0.10', + b'\x2b\x81\x04\x00\x0a') + + def __new__(cls, impl_cls: Type, oid: str, oid_der: bytes, name: Optional[str] = None, key_size_bits: Optional[int] = None) -> "EllipticCurveOID": + # preprocessing stage for enum members: + # - set enum_member.value to ObjectIdentifier(oid) + # - if curve is not None and curve.name is in ec._CURVE_TYPES, set enum_member.curve to curve + # - otherwise, set enum_member.curve to None + obj = object.__new__(cls) + if name is None: + newname = impl_cls.name + if not isinstance(newname, str): + raise TypeError(f"{impl_cls}.name is not string!") + name = newname + if key_size_bits is None: + newks = impl_cls.key_size + if not isinstance(newks, int): + raise TypeError(f"{impl_cls}.name is not string!") + key_size_bits = newks + + algs = {256: (HashAlgorithm.SHA256, SymmetricKeyAlgorithm.AES128), + 384: (HashAlgorithm.SHA384, SymmetricKeyAlgorithm.AES192), + 512: (HashAlgorithm.SHA512, SymmetricKeyAlgorithm.AES256), + 521: (HashAlgorithm.SHA512, SymmetricKeyAlgorithm.AES256)} + + (kdf_alg, kek_alg) = algs[key_size_bits] + + obj._value_ = ECFields(name, oid, oid_der, key_size_bits, kdf_alg, kek_alg, impl_cls) + + return obj + + @classmethod + def from_key_size(cls, key_size: int) -> Optional["EllipticCurveOID"]: + for c in EllipticCurveOID: + if c.value.key_size == key_size: + return c + warnings.warn(f"Cannot find any Elliptic curve of size: {key_size}") + return None + + @classmethod + def from_OID(cls, oid: bytes) -> Union["EllipticCurveOID", bytes]: + for c in EllipticCurveOID: + if c.value.OID_der == oid: + return c + warnings.warn(f"Unknown Elliptic curve OID: {oid!r}") + return oid + + @classmethod + def parse(cls, packet: bytearray) -> Union["EllipticCurveOID", bytes]: + oidlen = packet[0] + del packet[0] + ret = EllipticCurveOID.from_OID(bytes(packet[:oidlen])) + del packet[:oidlen] + return ret + + @property + def key_size(self) -> int: + return self.value.key_size + + @property + def oid(self) -> str: + return self.value.OID + + @property + def kdf_halg(self) -> HashAlgorithm: + return self.value.kdf_halg + + @property + def kek_alg(self) -> SymmetricKeyAlgorithm: + return self.value.kek_alg + + @property + def curve(self) -> Type: + return self.value.curve + + @property + def can_gen(self) -> bool: + return True + + def __bytes__(self) -> bytes: + return bytes([len(self.value.OID_der)]) + self.value.OID_der + + def __len__(self) -> int: + return len(self.value.OID_der) + 1 + + class RevocationReason(IntEnum): """Reasons explaining why a key or certificate was revoked.""" #: No reason was specified. This is the default reason. diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index dfffb93c..506cf404 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -16,8 +16,7 @@ except ImportError: collections_abc = collections -from pyasn1.codec.der import decoder -from pyasn1.codec.der import encoder +from typing import Union from cryptography.exceptions import InvalidSignature @@ -67,6 +66,7 @@ from ..symenc import _encrypt from ..types import Field +from ..types import Fingerprint __all__ = ['SubPackets', 'UserAttributeSubPackets', @@ -513,14 +513,14 @@ def __init__(self): self.oid = None def __len__(self): - return len(self.p) + len(encoder.encode(self.oid.value)) - 1 + return len(self.p) + len(self.oid) def __pubkey__(self): return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key(default_backend()) def __bytearray__(self): _b = bytearray() - _b += encoder.encode(self.oid.value)[1:] + _b += bytes(self.oid) _b += self.p.to_mpibytes() return _b @@ -537,14 +537,7 @@ def verify(self, subj, sigbytes, hash_alg): return True def parse(self, packet): - oidlen = packet[0] - del packet[0] - _oid = bytearray(b'\x06') - _oid.append(oidlen) - _oid += bytearray(packet[:oidlen]) - oid, _ = decoder.decode(bytes(_oid)) - self.oid = EllipticCurveOID(oid) - del packet[:oidlen] + self.oid = EllipticCurveOID.parse(packet) self.p = ECPoint(packet) if self.p.format != ECPointFormat.Standard: @@ -559,11 +552,11 @@ def __init__(self): self.oid = None def __len__(self): - return len(self.p) + len(encoder.encode(self.oid.value)) - 1 + return len(self.p) + len(self.oid) def __bytearray__(self): _b = bytearray() - _b += encoder.encode(self.oid.value)[1:] + _b += bytes(self.oid) _b += self.p.to_mpibytes() return _b @@ -588,14 +581,7 @@ def verify(self, subj, sigbytes, hash_alg): return True def parse(self, packet): - oidlen = packet[0] - del packet[0] - _oid = bytearray(b'\x06') - _oid.append(oidlen) - _oid += bytearray(packet[:oidlen]) - oid, _ = decoder.decode(bytes(_oid)) - self.oid = EllipticCurveOID(oid) - del packet[:oidlen] + self.oid = EllipticCurveOID.parse(packet) self.p = ECPoint(packet) if self.p.format != ECPointFormat.Native: @@ -611,7 +597,7 @@ def __init__(self): self.kdf = ECKDF() def __len__(self): - return len(self.p) + len(self.kdf) + len(encoder.encode(self.oid.value)) - 1 + return len(self.p) + len(self.kdf) + len(self.oid) def __pubkey__(self): if self.oid == EllipticCurveOID.Curve25519: @@ -619,9 +605,9 @@ def __pubkey__(self): else: return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key(default_backend()) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _b = bytearray() - _b += encoder.encode(self.oid.value)[1:] + _b += bytes(self.oid) _b += self.p.to_mpibytes() _b += self.kdf.__bytearray__() return _b @@ -661,15 +647,7 @@ def parse(self, packet): used to wrap the symmetric key used for the message encryption; see Section 8 for details """ - oidlen = packet[0] - del packet[0] - _oid = bytearray(b'\x06') - _oid.append(oidlen) - _oid += bytearray(packet[:oidlen]) - oid, _ = decoder.decode(bytes(_oid)) - - self.oid = EllipticCurveOID(oid) - del packet[:oidlen] + self.oid = EllipticCurveOID.parse(packet) self.p = ECPoint(packet) if self.oid == EllipticCurveOID.Curve25519: @@ -1087,12 +1065,12 @@ def parse(self, packet): self.encalg = packet[0] del packet[0] - def derive_key(self, s, curve, pkalg, fingerprint): + def derive_key(self, s: bytes, curve: EllipticCurveOID, pkalg: PubKeyAlgorithm, fingerprint: Fingerprint) -> bytes: # wrapper around the Concatenation KDF method provided by cryptography # assemble the additional data as defined in RFC 6637: # Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap || "Anonymous data = bytearray() - data += encoder.encode(curve.value)[1:] + data += bytes(curve) data.append(pkalg) data += b'\x03\x01' data.append(self.halg) @@ -1440,14 +1418,17 @@ def _compute_chksum(self): chs = sum(bytearray(self.s.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, oid): + def _generate(self, params: Union[int, EllipticCurveOID]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") - self.oid = EllipticCurveOID(oid) - - if not self.oid.can_gen: - raise ValueError("Curve not currently supported: {}".format(oid.name)) + if isinstance(params, int): + oid = EllipticCurveOID.from_key_size(params) + if oid is None: + raise ValueError("No supported Elliptic Curve of size {params}") + self.oid = oid + else: + self.oid = params pk = ec.generate_private_key(self.oid.curve(), default_backend()) pubn = pk.public_key().public_numbers() @@ -1489,14 +1470,20 @@ def _compute_chksum(self): chs = sum(bytearray(self.s.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, oid): + def _generate(self, params: Union[int, EllipticCurveOID]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") - self.oid = EllipticCurveOID(oid) + if isinstance(params, int): + oid = EllipticCurveOID.from_key_size(params) + if oid is None: + raise ValueError("No supported Elliptic Curve of size {params}") + self.oid = oid + else: + self.oid = params if self.oid != EllipticCurveOID.Ed25519: - raise ValueError("EdDSA only supported with {}".format(EllipticCurveOID.Ed25519)) + raise ValueError(f"EdDSA only supported with {EllipticCurveOID.Ed25519}, not {self.oid}") pk = ed25519.Ed25519PrivateKey.generate() x = pk.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) @@ -1563,8 +1550,14 @@ def __privkey__(self): else: return ECDSAPriv.__privkey__(self) - def _generate(self, oid): - _oid = EllipticCurveOID(oid) + def _generate(self, params: Union[int, EllipticCurveOID]) -> None: + if isinstance(params, int): + _oid = EllipticCurveOID.from_key_size(params) + if _oid is None: + raise ValueError("No supported Elliptic Curve of size {params}") + else: + _oid = params + if _oid == EllipticCurveOID.Curve25519: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") @@ -1580,7 +1573,7 @@ def _generate(self, oid): ), 'little')) self._compute_chksum() else: - ECDSAPriv._generate(self, oid) + ECDSAPriv._generate(self, _oid) self.kdf.halg = self.oid.kdf_halg self.kdf.encalg = self.oid.kek_alg diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 04929a7d..cdd10b3d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -20,11 +20,12 @@ from datetime import datetime, timezone -from typing import Any, List +from typing import Any, List, Optional, Union from cryptography.hazmat.primitives import hashes from .constants import CompressionAlgorithm +from .constants import EllipticCurveOID from .constants import Features from .constants import HashAlgorithm from .constants import ImageEncoding @@ -1516,12 +1517,14 @@ def key_algorithm(self): return self._key.pkalg @property - def key_size(self): + def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: """ The size pertaining to this key. ``int`` for non-EC key algorithms; :py:obj:`constants.EllipticCurveOID` for EC keys. .. versionadded:: 0.4.1 """ + if self._key is None: + return None if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA}: return self._key.keymaterial.oid # check if keymaterial is not an Opaque class containing a bytearray diff --git a/requirements.txt b/requirements.txt index e2a25384..6bc0dd57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ cryptography>=3.3.2 -pyasn1 diff --git a/setup.cfg b/setup.cfg index af26cfdd..c7691a75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,6 @@ packages = # TODO: fix support for cryptography >= 38.0.0 (https://github.com/SecurityInnovation/PGPy/issues/402) install_requires = cryptography>=3.3.2 - pyasn1 python_requires = >=3.6 # doc_requires = diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 8a801c9c..186bc2f3 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -20,7 +20,6 @@ from pgpy import PGPMessage from pgpy import PGPSignature from pgpy import PGPUID -from pgpy._curves import _openssl_get_supported_curves from pgpy.constants import CompressionAlgorithm from pgpy.constants import EllipticCurveOID from pgpy.constants import Features @@ -288,7 +287,7 @@ def test_add_subkey(self, pkspec, skspec): if not alg.can_gen: pytest.xfail('Key algorithm {} not yet supported'.format(alg.name)) - if isinstance(size, EllipticCurveOID) and ((not size.can_gen) or size.curve.name not in _openssl_get_supported_curves()): + if isinstance(size, EllipticCurveOID) and (not size.can_gen): pytest.xfail('Curve {} not yet supported'.format(size.curve.name)) key = self.keys[pkspec] @@ -511,7 +510,7 @@ def test_revoke_subkey(self, pkspec, skspec): if not alg.can_gen: pytest.xfail('Key algorithm {} not yet supported'.format(alg.name)) - if isinstance(size, EllipticCurveOID) and ((not size.can_gen) or size.curve.name not in _openssl_get_supported_curves()): + if isinstance(size, EllipticCurveOID) and (not size.can_gen): pytest.xfail('Curve {} not yet supported'.format(size.curve.name)) # revoke the subkey diff --git a/tox.ini b/tox.ini index 0956986e..937511c8 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,6 @@ passenv = deps = cryptography>=2.6 gpg==1.10.0 - pyasn1 six>=1.9.0 pytest pytest-cov From a1b1947513333f70b0fd420e5b57310ad0b0762a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 13 Jun 2023 17:59:03 -0400 Subject: [PATCH 081/287] HashAlgorithm: treat Unknown explicitly, like PubKeyAlgorithm --- pgpy/constants.py | 7 ++++++ pgpy/packet/packets.py | 49 ++++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 2d76e607..a983fec6 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -281,6 +281,7 @@ def decompress(self, data): class HashAlgorithm(IntEnum): """Supported hash algorithms.""" + Unknown = -1 Invalid = 0x00 MD5 = 0x01 SHA1 = 0x02 @@ -297,6 +298,12 @@ class HashAlgorithm(IntEnum): #SHA3_384 = 14 #SHA3_512 = 15 + @classmethod + def _missing_(cls, val: object) -> 'HashAlgorithm': + if not isinstance(val, int): + raise TypeError(f"cannot look up HashAlgorithm by non-int {type(val)}") + return cls.Unknown + def __init__(self, *args): super(self.__class__, self).__init__() self._tuned_count = 255 diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index ef9fba63..08c3e8d5 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -383,17 +383,17 @@ def pubalg_int(self, val: int) -> None: self.signature = sigs.get(self.pubalg, OpaqueSignature)() @sdproperty - def halg(self): + def halg(self) -> HashAlgorithm: return self._halg - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - try: - self._halg = HashAlgorithm(val) - - except ValueError: # pragma: no cover + @halg.register + def halg_int(self, val: int) -> None: + if isinstance(val, HashAlgorithm): self._halg = val + else: + self._halg = HashAlgorithm(val) + if self._halg is HashAlgorithm.Unknown: + self._opaque_halg = val @property def signature(self): @@ -424,7 +424,10 @@ def __bytearray__(self): _bytes.append(self._opaque_pubalg) else: _bytes.append(self.pubalg) - _bytes += self.int_to_bytes(self.halg) + if self.halg is HashAlgorithm.Unknown: + _bytes.append(self._opaque_halg) + else: + _bytes.append(self.halg) _bytes += self.subpackets.__bytearray__() _bytes += self.hash2 _bytes += self.signature.__bytearray__() @@ -452,7 +455,10 @@ def canonical_bytes(self): _body.append(self._opaque_pubalg) else: _body.append(self.pubalg) - _body += self.int_to_bytes(self.halg) + if self.halg is HashAlgorithm.Unknown: + _body.append(self._opaque_halg) + else: + _body.append(self.halg) _body += self.subpackets.__hashbytearray__() _body += self.int_to_bytes(0, minlen=2) # empty unhashed subpackets _body += self.hash2 @@ -471,6 +477,8 @@ def __copy__(self): if self._pubalg is PubKeyAlgorithm.Unknown: spkt._opaque_pubalg = self._opaque_pubalg spkt._halg = self._halg + if self._halg is HashAlgorithm.Unknown: + spkt._opaque_halg = self._opaque_halg spkt.subpackets = copy.copy(self.subpackets) spkt.hash2 = copy.copy(self.hash2) @@ -699,17 +707,17 @@ def pubalg_int(self, val): self.signature = DSASignature() @sdproperty - def halg(self): + def halg(self) -> HashAlgorithm: return self._halg - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - try: - self._halg = HashAlgorithm(val) - - except ValueError: # pragma: no cover + @halg.register + def halg_int(self, val: int) -> None: + if isinstance(val, HashAlgorithm): self._halg = val + else: + self._halg = HashAlgorithm(val) + if self._halg is HashAlgorithm.Unknown: + self._opaque_halg: int = val @sdproperty def signer(self): @@ -736,7 +744,10 @@ def __bytearray__(self): _bytes = bytearray() _bytes += super(OnePassSignatureV3, self).__bytearray__() _bytes += bytearray([self.sigtype]) - _bytes += bytearray([self.halg]) + if self.halg is HashAlgorithm.Unknown: + _bytes.append(self._opaque_halg) + else: + _bytes.append(self.halg) _bytes += bytearray([self.pubalg]) _bytes += binascii.unhexlify(self.signer.encode("latin-1")) _bytes += bytearray([int(self.nested)]) From 522df3ccc82a2618eb27cc38913af1aeca426c22 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 14 Jun 2023 14:38:04 -0400 Subject: [PATCH 082/287] When a curve is unknown, treat the EC point as an opaque MPI --- pgpy/packet/fields.py | 34 ++++++++++++++++++++++------------ tests/test_06_compatibility.py | 1 - 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 506cf404..70d44b98 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -539,9 +539,12 @@ def verify(self, subj, sigbytes, hash_alg): def parse(self, packet): self.oid = EllipticCurveOID.parse(packet) - self.p = ECPoint(packet) - if self.p.format != ECPointFormat.Standard: - raise PGPIncompatibleECPointFormatError("Only Standard format is valid for ECDSA") + if isinstance(self.oid, EllipticCurveOID): + self.p = ECPoint(packet) + if self.p.format != ECPointFormat.Standard: + raise PGPIncompatibleECPointFormatError("Only Standard format is valid for ECDSA") + else: + self.p = MPI(packet) class EdDSAPub(PubKey): @@ -583,9 +586,12 @@ def verify(self, subj, sigbytes, hash_alg): def parse(self, packet): self.oid = EllipticCurveOID.parse(packet) - self.p = ECPoint(packet) - if self.p.format != ECPointFormat.Native: - raise PGPIncompatibleECPointFormatError("Only Native format is valid for EdDSA") + if isinstance(self.oid, EllipticCurveOID): + self.p = ECPoint(packet) + if self.p.format != ECPointFormat.Native: + raise PGPIncompatibleECPointFormatError("Only Native format is valid for EdDSA") + else: + self.p = MPI(packet) class ECDHPub(PubKey): @@ -649,12 +655,16 @@ def parse(self, packet): """ self.oid = EllipticCurveOID.parse(packet) - self.p = ECPoint(packet) - if self.oid == EllipticCurveOID.Curve25519: - if self.p.format != ECPointFormat.Native: - raise PGPIncompatibleECPointFormatError("Only Native format is valid for Curve25519") - elif self.p.format != ECPointFormat.Standard: - raise PGPIncompatibleECPointFormatError("Only Standard format is valid for this curve") + if isinstance(self.oid, EllipticCurveOID): + self.p = ECPoint(packet) + if self.oid == EllipticCurveOID.Curve25519: + if self.p.format != ECPointFormat.Native: + raise PGPIncompatibleECPointFormatError("Only Native format is valid for Curve25519") + elif self.p.format != ECPointFormat.Standard: + raise PGPIncompatibleECPointFormatError("Only Standard format is valid for this curve") + else: + self.p = MPI(packet) + self.kdf.parse(packet) diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index 8a51fa3b..8ffec7a1 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -45,7 +45,6 @@ def test_cert_unknown_subkey_algo(self) -> None: @pytest.mark.parametrize('flavor', ['ecdsa', 'eddsa', 'ecdh']) def test_cert_unknown_curve(self, flavor:str) -> None: k:PGPKey - pytest.xfail(f'cannot handle certificates containing subkeys with unknown OIDs for {flavor}') (k, _) = PGPKey.from_file(f'tests/testdata/compatibility/bob_with_unknown_{flavor}_curve.pgp') assert k.check_soundness() == SecurityIssues.OK From cee0467985819a4f8e37e2c928e91bfed0cd20f5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 17:35:20 -0400 Subject: [PATCH 083/287] PEP-8: whitespace tuneup --- pgpy/constants.py | 1 + pgpy/pgp.py | 1 + pgpy/types.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index a983fec6..933c100b 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -600,6 +600,7 @@ def _missing_(cls, val: object) -> 'String2KeyType': raise TypeError(f"cannot look up String2KeyType by non-int {type(val)}") return cls.Unknown + class S2KGNUExtension(IntEnum): NoSecret = 1 Smartcard = 2 diff --git a/pgpy/pgp.py b/pgpy/pgp.py index cdd10b3d..457d43dd 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -648,6 +648,7 @@ def parse(self, packet: bytes) -> None: else: raise ValueError(f"Expected: Signature. Got: {format(pkt.__class__.__name__)}") + class PGPUID(ParentRef): @property def __sig__(self): diff --git a/pgpy/types.py b/pgpy/types.py index 49c89ed6..2270f7d5 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -748,6 +748,7 @@ def __repr__(self): class SorteDeque(collections.deque): """A deque subclass that tries to maintain sorted ordering using bisect""" + def insort(self, item): i = bisect.bisect_left(self, item) self.rotate(- i) From ba04c1119e845259911a5df3946058293ccf1009 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 22 Feb 2023 16:14:16 -0500 Subject: [PATCH 084/287] Avoid spurious CryptographyDeprecationWarning If someone actually wants to use IDEA, CAST5, or Blowfish, we want to see the CryptographyDeprecationWarning objects emitted by cryptography.hazmat.primitives.ciphers. But if we're just looking up a non-deprecated cipher we should just return it directly. This avoids the following kinds of warnings: pgpy/pgpy/constants.py:191: CryptographyDeprecationWarning: IDEA has been deprecated bs = {SymmetricKeyAlgorithm.IDEA: algorithms.IDEA, pgpy/pgpy/constants.py:193: CryptographyDeprecationWarning: CAST5 has been deprecated SymmetricKeyAlgorithm.CAST5: algorithms.CAST5, pgpy/pgpy/constants.py:194: CryptographyDeprecationWarning: Blowfish has been deprecated SymmetricKeyAlgorithm.Blowfish: algorithms.Blowfish, --- pgpy/constants.py | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 28a4561a..25707378 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -17,6 +17,7 @@ from cryptography.hazmat.backends import openssl from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm from .types import FlagEnum from .decorators import classproperty @@ -187,28 +188,33 @@ class SymmetricKeyAlgorithm(IntEnum): #: Camellia with 256-bit key Camellia256 = 0x0D - @property - def cipher(self): - bs = {SymmetricKeyAlgorithm.IDEA: algorithms.IDEA, - SymmetricKeyAlgorithm.TripleDES: algorithms.TripleDES, - SymmetricKeyAlgorithm.CAST5: algorithms.CAST5, - SymmetricKeyAlgorithm.Blowfish: algorithms.Blowfish, - SymmetricKeyAlgorithm.AES128: algorithms.AES, - SymmetricKeyAlgorithm.AES192: algorithms.AES, - SymmetricKeyAlgorithm.AES256: algorithms.AES, - SymmetricKeyAlgorithm.Twofish256: namedtuple('Twofish256', ['block_size'])(block_size=128), - SymmetricKeyAlgorithm.Camellia128: algorithms.Camellia, - SymmetricKeyAlgorithm.Camellia192: algorithms.Camellia, - SymmetricKeyAlgorithm.Camellia256: algorithms.Camellia} - - if self in bs: - return bs[self] - + def cipher(self, key: bytes) -> CipherAlgorithm: + if self is SymmetricKeyAlgorithm.IDEA: + return algorithms.IDEA(key) + elif self is SymmetricKeyAlgorithm.TripleDES: + return algorithms.TripleDES(key) + elif self is SymmetricKeyAlgorithm.CAST5: + return algorithms.CAST5(key) + elif self is SymmetricKeyAlgorithm.Blowfish: + return algorithms.Blowfish(key) + elif self in {SymmetricKeyAlgorithm.AES128, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES256}: + return algorithms.AES(key) + elif self in {SymmetricKeyAlgorithm.Camellia128, SymmetricKeyAlgorithm.Camellia192, SymmetricKeyAlgorithm.Camellia256}: + return algorithms.Camellia(key) raise NotImplementedError(repr(self)) @property - def is_supported(self): - return callable(self.cipher) + def is_supported(self) -> bool: + return self in {SymmetricKeyAlgorithm.IDEA, + SymmetricKeyAlgorithm.TripleDES, + SymmetricKeyAlgorithm.CAST5, + SymmetricKeyAlgorithm.Blowfish, + SymmetricKeyAlgorithm.AES128, + SymmetricKeyAlgorithm.AES192, + SymmetricKeyAlgorithm.AES256, + SymmetricKeyAlgorithm.Camellia128, + SymmetricKeyAlgorithm.Camellia192, + SymmetricKeyAlgorithm.Camellia256} @property def is_insecure(self): @@ -216,8 +222,14 @@ def is_insecure(self): return self in insecure_ciphers @property - def block_size(self): - return self.cipher.block_size + def block_size(self) -> int: + if self in {SymmetricKeyAlgorithm.IDEA, + SymmetricKeyAlgorithm.TripleDES, + SymmetricKeyAlgorithm.CAST5, + SymmetricKeyAlgorithm.Blowfish}: + return 64 + else: + return 128 @property def key_size(self): From fb885e672c2adc2616f403808a1e53b542f9d151 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 27 Mar 2023 19:03:10 +0900 Subject: [PATCH 085/287] Fix test: ensure that deprecation warnings don't interfere with test suite Once CAST5 is deprecated, a deprecation warning might arise in addition to the warnings about already-protected secret keys might not be the first warning. Instead, we have the test look through the warnings and validate the content of any matching warning. --- tests/test_10_exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 299f3456..45e89ea5 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -172,10 +172,10 @@ def test_protect_pubkey(self, rsa_pub, recwarn): def test_protect_protected_key(self, rsa_enc, recwarn): rsa_enc.protect('QwertyUiop', SymmetricKeyAlgorithm.CAST5, HashAlgorithm.SHA1) - w = recwarn.pop(UserWarning) - assert str(w.message) == "This key is already protected with a passphrase - " \ - "please unlock it before attempting to specify a new passphrase" - assert w.filename == __file__ + warning = "This key is already protected with a passphrase - please unlock it before attempting to specify a new passphrase" + msgs = list(filter(lambda x: str(x.message) == warning, recwarn)) + assert len(msgs) == 1 + assert msgs[0].filename == __file__ def test_unlock_wrong_passphrase(self, rsa_enc): with pytest.raises(PGPDecryptionError): From 221a1f15a42f3ef76ccafcddf66b7c4ade391bff Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 11 Feb 2023 12:17:00 -0500 Subject: [PATCH 086/287] drop use of imghdr imghdr is deprecated and will be removed in python 3.13 (see https://peps.python.org/pep-0594/#imghdr) The relevant code in imghdr is just: ``` def test_jpeg(h, f): """JPEG data with JFIF or Exif markers; and raw JPEG""" if h[6:10] in (b'JFIF', b'Exif'): return 'jpeg' elif h[:4] == b'\xff\xd8\xff\xdb': return 'jpeg' ``` So we transplant it directly --- pgpy/constants.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 28a4561a..983916d4 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -2,7 +2,6 @@ """ import bz2 import hashlib -import imghdr import os import zlib import warnings @@ -429,8 +428,7 @@ class ImageEncoding(IntEnum): @classmethod def encodingof(cls, imagebytes): - type = imghdr.what(None, h=imagebytes) - if type == 'jpeg': + if imagebytes[6:10] in (b'JFIF', b'Exif') or imagebytes[:4] == b'\xff\xd8\xff\xdb': return ImageEncoding.JPEG return ImageEncoding.Unknown # pragma: no cover From efa2fed7cb33d67680caac22fe7830f445c105b6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 17:43:33 -0400 Subject: [PATCH 087/287] PGPKey.from_file,from_blob: Drop failed filtering of keyid mapping This appears to have been introduced in 38a4f9f8b17fd40a2be942f58db2ecd8e67a32ee nearly 9 years ago, but it doesn't look like it ever worked. It's not clear to me why this index of keys by key ID is returned at all in these functions, but since September 2014 (v0.3.0) the index has always returned the key ID of the primary key anyway. There are a few things broken in this particular line: - the use of `~` if `fingerprint.keyid` doesn't work for some reason: that won't match anything. - and, None gets passed as the second object in the tuple, but the second object should be either True or False. Rather than try to salvage something that i don't understand in the first place, i figure it's better to drop it and acknowledge the status quo. --- pgpy/pgp.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..46a8f9bb 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2670,9 +2670,6 @@ def __call__(self, pkt): for pkt in group: # pragma: no cover orphaned.append(pkt) - # remove the reference to self from keys - [ keys.pop((getattr(self, 'fingerprint.keyid', '~'), None), t) for t in (True, False) ] - # return {'keys': keys, 'orphaned': orphaned} return keys From fd128d69927ee6901bee7de043faa7c6761f4401 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 16 Mar 2023 11:35:02 -0400 Subject: [PATCH 088/287] python cleanup: collections.abc was split out in python 3.3 --- pgpy/packet/fields.py | 7 ++----- pgpy/pgp.py | 11 ++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index b5afd325..f8a29ba0 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -10,10 +10,7 @@ import math import os -try: - import collections.abc as collections_abc -except ImportError: - collections_abc = collections +import collections.abc from typing import Union @@ -100,7 +97,7 @@ 'ECDHCipherText', ] -class SubPackets(collections_abc.MutableMapping, Field): +class SubPackets(collections.abc.MutableMapping, Field): _spmodule = signature def __init__(self): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 18de6171..538389f8 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -4,10 +4,7 @@ """ import binascii import collections -try: - import collections.abc as collections_abc -except ImportError: - collections_abc = collections +import collections.abc import contextlib import copy import functools @@ -596,7 +593,7 @@ def parse(self, packet): raise ValueError('Expected: Signature. Got: {:s}'.format(pkt.__class__.__name__)) -class PGPSignatures(collections_abc.Container, collections_abc.Iterable, collections_abc.Sized, Armorable, PGPObject): +class PGPSignatures(collections.abc.Container, collections.abc.Iterable, collections.abc.Sized, Armorable, PGPObject): '''OpenPGP detached signatures can often contain more than one signature in them.''' def __init__(self, signatures: List[PGPSignature] = []) -> None: @@ -611,7 +608,7 @@ def __contains__(self, thing: Any) -> bool: def __len__(self) -> int: return len(self._sigs) - def __iter__(self) -> collections_abc.Iterator[PGPSignature]: + def __iter__(self) -> collections.abc.Iterator[PGPSignature]: for sig in self._sigs: yield sig @@ -2730,7 +2727,7 @@ def __call__(self, pkt): return keys -class PGPKeyring(collections_abc.Container, collections_abc.Iterable, collections_abc.Sized): +class PGPKeyring(collections.abc.Container, collections.abc.Iterable, collections.abc.Sized): def __init__(self, *args): """ PGPKeyring objects represent in-memory keyrings that can contain any combination of supported private and public From 55c2ad49de4c4d4daec3d54c1a5c6bd8faff0c2b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 10 Feb 2023 12:52:23 -0500 Subject: [PATCH 089/287] drop __nonzero__ -- 3.x uses __bool__ (see https://stackoverflow.com/questions/8205558/defining-boolness-of-a-class-in-python for discussion) --- pgpy/packet/fields.py | 3 --- pgpy/packet/subpackets/signature.py | 6 ------ pgpy/types.py | 3 --- 3 files changed, 12 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index f8a29ba0..c4d89200 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -876,9 +876,6 @@ def __len__(self): def __bool__(self): return self.usage in [254, 255] - def __nonzero__(self): - return self.__bool__() - def __copy__(self): s2k = String2Key() s2k.usage = self.usage diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 32bd7a65..2859d83a 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -200,9 +200,6 @@ def __bytearray__(self): def __bool__(self): return self.bflag - def __nonzero__(self): - return self.__bool__() - def parse(self, packet): super().parse(packet) self.bflag = packet[:1] @@ -725,9 +722,6 @@ def __bytearray__(self): def __bool__(self): return self.primary - def __nonzero__(self): - return self.__bool__() - def parse(self, packet): super().parse(packet) self.primary = packet[:1] diff --git a/pgpy/types.py b/pgpy/types.py index 33bb4ad4..d33e67a4 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -644,9 +644,6 @@ def __bool__(self): for sigsub in self._subjects ) - def __nonzero__(self): - return self.__bool__() - def __and__(self, other): if not isinstance(other, SignatureVerification): raise TypeError(type(other)) From 0863a7b4723bb854c70c2125242f31caaa37452f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 2 May 2023 16:03:38 -0400 Subject: [PATCH 090/287] drop double export of fingerprint This was introduced (accidentally, i think) in 9ce9ad14ad553304b0f064a8b9e6a929b7527931 --- pgpy/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpy/types.py b/pgpy/types.py index d33e67a4..cc01ed7b 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -33,7 +33,6 @@ 'MetaDispatchable', 'Dispatchable', 'SignatureVerification', - 'Fingerprint', 'SorteDeque'] From 604772d7c9e9e04e1964b01321a3400846d1dd7a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 12:01:37 -0400 Subject: [PATCH 091/287] use functools.singledispatch instead of singledispatch functools.singledispatch was made available in Python 3.4 --- pgpy/decorators.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pgpy/decorators.py b/pgpy/decorators.py index f22f0bb4..d7bde115 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -4,11 +4,7 @@ import functools import logging -try: - from singledispatch import singledispatch - -except ImportError: # pragma: no cover - from functools import singledispatch +from functools import singledispatch from .errors import PGPError From bdcf97e7dc3dba83a6e89683bcd83d61acfb5733 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 12:51:18 -0400 Subject: [PATCH 092/287] add an explicit "return None" when selfsig has nothing --- pgpy/pgp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 538389f8..b361d49b 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -744,6 +744,7 @@ def selfsig(self): elif sig.signer: if self.parent.fingerprint == sig.signer: return sig + return None @property def signers(self): From 0fc8fd019d6a6a590937c95b27aa524afce50b1a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 15:07:06 +0900 Subject: [PATCH 093/287] Rename _encrypt and _decrypt to _cfb_encrypt and _cfb_decrypt This sets the stage to distinguish CFB from AEAD --- pgpy/packet/fields.py | 8 ++++---- pgpy/packet/packets.py | 16 ++++++++-------- pgpy/symenc.py | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c4d89200..ff62bb84 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -58,8 +58,8 @@ from ..errors import PGPError from ..errors import PGPIncompatibleECPointFormatError -from ..symenc import _decrypt -from ..symenc import _encrypt +from ..symenc import _cfb_decrypt +from ..symenc import _cfb_encrypt from ..types import Field from ..types import Fingerprint @@ -1172,7 +1172,7 @@ def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): pt += hashlib.new('sha1', pt).digest() # encrypt - self.encbytes = bytearray(_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) + self.encbytes = bytearray(_cfb_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) # delete pt and clear self del pt @@ -1197,7 +1197,7 @@ def decrypt_keyblob(self, passphrase): del passphrase # attempt to decrypt this key - pt = _decrypt(bytes(self.encbytes), bytes(sessionkey), self.s2k.encalg, bytes(self.s2k.iv)) + pt = _cfb_decrypt(bytes(self.encbytes), bytes(sessionkey), self.s2k.encalg, bytes(self.s2k.iv)) # check the hash to see if we decrypted successfully or not if self.s2k.usage == 254 and not pt[-20:] == hashlib.new('sha1', pt[:-20]).digest(): diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index b12370a8..c22bab20 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -45,8 +45,8 @@ from ..errors import PGPDecryptionError -from ..symenc import _decrypt -from ..symenc import _encrypt +from ..symenc import _cfb_decrypt +from ..symenc import _cfb_encrypt from ..types import Fingerprint @@ -619,7 +619,7 @@ def decrypt_sk(self, passphrase): return self.symalg, sk # otherwise, we now need to decrypt the encrypted session key - m = bytearray(_decrypt(bytes(self.ct), sk, self.symalg)) + m = bytearray(_cfb_decrypt(bytes(self.ct), sk, self.symalg)) del sk symalg = SymmetricKeyAlgorithm(m[0]) @@ -633,7 +633,7 @@ def encrypt_sk(self, passphrase, sk): esk = self.s2k.derive_key(passphrase) del passphrase - self.ct = _encrypt(self.int_to_bytes(self.symalg) + sk, esk, self.symalg) + self.ct = _cfb_encrypt(self.int_to_bytes(self.symalg) + sk, esk, self.symalg) # update header length and return sk self.update_hlen() @@ -1136,7 +1136,7 @@ def parse(self, packet): def decrypt(self, key, alg): # pragma: no cover block_size_bytes = alg.block_size // 8 - pt_prefix = _decrypt(bytes(self.ct[:block_size_bytes + 2]), bytes(key), alg) + pt_prefix = _cfb_decrypt(bytes(self.ct[:block_size_bytes + 2]), bytes(key), alg) # old Symmetrically Encrypted Data Packet required # to change iv after decrypting prefix @@ -1150,7 +1150,7 @@ def decrypt(self, key, alg): # pragma: no cover if not constant_time.bytes_eq(iv[-2:], ivl2): raise PGPDecryptionError("Decryption failed") - pt = _decrypt(bytes(self.ct[block_size_bytes + 2:]), bytes(key), alg, iv=iv_resync) + pt = _cfb_decrypt(bytes(self.ct[block_size_bytes + 2:]), bytes(key), alg, iv=iv_resync) return pt @@ -1607,12 +1607,12 @@ def encrypt(self, key, alg, data): mdc.update_hlen() data += mdc.__bytes__() - self.ct = _encrypt(data, key, alg) + self.ct = _cfb_encrypt(data, key, alg) self.update_hlen() def decrypt(self, key, alg): # iv, ivl2, pt = super(IntegrityProtectedSKEDataV1, self).decrypt(key, alg) - pt = _decrypt(bytes(self.ct), bytes(key), alg) + pt = _cfb_decrypt(bytes(self.ct), bytes(key), alg) # do the MDC checks _expected_mdcbytes = b'\xd3\x14' + hashlib.new('SHA1', pt[:-20]).digest() diff --git a/pgpy/symenc.py b/pgpy/symenc.py index 77537e48..b2a4e2ca 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -11,11 +11,11 @@ from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError -__all__ = ['_encrypt', - '_decrypt'] +__all__ = ['_cfb_encrypt', + '_cfb_decrypt'] -def _encrypt(pt, key, alg, iv=None): +def _cfb_encrypt(pt, key, alg, iv=None): if iv is None: iv = b'\x00' * (alg.block_size // 8) @@ -35,7 +35,7 @@ def _encrypt(pt, key, alg, iv=None): return bytearray(encryptor.update(pt) + encryptor.finalize()) -def _decrypt(ct, key, alg, iv=None): +def _cfb_decrypt(ct, key, alg, iv=None): if iv is None: """ Instead of using an IV, OpenPGP prefixes a string of length From 98718cd16b38b65a054791ca567d101270a21a1a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 13:18:10 +0900 Subject: [PATCH 094/287] PGPKey.encrypt: enumerate optional arguments and type signatures using a keyword hash that only pulls out one particular keyword is a recipe for API abuse. better to declare the function arguments explicitly. --- pgpy/pgp.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b361d49b..2a1b2f89 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2551,10 +2551,12 @@ def _filter_sigs(sigs): return sigv @KeyAction(KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage, is_public=True) - def encrypt(self, message, sessionkey=None, **prefs): - """encrypt(message[, sessionkey=None], **prefs) - - Encrypt a PGPMessage using this key. + def encrypt(self, + message:PGPMessage, + sessionkey:Optional[bytes]=None, + user:Optional[str]=None, + cipher:Optional[SymmetricKeyAlgorithm]=None) -> PGPMessage: + """Encrypt a PGPMessage using this key. :param message: The message to encrypt. :type message: :py:obj:`PGPMessage` @@ -2579,7 +2581,6 @@ def encrypt(self, message, sessionkey=None, **prefs): preference defaults and selection validation. :type user: ``str``, ``unicode`` """ - user = prefs.pop('user', None) uid = None if user is not None: uid = self.get_uid(user) @@ -2588,7 +2589,7 @@ def encrypt(self, message, sessionkey=None, **prefs): if uid is None and self.parent is not None: uid = next(iter(self.parent.userids), None) pref_cipher = next((c for c in uid.selfsig.cipherprefs if c.is_supported), SymmetricKeyAlgorithm.TripleDES) - cipher_algo = prefs.pop('cipher', pref_cipher) + cipher_algo = cipher if cipher is not None else pref_cipher if cipher_algo not in uid.selfsig.cipherprefs: warnings.warn("Selected symmetric algorithm not in key preferences", stacklevel=3) From cb2e0801bf877d136e50f7c778ee74e2d7ea27ab Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 12:01:52 +0900 Subject: [PATCH 095/287] Add a handful of type signatures --- pgpy/packet/subpackets/types.py | 6 ++++-- pgpy/packet/types.py | 4 +++- pgpy/pgp.py | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 8a55a8f2..c12f41ea 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -2,6 +2,8 @@ """ import abc +from typing import Optional + from ..types import VersionedHeader from ...decorators import sdproperty @@ -103,11 +105,11 @@ def parse(self, packet): # pragma: no cover class Signature(SubPacket): - __typeid__ = -1 + __typeid__:Optional[int] = -1 class UserAttribute(SubPacket): - __typeid__ = -1 + __typeid__:Optional[int] = -1 class Opaque(Signature, UserAttribute): diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index d8e7cd40..d3048b36 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -4,6 +4,8 @@ import abc import copy +from typing import Optional + from ..constants import PacketTag from ..decorators import sdproperty @@ -140,7 +142,7 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): - __typeid__ = -1 + __typeid__:Optional[int] = -1 __headercls__ = Header def __init__(self, _=None): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 2a1b2f89..dfa809c3 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -17,7 +17,7 @@ from datetime import datetime, timezone -from typing import Any, List, Optional, Union +from typing import Any, List, Optional, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -651,7 +651,7 @@ class PGPUID(ParentRef): def __sig__(self): return list(self._signatures) - def _splitstring(self): + def _splitstring(self) -> Tuple[str,str,str]: '''returns name, comment email from User ID string''' if not isinstance(self._uid, UserID): return "", "", "" @@ -674,12 +674,12 @@ def _splitstring(self): return (rfc2822['name'], rfc2822['comment'] or "", rfc2822['email'] or "") @property - def name(self): + def name(self) -> str: """If this is a User ID, the stored name. If this is not a User ID, this will be an empty string.""" return self._splitstring()[0] @property - def comment(self): + def comment(self) -> str: """ If this is a User ID, this will be the stored comment. If this is not a User ID, or there is no stored comment, this will be an empty string., @@ -687,7 +687,7 @@ def comment(self): return self._splitstring()[1] @property - def email(self): + def email(self) -> str: """ If this is a User ID, this will be the stored email address. If this is not a User ID, or there is no stored email address, this will be an empty string. @@ -695,7 +695,7 @@ def email(self): return self._splitstring()[2] @property - def userid(self): + def userid(self) -> Optional[str]: """ If this is a User ID, this will be the full UTF-8 string. If this is not a User ID, this will be ``None``. """ @@ -709,7 +709,7 @@ def image(self): return self._uid.image.image if isinstance(self._uid, UserAttribute) else None @property - def is_primary(self): + def is_primary(self) -> bool: """ If the most recent, valid self-signature specifies this as being primary, this will be True. Otherwise, False. """ @@ -718,21 +718,21 @@ def is_primary(self): return False @property - def is_uid(self): + def is_uid(self) -> bool: """ ``True`` if this is a User ID, otherwise False. """ return isinstance(self._uid, UserID) @property - def is_ua(self): + def is_ua(self) -> bool: """ ``True`` if this is a User Attribute, otherwise False. """ return isinstance(self._uid, UserAttribute) @property - def selfsig(self): + def selfsig(self) -> Optional[PGPSignature]: """ This will be the most recent, self-signature of this User ID or Attribute. If there isn't one, this will be ``None``. """ From 1f158e806ab5ca4066cb6d7a211d289597353109 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 15 Feb 2023 15:48:47 -0500 Subject: [PATCH 096/287] SKESK: clarify type signatures for encrypt_sk and decrypt_sk --- pgpy/packet/packets.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index c22bab20..22627fd9 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -10,6 +10,8 @@ from datetime import datetime, timezone +from typing import ByteString, Optional, Tuple, Union + from cryptography.hazmat.primitives import constant_time from cryptography.hazmat.primitives.asymmetric import padding @@ -513,12 +515,15 @@ class SKESessionKey(VersionedPacket): __typeid__ = 0x03 __ver__ = 0 + # FIXME: the type signature for this function is awkward because + # the symmetric algorithm used by the following SEIPDv2 packet is + # not encoded in the SKESKv6: @abc.abstractmethod - def decrypt_sk(self, passphrase): + def decrypt_sk(self, passphrase:Union[str,bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: raise NotImplementedError() @abc.abstractmethod - def encrypt_sk(self, passphrase, sk): + def encrypt_sk(self, passphrase:Union[str,bytes], sk:ByteString): raise NotImplementedError() @@ -609,7 +614,7 @@ def parse(self, packet): self.ct = packet[:ctend] del packet[:ctend] - def decrypt_sk(self, passphrase): + def decrypt_sk(self, passphrase:Union[str,bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: # derive the first session key from our passphrase sk = self.s2k.derive_key(passphrase) del passphrase @@ -627,12 +632,17 @@ def decrypt_sk(self, passphrase): return symalg, bytes(m) - def encrypt_sk(self, passphrase, sk): + + def encrypt_sk(self, passphrase:Union[str,bytes], sk:ByteString): # generate the salt and derive the key to encrypt sk with from it self.s2k.salt = bytearray(os.urandom(8)) esk = self.s2k.derive_key(passphrase) del passphrase + # note that by default, we assume that we're using same + # symmetric algorithm for the following SED or SEIPD packet. + # This is a reasonable simplification for generation, but it + # won't always be the same when parsing self.ct = _cfb_encrypt(self.int_to_bytes(self.symalg) + sk, esk, self.symalg) # update header length and return sk From 34ee5d093e6d3b6a152365101fd43b8817a77b6d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 2 May 2023 16:32:49 -0400 Subject: [PATCH 097/287] Add type signatures for PKESK.encrypt_sk and PKESK.decrypt_sk --- pgpy/packet/packets.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 22627fd9..19c60275 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -84,12 +84,14 @@ class PKESessionKey(VersionedPacket): __typeid__ = 0x01 __ver__ = 0 + # note that we don't have a good type signature for pk: it should be PrivKey, but from .fields, not the PrivKey in this file. @abc.abstractmethod - def decrypt_sk(self, pk): + def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: raise NotImplementedError() + # note that we don't have a good type signature for pk: it should be PubKey, but from .fields, not the PubKey in this file. @abc.abstractmethod - def encrypt_sk(self, pk, symalg, symkey): + def encrypt_sk(self, pk, symalg:Optional[SymmetricKeyAlgorithm], symkey:bytes) -> None: raise NotImplementedError() @@ -217,7 +219,7 @@ def __copy__(self): return sk - def decrypt_sk(self, pk): + def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: # pad up ct with null bytes if necessary ct = self.ct.me_mod_n.to_mpibytes()[2:] @@ -262,7 +264,7 @@ def decrypt_sk(self, pk): return (symalg, symkey) - def encrypt_sk(self, pk, symalg, symkey): + def encrypt_sk(self, pk, symalg:Optional[SymmetricKeyAlgorithm], symkey:bytes) -> None: m = bytearray(self.int_to_bytes(symalg) + symkey) m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) From b1c0e6efe720d7ba9666d2ce783d9397bd537020 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 14:02:49 -0400 Subject: [PATCH 098/287] PKESK: make dynamic arguments lists instead of tuples This keeps the types consistent for typechecking. Otherwise, we see errors complaining that decargs and encargs have different types depending on which codepath is followed. --- pgpy/packet/packets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 19c60275..38940d81 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -226,11 +226,11 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: ct = b'\x00' * ((pk.keymaterial.__privkey__().key_size // 8) - len(ct)) + ct decrypter = pk.keymaterial.__privkey__().decrypt - decargs = (ct, padding.PKCS1v15(),) + decargs = [ct, padding.PKCS1v15()] elif self.pkalg == PubKeyAlgorithm.ECDH: decrypter = pk - decargs = () + decargs = [] else: raise NotImplementedError(self.pkalg) @@ -270,11 +270,11 @@ def encrypt_sk(self, pk, symalg:Optional[SymmetricKeyAlgorithm], symkey:bytes) - if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: encrypter = pk.keymaterial.__pubkey__().encrypt - encargs = (bytes(m), padding.PKCS1v15(),) + encargs = [bytes(m), padding.PKCS1v15()] elif self.pkalg == PubKeyAlgorithm.ECDH: encrypter = pk - encargs = (bytes(m),) + encargs = [bytes(m)] else: raise NotImplementedError(self.pkalg) From ef038aa3e3328acc46fe782f3f1d300ef48ed89f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 17:05:33 -0400 Subject: [PATCH 099/287] PGPUID: make regex-matcher more comprehensible to typechecker --- pgpy/pgp.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index dfa809c3..94107419 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -657,7 +657,7 @@ def _splitstring(self) -> Tuple[str,str,str]: return "", "", "" if self._uid.uid == "": return "", "", "" - rfc2822 = re.match(r"""^ + output = re.match(r"""^ # name should always match something (?P.+?) # comment *optionally* matches text in parens following name @@ -669,7 +669,10 @@ def _splitstring(self) -> Tuple[str,str,str]: # but can immediately follow name if comment does not exist (\ <(?P.+)>)? $ - """, self._uid.uid, flags=re.VERBOSE).groupdict() + """, self._uid.uid, flags=re.VERBOSE) + if output is None: + raise ValueError("the standard User ID regex should have always matched something!") + rfc2822 = output.groupdict() return (rfc2822['name'], rfc2822['comment'] or "", rfc2822['email'] or "") From 65658288876aedb0192dade671baf7a56997589f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 17:08:51 -0400 Subject: [PATCH 100/287] Note that __ver__ of a packet is either int or None (unversioned) --- pgpy/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/types.py b/pgpy/types.py index cc01ed7b..20fd97be 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -572,7 +572,7 @@ def __headercls__(self): # pragma: no cover def __typeid__(self): # pragma: no cover return False - __ver__ = None + __ver__:Optional[int] = None class SignatureVerification: From 996c00e80f35bd384d05c943309dfa0d2d2e3bef Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 3 May 2023 17:19:04 -0400 Subject: [PATCH 101/287] packets.PubKey.fingerprint: property is of type Fingerprint --- pgpy/packet/packets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 38940d81..3937ee7d 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -79,7 +79,6 @@ 'IntegrityProtectedSKEDataV1', 'MDC'] - class PKESessionKey(VersionedPacket): __typeid__ = 0x01 __ver__ = 0 @@ -792,7 +791,7 @@ class PubKey(VersionedPacket, Primary, Public): __ver__ = 0 @abc.abstractproperty - def fingerprint(self): + def fingerprint(self) -> Fingerprint: """compute and return the fingerprint of the key""" From 2b906eb4828ec9ccd81602285bffa5d27b9db2d9 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 4 May 2023 16:10:37 -0400 Subject: [PATCH 102/287] pgpy/types.py: clean up mypy warnings --- pgpy/sopgpy.py | 2 +- pgpy/types.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 094fee65..7c04c137 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -365,7 +365,7 @@ def _check_sigs(self, for signer, cert in certs.items(): try: verif: pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) - goodsig: pgpy.types.SignatureVerification._sigsubj + goodsig: pgpy.types.SignatureVerification.sigsubj for goodsig in verif.good_signatures: sigtime = goodsig.signature.created # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: diff --git a/pgpy/types.py b/pgpy/types.py index 20fd97be..2142fd15 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -16,7 +16,7 @@ from enum import EnumMeta from enum import IntEnum -from typing import Union, Optional, Dict +from typing import Optional, Dict, Set, Tuple, Type, Union from .decorators import sdproperty @@ -441,7 +441,7 @@ class MetaDispatchable(abc.ABCMeta): MetaDispatchable is a metaclass for objects that subclass Dispatchable """ - _roots = set() + _roots:Set[Type] = set() """ _roots is a set of all currently registered RootClass class objects @@ -449,7 +449,7 @@ class MetaDispatchable(abc.ABCMeta): - it inherits (directly or indirectly) from Dispatchable - __typeid__ == -1 """ - _registry = {} + _registry:Dict[Union[Tuple[Type,int],Tuple[Type,int,int]],Type] = {} """ _registry is the Dispatchable class registry. It uses the following format: @@ -577,7 +577,7 @@ def __typeid__(self): # pragma: no cover class SignatureVerification: __slots__ = ("_subjects",) - _sigsubj = collections.namedtuple('sigsubj', ['issues', 'by', 'signature', 'subject']) + sigsubj = collections.namedtuple('sigsubj', ['issues', 'by', 'signature', 'subject']) @property def good_signatures(self): @@ -660,7 +660,7 @@ def add_sigsubj(self, signature, by, subject=None, issues=None): if issues is None: from .constants import SecurityIssues issues = SecurityIssues(0xFF) - self._subjects.append(self._sigsubj(issues, by, signature, subject)) + self._subjects.append(self.sigsubj(issues, by, signature, subject)) class FlagEnumMeta(EnumMeta): From 6c372b3182c7ba65d738dbc765f5b01f5f78e618 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 16:42:43 -0400 Subject: [PATCH 103/287] declare FlagList and ByteFlag types as None or some type it might be nicer to specify the allowed category of __flags__ in more details, but i do not know how to do that --- pgpy/packet/subpackets/signature.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 2859d83a..cbdbaa68 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -10,6 +10,8 @@ from datetime import timedelta from datetime import timezone +from typing import Optional,Type + from .types import EmbeddedSignatureHeader from .types import Signature @@ -89,7 +91,7 @@ def parse(self, packet): class FlagList(Signature): - __flags__ = None + __flags__:Optional[Type] = None @sdproperty def flags(self): @@ -132,7 +134,7 @@ def parse(self, packet): class ByteFlag(Signature): - __flags__ = None + __flags__:Optional[Type] = None @sdproperty def flags(self): From 0d1339d5527363348a17e49e66e881ef6a475831 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 16:48:07 -0400 Subject: [PATCH 104/287] Tell the typechecker that MPIs, PrivKey, and PubKey can all use variable-length Tuples --- pgpy/packet/fields.py | 6 +++--- pgpy/packet/types.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index ff62bb84..5c289d72 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -12,7 +12,7 @@ import collections.abc -from typing import Union +from typing import Tuple, Union from cryptography.exceptions import InvalidSignature @@ -327,7 +327,7 @@ def __sig__(self): class PubKey(MPIs): - __pubfields__ = () + __pubfields__:Tuple = () @property def __mpis__(self): @@ -1084,7 +1084,7 @@ def derive_key(self, s: bytes, curve: EllipticCurveOID, pkalg: PubKeyAlgorithm, class PrivKey(PubKey): - __privfields__ = () + __privfields__:Tuple = () @property def __mpis__(self): diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index d3048b36..52542a67 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -4,7 +4,7 @@ import abc import copy -from typing import Optional +from typing import Optional, Tuple from ..constants import PacketTag @@ -268,7 +268,7 @@ def __len__(self): class MPIs(Field): # this differs from MPI in that it's subclasses hold/parse several MPI fields # and, in the case of v4 private keys, also a String2Key specifier/information. - __mpis__ = () + __mpis__:Tuple = () def __len__(self): return sum(len(i) for i in self) From de8b582dfdf370c234fea1f3eb1988d0e021e9ad Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 16:50:33 -0400 Subject: [PATCH 105/287] Use the standard name for a namedtuple to satisfy the typechecker Without this, mypy complains: error: First argument to namedtuple() should be "_reason_for_revocation", not "ReasonForRevocation" [name-match] --- pgpy/pgp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 94107419..fd467e3a 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -87,7 +87,7 @@ class PGPSignature(Armorable, ParentRef, PGPObject): - _reason_for_revocation = collections.namedtuple('ReasonForRevocation', ['code', 'comment']) + ReasonForRevocation = collections.namedtuple('ReasonForRevocation', ['code', 'comment']) @property def __sig__(self): @@ -268,7 +268,7 @@ def revocation_key(self): def revocation_reason(self): if 'ReasonForRevocation' in self._signature.subpackets: subpacket = next(iter(self._signature.subpackets['ReasonForRevocation'])) - return self._reason_for_revocation(subpacket.code, subpacket.string) + return self.ReasonForRevocation(subpacket.code, subpacket.string) return None @property From ef046032391ad428dc5d3c6b7d8826b9604a0997 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 21:29:42 -0400 Subject: [PATCH 106/287] PGPSignature: document non-controversial type signatures --- pgpy/pgp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index fd467e3a..97c5179e 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -123,7 +123,7 @@ def embedded(self): return self.parent is not None @property - def expires_at(self): + def expires_at(self) -> Optional[datetime]: """ A :py:obj:`~datetime.datetime` of when this signature expires, if a signature expiration date is specified. Otherwise, ``None`` @@ -134,7 +134,7 @@ def expires_at(self): return None @property - def exportable(self): + def exportable(self) -> bool: """ ``False`` if this signature is marked as being not exportable. Otherwise, ``True``. """ @@ -166,7 +166,7 @@ def hashprefs(self): return [] @property - def hash_algorithm(self): + def hash_algorithm(self) -> HashAlgorithm: """ The :py:obj:`~constants.HashAlgorithm` used when computing this signature. """ @@ -250,7 +250,7 @@ def policy_uri(self): return '' @property - def revocable(self): + def revocable(self) -> bool: """ ``False`` if this signature is marked as being not revocable. Otherwise, ``True``. """ @@ -265,7 +265,7 @@ def revocation_key(self): return None @property - def revocation_reason(self): + def revocation_reason(self) -> Optional[ReasonForRevocation]: if 'ReasonForRevocation' in self._signature.subpackets: subpacket = next(iter(self._signature.subpackets['ReasonForRevocation'])) return self.ReasonForRevocation(subpacket.code, subpacket.string) From 2df15a044e9e72f69f41d65816a249dc84c1b870 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 13 May 2023 02:45:46 -0400 Subject: [PATCH 107/287] Add more simple type annotations --- pgpy/packet/fields.py | 6 +++--- pgpy/packet/types.py | 19 ++++++++++++++++--- pgpy/pgp.py | 20 +++++++++++--------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 5c289d72..83f9dca4 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1140,13 +1140,13 @@ def __privkey__(self): """return the requisite *PrivateKey class from the cryptography library""" @abc.abstractmethod - def _generate(self, key_size): + def _generate(self, key_size_or_oid:Union[int,EllipticCurveOID]) -> None: """Generate a new PrivKey""" def _compute_chksum(self): "Calculate the key checksum" - def publen(self): + def publen(self) -> int: return super().__len__() def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): @@ -1412,7 +1412,7 @@ def __privkey__(self): ecp = ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()) return ec.EllipticCurvePrivateNumbers(self.s, ecp).private_key(default_backend()) - def _compute_chksum(self): + def _compute_chksum(self) -> None: chs = sum(bytearray(self.s.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 52542a67..af4966af 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -14,6 +14,8 @@ from ..types import Field from ..types import Header as _Header +from ..constants import PubKeyAlgorithm + __all__ = ['Header', 'VersionedHeader', 'Packet', @@ -216,15 +218,26 @@ def parse(self, packet): # pragma: no cover # key marker classes for convenience class Key: - pass - + @abc.abstractproperty + def pkalg(self) -> PubKeyAlgorithm: + """The public key algorithm of the key""" class Public(Key): pass class Private(Key): - pass + @abc.abstractproperty + def pubkey(self) -> Public: + """compute and return the fingerprint of the key""" + + @abc.abstractproperty + def protected(self) -> bool: + """Whether the secret key material is protected by a password""" + + @abc.abstractproperty + def unlocked(self) -> bool: + """Is the secret key material is protected and also unlocked for use?""" class Primary(Key): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 97c5179e..d8170ea3 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1475,7 +1475,7 @@ def hashdata(self): return pub.__bytearray__()[len(pub.header):] @property - def is_expired(self): + def is_expired(self) -> bool: """``True`` if this key is expired, otherwise ``False``""" expires = self.expires_at if expires is not None: @@ -1484,27 +1484,27 @@ def is_expired(self): return False @property - def is_primary(self): + def is_primary(self) -> bool: """``True`` if this is a primary key; ``False`` if this is a subkey""" return isinstance(self._key, Primary) and not isinstance(self._key, Sub) @property - def is_protected(self): + def is_protected(self) -> bool: """``True`` if this is a private key that is protected with a passphrase, otherwise ``False``""" - if self.is_public: + if self._key is None or not isinstance(self._key, Private): return False return self._key.protected @property - def is_public(self): + def is_public(self) -> bool: """``True`` if this is a public key, otherwise ``False``""" return isinstance(self._key, Public) and not isinstance(self._key, Private) @property - def is_unlocked(self): + def is_unlocked(self) -> bool: """``False`` if this is a private key that is protected with a passphrase and has not yet been unlocked, otherwise ``True``""" - if self.is_public: + if self._key is None or not isinstance(self._key, Private): return True if not self.is_protected: @@ -1513,8 +1513,10 @@ def is_unlocked(self): return self._key.unlocked @property - def key_algorithm(self): + def key_algorithm(self) -> Optional[PubKeyAlgorithm]: """The :py:obj:`constants.PubKeyAlgorithm` pertaining to this key""" + if self._key is None: + return None return self._key.pkalg @property @@ -1535,7 +1537,7 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: return param.bit_length() @property - def magic(self): + def magic(self) -> str: return '{:s} KEY BLOCK'.format('PUBLIC' if (isinstance(self._key, Public) and not isinstance(self._key, Private)) else 'PRIVATE' if isinstance(self._key, Private) else '') From 77c89ea69047da1da7153159842fe56f2c00b79b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 01:40:47 -0400 Subject: [PATCH 108/287] _cfb_encrypt, _cfb_decrypt: add type signatures --- pgpy/symenc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pgpy/symenc.py b/pgpy/symenc.py index b2a4e2ca..f8189000 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -1,5 +1,8 @@ """ symenc.py """ + +from typing import Optional + from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend @@ -7,6 +10,8 @@ from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import modes +from .constants import SymmetricKeyAlgorithm + from .errors import PGPDecryptionError from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError @@ -15,7 +20,7 @@ '_cfb_decrypt'] -def _cfb_encrypt(pt, key, alg, iv=None): +def _cfb_encrypt(pt:bytes, key:bytes, alg:SymmetricKeyAlgorithm, iv:Optional[bytes]=None) -> bytearray: if iv is None: iv = b'\x00' * (alg.block_size // 8) @@ -35,7 +40,7 @@ def _cfb_encrypt(pt, key, alg, iv=None): return bytearray(encryptor.update(pt) + encryptor.finalize()) -def _cfb_decrypt(ct, key, alg, iv=None): +def _cfb_decrypt(ct:bytes, key:bytes, alg:SymmetricKeyAlgorithm, iv:Optional[bytes]=None) -> bytearray: if iv is None: """ Instead of using an IV, OpenPGP prefixes a string of length From ef0cb7973bda54b08012749da03c0ff079990430 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 13:48:16 -0400 Subject: [PATCH 109/287] SorteDeque: add type annotations --- pgpy/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index 2142fd15..b9fe5fcb 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -744,13 +744,13 @@ def __repr__(self): class SorteDeque(collections.deque): """A deque subclass that tries to maintain sorted ordering using bisect""" - def insort(self, item): + def insort(self, item) -> None: i = bisect.bisect_left(self, item) self.rotate(- i) self.appendleft(item) self.rotate(i) - def resort(self, item): # pragma: no cover + def resort(self, item) -> None: # pragma: no cover if item in self: # if item is already in self, see if it is still in sorted order. # if not, re-sort it by removing it and then inserting it into its sorted order @@ -763,7 +763,7 @@ def resort(self, item): # pragma: no cover # if item is not in self, just insert it in sorted order self.insort(item) - def check(self): # pragma: no cover + def check(self) -> None: # pragma: no cover """re-sort any items in self that are not sorted""" for unsorted in iter(self[i] for i in range(len(self) - 2) if not operator.le(self[i], self[i + 1])): self.resort(unsorted) From 7c63a6c0b85e6fa223f6cac355413820cc9ab61a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 12 May 2023 12:29:36 -0400 Subject: [PATCH 110/287] Add enumerated types for Signature and Attribute subpackets --- pgpy/constants.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index 4ad48e84..0195cf8c 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -32,6 +32,8 @@ 'CompressionAlgorithm', 'HashAlgorithm', 'RevocationReason', + 'SigSubpacketType', + 'AttributeType', 'ImageEncoding', 'SignatureType', 'KeyServerPreferences', @@ -499,6 +501,36 @@ class RevocationReason(IntEnum): #: User ID information is no longer valid. Only meaningful when revoking a certification of a user id. UserID = 0x20 +class SigSubpacketType(IntEnum): + CreationTime = 2 + SigExpirationTime = 3 + ExportableCertification = 4 + TrustSignature = 5 + RegularExpression = 6 + Revocable = 7 + KeyExpirationTime = 9 + PreferredSymmetricAlgorithms = 11 + RevocationKey = 12 + IssuerKeyID = 16 + NotationData = 20 + PreferredHashAlgorithms = 21 + PreferredCompressionAlgorithms = 22 + KeyServerPreferences = 23 + PreferredKeyServer=24 + PrimaryUserID = 25 + PolicyURI = 26 + KeyFlags = 27 + SignersUserID = 28 + ReasonForRevocation = 29 + Features = 30 + SignatureTarget = 31 + EmbeddedSignature = 32 + IssuerFingerprint = 33 + IntendedRecipientFingerprint = 35 + AttestedCertifications = 37 + +class AttributeType(IntEnum): + Image = 1 class ImageEncoding(IntEnum): Unknown = 0x00 From bbac9e8274ee3854d7084fcd12133c9c7efbbc35 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 17:45:13 -0400 Subject: [PATCH 111/287] PEP-8 cleanup (mostly whitespace fiddling) --- pgpy/constants.py | 11 +++++++---- pgpy/packet/fields.py | 6 +++--- pgpy/packet/packets.py | 18 +++++++++--------- pgpy/packet/subpackets/signature.py | 6 +++--- pgpy/packet/subpackets/types.py | 4 ++-- pgpy/packet/types.py | 5 +++-- pgpy/pgp.py | 10 +++++----- pgpy/symenc.py | 4 ++-- pgpy/types.py | 6 +++--- 9 files changed, 37 insertions(+), 33 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 0195cf8c..1c66de10 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -501,6 +501,7 @@ class RevocationReason(IntEnum): #: User ID information is no longer valid. Only meaningful when revoking a certification of a user id. UserID = 0x20 + class SigSubpacketType(IntEnum): CreationTime = 2 SigExpirationTime = 3 @@ -509,14 +510,14 @@ class SigSubpacketType(IntEnum): RegularExpression = 6 Revocable = 7 KeyExpirationTime = 9 - PreferredSymmetricAlgorithms = 11 + PreferredSymmetricAlgorithms = 11 RevocationKey = 12 IssuerKeyID = 16 - NotationData = 20 + NotationData = 20 PreferredHashAlgorithms = 21 PreferredCompressionAlgorithms = 22 KeyServerPreferences = 23 - PreferredKeyServer=24 + PreferredKeyServer = 24 PrimaryUserID = 25 PolicyURI = 26 KeyFlags = 27 @@ -525,13 +526,15 @@ class SigSubpacketType(IntEnum): Features = 30 SignatureTarget = 31 EmbeddedSignature = 32 - IssuerFingerprint = 33 + IssuerFingerprint = 33 IntendedRecipientFingerprint = 35 AttestedCertifications = 37 + class AttributeType(IntEnum): Image = 1 + class ImageEncoding(IntEnum): Unknown = 0x00 JPEG = 0x01 diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 83f9dca4..32bfbfba 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -327,7 +327,7 @@ def __sig__(self): class PubKey(MPIs): - __pubfields__:Tuple = () + __pubfields__: Tuple = () @property def __mpis__(self): @@ -1084,7 +1084,7 @@ def derive_key(self, s: bytes, curve: EllipticCurveOID, pkalg: PubKeyAlgorithm, class PrivKey(PubKey): - __privfields__:Tuple = () + __privfields__: Tuple = () @property def __mpis__(self): @@ -1140,7 +1140,7 @@ def __privkey__(self): """return the requisite *PrivateKey class from the cryptography library""" @abc.abstractmethod - def _generate(self, key_size_or_oid:Union[int,EllipticCurveOID]) -> None: + def _generate(self, key_size_or_oid: Union[int, EllipticCurveOID]) -> None: """Generate a new PrivKey""" def _compute_chksum(self): diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 3937ee7d..e6e73a98 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -79,18 +79,19 @@ 'IntegrityProtectedSKEDataV1', 'MDC'] + class PKESessionKey(VersionedPacket): __typeid__ = 0x01 __ver__ = 0 # note that we don't have a good type signature for pk: it should be PrivKey, but from .fields, not the PrivKey in this file. @abc.abstractmethod - def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: + def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() # note that we don't have a good type signature for pk: it should be PubKey, but from .fields, not the PubKey in this file. @abc.abstractmethod - def encrypt_sk(self, pk, symalg:Optional[SymmetricKeyAlgorithm], symkey:bytes) -> None: + def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: raise NotImplementedError() @@ -218,7 +219,7 @@ def __copy__(self): return sk - def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: + def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: # pad up ct with null bytes if necessary ct = self.ct.me_mod_n.to_mpibytes()[2:] @@ -263,7 +264,7 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: return (symalg, symkey) - def encrypt_sk(self, pk, symalg:Optional[SymmetricKeyAlgorithm], symkey:bytes) -> None: + def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: m = bytearray(self.int_to_bytes(symalg) + symkey) m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) @@ -520,11 +521,11 @@ class SKESessionKey(VersionedPacket): # the symmetric algorithm used by the following SEIPDv2 packet is # not encoded in the SKESKv6: @abc.abstractmethod - def decrypt_sk(self, passphrase:Union[str,bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: + def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() @abc.abstractmethod - def encrypt_sk(self, passphrase:Union[str,bytes], sk:ByteString): + def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString): raise NotImplementedError() @@ -615,7 +616,7 @@ def parse(self, packet): self.ct = packet[:ctend] del packet[:ctend] - def decrypt_sk(self, passphrase:Union[str,bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: + def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: # derive the first session key from our passphrase sk = self.s2k.derive_key(passphrase) del passphrase @@ -633,8 +634,7 @@ def decrypt_sk(self, passphrase:Union[str,bytes]) -> Tuple[Optional[SymmetricKey return symalg, bytes(m) - - def encrypt_sk(self, passphrase:Union[str,bytes], sk:ByteString): + def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString): # generate the salt and derive the key to encrypt sk with from it self.s2k.salt = bytearray(os.urandom(8)) esk = self.s2k.derive_key(passphrase) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index cbdbaa68..62e701ee 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -10,7 +10,7 @@ from datetime import timedelta from datetime import timezone -from typing import Optional,Type +from typing import Optional, Type from .types import EmbeddedSignatureHeader from .types import Signature @@ -91,7 +91,7 @@ def parse(self, packet): class FlagList(Signature): - __flags__:Optional[Type] = None + __flags__: Optional[Type] = None @sdproperty def flags(self): @@ -134,7 +134,7 @@ def parse(self, packet): class ByteFlag(Signature): - __flags__:Optional[Type] = None + __flags__: Optional[Type] = None @sdproperty def flags(self): diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index c12f41ea..c7802842 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -105,11 +105,11 @@ def parse(self, packet): # pragma: no cover class Signature(SubPacket): - __typeid__:Optional[int] = -1 + __typeid__: Optional[int] = -1 class UserAttribute(SubPacket): - __typeid__:Optional[int] = -1 + __typeid__: Optional[int] = -1 class Opaque(Signature, UserAttribute): diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index af4966af..fd629cab 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -144,7 +144,7 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): - __typeid__:Optional[int] = -1 + __typeid__: Optional[int] = -1 __headercls__ = Header def __init__(self, _=None): @@ -222,6 +222,7 @@ class Key: def pkalg(self) -> PubKeyAlgorithm: """The public key algorithm of the key""" + class Public(Key): pass @@ -281,7 +282,7 @@ def __len__(self): class MPIs(Field): # this differs from MPI in that it's subclasses hold/parse several MPI fields # and, in the case of v4 private keys, also a String2Key specifier/information. - __mpis__:Tuple = () + __mpis__: Tuple = () def __len__(self): return sum(len(i) for i in self) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index d8170ea3..8c981684 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -651,7 +651,7 @@ class PGPUID(ParentRef): def __sig__(self): return list(self._signatures) - def _splitstring(self) -> Tuple[str,str,str]: + def _splitstring(self) -> Tuple[str, str, str]: '''returns name, comment email from User ID string''' if not isinstance(self._uid, UserID): return "", "", "" @@ -2557,10 +2557,10 @@ def _filter_sigs(sigs): @KeyAction(KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage, is_public=True) def encrypt(self, - message:PGPMessage, - sessionkey:Optional[bytes]=None, - user:Optional[str]=None, - cipher:Optional[SymmetricKeyAlgorithm]=None) -> PGPMessage: + message: PGPMessage, + sessionkey: Optional[bytes] = None, + user: Optional[str] = None, + cipher: Optional[SymmetricKeyAlgorithm] = None) -> PGPMessage: """Encrypt a PGPMessage using this key. :param message: The message to encrypt. diff --git a/pgpy/symenc.py b/pgpy/symenc.py index f8189000..d9e8a856 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -20,7 +20,7 @@ '_cfb_decrypt'] -def _cfb_encrypt(pt:bytes, key:bytes, alg:SymmetricKeyAlgorithm, iv:Optional[bytes]=None) -> bytearray: +def _cfb_encrypt(pt: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional[bytes] = None) -> bytearray: if iv is None: iv = b'\x00' * (alg.block_size // 8) @@ -40,7 +40,7 @@ def _cfb_encrypt(pt:bytes, key:bytes, alg:SymmetricKeyAlgorithm, iv:Optional[byt return bytearray(encryptor.update(pt) + encryptor.finalize()) -def _cfb_decrypt(ct:bytes, key:bytes, alg:SymmetricKeyAlgorithm, iv:Optional[bytes]=None) -> bytearray: +def _cfb_decrypt(ct: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional[bytes] = None) -> bytearray: if iv is None: """ Instead of using an IV, OpenPGP prefixes a string of length diff --git a/pgpy/types.py b/pgpy/types.py index b9fe5fcb..a3945eb6 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -441,7 +441,7 @@ class MetaDispatchable(abc.ABCMeta): MetaDispatchable is a metaclass for objects that subclass Dispatchable """ - _roots:Set[Type] = set() + _roots: Set[Type] = set() """ _roots is a set of all currently registered RootClass class objects @@ -449,7 +449,7 @@ class MetaDispatchable(abc.ABCMeta): - it inherits (directly or indirectly) from Dispatchable - __typeid__ == -1 """ - _registry:Dict[Union[Tuple[Type,int],Tuple[Type,int,int]],Type] = {} + _registry: Dict[Union[Tuple[Type, int], Tuple[Type, int, int]], Type] = {} """ _registry is the Dispatchable class registry. It uses the following format: @@ -572,7 +572,7 @@ def __headercls__(self): # pragma: no cover def __typeid__(self): # pragma: no cover return False - __ver__:Optional[int] = None + __ver__: Optional[int] = None class SignatureVerification: From 56a3dbb7e3ea55e1532e7dd0b874d194d2594055 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 18:35:35 -0400 Subject: [PATCH 112/287] Armorable.ascii_unarmor: clean up type signatures The return object from this function is a bit fuzzy, and might be better created as an explicit object, rather than a dict. But for now, at least keep the type definitions reasonable. --- pgpy/types.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index a3945eb6..637ee631 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -16,7 +16,7 @@ from enum import EnumMeta from enum import IntEnum -from typing import Optional, Dict, Set, Tuple, Type, Union +from typing import Optional, Dict, List, Set, Tuple, Type, Union from .decorators import sdproperty @@ -115,7 +115,7 @@ def is_armor(text: Union[str, bytes, bytearray]) -> bool: return Armorable.__armor_regex.search(text) is not None @staticmethod - def ascii_unarmor(text: Union[str, bytes, bytearray]) -> Dict[str, Optional[Union[str, bytes, bytearray]]]: + def ascii_unarmor(text: Union[str, bytes, bytearray]) -> Dict[str, Optional[Union[str, bytes, bytearray, List[str]]]]: """ Takes an ASCII-armored PGP block and returns the decoded byte value. @@ -125,20 +125,18 @@ def ascii_unarmor(text: Union[str, bytes, bytearray]) -> Dict[str, Optional[Unio :returns: A ``dict`` containing information from ``text``, including the de-armored data. It can contain the following keys: ``magic``, ``headers``, ``hashes``, ``cleartext``, ``body``, ``crc``. """ - m = {'magic': None, 'headers': None, 'body': bytearray(), 'crc': None} - if not Armorable.is_utf8(text): - m['body'] = bytearray(text) - return m + if not isinstance(text, str) and not Armorable.is_utf8(text): + return {'magic': None, 'headers': None, 'body': bytearray(text), 'crc': None} if isinstance(text, (bytes, bytearray)): # pragma: no cover text = text.decode('latin-1') - m = Armorable.__armor_regex.search(text) + matcher = Armorable.__armor_regex.search(text) - if m is None: # pragma: no cover + if matcher is None: # pragma: no cover raise ValueError("Expected: ASCII-armored PGP data") - m = m.groupdict() + m = matcher.groupdict() if m['hashes'] is not None: m['hashes'] = m['hashes'].split(',') From b93a2b7666334248a85ed0d6162fa137a52e19fc Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 18:43:28 -0400 Subject: [PATCH 113/287] Packet() constructor is safe due to MetaDispatchable, despite Packet itself being abstrct --- pgpy/pgp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 8c981684..30d2b21f 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -633,7 +633,8 @@ def parse(self, packet: bytes) -> None: self.ascii_headers = unarmored['headers'] while data: - pkt = Packet(data) + # this is safe to do because of how MetaDispatchable works: + pkt = Packet(data) # type: ignore[abstract] if pkt.header.tag == PacketTag.Signature: if isinstance(pkt, Opaque): # skip unrecognized version. From 9002f31bd71688a8a8613a6ece62ec75c733b615 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 19:01:50 -0400 Subject: [PATCH 114/287] clean up type checking for CipherTexts --- pgpy/packet/packets.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index e6e73a98..60790a48 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -20,6 +20,7 @@ from .fields import ECDHPub, ECDHPriv, ECDHCipherText from .fields import EdDSAPub, EdDSAPriv, EdDSASignature from .fields import ElGCipherText, ElGPriv, ElGPub +from .fields import CipherText from .fields import OpaquePubKey from .fields import OpaquePrivKey from .fields import OpaqueSignature @@ -181,14 +182,13 @@ def pkalg_int(self, val: int) -> None: if self._pkalg is PubKeyAlgorithm.Invalid: self._opaque_pkalg: int = val - _c = {PubKeyAlgorithm.RSAEncryptOrSign: RSACipherText, - PubKeyAlgorithm.RSAEncrypt: RSACipherText, - PubKeyAlgorithm.ElGamal: ElGCipherText, - PubKeyAlgorithm.FormerlyElGamalEncryptOrSign: ElGCipherText, - PubKeyAlgorithm.ECDH: ECDHCipherText} - - ct = _c.get(self._pkalg, None) - self.ct = ct() if ct is not None else ct + self.ct: Optional[CipherText] = None + if self._pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt}: + self.ct = RSACipherText() + elif self._pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: + self.ct = ElGCipherText() + elif self._pkalg is PubKeyAlgorithm.ECDH: + self.ct = ECDHCipherText() def __init__(self): super().__init__() @@ -220,7 +220,7 @@ def __copy__(self): return sk def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: - if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: + if isinstance(self.ct, RSACipherText): # pad up ct with null bytes if necessary ct = self.ct.me_mod_n.to_mpibytes()[2:] ct = b'\x00' * ((pk.keymaterial.__privkey__().key_size // 8) - len(ct)) + ct @@ -228,7 +228,7 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: decrypter = pk.keymaterial.__privkey__().decrypt decargs = [ct, padding.PKCS1v15()] - elif self.pkalg == PubKeyAlgorithm.ECDH: + elif isinstance(self.ct, ECDHCipherText): decrypter = pk decargs = [] @@ -268,11 +268,11 @@ def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) m = bytearray(self.int_to_bytes(symalg) + symkey) m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) - if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: + if isinstance(self.ct, RSACipherText): encrypter = pk.keymaterial.__pubkey__().encrypt encargs = [bytes(m), padding.PKCS1v15()] - elif self.pkalg == PubKeyAlgorithm.ECDH: + elif isinstance(self.ct, ECDHCipherText): encrypter = pk encargs = [bytes(m)] From ea28e87010754437195e0ffe6e8e568d27a4df48 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 19:06:20 -0400 Subject: [PATCH 115/287] Clean up type checking for Signatures --- pgpy/packet/packets.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 60790a48..40083f73 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -21,6 +21,7 @@ from .fields import EdDSAPub, EdDSAPriv, EdDSASignature from .fields import ElGCipherText, ElGPriv, ElGPub from .fields import CipherText +from .fields import Signature as SignatureField from .fields import OpaquePubKey from .fields import OpaquePrivKey from .fields import OpaqueSignature @@ -354,12 +355,11 @@ class SignatureV4(Signature): __ver__ = 4 @sdproperty - def sigtype(self): + def sigtype(self) -> SignatureType: return self._sigtype - @sigtype.register(int) - @sigtype.register(SignatureType) - def sigtype_int(self, val): + @sigtype.register + def sigtype_int(self, val: int) -> None: self._sigtype = SignatureType(val) @sdproperty @@ -375,16 +375,16 @@ def pubalg_int(self, val: int) -> None: if self._pubalg is PubKeyAlgorithm.Unknown: self._opaque_pubalg: int = val - sigs = { - PubKeyAlgorithm.RSAEncryptOrSign: RSASignature, - PubKeyAlgorithm.RSAEncrypt: RSASignature, - PubKeyAlgorithm.RSASign: RSASignature, - PubKeyAlgorithm.DSA: DSASignature, - PubKeyAlgorithm.ECDSA: ECDSASignature, - PubKeyAlgorithm.EdDSA: EdDSASignature, - } - - self.signature = sigs.get(self.pubalg, OpaqueSignature)() + if self.pubalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSASign}: + self.signature: SignatureField = RSASignature() + elif self.pubalg is PubKeyAlgorithm.DSA: + self.signature = DSASignature() + elif self.pubalg is PubKeyAlgorithm.ECDSA: + self.signature = ECDSASignature() + elif self.pubalg is PubKeyAlgorithm.EdDSA: + self.signature = EdDSASignature() + else: + self.signature = OpaqueSignature() @sdproperty def halg(self) -> HashAlgorithm: From cb6a7643105ecfe30f7c6ae9cd0b7fa604eeccac Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 15 Jun 2023 19:17:15 -0400 Subject: [PATCH 116/287] clean up type checking for Key material fields --- pgpy/packet/packets.py | 45 ++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 40083f73..c2717a6a 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -22,6 +22,7 @@ from .fields import ElGCipherText, ElGPriv, ElGPub from .fields import CipherText from .fields import Signature as SignatureField +from .fields import PubKey as PubKeyField from .fields import OpaquePubKey from .fields import OpaquePrivKey from .fields import OpaqueSignature @@ -830,36 +831,20 @@ def pkalg_int(self, val: int) -> None: if self._pkalg is PubKeyAlgorithm.Unknown: self._opaque_pkalg: int = val - _c = { - # True means public - (True, PubKeyAlgorithm.RSAEncryptOrSign): RSAPub, - (True, PubKeyAlgorithm.RSAEncrypt): RSAPub, - (True, PubKeyAlgorithm.RSASign): RSAPub, - (True, PubKeyAlgorithm.DSA): DSAPub, - (True, PubKeyAlgorithm.ElGamal): ElGPub, - (True, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPub, - (True, PubKeyAlgorithm.ECDSA): ECDSAPub, - (True, PubKeyAlgorithm.ECDH): ECDHPub, - (True, PubKeyAlgorithm.EdDSA): EdDSAPub, - # False means private - (False, PubKeyAlgorithm.RSAEncryptOrSign): RSAPriv, - (False, PubKeyAlgorithm.RSAEncrypt): RSAPriv, - (False, PubKeyAlgorithm.RSASign): RSAPriv, - (False, PubKeyAlgorithm.DSA): DSAPriv, - (False, PubKeyAlgorithm.ElGamal): ElGPriv, - (False, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPriv, - (False, PubKeyAlgorithm.ECDSA): ECDSAPriv, - (False, PubKeyAlgorithm.ECDH): ECDHPriv, - (False, PubKeyAlgorithm.EdDSA): EdDSAPriv, - } - - k = (self.public, self.pkalg) - km = _c.get(k, None) - - self.keymaterial = (km or (OpaquePubKey if self.public else OpaquePrivKey))() - - # km = _c.get(k, None) - # self.keymaterial = km() if km is not None else km + if self.pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: + self.keymaterial: PubKeyField = (RSAPub if self.public else RSAPriv)() + elif self.pkalg is PubKeyAlgorithm.DSA: + self.keymaterial = (DSAPub if self.public else DSAPriv)() + elif self.pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: + self.keymaterial = (ElGPub if self.public else ElGPriv)() + elif self.pkalg is PubKeyAlgorithm.ECDSA: + self.keymaterial = (ECDSAPub if self.public else ECDSAPriv)() + elif self.pkalg is PubKeyAlgorithm.ECDH: + self.keymaterial = (ECDHPub if self.public else ECDHPriv)() + elif self.pkalg is PubKeyAlgorithm.EdDSA: + self.keymaterial = (EdDSAPub if self.public else EdDSAPriv)() + else: + self.keymaterial = (OpaquePubKey if self.public else OpaquePrivKey)() @property def public(self): From b72e3169ffc0265ce82183676ec8f4a1fa20366d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 13 May 2023 02:33:53 -0400 Subject: [PATCH 117/287] Test attempts to use EdDSA with anything but Ed25519 --- tests/test_10_exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 6a25ed06..2d5f3c68 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -81,6 +81,7 @@ def temp_key(): PubKeyAlgorithm.DSA: [512], PubKeyAlgorithm.ECDSA: [curve for curve in EllipticCurveOID if not curve.can_gen], PubKeyAlgorithm.ECDH: [curve for curve in EllipticCurveOID if not curve.can_gen], + PubKeyAlgorithm.EdDSA: [curve for curve in EllipticCurveOID if curve != EllipticCurveOID.Ed25519], } badkeyspec = [ (alg, size) for alg in key_algs_badsizes.keys() for size in key_algs_badsizes[alg] ] From 2f063979112fe171e0718301242a9990aed5b0eb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 2 Feb 2023 14:52:21 -0500 Subject: [PATCH 118/287] Ensure that IssuerFingerprint subpacket contains None or Fingerprint --- pgpy/packet/subpackets/signature.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 62e701ee..47403598 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -912,22 +912,24 @@ def version_bytearray(self, val): self.version = self.bytes_to_int(val) @sdproperty - def issuer_fingerprint(self): + def issuer_fingerprint(self) -> Optional[Fingerprint]: return self._issuer_fpr - @issuer_fingerprint.register(str) - @issuer_fingerprint.register(Fingerprint) - def issuer_fingerprint_str(self, val): - self._issuer_fpr = Fingerprint(val) + @issuer_fingerprint.register + def issuer_fingerprint_str(self, val: str) -> None: + if isinstance(val, Fingerprint): + self._issuer_fpr: Optional[Fingerprint] = val + else: + self._issuer_fpr = Fingerprint(val) - @issuer_fingerprint.register(bytearray) - def issuer_fingerprint_bytearray(self, val): - self.issuer_fingerprint = ''.join('{:02x}'.format(c) for c in val).upper() + @issuer_fingerprint.register + def issuer_fingerprint_bytearray(self, val: bytearray) -> None: + self.issuer_fingerprint = Fingerprint(''.join('{:02x}'.format(c) for c in val).upper()) - def __init__(self): + def __init__(self) -> None: super().__init__() self.version = 4 - self._issuer_fpr = "" + self._issuer_fpr = None def __bytearray__(self): _bytes = super().__bytearray__() From 62eee3e8684461dbc7291d26f3a70c292ae90d33 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 1 Feb 2023 13:37:13 -0500 Subject: [PATCH 119/287] clarify when a key must have a UID (the earlier arrangement of the comment was unclear, logic has not changed) --- pgpy/decorators.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pgpy/decorators.py b/pgpy/decorators.py index d7bde115..303f24d8 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -115,9 +115,11 @@ def _action(key, *args, **kwargs): if key._key is None: raise PGPError("No key!") - # if a key is in the process of being created, it needs to be allowed to certify its own user id - if len(key._uids) == 0 and key.is_primary and action is not key.certify.__wrapped__: - raise PGPError("Key is not complete - please add a User ID!") + # keys must have a user id: + if len(key._uids) == 0 and key.is_primary: + # if a key is in the process of being created, it needs to be allowed to certify its own user id + if action is not key.certify.__wrapped__: + raise PGPError("Key is not complete - please add a User ID!") with self.usage(key, kwargs.get('user', None)) as _key: self.check_attributes(key) From 0d6ec16d47b3bcce278b547e1ff89314c18293c3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 02:05:50 -0400 Subject: [PATCH 120/287] drop Backend enum and corresponding default_backend() calls Python's cryptography module is just going to use the default backend. There was never really any affordance in the PGPy API to select a different backend anyway, so we can just drop it as a simplification. --- pgpy/constants.py | 5 ----- pgpy/packet/fields.py | 36 +++++++++++++++++------------------- pgpy/symenc.py | 6 ++---- tests/test_99_regressions.py | 1 - 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 1c66de10..b3fb0680 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -22,7 +22,6 @@ from .decorators import classproperty __all__ = [ - 'Backend', 'ECFields', 'EllipticCurveOID', 'ECPointFormat', @@ -53,10 +52,6 @@ _hashtunedata = bytearray([10, 11, 12, 13, 14, 15, 16, 17] * 128 * 50) -class Backend(Enum): - OpenSSL = openssl.backend - - class ECPointFormat(IntEnum): # https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-07#appendix-B Standard = 0x04 diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 32bfbfba..3fcccecf 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -16,8 +16,6 @@ from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization @@ -386,7 +384,7 @@ class RSAPub(PubKey): __pubfields__ = ('n', 'e') def __pubkey__(self): - return rsa.RSAPublicNumbers(self.e, self.n).public_key(default_backend()) + return rsa.RSAPublicNumbers(self.e, self.n).public_key() def verify(self, subj, sigbytes, hash_alg): # zero-pad sigbytes if necessary @@ -407,7 +405,7 @@ class DSAPub(PubKey): def __pubkey__(self): params = dsa.DSAParameterNumbers(self.p, self.q, self.g) - return dsa.DSAPublicNumbers(self.y, params).public_key(default_backend()) + return dsa.DSAPublicNumbers(self.y, params).public_key() def verify(self, subj, sigbytes, hash_alg): try: @@ -510,7 +508,7 @@ def __len__(self): return len(self.p) + len(self.oid) def __pubkey__(self): - return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key(default_backend()) + return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key() def __bytearray__(self): _b = bytearray() @@ -568,7 +566,7 @@ def __copy__(self): def verify(self, subj, sigbytes, hash_alg): # GnuPG requires a pre-hashing with EdDSA # https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-06#section-14.8 - digest = hashes.Hash(hash_alg, backend=default_backend()) + digest = hashes.Hash(hash_alg) digest.update(subj) subj = digest.finalize() try: @@ -603,7 +601,7 @@ def __pubkey__(self): if self.oid == EllipticCurveOID.Curve25519: return x25519.X25519PublicKey.from_public_bytes(self.p.x) else: - return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key(default_backend()) + return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key() def __bytearray__(self) -> bytearray: _b = bytearray() @@ -1079,7 +1077,7 @@ def derive_key(self, s: bytes, curve: EllipticCurveOID, pkalg: PubKeyAlgorithm, data += b'Anonymous Sender ' data += binascii.unhexlify(fingerprint.replace(' ', '')) - ckdf = ConcatKDFHash(algorithm=getattr(hashes, self.halg.name)(), length=self.encalg.key_size // 8, otherinfo=bytes(data), backend=default_backend()) + ckdf = ConcatKDFHash(algorithm=getattr(hashes, self.halg.name)(), length=self.encalg.key_size // 8, otherinfo=bytes(data)) return ckdf.derive(s) @@ -1242,7 +1240,7 @@ def __privkey__(self): rsa.rsa_crt_dmp1(self.d, self.p), rsa.rsa_crt_dmq1(self.d, self.q), rsa.rsa_crt_iqmp(self.p, self.q), - rsa.RSAPublicNumbers(self.e, self.n)).private_key(default_backend()) + rsa.RSAPublicNumbers(self.e, self.n)).private_key() def _compute_chksum(self): chs = sum(sum(bytearray(c.to_mpibytes())) for c in (self.d, self.p, self.q, self.u)) % 65536 @@ -1253,7 +1251,7 @@ def _generate(self, key_size): raise PGPError("key is already populated") # generate some big numbers! - pk = rsa.generate_private_key(65537, key_size, default_backend()) + pk = rsa.generate_private_key(65537, key_size) pkn = pk.private_numbers() self.n = MPI(pkn.public_numbers.n) @@ -1314,7 +1312,7 @@ class DSAPriv(PrivKey, DSAPub): def __privkey__(self): params = dsa.DSAParameterNumbers(self.p, self.q, self.g) pn = dsa.DSAPublicNumbers(self.y, params) - return dsa.DSAPrivateNumbers(self.x, pn).private_key(default_backend()) + return dsa.DSAPrivateNumbers(self.x, pn).private_key() def _compute_chksum(self): chs = sum(bytearray(self.x.to_mpibytes())) % 65536 @@ -1325,7 +1323,7 @@ def _generate(self, key_size): raise PGPError("key is already populated") # generate some big numbers! - pk = dsa.generate_private_key(key_size, default_backend()) + pk = dsa.generate_private_key(key_size) pkn = pk.private_numbers() self.p = MPI(pkn.public_numbers.parameter_numbers.p) @@ -1410,7 +1408,7 @@ class ECDSAPriv(PrivKey, ECDSAPub): def __privkey__(self): ecp = ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()) - return ec.EllipticCurvePrivateNumbers(self.s, ecp).private_key(default_backend()) + return ec.EllipticCurvePrivateNumbers(self.s, ecp).private_key() def _compute_chksum(self) -> None: chs = sum(bytearray(self.s.to_mpibytes())) % 65536 @@ -1428,7 +1426,7 @@ def _generate(self, params: Union[int, EllipticCurveOID]) -> None: else: self.oid = params - pk = ec.generate_private_key(self.oid.curve(), default_backend()) + pk = ec.generate_private_key(self.oid.curve()) pubn = pk.public_key().public_numbers() self.p = ECPoint.from_values(self.oid.key_size, ECPointFormat.Standard, MPI(pubn.x), MPI(pubn.y)) self.s = MPI(pk.private_numbers().private_value) @@ -1514,7 +1512,7 @@ def decrypt_keyblob(self, passphrase): def sign(self, sigdata, hash_alg): # GnuPG requires a pre-hashing with EdDSA # https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-06#section-14.8 - digest = hashes.Hash(hash_alg, backend=default_backend()) + digest = hashes.Hash(hash_alg) digest.update(sigdata) sigdata = digest.finalize() return self.__privkey__().sign(sigdata) @@ -1695,7 +1693,7 @@ def encrypt(cls, pk, *args): ct.p = ECPoint.from_values(km.oid.key_size, ECPointFormat.Native, x) s = v.exchange(km.__pubkey__()) else: - v = ec.generate_private_key(km.oid.curve(), default_backend()) + v = ec.generate_private_key(km.oid.curve()) x = MPI(v.public_key().public_numbers().x) y = MPI(v.public_key().public_numbers().y) ct.p = ECPoint.from_values(km.oid.key_size, ECPointFormat.Standard, x, y) @@ -1705,7 +1703,7 @@ def encrypt(cls, pk, *args): z = km.kdf.derive_key(s, km.oid, PubKeyAlgorithm.ECDH, pk.fingerprint) # compute C - ct.c = aes_key_wrap(z, m, default_backend()) + ct.c = aes_key_wrap(z, m) return ct @@ -1716,7 +1714,7 @@ def decrypt(self, pk, *args): s = km.__privkey__().exchange(v) else: # assemble the public component of ephemeral key v - v = ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, km.oid.curve()).public_key(default_backend()) + v = ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, km.oid.curve()).public_key() # compute s using the inverse of how it was derived during encryption s = km.__privkey__().exchange(ec.ECDH(), v) @@ -1724,7 +1722,7 @@ def decrypt(self, pk, *args): z = km.kdf.derive_key(s, km.oid, PubKeyAlgorithm.ECDH, pk.fingerprint) # unwrap and unpad m - _m = aes_key_unwrap(z, self.c, default_backend()) + _m = aes_key_unwrap(z, self.c) padder = PKCS7(64).unpadder() return padder.update(_m) + padder.finalize() diff --git a/pgpy/symenc.py b/pgpy/symenc.py index d9e8a856..51173ae5 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -5,8 +5,6 @@ from cryptography.exceptions import UnsupportedAlgorithm -from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import modes @@ -31,7 +29,7 @@ def _cfb_encrypt(pt: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional raise PGPEncryptionError("Cipher {:s} not supported".format(alg.name)) try: - encryptor = Cipher(alg.cipher(key), modes.CFB(iv), default_backend()).encryptor() + encryptor = Cipher(alg.cipher(key), modes.CFB(iv)).encryptor() except UnsupportedAlgorithm as ex: # pragma: no cover raise PGPEncryptionError from ex @@ -52,7 +50,7 @@ def _cfb_decrypt(ct: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional iv = b'\x00' * (alg.block_size // 8) try: - decryptor = Cipher(alg.cipher(key), modes.CFB(iv), default_backend()).decryptor() + decryptor = Cipher(alg.cipher(key), modes.CFB(iv)).decryptor() except UnsupportedAlgorithm as ex: # pragma: no cover raise PGPDecryptionError from ex diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index fac50591..9d0dd26f 100644 --- a/tests/test_99_regressions.py +++ b/tests/test_99_regressions.py @@ -27,7 +27,6 @@ def test_reg_bug_56(): from pgpy.constants import PubKeyAlgorithm from pgpy.constants import SignatureType - from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding From b4e87f3abe7a4545bc797434aeaf64f28f7296c8 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 02:25:22 -0400 Subject: [PATCH 121/287] drop HashAlgorithm.tuned_count (not necessary, we just max out S2K) --- pgpy/constants.py | 8 -------- pgpy/pgp.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index b3fb0680..adad74b1 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -312,10 +312,6 @@ def _missing_(cls, val: object) -> 'HashAlgorithm': raise TypeError(f"cannot look up HashAlgorithm by non-int {type(val)}") return cls.Unknown - def __init__(self, *args): - super(self.__class__, self).__init__() - self._tuned_count = 255 - @property def hasher(self): return hashlib.new(self.name) @@ -324,10 +320,6 @@ def hasher(self): def digest_size(self): return self.hasher.digest_size - @property - def tuned_count(self): - return self._tuned_count - @property def is_supported(self): return True diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 30d2b21f..79804179 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1276,7 +1276,7 @@ def encrypt(self, passphrase, sessionkey=None, **prefs): skesk.s2k.specifier = 3 skesk.s2k.halg = hash_algo skesk.s2k.encalg = cipher_algo - skesk.s2k.count = skesk.s2k.halg.tuned_count + skesk.s2k.count = 255 if sessionkey is None: sessionkey = cipher_algo.gen_key() From d7acaeb063f7af578d648dc9c288ca75ec79ec4f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 15:55:10 -0400 Subject: [PATCH 122/287] Add named S2KUsage enum, in alignment with the new registry in crypto-refresh There should be no substantive change here, just making the text easier to read --- pgpy/constants.py | 24 +++++++++++++++++++++++ pgpy/packet/fields.py | 45 ++++++++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index adad74b1..609dd121 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -37,6 +37,7 @@ 'SignatureType', 'KeyServerPreferences', 'S2KGNUExtension', + 'S2KUsage', 'SecurityIssues', 'String2KeyType', 'TrustLevel', @@ -245,6 +246,29 @@ def validate_params(self, size): return SecurityIssues.BrokenAsymmetricFunc +class S2KUsage(IntEnum): + '''S2KUsage octet for secret key protection.''' + Unprotected = 0 + + # Legacy keys might be protected directly with a SymmetricKeyAlgorithm (this is a bad idea): + IDEA = 1 + TripleDES = 2 + CAST5 = 3 + Blowfish = 4 + AES128 = 7 + AES192 = 8 + AES256 = 9 + Twofish256 = 10 + Camellia128 = 11 + Camellia192 = 12 + Camellia256 = 13 + + # sensible use of tamper-resistant CFB: + CFB = 254 + # legacy use of CFB: + MalleableCFB = 255 + + class CompressionAlgorithm(IntEnum): """Supported compression algorithms.""" #: No compression diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 3fcccecf..0d2ccba9 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -49,6 +49,7 @@ from ..constants import String2KeyType from ..constants import S2KGNUExtension from ..constants import SymmetricKeyAlgorithm +from ..constants import S2KUsage from ..decorators import sdproperty @@ -815,9 +816,9 @@ def count_int(self, val): raise ValueError("count must be between 0 and 256") self._count = val - def __init__(self): + def __init__(self) -> None: super().__init__() - self.usage = 0 + self.usage: S2KUsage = S2KUsage.Unprotected self.encalg = 0 self.specifier = 0 self.iv = None @@ -868,13 +869,13 @@ def _experimental_bytearray(self, _bytes): _bytes += self.scserial return _bytes - def __len__(self): + def __len__(self) -> int: return len(self.__bytearray__()) - def __bool__(self): - return self.usage in [254, 255] + def __bool__(self) -> bool: + return self.usage in [S2KUsage.CFB, S2KUsage.MalleableCFB] - def __copy__(self): + def __copy__(self) -> 'String2Key': s2k = String2Key() s2k.usage = self.usage s2k.encalg = self.encalg @@ -890,8 +891,8 @@ def __copy__(self): s2k.scserial = self.scserial return s2k - def parse(self, packet, iv=True): - self.usage = packet[0] + def parse(self, packet: bytearray, iv: bool = True) -> None: + self.usage = S2KUsage(packet[0]) del packet[0] if bool(self): @@ -1111,7 +1112,7 @@ def __bytearray__(self): for field in self.__privfields__: _bytes += getattr(self, field).to_mpibytes() - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: _bytes += self.chksum return _bytes @@ -1149,7 +1150,7 @@ def publen(self) -> int: def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): # PGPy will only ever use iterated and salted S2k mode - self.s2k.usage = 254 + self.s2k.usage = S2KUsage.CFB self.s2k.encalg = enc_alg self.s2k.specifier = String2KeyType.Iterated self.s2k.iv = enc_alg.gen_iv() @@ -1198,12 +1199,12 @@ def decrypt_keyblob(self, passphrase): pt = _cfb_decrypt(bytes(self.encbytes), bytes(sessionkey), self.s2k.encalg, bytes(self.s2k.iv)) # check the hash to see if we decrypted successfully or not - if self.s2k.usage == 254 and not pt[-20:] == hashlib.new('sha1', pt[:-20]).digest(): + if self.s2k.usage is S2KUsage.CFB and not pt[-20:] == hashlib.new('sha1', pt[:-20]).digest(): # if the usage byte is 254, key material is followed by a 20-octet sha-1 hash of the rest # of the key material block raise PGPDecryptionError("Passphrase was incorrect!") - if self.s2k.usage == 255 and not self.bytes_to_int(pt[-2:]) == (sum(bytearray(pt[:-2])) % 65536): # pragma: no cover + if self.s2k.usage is S2KUsage.MalleableCFB and not self.bytes_to_int(pt[-2:]) == (sum(bytearray(pt[:-2])) % 65536): # pragma: no cover # if the usage byte is 255, key material is followed by a 2-octet checksum of the rest # of the key material block raise PGPDecryptionError("Passphrase was incorrect!") @@ -1281,7 +1282,7 @@ def parse(self, packet): self.q = MPI(packet) self.u = MPI(packet) - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: self.chksum = packet[:2] del packet[:2] @@ -1298,7 +1299,7 @@ def decrypt_keyblob(self, passphrase): self.q = MPI(kb) self.u = MPI(kb) - if self.s2k.usage in [254, 255]: + if self.s2k.usage in {S2KUsage.CFB, S2KUsage.MalleableCFB}: self.chksum = kb del kb @@ -1347,7 +1348,7 @@ def parse(self, packet): else: self.encbytes = packet - if self.s2k.usage in [0, 255]: + if self.s2k.usage in {S2KUsage.Unprotected, S2KUsage.MalleableCFB}: self.chksum = packet[:2] del packet[:2] @@ -1357,7 +1358,7 @@ def decrypt_keyblob(self, passphrase): self.x = MPI(kb) - if self.s2k.usage in [254, 255]: + if self.s2k.usage in {S2KUsage.CFB, S2KUsage.MalleableCFB}: self.chksum = kb del kb @@ -1388,7 +1389,7 @@ def parse(self, packet): else: self.encbytes = packet - if self.s2k.usage in [0, 255]: + if self.s2k.usage in {S2KUsage.Unprotected, S2KUsage.MalleableCFB}: self.chksum = packet[:2] del packet[:2] @@ -1398,7 +1399,7 @@ def decrypt_keyblob(self, passphrase): self.x = MPI(kb) - if self.s2k.usage in [254, 255]: + if self.s2k.usage in [S2KUsage.CFB, S2KUsage.MalleableCFB]: self.chksum = kb del kb @@ -1439,7 +1440,7 @@ def parse(self, packet): if not self.s2k: self.s = MPI(packet) - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: self.chksum = packet[:2] del packet[:2] else: @@ -1497,7 +1498,7 @@ def parse(self, packet): if not self.s2k: self.s = MPI(packet) - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: self.chksum = packet[:2] del packet[:2] else: @@ -1524,7 +1525,7 @@ def __bytearray__(self): _b += self.s2k.__bytearray__() if not self.s2k: _b += self.s.to_mpibytes() - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: _b += self.chksum else: _b += self.encbytes @@ -1582,7 +1583,7 @@ def parse(self, packet): if not self.s2k: self.s = MPI(packet) - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected: self.chksum = packet[:2] del packet[:2] else: From 8b326f29e47817f21d0735f377e84f1930587882 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 16 May 2023 17:14:56 -0400 Subject: [PATCH 123/287] HashAlgorithm.digest() -- one-off hash from the cryptography module --- pgpy/constants.py | 7 +++++++ pgpy/packet/fields.py | 7 +++---- pgpy/packet/packets.py | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 609dd121..8a34f37a 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import ec, x25519, ed25519 from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm +from cryptography.hazmat.primitives import hashes from .types import FlagEnum from .decorators import classproperty @@ -369,6 +370,12 @@ def is_considered_secure(self): return issues + def digest(self, data: bytes) -> bytes: + 'shortcut for computing a quick one-off digest' + ctx = hashes.Hash(getattr(hashes, self.name)()) + ctx.update(data) + return ctx.finalize() + class ECFields(NamedTuple): name: str diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 0d2ccba9..52359dd2 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -5,7 +5,6 @@ import binascii import collections import copy -import hashlib import itertools import math import os @@ -1168,7 +1167,7 @@ def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): pt += getattr(self, pf).to_mpibytes() # append a SHA-1 hash of the plaintext so far to the plaintext - pt += hashlib.new('sha1', pt).digest() + pt += HashAlgorithm.SHA1.digest(pt) # encrypt self.encbytes = bytearray(_cfb_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) @@ -1199,7 +1198,7 @@ def decrypt_keyblob(self, passphrase): pt = _cfb_decrypt(bytes(self.encbytes), bytes(sessionkey), self.s2k.encalg, bytes(self.s2k.iv)) # check the hash to see if we decrypted successfully or not - if self.s2k.usage is S2KUsage.CFB and not pt[-20:] == hashlib.new('sha1', pt[:-20]).digest(): + if self.s2k.usage is S2KUsage.CFB and not pt[-20:] == HashAlgorithm.SHA1.digest(pt[:-20]): # if the usage byte is 254, key material is followed by a 20-octet sha-1 hash of the rest # of the key material block raise PGPDecryptionError("Passphrase was incorrect!") @@ -1303,7 +1302,7 @@ def decrypt_keyblob(self, passphrase): self.chksum = kb del kb - def sign(self, sigdata, hash_alg): + def sign(self, sigdata:bytes, hash_alg:HashAlgorithm) -> bytes: return self.__privkey__().sign(sigdata, padding.PKCS1v15(), hash_alg) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index c2717a6a..1c15df20 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1599,7 +1599,7 @@ def encrypt(self, key, alg, data): data = iv + iv[-2:] + data mdc = MDC() - mdc.mdc = binascii.hexlify(hashlib.new('SHA1', data + b'\xd3\x14').digest()) + mdc.mdc = binascii.hexlify(HashAlgorithm.SHA1.digest(data + b'\xd3\x14')) mdc.update_hlen() data += mdc.__bytes__() @@ -1611,7 +1611,7 @@ def decrypt(self, key, alg): pt = _cfb_decrypt(bytes(self.ct), bytes(key), alg) # do the MDC checks - _expected_mdcbytes = b'\xd3\x14' + hashlib.new('SHA1', pt[:-20]).digest() + _expected_mdcbytes = b'\xd3\x14' + HashAlgorithm.SHA1.digest(pt[:-20]) if not constant_time.bytes_eq(bytes(pt[-22:]), _expected_mdcbytes): raise PGPDecryptionError("Decryption failed") # pragma: no cover From 88784cbbef0f03fc22d449973a7f3f65dffb7ae3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 04:17:28 -0400 Subject: [PATCH 124/287] Fingerprint: learn its version explicitly This means we also adjust the IssuerFingerprint and IntendedRecipients subpackets to just expose a fingerprint. --- pgpy/packet/subpackets/signature.py | 152 +++++++++++----------------- pgpy/types.py | 37 +++++-- 2 files changed, 86 insertions(+), 103 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 47403598..64c9841e 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -10,7 +10,7 @@ from datetime import timedelta from datetime import timezone -from typing import Optional, Type +from typing import Optional, Type, Union from .types import EmbeddedSignatureHeader from .types import Signature @@ -882,43 +882,39 @@ def parse(self, packet): class IssuerFingerprint(Signature): - ''' - (from RFC4880bis-07) - 5.2.3.28. Issuer Fingerprint + '''(from crypto-refresh-07) + 5.2.3.35. Issuer Fingerprint (1 octet key version number, N octets of fingerprint) - The OpenPGP Key fingerprint of the key issuing the signature. This - subpacket SHOULD be included in all signatures. If the version of - the issuing key is 4 and an Issuer subpacket is also included in the - signature, the key ID of the Issuer subpacket MUST match the low 64 - bits of the fingerprint. - - Note that the length N of the fingerprint for a version 4 key is 20 - octets; for a version 5 key N is 32. + The OpenPGP Key fingerprint of the key issuing the signature. This + subpacket SHOULD be included in all signatures. If the version of + the issuing key is 4 and an Issuer Key ID subpacket (Section + 5.2.3.12) is also included in the signature, the key ID of the + Issuer Key ID subpacket MUST match the low 64 bits of the + fingerprint. + + Note that the length N of the fingerprint for a version 4 key is + 20 octets; for a version 6 key N is 32. Since the version of the + signature is bound to the version of the key, the version octet + here MUST match the version of the signature. If the version octet + does not match the signature version, the receiving implementation + MUST treat it as a malformed signature (see Section 5.2.5). ''' __typeid__ = 0x21 - @sdproperty - def version(self): - return self._version - - @version.register(int) - def version_int(self, val): - self._version = val - - @version.register(bytearray) - def version_bytearray(self, val): - self.version = self.bytes_to_int(val) + def __init__(self) -> None: + super(IssuerFingerprint, self).__init__() + self._issuer_fpr: Optional[Fingerprint] = None @sdproperty def issuer_fingerprint(self) -> Optional[Fingerprint]: return self._issuer_fpr @issuer_fingerprint.register - def issuer_fingerprint_str(self, val: str) -> None: + def issuer_fingerprint_set(self, val: Union[str, bytearray, bytes, Fingerprint]) -> None: if isinstance(val, Fingerprint): - self._issuer_fpr: Optional[Fingerprint] = val + self._issuer_fpr = val else: self._issuer_fpr = Fingerprint(val) @@ -926,101 +922,73 @@ def issuer_fingerprint_str(self, val: str) -> None: def issuer_fingerprint_bytearray(self, val: bytearray) -> None: self.issuer_fingerprint = Fingerprint(''.join('{:02x}'.format(c) for c in val).upper()) - def __init__(self) -> None: - super().__init__() - self.version = 4 - self._issuer_fpr = None - - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() - _bytes += self.int_to_bytes(self.version) - _bytes += self.issuer_fingerprint.__bytes__() + _bytes += self.issuer_fingerprint.__wireformat__() return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) - self.version = packet[:1] - del packet[:1] + version = packet[0] + del packet[0] - if self.version == 4: - fpr_len = 20 - elif self.version == 5: # pragma: no cover - fpr_len = 32 - else: # pragma: no cover - fpr_len = self.header.length - 1 + fpr_len = self.header.length - 2 - self.issuer_fingerprint = packet[:fpr_len] + self.issuer_fingerprint = Fingerprint(packet[:fpr_len], version) del packet[:fpr_len] class IntendedRecipient(Signature): - ''' - (from RFC4880bis-08) - 5.2.3.29. Intended Recipient + '''(from crypto-refresh-08) + 5.2.3.36. Intended Recipient Fingerprint (1 octet key version number, N octets of fingerprint) - The OpenPGP Key fingerprint of the intended recipient primary key. - If one or more subpackets of this type are included in a signature, - it SHOULD be considered valid only in an encrypted context, where the - key it was encrypted to is one of the indicated primary keys, or one - of their subkeys. This can be used to prevent forwarding a signature - outside of its intended, encrypted context. + The OpenPGP Key fingerprint of the intended recipient primary + key. If one or more subpackets of this type are included in a + signature, it SHOULD be considered valid only in an encrypted + context, where the key it was encrypted to is one of the indicated + primary keys, or one of their subkeys. This can be used to prevent + forwarding a signature outside of its intended, encrypted context + (see Section 14.11). + + Note that the length N of the fingerprint for a version 4 key is + 20 octets; for a version 6 key N is 32. - Note that the length N of the fingerprint for a version 4 key is 20 - octets; for a version 5 key N is 32. + An implementation SHOULD generate this subpacket when creating a + signed and encrypted message. ''' __typeid__ = 0x23 - @sdproperty - def version(self): - return self._version - - @version.register(int) - def version_int(self, val): - self._version = val - - @version.register(bytearray) - def version_bytearray(self, val): - self.version = self.bytes_to_int(val) + def __init__(self) -> None: + super(IntendedRecipient, self).__init__() + self._intended_recipient: Optional[Fingerprint] = None @sdproperty - def intended_recipient(self): + def intended_recipient(self) -> Fingerprint: + if self._intended_recipient is None: + raise ValueError("tried to access the intended recipient fingerprint before it was set") return self._intended_recipient - @intended_recipient.register(str) - @intended_recipient.register(Fingerprint) - def intended_recipient_str(self, val): - self._intended_recipient = Fingerprint(val) - - @intended_recipient.register(bytearray) - def intended_recipient_bytearray(self, val): - self.intended_recipient = ''.join('{:02x}'.format(c) for c in val).upper() + @intended_recipient.register + def intended_recipient_set(self, val: Union[str, bytearray, bytes, Fingerprint]) -> None: + if not isinstance(val, Fingerprint): + val = Fingerprint(val) + self._intended_recipient = val - def __init__(self): - super().__init__() - self.version = 4 - self._intended_recipient = "" - - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() - _bytes += self.int_to_bytes(self.version) - _bytes += self.intended_recipient.__bytes__() + _bytes += self.intended_recipient.__wireformat__() return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) - self.version = packet[:1] - del packet[:1] + version: int = packet[0] + del packet[0] - if self.version == 4: - fpr_len = 20 - elif self.version == 5: # pragma: no cover - fpr_len = 32 - else: # pragma: no cover - fpr_len = self.header.length - 1 + fpr_len = self.header.length - 2 - self.intended_recipient = packet[:fpr_len] + self.intended_recipient = Fingerprint(packet[:fpr_len], version=version) del packet[:fpr_len] diff --git a/pgpy/types.py b/pgpy/types.py index 637ee631..ccb42203 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -687,19 +687,34 @@ def keyid(self): def shortid(self): return self[-8:] - def __new__(cls, content): + @sdproperty + def version(self) -> int: + 'Returns None if the version is unknown' + return self._version + + @version.register + def version_int(self, version: int) -> None: + self._version: int = version + + def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> "Fingerprint": if isinstance(content, Fingerprint): return content + if isinstance(content, (bytes, bytearray)): + if len(content) != 20: + raise ValueError(f'binary Fingerprint must be 20 bytes, not {len(content)}') + return Fingerprint(binascii.b2a_hex(content).decode('latin-1').upper()) # validate input before continuing: this should be a string of 40 hex digits content = content.upper().replace(' ', '') - if not re.match(r'^[0-9A-F]+$', content): + if not re.match(r'^[0-9A-F]{40}$', content): raise ValueError('Fingerprint must be a string of 40 hex digits') - return str.__new__(cls, content) + ret = str.__new__(cls, content) + ret._version = 4 if version is None else version + return ret def __eq__(self, other): if isinstance(other, Fingerprint): - return str(self) == str(other) + return str(self) == str(other) and self._version == other._version if isinstance(other, (str, bytes, bytearray)): if isinstance(other, (bytes, bytearray)): # pragma: no cover @@ -718,8 +733,11 @@ def __ne__(self, other): def __hash__(self): return hash(str(self)) - def __bytes__(self): - return binascii.unhexlify(self.encode("latin-1")) + def __bytes__(self) -> bytes: + return binascii.a2b_hex(self.encode("latin-1")) + + def __wireformat__(self) -> bytes: + return bytes([self._version]) + bytes(self) def __pretty__(self): content = self @@ -732,11 +750,8 @@ def __pretty__(self): ] return ' '.join(' '.join(c for c in half) for half in halves) - def __repr__(self): - return '{classname}({fp})'.format( - classname=self.__class__.__name__, - fp=self.__pretty__() - ) + def __repr__(self) -> str: + return f'{self.__class__.__name__}v{self._version}({self.__pretty__()})' class SorteDeque(collections.deque): From cdc4799537c9adef1527ba76f25b306338007e4c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 18 May 2023 18:01:09 -0400 Subject: [PATCH 125/287] Drop all use of hashlib We leave one use of hashlib in the tests, but all cryptographic digests in the running code get pulled from the cryptography module. --- docs/source/changelog.rst | 5 +++++ pgpy/constants.py | 9 ++++----- pgpy/packet/fields.py | 6 +++--- pgpy/packet/packets.py | 5 ++--- pgpy/pgp.py | 6 +++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 55a6754a..b1cea86e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -34,6 +34,11 @@ UTF-8. EllipticCurveOID.Invalid was removed -- EllipticCurveOID only enumerates supported curves now. +HashAlgorithm.hasher now returns a +cryptography.hazmat.primitives.hashes.Hash object, not a hashlib.HASH +object. The main difference between these interfaces is the use of +finalize() instead of digest(). + v0.6.0 ====== diff --git a/pgpy/constants.py b/pgpy/constants.py index 8a34f37a..b07117b1 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -1,7 +1,6 @@ """ constants.py """ import bz2 -import hashlib import os import zlib import warnings @@ -338,12 +337,12 @@ def _missing_(cls, val: object) -> 'HashAlgorithm': return cls.Unknown @property - def hasher(self): - return hashlib.new(self.name) + def hasher(self) -> hashes.Hash: + return hashes.Hash(getattr(hashes, self.name)()) @property - def digest_size(self): - return self.hasher.digest_size + def digest_size(self) -> int: + return getattr(hashes, self.name).digest_size @property def is_supported(self): diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 52359dd2..a9e9b8fb 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -997,7 +997,7 @@ def derive_key(self, passphrase): del hashdata # and return the key! - return b''.join(hc.digest() for hc in h)[:(keylen // 8)] + return b''.join(hc.finalize() for hc in h)[:(keylen // 8)] class ECKDF(Field): @@ -1155,7 +1155,7 @@ def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): self.s2k.iv = enc_alg.gen_iv() self.s2k.halg = hash_alg self.s2k.salt = bytearray(os.urandom(8)) - self.s2k.count = hash_alg.tuned_count + self.s2k.count = 255 # now that String-to-Key is ready to go, derive sessionkey from passphrase # and then unreference passphrase @@ -1302,7 +1302,7 @@ def decrypt_keyblob(self, passphrase): self.chksum = kb del kb - def sign(self, sigdata:bytes, hash_alg:HashAlgorithm) -> bytes: + def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: return self.__privkey__().sign(sigdata, padding.PKCS1v15(), hash_alg) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 1c15df20..503bc843 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -4,7 +4,6 @@ import binascii import calendar import copy -import hashlib import os import warnings @@ -855,7 +854,7 @@ def fingerprint(self): # A V4 fingerprint is the 160-bit SHA-1 hash of the octet 0x99, followed by the two-octet packet length, # followed by the entire Public-Key packet starting with the version field. The Key ID is the # low-order 64 bits of the fingerprint. - fp = hashlib.new('sha1') + fp = HashAlgorithm.SHA1.hasher plen = self.keymaterial.publen() bcde_len = self.int_to_bytes(6 + plen, 2) @@ -877,7 +876,7 @@ def fingerprint(self): fp.update(self.keymaterial.__bytearray__()[:plen]) # and return the digest - return Fingerprint(fp.hexdigest().upper()) + return Fingerprint(fp.finalize()) def __init__(self): super().__init__() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 79804179..0b4e7960 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -393,7 +393,7 @@ def attests_to(self, othersig): raise TypeError h = self.hash_algorithm.hasher h.update(othersig._signature.canonical_bytes()) - return h.digest() in self.attested_certifications + return h.finalize() in self.attested_certifications def hashdata(self, subject): _data = bytearray() @@ -2053,7 +2053,7 @@ def _sign(self, subject, sig, **prefs): sigdata = sig.hashdata(subject) h2 = sig.hash_algorithm.hasher h2.update(sigdata) - sig._signature.hash2 = bytearray(h2.digest()[:2]) + sig._signature.hash2 = bytearray(h2.finalize()[:2]) _sig = self._key.sign(sigdata, getattr(hashes, sig.hash_algorithm.name)()) if _sig is NotImplemented: @@ -2259,7 +2259,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): if isinstance(attestation, PGPSignature) and attestation.type in cert_sigtypes: h = sig.hash_algorithm.hasher h.update(attestation._signature.canonical_bytes()) - attestations.add(h.digest()) + attestations.add(h.finalize()) elif isinstance(attestation, (bytes, bytearray)) and len(attestation) == sig.hash_algorithm.digest_size: attestations.add(attestation) else: From 1cd7a384f199de3772c0558bfb5bb30187c1fc5a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 18 May 2023 15:46:18 -0400 Subject: [PATCH 126/287] Move make_onepass to Signature packets we keep PGPSignature.make_onepass to avoid an API change, but the logic of how to generate an OPS belongs in the Signature packet itself anyway. --- pgpy/packet/packets.py | 20 ++++++++++++++++++++ pgpy/pgp.py | 10 ++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 503bc843..05ea1f77 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -302,6 +302,10 @@ class Signature(VersionedPacket): __typeid__ = 0x02 __ver__ = 0 + @abc.abstractmethod + def make_onepass(self) -> 'OnePassSignature': + raise NotImplementedError() + class SignatureV4(Signature): """ @@ -512,6 +516,22 @@ def parse(self, packet): self.signature.parse(packet) + def make_onepass(self) -> 'OnePassSignatureV3': + signer = self.signer + if signer is None: + raise ValueError("Cannot make a one-pass signature without knowledge of who the signer is") + if isinstance(signer, Fingerprint): + signer = signer.keyid + + onepass = OnePassSignatureV3() + onepass.sigtype = self.sigtype + onepass.halg = self.halg + onepass.pubalg = self.pubalg + + onepass._signer = signer + onepass.update_hlen() + return onepass + class SKESessionKey(VersionedPacket): __typeid__ = 0x03 diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 0b4e7960..9834b341 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -562,14 +562,8 @@ def hashdata(self, subject): _data += self.int_to_bytes(hlen, 4) return bytes(_data) - def make_onepass(self): - onepass = OnePassSignatureV3() - onepass.sigtype = self.type - onepass.halg = self.hash_algorithm - onepass.pubalg = self.key_algorithm - onepass.signer = self.signer - onepass.update_hlen() - return onepass + def make_onepass(self) -> OnePassSignature: + return self._signature.make_onepass() def parse(self, packet): unarmored = self.ascii_unarmor(packet) From 878bc486506a33670cc405b52258407a8636d288 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 14:00:11 -0400 Subject: [PATCH 127/287] String2Key: improve type signatures --- pgpy/packet/fields.py | 72 +++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index a9e9b8fb..7601e37a 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -11,7 +11,7 @@ import collections.abc -from typing import Tuple, Union +from typing import Optional, Tuple, Union from cryptography.exceptions import InvalidSignature @@ -766,13 +766,15 @@ class String2Key(Field): context(s) as with the other S2K algorithms. """ @sdproperty - def encalg(self): + def encalg(self) -> SymmetricKeyAlgorithm: return self._encalg - @encalg.register(int) - @encalg.register(SymmetricKeyAlgorithm) - def encalg_int(self, val): - self._encalg = SymmetricKeyAlgorithm(val) + @encalg.register + def encalg_int(self, val: int) -> None: + if isinstance(val, SymmetricKeyAlgorithm): + self._encalg: SymmetricKeyAlgorithm = val + else: + self._encalg = SymmetricKeyAlgorithm(val) @sdproperty def specifier(self) -> String2KeyType: @@ -781,50 +783,54 @@ def specifier(self) -> String2KeyType: @specifier.register def specifier_int(self, val: int) -> None: if isinstance(val, String2KeyType): - self._specifier = val + self._specifier: String2KeyType = val else: self._specifier = String2KeyType(val) if self._specifier is String2KeyType.Unknown: self._opaque_specifier: int = val @sdproperty - def gnuext(self): + def gnuext(self) -> S2KGNUExtension: return self._gnuext - @gnuext.register(int) - @gnuext.register(S2KGNUExtension) - def gnuext_int(self, val): - self._gnuext = S2KGNUExtension(val) + @gnuext.register + def gnuext_int(self, val: int) -> None: + if isinstance(val, S2KGNUExtension): + self._gnuext: S2KGNUExtension = val + else: + self._gnuext = S2KGNUExtension(val) @sdproperty - def halg(self): + def halg(self) -> HashAlgorithm: return self._halg - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - self._halg = HashAlgorithm(val) + @halg.register + def halg_int(self, val: int) -> None: + if isinstance(val, HashAlgorithm): + self._halg = val + else: + self._halg = HashAlgorithm(val) @sdproperty - def count(self): + def count(self) -> int: return (16 + (self._count & 15)) << ((self._count >> 4) + 6) - @count.register(int) - def count_int(self, val): + @count.register + def count_int(self, val: int) -> None: if val < 0 or val > 255: # pragma: no cover raise ValueError("count must be between 0 and 256") self._count = val def __init__(self) -> None: super().__init__() - self.usage: S2KUsage = S2KUsage.Unprotected - self.encalg = 0 - self.specifier = 0 - self.iv = None + self.usage = S2KUsage.Unprotected + self._encalg = SymmetricKeyAlgorithm.Plaintext + self._specifier = String2KeyType.Unknown + self.iv: Optional[bytearray] = None # specifier-specific fields # simple, salted, iterated - self.halg = 0 + self._halg = HashAlgorithm.Unknown # salted, iterated self.salt = bytearray() @@ -836,9 +842,9 @@ def __init__(self) -> None: self.gnuext = 1 # GNU extension smartcard - self.scserial = None + self.scserial: Optional[bytearray] = None - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes.append(self.usage) if bool(self): @@ -859,7 +865,7 @@ def __bytearray__(self): _bytes += self.iv return _bytes - def _experimental_bytearray(self, _bytes): + def _experimental_bytearray(self, _bytes: bytearray) -> bytearray: if self.specifier == String2KeyType.GNUExtension: _bytes += b'\x00GNU' _bytes.append(self.gnuext) @@ -895,13 +901,13 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: del packet[0] if bool(self): - self.encalg = packet[0] + self.encalg = SymmetricKeyAlgorithm(packet[0]) del packet[0] self.specifier = packet[0] del packet[0] - if self.specifier == String2KeyType.GNUExtension: + if self.specifier is String2KeyType.GNUExtension: return self._experimental_parse(packet, iv) if self.specifier >= String2KeyType.Simple: @@ -913,7 +919,7 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: self.salt = packet[:8] del packet[:8] - if self.specifier == String2KeyType.Iterated: + if self.specifier is String2KeyType.Iterated: self.count = packet[0] del packet[0] @@ -921,7 +927,7 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: self.iv = packet[:(self.encalg.block_size // 8)] del packet[:(self.encalg.block_size // 8)] - def _experimental_parse(self, packet, iv=True): + def _experimental_parse(self, packet: bytearray, iv: bool = True) -> None: """ https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=3046523da62c576cf6a765a8b0829876cfdc6b3b;hb=b0f0791e4ade845b2a0e2a94dbda4f3bf1ceb039#l1346 @@ -957,7 +963,7 @@ def _experimental_parse(self, packet, iv=True): self.scserial = packet[:slen] del packet[:slen] - def derive_key(self, passphrase): + def derive_key(self, passphrase: Union[str, bytes]) -> bytes: ##TODO: raise an exception if self.usage is not 254 or 255 keylen = self.encalg.key_size hashlen = self.halg.digest_size * 8 From 6e225cd10921cb372a336674f8848f163c6f2e1b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Apr 2023 17:52:57 -0400 Subject: [PATCH 128/287] Each public key implementation should know what "pubkey algo" it uses --- pgpy/packet/fields.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 7601e37a..dae09426 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -326,6 +326,7 @@ def __sig__(self): class PubKey(MPIs): __pubfields__: Tuple = () + __pubkey_algo__: Optional[PubKeyAlgorithm] = None @property def __mpis__(self): @@ -382,6 +383,7 @@ def parse(self, packet): class RSAPub(PubKey): __pubfields__ = ('n', 'e') + __pubkey_algo__ = PubKeyAlgorithm.RSAEncryptOrSign def __pubkey__(self): return rsa.RSAPublicNumbers(self.e, self.n).public_key() @@ -402,6 +404,7 @@ def parse(self, packet): class DSAPub(PubKey): __pubfields__ = ('p', 'q', 'g', 'y') + __pubkey_algo__ = PubKeyAlgorithm.DSA def __pubkey__(self): params = dsa.DSAParameterNumbers(self.p, self.q, self.g) @@ -423,6 +426,7 @@ def parse(self, packet): class ElGPub(PubKey): __pubfields__ = ('p', 'g', 'y') + __pubkey_algo__ = PubKeyAlgorithm.ElGamal def __pubkey__(self): raise NotImplementedError() @@ -499,6 +503,7 @@ def __copy__(self): class ECDSAPub(PubKey): __pubfields__ = ('p',) + __pubkey_algo__ = PubKeyAlgorithm.ECDSA def __init__(self): super().__init__() @@ -541,6 +546,7 @@ def parse(self, packet): class EdDSAPub(PubKey): __pubfields__ = ('p', ) + __pubkey_algo__ = PubKeyAlgorithm.EdDSA def __init__(self): super().__init__() @@ -588,6 +594,7 @@ def parse(self, packet): class ECDHPub(PubKey): __pubfields__ = ('p',) + __pubkey_algo__ = PubKeyAlgorithm.ECDH def __init__(self): super().__init__() From 0bd8c1a3943ffe69e54bfe4dc73f52ca33c59bd8 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 12 May 2023 12:32:58 -0400 Subject: [PATCH 129/287] Overhaul dispatch mechanism to use Enums instead of ints This gives clearer semantics to any reader, and puts the mappings of magic numbers all in pgpy/constants.py instead of scattered throughout the codebase. It also improves type annotation coverage. --- pgpy/packet/packets.py | 35 ++++++++--------- pgpy/packet/subpackets/signature.py | 51 +++++++++++++------------ pgpy/packet/subpackets/types.py | 11 ++++-- pgpy/packet/subpackets/userattribute.py | 3 +- pgpy/packet/types.py | 6 ++- pgpy/types.py | 21 +++++----- 6 files changed, 69 insertions(+), 58 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 05ea1f77..f886b1fc 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -37,6 +37,7 @@ from .types import Sub from .types import VersionedPacket +from ..constants import PacketTag from ..constants import CompressionAlgorithm from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm @@ -83,7 +84,7 @@ class PKESessionKey(VersionedPacket): - __typeid__ = 0x01 + __typeid__ = PacketTag.PublicKeyEncryptedSessionKey __ver__ = 0 # note that we don't have a good type signature for pk: it should be PrivKey, but from .fields, not the PrivKey in this file. @@ -299,7 +300,7 @@ def parse(self, packet): class Signature(VersionedPacket): - __typeid__ = 0x02 + __typeid__ = PacketTag.Signature __ver__ = 0 @abc.abstractmethod @@ -534,7 +535,7 @@ def make_onepass(self) -> 'OnePassSignatureV3': class SKESessionKey(VersionedPacket): - __typeid__ = 0x03 + __typeid__ = PacketTag.SymmetricKeyEncryptedSessionKey __ver__ = 0 # FIXME: the type signature for this function is awkward because @@ -671,7 +672,7 @@ def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString): class OnePassSignature(VersionedPacket): - __typeid__ = 0x04 + __typeid__ = PacketTag.OnePassSignature __ver__ = 0 @@ -802,12 +803,12 @@ def parse(self, packet): class PrivKey(VersionedPacket, Primary, Private): - __typeid__ = 0x05 + __typeid__ = PacketTag.SecretKey __ver__ = 0 class PubKey(VersionedPacket, Primary, Public): - __typeid__ = 0x06 + __typeid__ = PacketTag.PublicKey __ver__ = 0 @abc.abstractproperty @@ -1005,7 +1006,7 @@ def sign(self, sigdata, hash_alg): class PrivSubKey(VersionedPacket, Sub, Private): - __typeid__ = 0x07 + __typeid__ = PacketTag.SecretSubKey __ver__ = 0 @@ -1043,7 +1044,7 @@ class CompressedData(Packet): BZip2-compressed packets are compressed using the BZip2 [BZ2] algorithm. """ - __typeid__ = 0x08 + __typeid__ = PacketTag.CompressedData @sdproperty def calg(self): @@ -1127,7 +1128,7 @@ class SKEData(Packet): incorrect. See the "Security Considerations" section for hints on the proper use of this "quick check". """ - __typeid__ = 0x09 + __typeid__ = PacketTag.SymmetricallyEncryptedData def __init__(self): super().__init__() @@ -1171,7 +1172,7 @@ def decrypt(self, key, alg): # pragma: no cover class Marker(Packet): - __typeid__ = 0x0a + __typeid__ = PacketTag.Marker def __init__(self): super().__init__() @@ -1235,7 +1236,7 @@ class LiteralData(Packet): normal line endings). These should be converted to native line endings by the receiving software. """ - __typeid__ = 0x0B + __typeid__ = PacketTag.LiteralData @sdproperty def mtime(self): @@ -1326,7 +1327,7 @@ class Trust(Packet): transferred to other users, and they SHOULD be ignored on any input other than local keyring files. """ - __typeid__ = 0x0C + __typeid__ = PacketTag.Trust @sdproperty def trustlevel(self): @@ -1380,7 +1381,7 @@ class UserID(Packet): restrictions on its content. The packet length in the header specifies the length of the User ID. """ - __typeid__ = 0x0D + __typeid__ = PacketTag.UserID def __init__(self, uid=""): super().__init__() @@ -1415,7 +1416,7 @@ def parse(self, packet): class PubSubKey(VersionedPacket, Sub, Public): - __typeid__ = 0x0E + __typeid__ = PacketTag.PublicSubKey __ver__ = 0 @@ -1456,7 +1457,7 @@ class UserAttribute(Packet): not recognize. Subpacket types 100 through 110 are reserved for private or experimental use. """ - __typeid__ = 0x11 + __typeid__ = PacketTag.UserAttribute @property def image(self): @@ -1487,7 +1488,7 @@ def update_hlen(self): class IntegrityProtectedSKEData(VersionedPacket): - __typeid__ = 0x12 + __typeid__ = PacketTag.SymmetricallyEncryptedIntegrityProtectedData __ver__ = 0 @@ -1673,7 +1674,7 @@ class MDC(Packet): in the data hash. While this is a bit restrictive, it reduces complexity. """ - __typeid__ = 0x13 + __typeid__ = PacketTag.ModificationDetectionCode def __init__(self): super().__init__() diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 64c9841e..fee53219 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -24,6 +24,7 @@ from ...constants import PubKeyAlgorithm from ...constants import RevocationKeyClass from ...constants import RevocationReason +from ...constants import SigSubpacketType from ...constants import SymmetricKeyAlgorithm from ...decorators import sdproperty @@ -218,7 +219,7 @@ class CreationTime(Signature): MUST be present in the hashed area. """ - __typeid__ = 0x02 + __typeid__ = SigSubpacketType.CreationTime @sdproperty def created(self): @@ -263,7 +264,7 @@ class SignatureExpirationTime(Signature): after the signature creation time that the signature expires. If this is not present or has a value of zero, it never expires. """ - __typeid__ = 0x03 + __typeid__ = SigSubpacketType.SigExpirationTime @sdproperty def expires(self): @@ -326,7 +327,7 @@ class ExportableCertification(Boolean): (for example, a key server). Such implementations always trim local certifications from any key they handle. """ - __typeid__ = 0x04 + __typeid__ = SigSubpacketType.ExportableCertification class TrustSignature(Signature): @@ -348,7 +349,7 @@ class TrustSignature(Signature): greater indicate complete trust. Implementations SHOULD emit values of 60 for partial trust and 120 for complete trust. """ - __typeid__ = 0x05 + __typeid__ = SigSubpacketType.TrustSignature @sdproperty def level(self): @@ -408,7 +409,7 @@ class RegularExpression(Signature): "almost public domain" regular expression [REGEX] package. A description of the syntax is found in Section 8 below. """ - __typeid__ = 0x06 + __typeid__ = SigSubpacketType.RegularExpression @sdproperty def regex(self): @@ -450,7 +451,7 @@ class Revocable(Boolean): signature for the life of his key. If this packet is not present, the signature is revocable. """ - __typeid__ = 0x07 + __typeid__ = SigSubpacketType.Revocable class KeyExpirationTime(SignatureExpirationTime): @@ -464,7 +465,7 @@ class KeyExpirationTime(SignatureExpirationTime): or has a value of zero, the key never expires. This is found only on a self-signature. """ - __typeid__ = 0x09 + __typeid__ = SigSubpacketType.KeyExpirationTime class PreferredSymmetricAlgorithms(FlagList): @@ -480,7 +481,7 @@ class PreferredSymmetricAlgorithms(FlagList): Algorithm numbers are in Section 9. This is only found on a self- signature. """ - __typeid__ = 0x0B + __typeid__ = SigSubpacketType.PreferredSymmetricAlgorithms __flags__ = SymmetricKeyAlgorithm @@ -507,7 +508,7 @@ class RevocationKey(Signature): isolate this subpacket within a separate signature so that it is not combined with other subpackets that need to be exported. """ - __typeid__ = 0x0C + __typeid__ = SigSubpacketType.RevocationKey @sdproperty def keyclass(self): @@ -576,7 +577,7 @@ def parse(self, packet): class Issuer(Signature): - __typeid__ = 0x10 + __typeid__ = SigSubpacketType.IssuerKeyID @sdproperty def issuer(self): @@ -602,7 +603,7 @@ def parse(self, packet): class NotationData(Signature): - __typeid__ = 0x14 + __typeid__ = SigSubpacketType.NotationData @sdproperty def flags(self): @@ -679,26 +680,26 @@ def parse(self, packet): class PreferredHashAlgorithms(FlagList): - __typeid__ = 0x15 + __typeid__ = SigSubpacketType.PreferredHashAlgorithms __flags__ = HashAlgorithm class PreferredCompressionAlgorithms(FlagList): - __typeid__ = 0x16 + __typeid__ = SigSubpacketType.PreferredCompressionAlgorithms __flags__ = CompressionAlgorithm class KeyServerPreferences(ByteFlag): - __typeid__ = 0x17 + __typeid__ = SigSubpacketType.KeyServerPreferences __flags__ = _KeyServerPreferences class PreferredKeyServer(URI): - __typeid__ = 0x18 + __typeid__ = SigSubpacketType.PreferredKeyServer class PrimaryUserID(Signature): - __typeid__ = 0x19 + __typeid__ = SigSubpacketType.PrimaryUserID @sdproperty def primary(self): @@ -731,16 +732,16 @@ def parse(self, packet): class Policy(URI): - __typeid__ = 0x1a + __typeid__ = SigSubpacketType.PolicyURI class KeyFlags(ByteFlag): - __typeid__ = 0x1B + __typeid__ = SigSubpacketType.KeyFlags __flags__ = _KeyFlags class SignersUserID(Signature): - __typeid__ = 0x1C + __typeid__ = SigSubpacketType.SignersUserID @sdproperty def userid(self): @@ -770,7 +771,7 @@ def parse(self, packet): class ReasonForRevocation(Signature): - __typeid__ = 0x1D + __typeid__ = SigSubpacketType.ReasonForRevocation @sdproperty def code(self): @@ -817,7 +818,7 @@ def parse(self, packet): class Features(ByteFlag): - __typeid__ = 0x1E + __typeid__ = SigSubpacketType.Features __flags__ = _Features @@ -825,7 +826,7 @@ class Features(ByteFlag): class EmbeddedSignature(Signature): - __typeid__ = 0x20 + __typeid__ = SigSubpacketType.EmbeddedSignature @sdproperty def _sig(self): @@ -901,7 +902,7 @@ class IssuerFingerprint(Signature): does not match the signature version, the receiving implementation MUST treat it as a malformed signature (see Section 5.2.5). ''' - __typeid__ = 0x21 + __typeid__ = SigSubpacketType.IssuerFingerprint def __init__(self) -> None: super(IssuerFingerprint, self).__init__() @@ -958,7 +959,7 @@ class IntendedRecipient(Signature): An implementation SHOULD generate this subpacket when creating a signed and encrypted message. ''' - __typeid__ = 0x23 + __typeid__ = SigSubpacketType.IntendedRecipientFingerprint def __init__(self) -> None: super(IntendedRecipient, self).__init__() @@ -1066,7 +1067,7 @@ class AttestedCertifications(Signature): key holder needs only to publish a more recent Attestation Key Signature with an empty Attested Certifications subpacket. ''' - __typeid__ = 0x25 + __typeid__ = SigSubpacketType.AttestedCertifications @sdproperty def attested_certifications(self): diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index c7802842..29082579 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -2,7 +2,10 @@ """ import abc -from typing import Optional +from typing import Optional, Union + +from ...constants import SigSubpacketType +from ...constants import AttributeType from ..types import VersionedHeader @@ -82,7 +85,7 @@ def __init__(self): if ( self.header.typeid == -1 and (not hasattr(self.__typeid__, '__abstractmethod__')) - and (self.__typeid__ not in {-1, None}) + and (self.__typeid__ is not None) ): self.header.typeid = self.__typeid__ @@ -105,11 +108,11 @@ def parse(self, packet): # pragma: no cover class Signature(SubPacket): - __typeid__: Optional[int] = -1 + __typeid__: Optional[SigSubpacketType] = None class UserAttribute(SubPacket): - __typeid__: Optional[int] = -1 + __typeid__: Optional[AttributeType] = None class Opaque(Signature, UserAttribute): diff --git a/pgpy/packet/subpackets/userattribute.py b/pgpy/packet/subpackets/userattribute.py index dec7150f..72ff8c31 100644 --- a/pgpy/packet/subpackets/userattribute.py +++ b/pgpy/packet/subpackets/userattribute.py @@ -4,6 +4,7 @@ from .types import UserAttribute +from ...constants import AttributeType from ...constants import ImageEncoding from ...decorators import sdproperty @@ -46,7 +47,7 @@ class Image(UserAttribute): version of the image header or if a specified encoding format value is not recognized. """ - __typeid__ = 0x01 + __typeid__ = AttributeType.Image @sdproperty def version(self): diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index fd629cab..2d59f640 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -4,12 +4,13 @@ import abc import copy -from typing import Optional, Tuple +from typing import Optional, Tuple, Union from ..constants import PacketTag from ..decorators import sdproperty +from ..types import DispatchGuidance from ..types import Dispatchable from ..types import Field from ..types import Header as _Header @@ -144,7 +145,7 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): - __typeid__: Optional[int] = -1 + __typeid__: Optional[Union[PacketTag, DispatchGuidance]] = None __headercls__ = Header def __init__(self, _=None): @@ -173,6 +174,7 @@ def parse(self, packet): class VersionedPacket(Packet): + __typeid__: Union[PacketTag, DispatchGuidance] = DispatchGuidance.NoDispatch __headercls__ = VersionedHeader def __init__(self): diff --git a/pgpy/types.py b/pgpy/types.py index ccb42203..0d1f766d 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -32,6 +32,7 @@ 'Header', 'MetaDispatchable', 'Dispatchable', + 'DispatchGuidance', 'SignatureVerification', 'SorteDeque'] @@ -434,6 +435,11 @@ def __init__(self): self._partial = False +class DispatchGuidance(IntEnum): + "Identify classes that should be left alone by PGPy's internal dispatch mechanism" + NoDispatch = -1 + + class MetaDispatchable(abc.ABCMeta): """ MetaDispatchable is a metaclass for objects that subclass Dispatchable @@ -445,9 +451,9 @@ class MetaDispatchable(abc.ABCMeta): A RootClass is successfully registered if the following things are true: - it inherits (directly or indirectly) from Dispatchable - - __typeid__ == -1 + - __typeid__ is None """ - _registry: Dict[Union[Tuple[Type, int], Tuple[Type, int, int]], Type] = {} + _registry: Dict[Union[Tuple[Type, Optional[IntEnum]], Tuple[Type, IntEnum, int]], Type] = {} """ _registry is the Dispatchable class registry. It uses the following format: @@ -482,12 +488,12 @@ class MetaDispatchable(abc.ABCMeta): def __new__(mcs, name, bases, attrs): # NOQA ncls = super().__new__(mcs, name, bases, attrs) - if not hasattr(ncls.__typeid__, '__isabstractmethod__'): - if ncls.__typeid__ == -1 and not issubclass(ncls, tuple(MetaDispatchable._roots)): + if ncls.__typeid__ is not DispatchGuidance.NoDispatch: + if ncls.__typeid__ is None and not issubclass(ncls, tuple(MetaDispatchable._roots)): # this is a root class MetaDispatchable._roots.add(ncls) - elif issubclass(ncls, tuple(MetaDispatchable._roots)) and ncls.__typeid__ != -1: + elif issubclass(ncls, tuple(MetaDispatchable._roots)): for rcls in (root for root in MetaDispatchable._roots if issubclass(ncls, root)): if (rcls, ncls.__typeid__) not in MetaDispatchable._registry: MetaDispatchable._registry[(rcls, ncls.__typeid__)] = ncls @@ -561,15 +567,12 @@ def _makeobj(cls): class Dispatchable(PGPObject, metaclass=MetaDispatchable): + __typeid__: Optional[IntEnum] = DispatchGuidance.NoDispatch @abc.abstractproperty def __headercls__(self): # pragma: no cover return False - @abc.abstractproperty - def __typeid__(self): # pragma: no cover - return False - __ver__: Optional[int] = None From 86aa0556d34f1e2b74964add7c5a29ab16b0170b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 11 May 2023 16:42:50 -0400 Subject: [PATCH 130/287] Drop check for __typeid__.__abstractmethod__ this was introduced in 12d085a51074c9443da2c0da00caf443e9204318 and it's not clear why it needs to be there. __typeid__ is typically an Optional[int] (or Optional[IntEnum]), so i can't tell what this check would prevent --- pgpy/packet/subpackets/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 29082579..35047d5a 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -84,7 +84,6 @@ def __init__(self): if ( self.header.typeid == -1 - and (not hasattr(self.__typeid__, '__abstractmethod__')) and (self.__typeid__ is not None) ): self.header.typeid = self.__typeid__ From 64630b45581540236b651181a1d2b8bfb0c987c0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 13 May 2023 03:30:27 -0400 Subject: [PATCH 131/287] Consolidate fingerprint-bearing subpackets IntendedRecipient and IssuerFingerprint subpackets have the same wire formats, just different semantics. Consolidate the codebase here. Note that a freshly-created subpacket like this will emit an all-zeros fingerprint if you ask it for one, but will refuse to render itself into a wireformat. --- pgpy/packet/subpackets/signature.py | 104 ++++++++++++---------------- pgpy/pgp.py | 2 +- 2 files changed, 47 insertions(+), 59 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index fee53219..2ba6a014 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -36,6 +36,7 @@ 'FlagList', 'ByteFlag', 'Boolean', + 'FingerprintSubpacket', 'CreationTime', 'SignatureExpirationTime', 'ExportableCertification', @@ -209,6 +210,42 @@ def parse(self, packet): del packet[:1] +class FingerprintSubpacket(Signature): + def __init__(self) -> None: + super().__init__() + self._fpr: Optional[Fingerprint] = None + + @sdproperty + def fingerprint(self) -> Fingerprint: + if self._fpr is None: + # if we raise an exception here, then hasattr(fingerprint) will raise an exception as well. + return Fingerprint(b'\x00' * 20, version=4) + return self._fpr + + @fingerprint.register + def fingerprint_set(self, val: Union[str, bytes, bytearray]) -> None: + if not isinstance(val, Fingerprint): + val = Fingerprint(val) + self._fpr = val + + def __bytearray__(self) -> bytearray: + if self._fpr is None: + raise ValueError(f"Tried to write out the fingerprint from {self.__class__.__name__} before it was set") + _bytes = super().__bytearray__() + _bytes += self.fingerprint.__wireformat__() + return _bytes + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + version: int = packet[0] + del packet[0] + + fpr_len = self.header.length - 2 + + self.fingerprint = Fingerprint(bytes(packet[:fpr_len]), version) + del packet[:fpr_len] + + class CreationTime(Signature): """ 5.2.3.4. Signature Creation Time @@ -882,7 +919,7 @@ def parse(self, packet): self._sig.parse(packet) -class IssuerFingerprint(Signature): +class IssuerFingerprint(FingerprintSubpacket): '''(from crypto-refresh-07) 5.2.3.35. Issuer Fingerprint @@ -904,42 +941,16 @@ class IssuerFingerprint(Signature): ''' __typeid__ = SigSubpacketType.IssuerFingerprint - def __init__(self) -> None: - super(IssuerFingerprint, self).__init__() - self._issuer_fpr: Optional[Fingerprint] = None - @sdproperty - def issuer_fingerprint(self) -> Optional[Fingerprint]: - return self._issuer_fpr - - @issuer_fingerprint.register - def issuer_fingerprint_set(self, val: Union[str, bytearray, bytes, Fingerprint]) -> None: - if isinstance(val, Fingerprint): - self._issuer_fpr = val - else: - self._issuer_fpr = Fingerprint(val) + def issuer_fingerprint(self) -> Fingerprint: + return self.fingerprint @issuer_fingerprint.register - def issuer_fingerprint_bytearray(self, val: bytearray) -> None: - self.issuer_fingerprint = Fingerprint(''.join('{:02x}'.format(c) for c in val).upper()) - - def __bytearray__(self) -> bytearray: - _bytes = super().__bytearray__() - _bytes += self.issuer_fingerprint.__wireformat__() - return _bytes - - def parse(self, packet: bytearray) -> None: - super().parse(packet) - version = packet[0] - del packet[0] - - fpr_len = self.header.length - 2 - - self.issuer_fingerprint = Fingerprint(packet[:fpr_len], version) - del packet[:fpr_len] + def issuer_fingerprint_set(self, val: Union[str, bytes, bytearray]) -> None: + self.fingerprint = val -class IntendedRecipient(Signature): +class IntendedRecipient(IssuerFingerprint): '''(from crypto-refresh-08) 5.2.3.36. Intended Recipient Fingerprint @@ -961,36 +972,13 @@ class IntendedRecipient(Signature): ''' __typeid__ = SigSubpacketType.IntendedRecipientFingerprint - def __init__(self) -> None: - super(IntendedRecipient, self).__init__() - self._intended_recipient: Optional[Fingerprint] = None - @sdproperty def intended_recipient(self) -> Fingerprint: - if self._intended_recipient is None: - raise ValueError("tried to access the intended recipient fingerprint before it was set") - return self._intended_recipient + return self.fingerprint @intended_recipient.register - def intended_recipient_set(self, val: Union[str, bytearray, bytes, Fingerprint]) -> None: - if not isinstance(val, Fingerprint): - val = Fingerprint(val) - self._intended_recipient = val - - def __bytearray__(self) -> bytearray: - _bytes = super().__bytearray__() - _bytes += self.intended_recipient.__wireformat__() - return _bytes - - def parse(self, packet: bytearray) -> None: - super().parse(packet) - version: int = packet[0] - del packet[0] - - fpr_len = self.header.length - 2 - - self.intended_recipient = Fingerprint(packet[:fpr_len], version=version) - del packet[:fpr_len] + def intended_recipient_set(self, val: Union[str, bytes, bytearray]) -> None: + self.fingerprint = val class AttestedCertifications(Signature): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 9834b341..b0f4af6d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2042,7 +2042,7 @@ def _sign(self, subject, sig, **prefs): if prefs.pop('include_issuer_fingerprint', True): if isinstance(self._key, PrivKeyV4): - sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _version=4, _issuer_fpr=self.fingerprint) + sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _version=4, _fpr=self.fingerprint) sigdata = sig.hashdata(subject) h2 = sig.hash_algorithm.hasher From e3691aea5eb1e076191285c30db8bb4f1f9023b3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 22:59:06 -0500 Subject: [PATCH 132/287] place KeyFlags ("usage") subpacket in order with other subpackets placing subpackets in ascending order of subpacket id makes PGPy-generated certificates and signatures less distinguishable from other implementations. --- pgpy/pgp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b0f4af6d..1b4dc364 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2187,9 +2187,6 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): usage = prefs.pop('usage', None) exportable = prefs.pop('exportable', None) - if usage is not None: - sig._signature.subpackets.addnew('KeyFlags', hashed=True, flags=usage) - if exportable is not None: sig._signature.subpackets.addnew('ExportableCertification', hashed=True, bflag=exportable) @@ -2230,6 +2227,9 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): if compression_prefs is not None: sig._signature.subpackets.addnew('PreferredCompressionAlgorithms', hashed=True, flags=compression_prefs) + if usage is not None: + sig._signature.subpackets.addnew('KeyFlags', hashed=True, flags=usage) + if keyserver_flags is not None: sig._signature.subpackets.addnew('KeyServerPreferences', hashed=True, flags=keyserver_flags) From 72b77fe99f1fa3845401229d563698aacee669d8 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 23:02:25 -0500 Subject: [PATCH 133/287] Allow the user to explicitly set `features` flags This permits generation of packets that advertise support that PGPy can't provide so it's dangerous to use. --- pgpy/pgp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 1b4dc364..918ac279 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2206,6 +2206,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): keyserver = prefs.pop('keyserver', None) primary_uid = prefs.pop('primary', None) attested_certifications = prefs.pop('attested_certifications', []) + features = prefs.pop('features', Features.pgpy_features) if key_expires is not None: # key expires should be a timedelta, so if it's a datetime, turn it into a timedelta @@ -2244,7 +2245,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): SignatureType.CertRevocation} # Features is always set on certifications: if sig._signature.sigtype in cert_sigtypes: - sig._signature.subpackets.addnew('Features', hashed=True, flags=Features.pgpy_features) + sig._signature.subpackets.addnew('Features', hashed=True, flags=features) # If this is an attestation, then we must include a Attested Certifications subpacket: if sig._signature.sigtype == SignatureType.Attestation: From 1fe2a14f37acd2adfbd2ee008f08a48ea4473602 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 23:01:02 -0500 Subject: [PATCH 134/287] Ensure feature flags can be placed on Direct Key signatures --- pgpy/pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 918ac279..0765cab1 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2242,7 +2242,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): cert_sigtypes = {SignatureType.Generic_Cert, SignatureType.Persona_Cert, SignatureType.Casual_Cert, SignatureType.Positive_Cert, - SignatureType.CertRevocation} + SignatureType.CertRevocation, SignatureType.DirectlyOnKey} # Features is always set on certifications: if sig._signature.sigtype in cert_sigtypes: sig._signature.subpackets.addnew('Features', hashed=True, flags=features) From a2c1317727a93262245aac892ec9db469b31ec5a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 7 Mar 2023 11:22:39 -0500 Subject: [PATCH 135/287] When adding a subpacket to the hashed region, remove it from unhashed If we're signing specific subpacket content, there's no reason to include the same subpacket in the unhashed area; and if the content disagrees, there's no reason why any implementation should prefer the unhashed data over the hashed data. --- pgpy/packet/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index dae09426..e142a06c 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -182,7 +182,10 @@ def addnew(self, spname, hashed=False, **kwargs): nsp.update_hlen() if hashed: self['h_' + spname] = nsp - + # remove unhashed version of this subpacket -- we do not want a conflict + unhashed_subpackets = list(filter(lambda x: x[0] == spname, self._unhashed_sp.keys())) + for unhashed_subpacket in unhashed_subpackets: + del self._unhashed_sp[unhashed_subpacket] else: self[spname] = nsp From ff345c1aec2931f12d92708d06bfb1dc695bd829 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 7 Mar 2023 11:23:32 -0500 Subject: [PATCH 136/287] Create normalized signatures by sorting subpackets by type ID --- pgpy/packet/fields.py | 10 ++++++++++ pgpy/pgp.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index e142a06c..d495f3d3 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -193,6 +193,16 @@ def update_hlen(self): for sp in self: sp.update_hlen() + def _normalize(self) -> None: + '''Order subpackets by subpacket tag number + + This private interface must only be called a Subpackets object + before it is signed, otherwise it will break the signature + + ''' + self._hashed_sp = collections.OrderedDict(sorted(self._hashed_sp.items(), key=lambda x: (x[1].__typeid__, x[0][1]))) + self._unhashed_sp = collections.OrderedDict(sorted(self._unhashed_sp.items(), key=lambda x: (x[1].__typeid__, x[0][1]))) + def parse(self, packet): hl = self.bytes_to_int(packet[:2]) del packet[:2] diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 0765cab1..9ce0dd5c 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2044,6 +2044,9 @@ def _sign(self, subject, sig, **prefs): if isinstance(self._key, PrivKeyV4): sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _version=4, _fpr=self.fingerprint) + # place the subpackets in order by the subpacket type identifier octet + sig._signature.subpackets._normalize() + sigdata = sig.hashdata(subject) h2 = sig.hash_algorithm.hasher h2.update(sigdata) From 3efde16053f1c2fd8c5fb32d61ddbe20b0100734 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 7 Mar 2023 14:37:25 -0500 Subject: [PATCH 137/287] string representation of subpacket should report when it is critical --- pgpy/packet/subpackets/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 35047d5a..a802a924 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -95,7 +95,7 @@ def __len__(self): return (self.header.llen + self.header.length) def __repr__(self): - return "<{} [0x{:02x}] at 0x{:x}>".format(self.__class__.__name__, self.header.typeid, id(self)) + return "<{} [0x{:02x}] {}at 0x{:x}>".format(self.__class__.__name__, self.header.typeid, 'critical! ' if self.header.critical else '', id(self)) def update_hlen(self): self.header.length = (len(self.__bytearray__()) - len(self.header)) + 1 From 717d240e009ed81538d394e06665ff3f8a73b94f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 7 Mar 2023 14:37:55 -0500 Subject: [PATCH 138/287] Make it possible to add a critical subpacket --- pgpy/packet/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index d495f3d3..ae2ce38d 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -174,8 +174,10 @@ def __copy__(self): return sp - def addnew(self, spname, hashed=False, **kwargs): + def addnew(self, spname: str, hashed: bool = False, critical: bool = False, **kwargs) -> None: nsp = getattr(self._spmodule, spname)() + if critical: + nsp.header.critical = True for p, v in kwargs.items(): if hasattr(nsp, p): setattr(nsp, p, v) From fb318ab74382bf488df88e9bafa9009cf2b0d82b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 7 Mar 2023 14:38:29 -0500 Subject: [PATCH 139/287] Make CreationTime and KeyFlags subpackets critical by default --- pgpy/pgp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 9ce0dd5c..2cc71844 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -331,7 +331,7 @@ def new(cls, sigtype, pkalg, halg, signer, created=None): sigpkt = SignatureV4() sigpkt.header.tag = 2 sigpkt.header.version = 4 - sigpkt.subpackets.addnew('CreationTime', hashed=True, created=created) + sigpkt.subpackets.addnew('CreationTime', critical=True, hashed=True, created=created) sigpkt.subpackets.addnew('Issuer', _issuer=signer) sigpkt.sigtype = sigtype @@ -2232,7 +2232,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): sig._signature.subpackets.addnew('PreferredCompressionAlgorithms', hashed=True, flags=compression_prefs) if usage is not None: - sig._signature.subpackets.addnew('KeyFlags', hashed=True, flags=usage) + sig._signature.subpackets.addnew('KeyFlags', hashed=True, critical=True, flags=usage) if keyserver_flags is not None: sig._signature.subpackets.addnew('KeyServerPreferences', hashed=True, flags=keyserver_flags) @@ -2394,7 +2394,7 @@ def bind(self, key, **prefs): usage = prefs.pop('usage', None) if usage is not None: - sig._signature.subpackets.addnew('KeyFlags', hashed=True, flags=usage) + sig._signature.subpackets.addnew('KeyFlags', hashed=True, critical=True, flags=usage) crosssig = None # if possible, have the subkey create a primary key binding signature From bb17b593aca1856d428d16a773633ae883346ae0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Feb 2023 16:43:42 -0500 Subject: [PATCH 140/287] when algorithm IDs don't need MPIs, enable a different way to emit them. This sets the stage for newer pubkey algorithms that don't use the MPI wire format construct. --- pgpy/packet/fields.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index ae2ce38d..a69163ae 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1127,6 +1127,11 @@ def __init__(self): for field in self.__privfields__: setattr(self, field, MPI(0)) + def _append_private_fields(self, _bytes: bytearray) -> None: + '''override this function if the private fields are not MPIs''' + for field in self.__privfields__: + _bytes += getattr(self, field).to_mpibytes() + def __bytearray__(self): _bytes = bytearray() _bytes += super().__bytearray__() @@ -1136,8 +1141,7 @@ def __bytearray__(self): _bytes += self.encbytes else: - for field in self.__privfields__: - _bytes += getattr(self, field).to_mpibytes() + self._append_private_fields(_bytes) if self.s2k.usage is S2KUsage.Unprotected: _bytes += self.chksum @@ -1191,8 +1195,7 @@ def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): del passphrase pt = bytearray() - for pf in self.__privfields__: - pt += getattr(self, pf).to_mpibytes() + self._append_private_fields(pt) # append a SHA-1 hash of the plaintext so far to the plaintext pt += HashAlgorithm.SHA1.digest(pt) From 7b83b7e50ca84b5db46ba32f92951a3ff75a80a6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 00:01:36 -0400 Subject: [PATCH 141/287] PGPKey.userids always returns a list of PGPUIDs --- pgpy/pgp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 2cc71844..394e2129 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -17,7 +17,7 @@ from datetime import datetime, timezone -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Deque, List, Mapping, Optional, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -1621,7 +1621,7 @@ def subkeys(self): return self._children @property - def userids(self): + def userids(self) -> List[PGPUID]: """A ``list`` of :py:obj:`PGPUID` objects containing User ID information about this key""" return [ u for u in self._uids if u.is_uid ] @@ -1666,7 +1666,7 @@ def new(cls, key_algorithm, key_size, created=None): return key - def __init__(self): + def __init__(self) -> None: """ PGPKey objects represent OpenPGP compliant keys along with all of their associated data. @@ -1681,9 +1681,9 @@ def __init__(self): """ super().__init__() self._key = None - self._children = collections.OrderedDict() + self._children: Mapping[bytes, PGPKey] = collections.OrderedDict() self._signatures = SorteDeque() - self._uids = SorteDeque() + self._uids: Deque[PGPUID] = SorteDeque() self._sibling = None self._self_verified = None self._require_usage_flags = True From 5755e6dfa168db2e1c84f9cdbaa0695b87cc583c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 19:12:36 -0400 Subject: [PATCH 142/287] constants.py: add type annotations --- pgpy/constants.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index b07117b1..aca65b1e 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -137,7 +137,7 @@ def is_supported(self) -> bool: SymmetricKeyAlgorithm.Camellia256} @property - def is_insecure(self): + def is_insecure(self) -> bool: insecure_ciphers = {SymmetricKeyAlgorithm.IDEA} return self in insecure_ciphers @@ -152,7 +152,7 @@ def block_size(self) -> int: return 128 @property - def key_size(self): + def key_size(self) -> int: ks = {SymmetricKeyAlgorithm.IDEA: 128, SymmetricKeyAlgorithm.TripleDES: 192, SymmetricKeyAlgorithm.CAST5: 128, @@ -170,10 +170,10 @@ def key_size(self): raise NotImplementedError(repr(self)) - def gen_iv(self): + def gen_iv(self) -> bytes: return os.urandom(self.block_size // 8) - def gen_key(self): + def gen_key(self) -> bytes: return os.urandom(self.key_size // 8) @@ -204,7 +204,7 @@ def _missing_(cls, val: object) -> 'PubKeyAlgorithm': return cls.Unknown @property - def can_gen(self): + def can_gen(self) -> bool: return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, @@ -212,20 +212,20 @@ def can_gen(self): PubKeyAlgorithm.EdDSA} @property - def can_encrypt(self): # pragma: no cover + def can_encrypt(self) -> bool: # pragma: no cover return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.ECDH} @property - def can_sign(self): + def can_sign(self) -> bool: return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA} @property - def deprecated(self): + def deprecated(self) -> bool: return self in {PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign} - def validate_params(self, size): + def validate_params(self, size) -> 'SecurityIssues': min_size = MINIMUM_ASYMMETRIC_KEY_LENGTHS.get(self) if min_size is not None: if isinstance(min_size, set): @@ -280,7 +280,7 @@ class CompressionAlgorithm(IntEnum): #: Bzip2 BZ2 = 0x03 - def compress(self, data): + def compress(self, data: bytes) -> bytes: if self is CompressionAlgorithm.Uncompressed: return data @@ -295,7 +295,7 @@ def compress(self, data): raise NotImplementedError(self) - def decompress(self, data): + def decompress(self, data: bytes) -> bytes: if self is CompressionAlgorithm.Uncompressed: return data @@ -345,19 +345,19 @@ def digest_size(self) -> int: return getattr(hashes, self.name).digest_size @property - def is_supported(self): + def is_supported(self) -> bool: return True @property - def is_second_preimage_resistant(self): + def is_second_preimage_resistant(self) -> bool: return self in {HashAlgorithm.SHA1} @property - def is_collision_resistant(self): + def is_collision_resistant(self) -> bool: return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512} @property - def is_considered_secure(self): + def is_considered_secure(self) -> 'SecurityIssues': if self.is_collision_resistant: return SecurityIssues.OK @@ -557,7 +557,7 @@ class ImageEncoding(IntEnum): JPEG = 0x01 @classmethod - def encodingof(cls, imagebytes): + def encodingof(cls, imagebytes: bytes) -> 'ImageEncoding': if imagebytes[6:10] in (b'JFIF', b'Exif') or imagebytes[:4] == b'\xff\xd8\xff\xdb': return ImageEncoding.JPEG return ImageEncoding.Unknown # pragma: no cover @@ -708,7 +708,7 @@ class Features(FlagEnum): UnknownFeature80 = 0x80 @classproperty - def pgpy_features(cls): + def pgpy_features(cls) -> 'Features': return Features.ModificationDetection @@ -743,7 +743,7 @@ class SecurityIssues(IntFlag): NoSelfSignature = (1 << 10) @property - def causes_signature_verify_to_fail(self): + def causes_signature_verify_to_fail(self) -> bool: return self in { SecurityIssues.WrongSig, SecurityIssues.Expired, From 43db15b7527a603c4b2c15a7e30db2b4e5897f4c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 13 May 2023 05:20:07 -0400 Subject: [PATCH 143/287] PrivKey._generate should make reasonable choices if params are None --- pgpy/packet/fields.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index a69163ae..a254b755 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1170,7 +1170,7 @@ def __privkey__(self): """return the requisite *PrivateKey class from the cryptography library""" @abc.abstractmethod - def _generate(self, key_size_or_oid: Union[int, EllipticCurveOID]) -> None: + def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: """Generate a new PrivKey""" def _compute_chksum(self): @@ -1255,7 +1255,7 @@ class OpaquePrivKey(PrivKey, OpaquePubKey): # pragma: no cover def __privkey__(self): return NotImplemented - def _generate(self, key_size): + def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: # return NotImplemented raise NotImplementedError() @@ -1277,10 +1277,16 @@ def _compute_chksum(self): chs = sum(sum(bytearray(c.to_mpibytes())) for c in (self.d, self.p, self.q, self.u)) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, key_size): + def _generate(self, key_size: Optional[Union[int, EllipticCurveOID]]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("key is already populated") + if key_size is None: # choose a default RSA key size for the user + key_size = 3072 + + if not isinstance(key_size, int): + raise PGPError(f"Did not understand RSA key size {key_size}") + # generate some big numbers! pk = rsa.generate_private_key(65537, key_size) pkn = pk.private_numbers() @@ -1349,10 +1355,16 @@ def _compute_chksum(self): chs = sum(bytearray(self.x.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, key_size): + def _generate(self, key_size: Optional[Union[int, EllipticCurveOID]]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("key is already populated") + if key_size is None: # choose a default DSA key size for the user + key_size = 3072 + + if not isinstance(key_size, int): + raise PGPError(f"Did not understand DSA key size {key_size}") + # generate some big numbers! pk = dsa.generate_private_key(key_size) pkn = pk.private_numbers() @@ -1406,7 +1418,7 @@ def _compute_chksum(self): chs = sum(bytearray(self.x.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, key_size): + def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: raise NotImplementedError(PubKeyAlgorithm.ElGamal) def parse(self, packet): @@ -1445,11 +1457,14 @@ def _compute_chksum(self) -> None: chs = sum(bytearray(self.s.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, params: Union[int, EllipticCurveOID]) -> None: + def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") - if isinstance(params, int): + if params is None: + # select a default ECDSA elliptic curve for the user: + self.oid = EllipticCurveOID.NIST_P256 + elif isinstance(params, int): oid = EllipticCurveOID.from_key_size(params) if oid is None: raise ValueError("No supported Elliptic Curve of size {params}") @@ -1497,11 +1512,13 @@ def _compute_chksum(self): chs = sum(bytearray(self.s.to_mpibytes())) % 65536 self.chksum = bytearray(self.int_to_bytes(chs, 2)) - def _generate(self, params: Union[int, EllipticCurveOID]) -> None: + def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") - if isinstance(params, int): + if params is None: + self.oid = EllipticCurveOID.Ed25519 + elif isinstance(params, int): oid = EllipticCurveOID.from_key_size(params) if oid is None: raise ValueError("No supported Elliptic Curve of size {params}") @@ -1577,8 +1594,10 @@ def __privkey__(self): else: return ECDSAPriv.__privkey__(self) - def _generate(self, params: Union[int, EllipticCurveOID]) -> None: - if isinstance(params, int): + def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: + if params is None: # choose a default curve for the ECDH user + _oid: Optional[EllipticCurveOID] = EllipticCurveOID.Curve25519 + elif isinstance(params, int): _oid = EllipticCurveOID.from_key_size(params) if _oid is None: raise ValueError("No supported Elliptic Curve of size {params}") From 5ed3fc4789bd21456d59fa65e5ea1fb045cb0936 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 15 May 2023 17:31:56 -0400 Subject: [PATCH 144/287] Deprecate short ID Short IDs are only 32-bits long. They are cheap to brute force, while still being unintelligible and difficult to transcribe correctly for humans. --- pgpy/pgp.py | 3 ++- pgpy/types.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 394e2129..89111882 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2828,7 +2828,8 @@ def _add_key(self, pgpkey): # aliases self._add_alias(pgpkey.fingerprint, pkid) self._add_alias(pgpkey.fingerprint.keyid, pkid) - self._add_alias(pgpkey.fingerprint.shortid, pkid) + if pgpkey.fingerprint.version == 4: + self._add_alias(pgpkey.fingerprint.shortid, pkid) for uid in pgpkey.userids: self._add_alias(uid.name, pkid) if uid.comment: diff --git a/pgpy/types.py b/pgpy/types.py index 0d1f766d..cff86388 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -687,7 +687,9 @@ def keyid(self): return self[-16:] @property - def shortid(self): + def shortid(self) -> str: + if self._version != 4: + raise ValueError("shortid can only ever be used on version 4 keys") return self[-8:] @sdproperty @@ -726,7 +728,7 @@ def __eq__(self, other): other = other.replace(' ', '') return any([str(self) == other, self.keyid == other, - self.shortid == other]) + self._version == 4 and self.shortid == other]) return False # pragma: no cover From dad51bb01fbe8a05506f0b46bd72cc02c09dece4 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 20:10:25 -0400 Subject: [PATCH 145/287] String2Key: auto-generate salt --- pgpy/constants.py | 7 +++++++ pgpy/packet/fields.py | 17 ++++++++++++++++- pgpy/packet/packets.py | 5 ++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index aca65b1e..d139a192 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -662,6 +662,13 @@ def _missing_(cls, val: object) -> 'String2KeyType': raise TypeError(f"cannot look up String2KeyType by non-int {type(val)}") return cls.Unknown + @property + def salt_length(self) -> int: + ks = {String2KeyType.Salted: 8, + String2KeyType.Iterated: 8, + } + return ks.get(self, 0) + class S2KGNUExtension(IntEnum): NoSecret = 1 diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index a254b755..6abbf83c 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -833,6 +833,21 @@ def halg_int(self, val: int) -> None: else: self._halg = HashAlgorithm(val) + @sdproperty + def salt(self) -> bytes: + if self._specifier.salt_length == 0: + return b'' + if self._salt is None: + self._salt: Optional[bytes] = os.urandom(self._specifier.salt_length) + return self._salt + @salt.register + def salt_bytes(self, val: Union[bytes, bytearray]) -> None: + if self._specifier.salt_length == 0: + raise ValueError(f"salt cannnot be set for String2KeyType {self._specifier!r}") + if len(val) != self._specifier.salt_length: + raise ValueError(f"salt for String2KeyType {self._specifier!r} should be {self._specifier.salt_length}, not {len(val)}") + self._salt = bytes(val) + @sdproperty def count(self) -> int: return (16 + (self._count & 15)) << ((self._count >> 4) + 6) @@ -855,7 +870,7 @@ def __init__(self) -> None: self._halg = HashAlgorithm.Unknown # salted, iterated - self.salt = bytearray() + self._salt = None # iterated self.count = 0 diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index f886b1fc..5fdccdbb 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -655,9 +655,8 @@ def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricK return symalg, bytes(m) - def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString): - # generate the salt and derive the key to encrypt sk with from it - self.s2k.salt = bytearray(os.urandom(8)) + def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: + # derive the key to encrypt sk with from it (salt will be generated automatically if it is not yet set) esk = self.s2k.derive_key(passphrase) del passphrase From 7204a6475f23dc66a2ad0cb7f0abacf571f47b0b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 27 Mar 2023 09:33:38 +0900 Subject: [PATCH 146/287] Correct tests for String2Key https://datatracker.ietf.org/doc/html/rfc4880#section-3.7.2.1 says: > These are followed by an Initial Vector of the same length as the > block size of the cipher for the decryption of the secret values, if > they are encrypted, and then the secret-key values themselves. But the test used a uniform IV of length 8 -- this corrects the test. --- tests/test_01_packetfields.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_01_packetfields.py b/tests/test_01_packetfields.py index 0803596e..d9437a08 100644 --- a/tests/test_01_packetfields.py +++ b/tests/test_01_packetfields.py @@ -297,7 +297,7 @@ def test_load(self, uasubpacket): # hash algorithm list b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B', ] -_iv = b'\xDE\xAD\xBE\xEF\xDE\xAD\xBE\xEF' +_iv = b'\xB1\x36\x32\x44\x56\x26\x1B\xFD\x54\x7D\xDE\x66\x00\x21\x31\x55' _salt = b'\xC0\xDE\xC0\xDE\xC0\xDE\xC0\xDE' _count = b'\x10' # expands from 0x10 to 2048 _gnu_scserials = [ @@ -308,11 +308,11 @@ def test_load(self, uasubpacket): ] # simple S2Ks -sis2ks = [bytearray(i) + _iv for i in itertools.product(*(_s2k_parts[:2] + [b'\x00'] + _s2k_parts[2:]))] +sis2ks = [bytearray(i) + _iv[:SymmetricKeyAlgorithm(i[1]).block_size//8] for i in itertools.product(*(_s2k_parts[:2] + [b'\x00'] + _s2k_parts[2:]))] # salted S2Ks -sas2ks = [bytearray(i) + _salt + _iv for i in itertools.product(*(_s2k_parts[:2] + [b'\x01'] + _s2k_parts[2:]))] +sas2ks = [bytearray(i) + _salt + _iv[:SymmetricKeyAlgorithm(i[1]).block_size//8] for i in itertools.product(*(_s2k_parts[:2] + [b'\x01'] + _s2k_parts[2:]))] # iterated S2Ks -is2ks = [bytearray(i) + _salt + _count + _iv for i in itertools.product(*(_s2k_parts[:2] + [b'\x03'] + _s2k_parts[2:]))] +is2ks = [bytearray(i) + _salt + _count + _iv[:SymmetricKeyAlgorithm(i[1]).block_size//8] for i in itertools.product(*(_s2k_parts[:2] + [b'\x03'] + _s2k_parts[2:]))] # GNU extension S2Ks gnus2ks = [bytearray(b'\xff\x00\x65\x00GNU' + i) for i in ([b'\x01'] + [b'\x02' + bytearray([len(s)]) + s for s in _gnu_scserials])] @@ -332,7 +332,7 @@ def test_simple_string2key(self, sis2k): assert s.halg in HashAlgorithm assert s.encalg in SymmetricKeyAlgorithm assert s.specifier == String2KeyType.Simple - assert s.iv == _iv + assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('sas2k', sas2ks) def test_salted_string2key(self, sas2k): @@ -349,7 +349,7 @@ def test_salted_string2key(self, sas2k): assert s.encalg in SymmetricKeyAlgorithm assert s.specifier == String2KeyType.Salted assert s.salt == _salt - assert s.iv == _iv + assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('is2k', is2ks) def test_iterated_string2key(self, is2k): @@ -367,7 +367,7 @@ def test_iterated_string2key(self, is2k): assert s.specifier == String2KeyType.Iterated assert s.salt == _salt assert s.count == 2048 - assert s.iv == _iv + assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('gnus2k', gnus2ks) def test_gnu_extension_string2key(self, gnus2k): From 4c364ab84568e3f4a7a4be594ca3bfe5015a893b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 17:49:48 +0900 Subject: [PATCH 147/287] String2Key: auto-populate iv --- pgpy/constants.py | 10 ++++++++- pgpy/packet/fields.py | 49 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index d139a192..a8c3fe13 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -667,7 +667,15 @@ def salt_length(self) -> int: ks = {String2KeyType.Salted: 8, String2KeyType.Iterated: 8, } - return ks.get(self, 0) + return ks.get(self, 0) + + @property + def has_iv(self) -> bool: + 'When this S2K type is used for secret key protection, should we expect an IV to follow?' + return self in [String2KeyType.Simple, + String2KeyType.Salted, + String2KeyType.Iterated, + ] class S2KGNUExtension(IntEnum): diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 6abbf83c..43c98f33 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -840,6 +840,7 @@ def salt(self) -> bytes: if self._salt is None: self._salt: Optional[bytes] = os.urandom(self._specifier.salt_length) return self._salt + @salt.register def salt_bytes(self, val: Union[bytes, bytearray]) -> None: if self._specifier.salt_length == 0: @@ -848,6 +849,44 @@ def salt_bytes(self, val: Union[bytes, bytearray]) -> None: raise ValueError(f"salt for String2KeyType {self._specifier!r} should be {self._specifier.salt_length}, not {len(val)}") self._salt = bytes(val) + @property + def _iv_length(self) -> int: + if self.usage is S2KUsage.Unprotected: + return 0 + elif self.usage in [S2KUsage.MalleableCFB, S2KUsage.CFB]: + if not self.specifier.has_iv: + # this is likely some sort of weird extension case + return 0 + return self.encalg.block_size // 8 + else: + return SymmetricKeyAlgorithm(self.usage).block_size // 8 + + def gen_iv(self) -> None: + ivlen = self._iv_length + if self._iv is None and ivlen: + self._iv: Optional[bytes] = os.urandom(ivlen) + + @sdproperty + def iv(self) -> Optional[bytes]: + ivlen = self._iv_length + if ivlen == 0: + return None + return self._iv + + @iv.register + def iv_bytearray(self, val: Optional[Union[bytearray, bytes]]) -> None: + ivlen = self._iv_length + if ivlen == 0: + if val is not None and len(val) > 0: + raise PGPError(f"setting an IV of length {len(val)} when it should be nothing") + self._iv = None + else: + if val is not None: + if len(val) != ivlen: + raise PGPError(f"setting an IV of length {len(val)} when it should be {ivlen}") + val = bytes(val) + self._iv = val + @sdproperty def count(self) -> int: return (16 + (self._count & 15)) << ((self._count >> 4) + 6) @@ -863,7 +902,7 @@ def __init__(self) -> None: self.usage = S2KUsage.Unprotected self._encalg = SymmetricKeyAlgorithm.Plaintext self._specifier = String2KeyType.Unknown - self.iv: Optional[bytearray] = None + self._iv = None # specifier-specific fields # simple, salted, iterated @@ -961,8 +1000,10 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: del packet[0] if iv: - self.iv = packet[:(self.encalg.block_size // 8)] - del packet[:(self.encalg.block_size // 8)] + ivlen = self._iv_length + if ivlen: + self.iv = packet[:(ivlen)] + del packet[:(ivlen)] def _experimental_parse(self, packet: bytearray, iv: bool = True) -> None: """ @@ -1199,7 +1240,7 @@ def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): self.s2k.usage = S2KUsage.CFB self.s2k.encalg = enc_alg self.s2k.specifier = String2KeyType.Iterated - self.s2k.iv = enc_alg.gen_iv() + self.s2k.gen_iv() self.s2k.halg = hash_alg self.s2k.salt = bytearray(os.urandom(8)) self.s2k.count = 255 From 3c2d0af7117f0a81c49725f3e92f5e79f6cca5d5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 6 May 2023 13:02:39 -0400 Subject: [PATCH 148/287] Add KeyID object by analogy with Fingerprint --- pgpy/types.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pgpy/types.py b/pgpy/types.py index cff86388..048c7748 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -30,6 +30,7 @@ 'FlagEnum', 'FlagEnumMeta', 'Header', + 'KeyID', 'MetaDispatchable', 'Dispatchable', 'DispatchGuidance', @@ -676,6 +677,63 @@ def __rand__(self, other): # pragma: no cover FlagEnum = FlagEnumMeta('FlagEnum', (IntEnum,), namespace) +class KeyID(str): + ''' + This class represents an 8-octet key ID, which is used on the wire in a v3 PKESK packet. + + If a uint64 basic type existed in the stdlib, it would have been beter to use that instead. + ''' + def __new__(cls, content: Union[str, bytes, bytearray]) -> "KeyID": + if isinstance(content, str): + if not re.match(r'^[0-9A-F]{16}$', content): + raise ValueError(f'Initializing a KeyID from a string requires it to be 16 uppercase hex digits, not "{content}"') + return str.__new__(cls, content) + elif isinstance(content, (bytes, bytearray)): + if len(content) != 8: + raise ValueError(f'Initializing a KeyID from a bytes or bytearray requires exactly 8 bytes, not {content!r}') + return str.__new__(cls, binascii.b2a_hex(content).decode('latin1').upper()) + else: + raise TypeError(f'cannot initialize a KeyID from {type(content)}') + + @classmethod + def from_bytes(cls, b: bytes) -> "KeyID": + return cls(binascii.b2a_hex(b).decode('latin-1').upper()) + + @classmethod + def parse(cls, b: bytearray) -> "KeyID": + 'read a Key ID off the wire and consume the series of bytes that represent the Key ID. Produces a new KeyID object' + ret = cls.from_bytes(b[:8]) + del b[:8] + return ret + + def __eq__(self, other: object) -> bool: + if isinstance(other, KeyID): + return str(self) == str(other) + if isinstance(other, Fingerprint): + return str(self) == str(other.keyid) + if isinstance(other, str): + if re.match(r'^[0-9A-F]{16}$', other): + return str(self) == other + if isinstance(other, bytes): + return bytes(self) == other + return False + + def __ne__(self, other: object) -> bool: + return not (self == other) + + def __int__(self) -> int: + return int.from_bytes(bytes(self), byteorder='big', signed=False) + + def __hash__(self) -> int: + return hash(str(self)) + + def __bytes__(self) -> bytes: + return binascii.a2b_hex(self) + + def __repr__(self) -> str: + return f"KeyID({self})" + + class Fingerprint(str): """ A subclass of ``str``. Can be compared using == and != to ``str``, ``unicode``, and other :py:obj:`Fingerprint` instances. From a1b918aa4c0d735cf624da375ea28b0217d55b03 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 13:57:20 -0400 Subject: [PATCH 149/287] PGPSignature.signer_fingerprint returns None when absent (API change) --- docs/source/changelog.rst | 6 ++++++ pgpy/pgp.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b1cea86e..3692aa2e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -39,6 +39,12 @@ cryptography.hazmat.primitives.hashes.Hash object, not a hashlib.HASH object. The main difference between these interfaces is the use of finalize() instead of digest(). +The following property of PGPSignature now returns None if the +corresponding subpacket is not present (it used to return an empty +string in that case): + +* signer_fingerprint + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 89111882..770fe634 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -296,13 +296,13 @@ def signer(self): return self._signature.signer @property - def signer_fingerprint(self): + def signer_fingerprint(self) -> Optional[Fingerprint]: """ - The fingerprint of the key that generated this signature, if it contained. Otherwise, an empty ``str``. + The fingerprint of the key that generated this signature, if it contained. Otherwise, None. """ if 'IssuerFingerprint' in self._signature.subpackets: return next(iter(self._signature.subpackets['IssuerFingerprint'])).issuer_fingerprint - return '' + return None @property def intended_recipients(self): From 1b9dc5ee618a72e3edc169df74d62518bf95e368 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 12:48:45 -0400 Subject: [PATCH 150/287] Type signatures: declare where KeyID and Fingerprints might be returned --- docs/source/changelog.rst | 5 +++-- pgpy/packet/packets.py | 18 ++++++--------- pgpy/packet/subpackets/signature.py | 13 ++++++----- pgpy/pgp.py | 8 +++---- tests/test_03_armor.py | 35 +++++++++++++++-------------- 5 files changed, 39 insertions(+), 40 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3692aa2e..433b08f8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -39,10 +39,11 @@ cryptography.hazmat.primitives.hashes.Hash object, not a hashlib.HASH object. The main difference between these interfaces is the use of finalize() instead of digest(). -The following property of PGPSignature now returns None if the -corresponding subpacket is not present (it used to return an empty +The following properties of PGPSignature now return None if the +corresponding subpacket is not present (they used to return an empty string in that case): +* signer * signer_fingerprint v0.6.0 diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 5fdccdbb..db97197d 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -413,7 +413,7 @@ def signature(self, val): self._signature = val @property - def signer(self): + def signer(self) -> KeyID: return self.subpackets['Issuer'][-1].issuer def __init__(self): @@ -751,23 +751,19 @@ def halg_int(self, val: int) -> None: self._opaque_halg: int = val @sdproperty - def signer(self): + def signer(self) -> KeyID: return self._signer - @signer.register(str) - def signer_str(self, val): - self._signer = val - - @signer.register(bytearray) - def signer_bin(self, val): - self._signer = binascii.hexlify(val).upper().decode('latin-1') + @signer.register + def signer_bin(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]): + self._signer = KeyID(val) def __init__(self): super().__init__() self._sigtype = None self._halg = None self._pubalg = None - self._signer = b'\x00' * 8 + self._signer = KeyID(b'\x00' * 8) self.nested = False def __bytearray__(self): @@ -779,7 +775,7 @@ def __bytearray__(self): else: _bytes.append(self.halg) _bytes += bytearray([self.pubalg]) - _bytes += binascii.unhexlify(self.signer.encode("latin-1")) + _bytes += bytes(self.signer) _bytes += bytearray([int(not self.nested)]) return _bytes diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 2ba6a014..6f8f445f 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -30,6 +30,7 @@ from ...decorators import sdproperty from ...types import Fingerprint +from ...types import KeyID __all__ = ['URI', @@ -617,20 +618,20 @@ class Issuer(Signature): __typeid__ = SigSubpacketType.IssuerKeyID @sdproperty - def issuer(self): + def issuer(self) -> KeyID: return self._issuer - @issuer.register(bytearray) - def issuer_bytearray(self, val): - self._issuer = binascii.hexlify(val).upper().decode('latin-1') + @issuer.register + def issuer_set(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]): + self._issuer = KeyID(val) def __init__(self): super().__init__() - self.issuer = bytearray() + self.issuer = bytearray(b'\x00' * 8) def __bytearray__(self): _bytes = super().__bytearray__() - _bytes += binascii.unhexlify(self._issuer.encode()) + _bytes += bytes(self._issuer) return _bytes def parse(self, packet): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 770fe634..cd0640b8 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -289,7 +289,7 @@ def attested_certifications(self): return ret @property - def signer(self): + def signer(self) -> Optional[KeyID]: """ The 16-character Key ID of the key that generated this signature. """ @@ -959,8 +959,8 @@ def is_signed(self): return len(self._signatures) > 0 @property - def issuers(self): - """A ``set`` containing all key ids (if any) which have signed or encrypted this message.""" + def issuers(self) -> Set[Union[KeyID, Fingerprint]]: + """A ``set`` containing all key ids and Fingerprint (if any) which are indicated to have signed or encrypted this message.""" return self.encrypters | self.signers @property @@ -987,7 +987,7 @@ def signatures(self): return list(self._signatures) @property - def signers(self): + def signers(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` containing all key ids (if any) which have signed this message.""" return {m.signer for m in self._signatures} diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index 9f4f1bde..c10f40b8 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -13,6 +13,7 @@ from pgpy.pgp import PGPMessage from pgpy.pgp import PGPSignature from pgpy.types import Armorable +from pgpy.types import KeyID blocks = sorted(glob.glob('tests/testdata/blocks/*.asc')) @@ -56,9 +57,9 @@ ('is_compressed', False), ('is_encrypted', False), ('is_signed', True), - ('issuers', {'2A834D8E5918E886'}), + ('issuers', {KeyID('2A834D8E5918E886')}), ('message', b"This is stored, literally\\!\n\n"), - ('signers', {'2A834D8E5918E886'}), + ('signers', {KeyID('2A834D8E5918E886')}), ('type', 'literal'),], 'tests/testdata/blocks/message.two_onepass.asc': @@ -67,9 +68,9 @@ ('is_compressed', False), ('is_encrypted', False), ('is_signed', True), - ('issuers', {'2A834D8E5918E886', 'A5DCDC966453140E'}), + ('issuers', {KeyID('2A834D8E5918E886'), KeyID('A5DCDC966453140E')}), ('message', b"This is stored, literally\\!\n\n"), - ('signers', {'2A834D8E5918E886', 'A5DCDC966453140E'}), + ('signers', {KeyID('2A834D8E5918E886'), KeyID('A5DCDC966453140E')}), ('type', 'literal'),], 'tests/testdata/blocks/message.signed.asc': @@ -78,9 +79,9 @@ ('is_compressed', False), ('is_encrypted', False), ('is_signed', True), - ('issuers', {'2A834D8E5918E886'}), + ('issuers', {KeyID('2A834D8E5918E886')}), ('message', b"This is stored, literally\\!\n\n"), - ('signers', {'2A834D8E5918E886'}), + ('signers', {KeyID('2A834D8E5918E886')}), ('type', 'literal'),], 'tests/testdata/blocks/cleartext.asc': @@ -88,9 +89,9 @@ ('is_compressed', False), ('is_encrypted', False), ('is_signed', True), - ('issuers', {'2A834D8E5918E886'}), + ('issuers', {KeyID('2A834D8E5918E886')}), ('message', "This is stored, literally\\!\n"), - ('signers', {'2A834D8E5918E886'}), + ('signers', {KeyID('2A834D8E5918E886')}), ('type', 'cleartext'),], 'tests/testdata/blocks/cleartext.twosigs.asc': @@ -98,35 +99,35 @@ ('is_compressed', False), ('is_encrypted', False), ('is_signed', True), - ('issuers', {'2A834D8E5918E886', 'A5DCDC966453140E'}), + ('issuers', {KeyID('2A834D8E5918E886'), KeyID('A5DCDC966453140E')}), ('message', "This is stored, literally\\!\n"), - ('signers', {'2A834D8E5918E886', 'A5DCDC966453140E'}), + ('signers', {KeyID('2A834D8E5918E886'), KeyID('A5DCDC966453140E')}), ('type', 'cleartext'),], 'tests/testdata/blocks/message.encrypted.asc': - [('encrypters', {'EEE097A017B979CA'}), + [('encrypters', {KeyID('EEE097A017B979CA')}), ('is_compressed', False), ('is_encrypted', True), ('is_signed', False), - ('issuers', {'EEE097A017B979CA'}), + ('issuers', {KeyID('EEE097A017B979CA')}), ('signers', set()), ('type', 'encrypted')], 'tests/testdata/blocks/message.encrypted.signed.asc': - [('encrypters', {'EEE097A017B979CA'}), + [('encrypters', {KeyID('EEE097A017B979CA')}), ('is_compressed', False), ('is_encrypted', True), ('is_signed', False), - ('issuers', {'EEE097A017B979CA'}), + ('issuers', {KeyID('EEE097A017B979CA')}), ('signers', set()), ('type', 'encrypted')], 'tests/testdata/blocks/message.ecc.encrypted.asc': - [('encrypters', {'77CEB7A34089AB73'}), + [('encrypters', {KeyID('77CEB7A34089AB73')}), ('is_compressed', False), ('is_encrypted', True), ('is_signed', False), - ('issuers', {'77CEB7A34089AB73'}), + ('issuers', {KeyID('77CEB7A34089AB73')}), ('signers', set()), ('type', 'encrypted')], @@ -142,7 +143,7 @@ ('key_algorithm', PubKeyAlgorithm.RSAEncryptOrSign), ('magic', "PUBLIC KEY BLOCK"), ('parent', None), - ('signers', {'560CF308EF60CFA3'}),], + ('signers', {KeyID('560CF308EF60CFA3')}),], 'tests/testdata/blocks/expyro.asc': [('created', datetime(1970, 1, 1, tzinfo=timezone.utc)), From 066454fc0b180676562057f2d0e05f948b3b9a75 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 13:27:27 -0400 Subject: [PATCH 151/287] return issuer fingerprints as well as issuer key IDs when available --- pgpy/pgp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index cd0640b8..e7cf54ca 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -745,11 +745,11 @@ def selfsig(self) -> Optional[PGPSignature]: return None @property - def signers(self): + def signers(self) -> Set[Union[KeyID, Fingerprint]]: """ This will be a set of all of the key ids which have signed this User ID or Attribute. """ - return {s.signer for s in self.__sig__} + return {s.signer for s in self.__sig__} | {s.signer_fingerprint for s in self.__sig__ if s.signer_fingerprint is not None} @property def hashdata(self): @@ -989,7 +989,7 @@ def signatures(self): @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` containing all key ids (if any) which have signed this message.""" - return {m.signer for m in self._signatures} + return {m.signer for m in self._signatures} | {m.signer_fingerprint for m in self._signatures if m.signer_fingerprint is not None} @property def type(self): @@ -1602,9 +1602,9 @@ def self_signatures(self): if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])) @property - def signers(self): + def signers(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` of key ids of keys that were used to sign this key""" - return {sig.signer for sig in self.__sig__} + return set(sig.signer for sig in self.__sig__) | set(sig.signer_fingerprint for sig in self.__sig__ if sig.signer_fingerprint is not None) @property def revocation_signatures(self): From ad533fdffc96b6fd04fa0725d11d04231e5e721d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 6 May 2023 13:03:29 -0400 Subject: [PATCH 152/287] Add type signatures for Fingerprint methods --- pgpy/types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index 048c7748..d74f9049 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -741,8 +741,8 @@ class Fingerprint(str): Primarily used as a key for internal dictionaries, so it ignores spaces when comparing and hashing """ @property - def keyid(self): - return self[-16:] + def keyid(self) -> KeyID: + return KeyID(self[-16:]) @property def shortid(self) -> str: @@ -775,7 +775,7 @@ def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> "Finger ret._version = 4 if version is None else version return ret - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, Fingerprint): return str(self) == str(other) and self._version == other._version @@ -790,10 +790,10 @@ def __eq__(self, other): return False # pragma: no cover - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not (self == other) - def __hash__(self): + def __hash__(self) -> int: return hash(str(self)) def __bytes__(self) -> bytes: @@ -802,7 +802,7 @@ def __bytes__(self) -> bytes: def __wireformat__(self) -> bytes: return bytes([self._version]) + bytes(self) - def __pretty__(self): + def __pretty__(self) -> str: content = self if not bool(re.match(r'^[A-F0-9]{40}$', content)): raise ValueError("Expected: String of 40 hex digits") From 109ecf1a697d03c4934868ed69b56e9d9d5917df Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 7 May 2023 13:44:54 -0400 Subject: [PATCH 153/287] ensure that we can test for equality between a keyID and a fingerprint --- pgpy/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpy/types.py b/pgpy/types.py index d74f9049..e7b39701 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -778,6 +778,8 @@ def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> "Finger def __eq__(self, other: object) -> bool: if isinstance(other, Fingerprint): return str(self) == str(other) and self._version == other._version + if isinstance(other, KeyID): + return self.keyid == other if isinstance(other, (str, bytes, bytearray)): if isinstance(other, (bytes, bytearray)): # pragma: no cover From a3b95260926ea2f784123fda9b25e061b350f49e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 8 May 2023 17:12:46 -0400 Subject: [PATCH 154/287] add type signatures for PKESK.encrypter --- pgpy/packet/packets.py | 28 +++++++++++++++++++++------- pgpy/pgp.py | 7 ++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index db97197d..36746277 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -54,6 +54,7 @@ from ..symenc import _cfb_encrypt from ..types import Fingerprint +from ..types import KeyID __all__ = ['PKESessionKey', 'PKESessionKeyV3', @@ -97,6 +98,11 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: raise NotImplementedError() + # a PKESK should return a pointer to the recipient, or None + @abc.abstractproperty + def encrypter(self) -> Optional[Union[KeyID, Fingerprint]]: + raise NotImplementedError() + class PKESessionKeyV3(PKESessionKey): """ @@ -164,12 +170,17 @@ class PKESessionKeyV3(PKESessionKey): __ver__ = 3 @sdproperty - def encrypter(self): + def encrypter(self) -> Optional[KeyID]: return self._encrypter - @encrypter.register(bytearray) - def encrypter_bin(self, val): - self._encrypter = binascii.hexlify(val).upper().decode('latin-1') + @encrypter.register + def encrypter_bin(self, val: Union[bytearray,KeyID]) -> None: + if isinstance(val, KeyID): + self._encrypter: Optional[KeyID] + elif val == b'\x00' * 8: + self._encrypter = None + else: + self._encrypter = KeyID(val) @sdproperty def pkalg(self): @@ -192,16 +203,19 @@ def pkalg_int(self, val: int) -> None: elif self._pkalg is PubKeyAlgorithm.ECDH: self.ct = ECDHCipherText() - def __init__(self): + def __init__(self) -> None: super().__init__() - self.encrypter = bytearray(8) + self._encrypter = None self.pkalg = 0 self.ct = None def __bytearray__(self): _bytes = bytearray() _bytes += super().__bytearray__() - _bytes += binascii.unhexlify(self.encrypter.encode()) + if self._encrypter is None: + _bytes += b'\x00' * 8 + else: + _bytes += bytes(self._encrypter) if self.pkalg == PubKeyAlgorithm.Invalid: _bytes.append(self._opaque_pkalg) else: diff --git a/pgpy/pgp.py b/pgpy/pgp.py index e7cf54ca..a2f348ff 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -17,7 +17,7 @@ from datetime import datetime, timezone -from typing import Any, Deque, List, Mapping, Optional, Tuple, Union +from typing import Any, Deque, List, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -73,6 +73,7 @@ from .types import Armorable from .types import Fingerprint +from .types import KeyID from .types import ParentRef from .types import PGPObject from .types import SignatureVerification @@ -924,9 +925,9 @@ def dash_escape(text): return re.subn(r'^-', '- -', text, flags=re.MULTILINE)[0] @property - def encrypters(self): + def encrypters(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` containing all key ids (if any) to which this message was encrypted.""" - return {m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey)} + return {m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey) and m.encrypter is not None} @property def filename(self): From 00d26e4336498929c222b98aae1e6e25beec3039 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 14:55:33 -0400 Subject: [PATCH 155/287] fixup! add type signatures for PKESK.encrypter --- pgpy/packet/packets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 36746277..73ce2baf 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -174,7 +174,7 @@ def encrypter(self) -> Optional[KeyID]: return self._encrypter @encrypter.register - def encrypter_bin(self, val: Union[bytearray,KeyID]) -> None: + def encrypter_bin(self, val: Union[bytearray, KeyID]) -> None: if isinstance(val, KeyID): self._encrypter: Optional[KeyID] elif val == b'\x00' * 8: From 2110404a81fab0c3e4b30dd78a642f1a163b4bef Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 8 May 2023 14:57:03 -0400 Subject: [PATCH 156/287] FingerprintDict: add an OrderedDict that is indexed by fingerprint, but whose values can also be reached by KeyID --- pgpy/types.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/pgpy/types.py b/pgpy/types.py index e7b39701..48edd65f 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -16,7 +16,7 @@ from enum import EnumMeta from enum import IntEnum -from typing import Optional, Dict, List, Set, Tuple, Type, Union +from typing import Optional, Dict, List, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic from .decorators import sdproperty @@ -27,6 +27,8 @@ 'PGPObject', 'Field', 'Fingerprint', + 'FingerprintDict', + 'FingerprintValue', 'FlagEnum', 'FlagEnumMeta', 'Header', @@ -845,3 +847,59 @@ def check(self) -> None: # pragma: no cover """re-sort any items in self that are not sorted""" for unsorted in iter(self[i] for i in range(len(self) - 2) if not operator.le(self[i], self[i + 1])): self.resort(unsorted) + + +FingerprintValue = TypeVar('FingerprintValue') + + +class FingerprintDict(Generic[FingerprintValue], OrderedDict[Union[Fingerprint, KeyID, str], FingerprintValue]): + '''An ordered collection of PGPKey objects indexable by either KeyID or fingerprint. + + Internally, they are all indexed by Fingerprint, but they can also + be reached by KeyID, or by string representations of either Key + ID or Fingerprint. + + ''' + @staticmethod + def _normalize_input(k: Union[Fingerprint, KeyID, str]) -> Union[Fingerprint, KeyID]: + if isinstance(k, (KeyID, Fingerprint)): + return k + if len(k) == 16: + return KeyID(k) + else: + return Fingerprint(k) + + def _get_fpr_for_keyid(self, k: KeyID) -> Optional[Fingerprint]: + # FIXME: if more than one fingerprint matches the key ID, what do we do? + for fpr in super().keys(): + if not isinstance(fpr, Fingerprint): + raise TypeError(f"keys should only be Fingerprint, somehow FingerprintDict got a key of type {type(fpr)}") + if fpr.keyid == k: + return fpr + return None + + def __setitem__(self, k: Union[Fingerprint, KeyID, str], v: FingerprintValue) -> None: + if not isinstance(k, Fingerprint): + raise TypeError(f"items can only be added to a FingerprintDict by Fingerprint, not {type(k)}") + return super().__setitem__(k, v) + + def __getitem__(self, k: Union[Fingerprint, KeyID, str]) -> FingerprintValue: + k = FingerprintDict._normalize_input(k) + if isinstance(k, Fingerprint): + return super().__getitem__(k) + fpr = self._get_fpr_for_keyid(k) + if fpr is None: + raise KeyError(k) + return super().__getitem__(fpr) + + # FIXME: __getitem__ only replaces how the [] operator works. The + # "get" method only works by using Fingerprints. Trying to + # override get causes complaints from the typechecker. + + def __contains__(self, k: object): + if not isinstance(k, (str, KeyID, Fingerprint)): + raise TypeError(k) + k = FingerprintDict._normalize_input(k) + if isinstance(k, Fingerprint): + return super().__contains__(k) + return self._get_fpr_for_keyid(k) is not None From 026c7b96ddd4947d0ff7143dba8be55077dd96b7 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 12:44:18 -0400 Subject: [PATCH 157/287] Avoid inspecting dervied Generic super classes during a copy --- tests/test_04_copy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_04_copy.py b/tests/test_04_copy.py index 2cf8fba0..812f5c25 100644 --- a/tests/test_04_copy.py +++ b/tests/test_04_copy.py @@ -36,6 +36,10 @@ def walk_obj(obj, prefix=""): if hasattr(obj.__class__, name): continue + # avoid descending into members of a derived generic class: + if name == '__orig_class__': + continue + yield '{}{}'.format(prefix, name), val if not isinstance(val, Enum): From af3436e6558ed40b69dfce62081a8237bbe26aaa Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 2 May 2023 16:50:11 -0400 Subject: [PATCH 158/287] PGPKey.subkeys now returns a FingerprintDict (API change) This allows subkey selection by Fingerprint as well as Key ID We use the FingerprintDict more efficiently, and we also define issuer_matches and signing_subkey for simplicity. PGPKey.decrypt now also can find the correct key/subkey even if the PKESK is identified by fingerprint. --- docs/source/changelog.rst | 5 ++++ pgpy/pgp.py | 63 ++++++++++++++++++++++++++------------- tests/test_03_armor.py | 2 +- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 433b08f8..e2af4c8f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -46,6 +46,11 @@ string in that case): * signer * signer_fingerprint +PGPKey.subkeys now returns an OrderedDict indexed by Fingerprint +instead of KeyID. When accessing this property via subscript (i.e., +key.subkeys[x]), you can *also* index it by KeyID, but using a full +Fingerprint is recommended. + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index a2f348ff..9db739a9 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -74,6 +74,7 @@ from .types import Armorable from .types import Fingerprint from .types import KeyID +from .types import FingerprintDict from .types import ParentRef from .types import PGPObject from .types import SignatureVerification @@ -1553,7 +1554,7 @@ def pubkey(self): pub._key = self._key.pubkey() # get the public half of each subkey - for skid, subkey in self.subkeys.items(): + for subkey in self._children.values(): pub |= subkey.pubkey # copy user ids and user attributes @@ -1616,9 +1617,9 @@ def revocation_signatures(self): if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])) @property - def subkeys(self): + def subkeys(self) -> FingerprintDict["PGPKey"]: """An :py:obj:`~collections.OrderedDict` of subkeys bound to this primary key, if applicable, - selected by 16-character keyid.""" + indexd by keyid and fingerprint.""" return self._children @property @@ -1682,7 +1683,7 @@ def __init__(self) -> None: """ super().__init__() self._key = None - self._children: Mapping[bytes, PGPKey] = collections.OrderedDict() + self._children = FingerprintDict["PGPKey"]() self._signatures = SorteDeque() self._uids: Deque[PGPUID] = SorteDeque() self._sibling = None @@ -1717,10 +1718,10 @@ def __repr__(self): def __contains__(self, item): if isinstance(item, PGPKey): # pragma: no cover - return item.fingerprint.keyid in self.subkeys + return item.fingerprint in self._children if isinstance(item, Fingerprint): # pragma: no cover - return item.keyid in self.subkeys + return item in self._children if isinstance(item, PGPUID): return item in self._uids @@ -1736,7 +1737,7 @@ def __or__(self, other, from_sib=False): elif isinstance(other, PGPKey) and not other.is_primary and other.is_public == self.is_public: other._parent = self - self._children[other.fingerprint.keyid] = other + self._children[other.fingerprint] = other elif isinstance(other, PGPSignature): self._signatures.insort(other) @@ -1947,7 +1948,7 @@ def add_subkey(self, key, **prefs): key._key = npk key._key.update_hlen() - self._children[key.fingerprint.keyid] = key + self._children[key.fingerprint] = key key._parent = self ##TODO: skip this step if the key already has a subkey binding signature @@ -2400,13 +2401,13 @@ def bind(self, key, **prefs): crosssig = None # if possible, have the subkey create a primary key binding signature if key.key_algorithm.can_sign and prefs.pop('crosssign', True): - subkeyid = key.fingerprint.keyid + subkey_fpr = key.fingerprint if not key.is_public: crosssig = key.bind(self) - elif subkeyid in self.subkeys: # pragma: no cover - crosssig = self.subkeys[subkeyid].bind(self) + elif subkey_fpr in self._children: # pragma: no cover + crosssig = self._children[subkey_fpr].bind(self) if crosssig is None: if usage is None: @@ -2471,6 +2472,24 @@ def check_management(self, self_verifying=False): def check_soundness(self, self_verifying=False): return self.check_management(self_verifying) | self.check_primitives() + def issuer_matches(self, sig: PGPSignature) -> bool: + '''Returns true if the signature indicates that it was made by this key or one of its subkeys''' + if sig.signer_fingerprint is not None and (sig.signer_fingerprint == self.fingerprint or sig.signer_fingerprint in self._children): + return True + if sig.signer is not None and sig.signer == self.fingerprint.keyid or sig.signer in self._children: + return True + return False + + def signing_subkey(self, sig: PGPSignature) -> Optional["PGPKey"]: + '''returns None if this was not issued by a subkey; otherwise, returns the subkey that issued it. + + note that this does *not* return the primary key''' + if sig.signer_fingerprint is not None: + return self._children.get(sig.signer_fingerprint) + if sig.signer is not None and sig.signer in self._children: + return self._children[sig.signer] + return None + def verify(self, subject, signature=None): """ Verify a subject with a signature using this key. @@ -2490,9 +2509,8 @@ def verify(self, subject, signature=None): raise TypeError("Unexpected signature value: {:s}".format(str(type(signature)))) def _filter_sigs(sigs): - _ids = {self.fingerprint.keyid} | set(self.subkeys) for sig in sigs: - if sig.signer in _ids: + if self.issuer_matches(sig): yield sig # collect signature(s) @@ -2519,7 +2537,7 @@ def _filter_sigs(sigs): for sig in _filter_sigs(subkey.__sig__): sspairs.append((sig, subkey)) - elif signature.signer in {self.fingerprint.keyid} | set(self.subkeys): + elif self.issuer_matches(signature): sspairs += [(signature, subject)] if len(sspairs) == 0: @@ -2528,8 +2546,9 @@ def _filter_sigs(sigs): # finally, start verifying signatures sigv = SignatureVerification() for sig, subj in sspairs: - if self.fingerprint.keyid != sig.signer and sig.signer in self.subkeys: - sigv &= self.subkeys[sig.signer].verify(subj, sig) + signing_subkey = self.signing_subkey(sig) + if signing_subkey is not None: + sigv &= signing_subkey.verify(subj, sig) else: if isinstance(subj, PGPKey): @@ -2638,12 +2657,14 @@ def decrypt(self, message): warnings.warn("This message is not encrypted", stacklevel=3) return message - if self.fingerprint.keyid not in message.encrypters: - sks = set(self.subkeys) + if self.fingerprint not in message.encrypters and self.fingerprint.keyid not in message.encrypters: + subkey_fprs = set(self.subkeys) + subkey_keyids = set(fpr.keyid for fpr in subkey_fprs) + subkeys = subkey_fprs | subkey_keyids mis = set(message.encrypters) - if sks & mis: - skid = list(sks & mis)[0] - return self.subkeys[skid].decrypt(message) + if subkeys & mis: + subkey = list(subkeys & mis)[0] + return self.subkeys[subkey].decrypt(message) raise PGPError("Cannot decrypt the provided message with this key") diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index c10f40b8..aa3c2ff8 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -206,7 +206,7 @@ ('policy_uri', ''), ('revocable', True), ('revocation_key', None), - ('signer', 'FCAE54F74BA27CF7'), + ('signer', KeyID('FCAE54F74BA27CF7')), ('type', SignatureType.BinaryDocument)], 'tests/testdata/blocks/eccpubkey.asc': From 82f1bdb74ec761de7e6c920d20848992a52a900d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 21:32:13 -0400 Subject: [PATCH 159/287] PGPSignature API change: keyserver,policy_uri return None if no subpacket present This is a change in the API to more accurately reflect what is happening in a signature. It's possible to have a subpacket that contains the empty string, and this allows a consumer to distinguish between the two cases. --- docs/source/changelog.rst | 2 ++ pgpy/pgp.py | 12 ++++++------ tests/test_03_armor.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e2af4c8f..7c284958 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -43,6 +43,8 @@ The following properties of PGPSignature now return None if the corresponding subpacket is not present (they used to return an empty string in that case): +* keyserver +* policy_uri * signer * signer_fingerprint diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 9db739a9..6fdf3573 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -214,13 +214,13 @@ def key_flags(self): return set() @property - def keyserver(self): + def keyserver(self) -> Optional[str]: """ - The preferred key server specified in this signature, if any. Otherwise, an empty ``str``. + The preferred key server specified in this signature, if any. Otherwise, None. """ if 'PreferredKeyServer' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredKeyServer'])).uri - return '' + return None @property def keyserverprefs(self): @@ -243,13 +243,13 @@ def notation(self): return {nd.name: nd.value for nd in self._signature.subpackets['NotationData']} @property - def policy_uri(self): + def policy_uri(self) -> Optional[str]: """ - The policy URI specified in this signature, if any. Otherwise, an empty ``str``. + The policy URI specified in this signature, if any. Otherwise, None. """ if 'Policy' in self._signature.subpackets: return next(iter(self._signature.subpackets['Policy'])).uri - return '' + return None @property def revocable(self) -> bool: diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index aa3c2ff8..b72c4c04 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -199,11 +199,11 @@ ('is_expired', False), ('key_algorithm', PubKeyAlgorithm.RSAEncryptOrSign), ('key_flags', set()), - ('keyserver', ''), ('keyserverprefs', []), + ('keyserver', None), ('magic', "SIGNATURE"), ('notation', {}), - ('policy_uri', ''), + ('policy_uri', None), ('revocable', True), ('revocation_key', None), ('signer', KeyID('FCAE54F74BA27CF7')), From 4840a2adbf87b0751dd39fa77e208a02f92f05a5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 21:43:02 -0400 Subject: [PATCH 160/287] API change: Drop FlagEnum, use enum.IntFlag For features, keyserverprefs, and key_flags, we also now return a simple IntFlags object (not a set), and None when no corresponding subpacket is present. --- docs/source/changelog.rst | 8 +++++ pgpy/constants.py | 13 ++++---- pgpy/packet/packets.py | 10 +++--- pgpy/packet/subpackets/signature.py | 52 +++++++++++++++-------------- pgpy/pgp.py | 23 +++++++------ pgpy/types.py | 15 --------- tests/test_00_exports.py | 2 +- tests/test_03_armor.py | 6 ++-- tests/test_05_actions.py | 4 +-- 9 files changed, 65 insertions(+), 68 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 7c284958..5294ee9f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -48,6 +48,14 @@ string in that case): * signer * signer_fingerprint +And the following properties of PGPSignature now return an +enum.IntFlag object instead of a set of custom FlagEnum objects. When +the corresponding subpacket is not present at all, they return None: + +* key_flags +* keyserverprefs +* features + PGPKey.subkeys now returns an OrderedDict indexed by Fingerprint instead of KeyID. When accessing this property via subscript (i.e., key.subkeys[x]), you can *also* index it by KeyID, but using a full diff --git a/pgpy/constants.py b/pgpy/constants.py index a8c3fe13..852172c5 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -18,7 +18,6 @@ from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm from cryptography.hazmat.primitives import hashes -from .types import FlagEnum from .decorators import classproperty __all__ = [ @@ -644,7 +643,7 @@ class SignatureType(IntEnum): ThirdParty_Confirmation = 0x50 -class KeyServerPreferences(FlagEnum): +class KeyServerPreferences(IntFlag): NoModify = 0x80 @@ -693,7 +692,7 @@ class TrustLevel(IntEnum): Ultimate = 6 -class KeyFlags(FlagEnum): +class KeyFlags(IntFlag): """Flags that determine a key's capabilities.""" #: Signifies that a key may be used to certify keys and user ids. Primary keys always have this, even if it is not specified. Certify = 0x01 @@ -712,7 +711,7 @@ class KeyFlags(FlagEnum): MultiPerson = 0x80 -class Features(FlagEnum): +class Features(IntFlag): ModificationDetection = 0x01 UnknownFeature02 = 0x02 UnknownFeature04 = 0x04 @@ -727,16 +726,16 @@ def pgpy_features(cls) -> 'Features': return Features.ModificationDetection -class RevocationKeyClass(FlagEnum): +class RevocationKeyClass(IntFlag): Sensitive = 0x40 Normal = 0x80 -class NotationDataFlags(FlagEnum): +class NotationDataFlags(IntFlag): HumanReadable = 0x80 -class TrustFlags(FlagEnum): +class TrustFlags(IntFlag): Revoked = 0x20 SubRevoked = 0x40 Disabled = 0x80 diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 73ce2baf..0eee1bcd 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1353,11 +1353,13 @@ def trustflags(self): @trustflags.register(list) def trustflags_list(self, val): - self._trustflags = val + self._trustflags = TrustFlags(sum(val)) - @trustflags.register(int) - def trustflags_int(self, val): - self._trustflags = TrustFlags & val + @trustflags.register + def trustflags_int(self, val: Union[int, TrustFlags]): + if not isinstance(val, TrustFlags): + val = TrustFlags(val) + self._trustflags = val def __init__(self): super().__init__() diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 6f8f445f..0f75b2af 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -10,7 +10,9 @@ from datetime import timedelta from datetime import timezone -from typing import Optional, Type, Union +from enum import IntFlag + +from typing import Optional, Type, Union, List, Set from .types import EmbeddedSignatureHeader from .types import Signature @@ -143,19 +145,17 @@ class ByteFlag(Signature): def flags(self): return self._flags - @flags.register(set) - @flags.register(list) - def flags_seq(self, val): - self._flags = set(val) - - @flags.register(int) - @flags.register(_KeyFlags) - @flags.register(_Features) - def flags_int(self, val): + @flags.register + def flags_seq(self, val: Union[set, list]): if self.__flags__ is None: # pragma: no cover raise AttributeError("Error: __flags__ not set!") + self._flags = self.__flags__(sum(val)) - self._flags |= (self.__flags__ & val) + @flags.register + def flags_int(self, val: int): + if self.__flags__ is None: # pragma: no cover + raise AttributeError("Error: __flags__ not set!") + self._flags |= self.__flags__(val) @flags.register(bytearray) def flags_bytearray(self, val): @@ -552,14 +552,15 @@ class RevocationKey(Signature): def keyclass(self): return self._keyclass - @keyclass.register(list) - def keyclass_list(self, val): - self._keyclass = val + @keyclass.register + def keyclass_list(self, val: Union[list, set]): + self._keyclass = RevocationKeyClass(sum(val)) - @keyclass.register(int) - @keyclass.register(RevocationKeyClass) - def keyclass_int(self, val): - self._keyclass += RevocationKeyClass & val + @keyclass.register + def keyclass_int(self, val: int): + if not isinstance(val, RevocationKeyClass): + val = RevocationKeyClass(val) + self._keyclass |= val @keyclass.register(bytearray) def keyclass_bytearray(self, val): @@ -647,14 +648,15 @@ class NotationData(Signature): def flags(self): return self._flags - @flags.register(list) - def flags_list(self, val): - self._flags = val + @flags.register + def flags_list(self, val: Union[set, list]): + self._flags = NotationDataFlags(sum(val)) - @flags.register(int) - @flags.register(NotationDataFlags) - def flags_int(self, val): - self.flags += NotationDataFlags & val + @flags.register + def flags_int(self, val: int): + if not isinstance(val, NotationDataFlags): + val = NotationDataFlags(val) + self._flags |= val @flags.register(bytearray) def flags_bytearray(self, val): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 6fdf3573..6c73dac5 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -27,6 +27,7 @@ from .constants import HashAlgorithm from .constants import ImageEncoding from .constants import KeyFlags +from .constants import KeyServerPreferences from .constants import NotationDataFlags from .constants import PacketTag from .constants import PubKeyAlgorithm @@ -146,13 +147,13 @@ def exportable(self) -> bool: return True @property - def features(self): + def features(self) -> Optional[Features]: """ - A ``set`` of implementation features specified in this signature, if any. Otherwise, an empty ``set``. + The implementation Features specified in this signature, if any. Otherwise, None """ if 'Features' in self._signature.subpackets: return next(iter(self._signature.subpackets['Features'])).flags - return set() + return None @property def hash2(self): @@ -205,13 +206,13 @@ def key_expiration(self): return None @property - def key_flags(self): + def key_flags(self) -> Optional[KeyFlags]: """ - A ``set`` of :py:obj:`~constants.KeyFlags` specified in this signature, if any. Otherwise, an empty ``set``. + The KeyFlags specified in this signature, if any. Otherwise, None. """ if 'KeyFlags' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyFlags'])).flags - return set() + return None @property def keyserver(self) -> Optional[str]: @@ -223,13 +224,13 @@ def keyserver(self) -> Optional[str]: return None @property - def keyserverprefs(self): + def keyserverprefs(self) -> Optional[KeyServerPreferences]: """ - A ``list`` of :py:obj:`~constants.KeyServerPreferences` in this signature, if any. Otherwise, an empty ``list``. + The KeyServerPreferences` in this signature, if any. Otherwise, None. """ if 'KeyServerPreferences' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyServerPreferences'])).flags - return [] + return None @property def magic(self): @@ -1967,7 +1968,7 @@ def _get_key_flags(self, user=None): user = next(iter(self.userids)) # RFC 4880 says that primary keys *must* be capable of certification - return {KeyFlags.Certify} | (user.selfsig.key_flags if user.selfsig else set()) + return KeyFlags.Certify | (user.selfsig.key_flags if user.selfsig and user.selfsig.key_flags is not None else KeyFlags(0)) return next(self.self_signatures).key_flags @@ -2027,7 +2028,7 @@ def _sign(self, subject, sig, **prefs): # mark all notations as human readable unless value is a bytearray flags = NotationDataFlags.HumanReadable if isinstance(value, bytearray): - flags = 0x00 + flags = NotationDataFlags(0) sig._signature.subpackets.addnew('NotationData', hashed=True, flags=flags, name=name, value=value) diff --git a/pgpy/types.py b/pgpy/types.py index 48edd65f..52139e49 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -13,7 +13,6 @@ import warnings import weakref -from enum import EnumMeta from enum import IntEnum from typing import Optional, Dict, List, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic @@ -29,8 +28,6 @@ 'Fingerprint', 'FingerprintDict', 'FingerprintValue', - 'FlagEnum', - 'FlagEnumMeta', 'Header', 'KeyID', 'MetaDispatchable', @@ -667,18 +664,6 @@ def add_sigsubj(self, signature, by, subject=None, issues=None): self._subjects.append(self.sigsubj(issues, by, signature, subject)) -class FlagEnumMeta(EnumMeta): - def __and__(self, other): - return { f for f in iter(self) if f.value & other } - - def __rand__(self, other): # pragma: no cover - return self & other - - -namespace = FlagEnumMeta.__prepare__('FlagEnum', (IntEnum,)) -FlagEnum = FlagEnumMeta('FlagEnum', (IntEnum,), namespace) - - class KeyID(str): ''' This class represents an 8-octet key ID, which is used on the wire in a v3 PKESK packet. diff --git a/tests/test_00_exports.py b/tests/test_00_exports.py index 01cb63c3..51f3cc14 100644 --- a/tests/test_00_exports.py +++ b/tests/test_00_exports.py @@ -22,7 +22,7 @@ def get_module_objs(module): # return a set of strings that represent the names of objects defined in that module - return { n for n, o in inspect.getmembers(module, lambda m: inspect.getmodule(m) is module) } | ({'FlagEnum',} if module is importlib.import_module('pgpy.types') else set()) # dirty workaround until six fixes metaclass stuff to support EnumMeta in Python >= 3.6 + return { n for n, o in inspect.getmembers(module, lambda m: inspect.getmodule(m) is module) } def get_module_all(module): diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index b72c4c04..c217018e 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -192,15 +192,15 @@ ('created', datetime.fromtimestamp(1402615373, timezone.utc)), ('embedded', False), ('exportable', True), - ('features', set()), + ('features', None), ('hash2', b'\xc4\x24'), ('hashprefs', []), ('hash_algorithm', HashAlgorithm.SHA512), ('is_expired', False), ('key_algorithm', PubKeyAlgorithm.RSAEncryptOrSign), - ('key_flags', set()), - ('keyserverprefs', []), + ('key_flags', None), ('keyserver', None), + ('keyserverprefs', None), ('magic', "SIGNATURE"), ('notation', {}), ('policy_uri', None), diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index fb605ba2..e66ae531 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -345,10 +345,10 @@ def test_add_altuid(self, pkspec): assert sig.cipherprefs == [SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.Camellia256] assert sig.hashprefs == [HashAlgorithm.SHA384] assert sig.compprefs == [CompressionAlgorithm.ZLIB] - assert sig.features == {Features.ModificationDetection} + assert sig.features == Features.ModificationDetection assert sig.key_expiration == expiration - key.created assert sig.keyserver == 'about:none' - assert sig.keyserverprefs == {KeyServerPreferences.NoModify} + assert sig.keyserverprefs == KeyServerPreferences.NoModify assert uid.is_primary is False From 87afd8e8bd6a0071fa99b654ba4c3e2b4768ff26 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 9 May 2023 21:59:09 -0400 Subject: [PATCH 161/287] Simplify FlagList sdproperty registration --- pgpy/packet/subpackets/signature.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 0f75b2af..4205ce16 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -102,17 +102,12 @@ class FlagList(Signature): def flags(self): return self._flags - @flags.register(list) - @flags.register(tuple) - def flags_list(self, val): + @flags.register + def flags_list(self, val: Union[list, tuple]): self._flags = list(val) - @flags.register(int) - @flags.register(CompressionAlgorithm) - @flags.register(HashAlgorithm) - @flags.register(PubKeyAlgorithm) - @flags.register(SymmetricKeyAlgorithm) - def flags_int(self, val): + @flags.register + def flags_int(self, val: int): if self.__flags__ is None: # pragma: no cover raise AttributeError("Error: __flags__ not set!") From 42b5b26e83937ece794ffd78f11a828018022ee5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 16 Jun 2023 22:05:38 -0400 Subject: [PATCH 162/287] String2Key: break out S2KSpecifier as an independent subfield This is useful because the S2KSpecifier will be reused in several different places during implementation of the crypto refresh, witho ut the usage octet, the IV, or the encryption algorithm identifier. This also paves the way for a simpler addition of other S2K types (including Argon2) --- docs/source/changelog.rst | 6 + pgpy/packet/fields.py | 472 ++++++++++++++++++++-------------- tests/test_01_packetfields.py | 26 +- tests/test_05_actions.py | 2 +- 4 files changed, 295 insertions(+), 211 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 5294ee9f..4c1a570d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -61,6 +61,12 @@ instead of KeyID. When accessing this property via subscript (i.e., key.subkeys[x]), you can *also* index it by KeyID, but using a full Fingerprint is recommended. +S2KSpecifier is usable wherever any String2Key object appears (i.e., +bothSKESK and Secret Key protection). The String2Key object itself is +only for Secret Key protection, and it now contains an explicit +S2KSpecifier member, rather than containing all S2K parameters +directly. + v0.6.0 ====== diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 43c98f33..4638dc32 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -13,6 +13,8 @@ from typing import Optional, Tuple, Union +from warnings import warn + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes @@ -79,6 +81,7 @@ 'ECDSAPub', 'EdDSAPub', 'ECDHPub', + 'S2KSpecifier', 'String2Key', 'ECKDF', 'PrivKey', @@ -682,8 +685,14 @@ def parse(self, packet): self.kdf.parse(packet) -class String2Key(Field): +class S2KSpecifier(Field): """ + This is just the S2K specifier and its various options + This is useful because it works in SKESK objects directly. + + In the context of a Secret Key protection, you need more than just + this: instead, look into the String2Key object. + 3.7. String-to-Key (S2K) Specifiers String-to-key (S2K) specifiers are used to convert passphrase strings @@ -787,74 +796,286 @@ class String2Key(Field): After the hashing is done, the data is unloaded from the hash context(s) as with the other S2K algorithms. """ - @sdproperty - def encalg(self) -> SymmetricKeyAlgorithm: - return self._encalg - @encalg.register - def encalg_int(self, val: int) -> None: - if isinstance(val, SymmetricKeyAlgorithm): - self._encalg: SymmetricKeyAlgorithm = val - else: - self._encalg = SymmetricKeyAlgorithm(val) + def __init__(self, + s2ktype: String2KeyType = String2KeyType.Iterated, + halg: HashAlgorithm = HashAlgorithm.SHA256, + salt: Optional[bytes] = None, + iteration_count: int = 65011712, # default to maximum iterations + gnupg_extension: S2KGNUExtension = S2KGNUExtension.NoSecret, + smartcard_serial: Optional[bytes] = None, + ): + if salt is not None: + if s2ktype.salt_length == 0: + raise ValueError(f"No salt for S2KSpecifier type {s2ktype!r}") + elif len(salt) != s2ktype.salt_length: + raise ValueError(f"S2KSpecifier salt for {s2ktype!r} must be {s2ktype.salt_length} octets, not {len(salt)}") + if smartcard_serial is not None: + if s2ktype != String2KeyType.GNUExtension: + raise ValueError(f"Smartcard serial number should only be specfied for GNUExtension S2KSpecifier, not {s2ktype!r}") + if gnupg_extension != S2KGNUExtension.Smartcard: + raise ValueError(f"Smartcard serial number should only be specified with S2KGNUExtension Smartcard, not {gnupg_extension!r}") + if len(smartcard_serial) > 16: + raise ValueError(f"Smartcard serial number should be 16 octets or less, not {len(smartcard_serial)}") + super().__init__() + self._type: String2KeyType = s2ktype + self._halg: HashAlgorithm = halg + self._salt: Optional[bytes] = None + if salt is not None: + self._salt = bytes(salt) + self._count = 65011712 # the default! + if s2ktype is String2KeyType.Iterated: + self.iteration_count = iteration_count + self._gnupg_extension: S2KGNUExtension = gnupg_extension + self._smartcard_serial: Optional[bytes] = None + if smartcard_serial is not None: + self.smartcard_serial = bytes(smartcard_serial) + + def __copy__(self) -> "S2KSpecifier": + s2k = S2KSpecifier() + s2k._type = self._type + if self._type is String2KeyType.Unknown: + s2k._opaque_type = self._opaque_type + + s2k._halg = self._halg + s2k._salt = copy.copy(self._salt) + s2k._count = self._count + s2k._gnupg_extension = self._gnupg_extension + s2k._smartcard_serial = copy.copy(self._smartcard_serial) + return s2k @sdproperty - def specifier(self) -> String2KeyType: - return self._specifier - - @specifier.register - def specifier_int(self, val: int) -> None: - if isinstance(val, String2KeyType): - self._specifier: String2KeyType = val - else: - self._specifier = String2KeyType(val) - if self._specifier is String2KeyType.Unknown: - self._opaque_specifier: int = val + def iteration_count(self) -> int: + if self._type is None: + raise ValueError(f"Cannot retrieve iteration count when S2KSpecifier type is unset") + if self._type is not String2KeyType.Iterated: + raise ValueError(f"Cannot retrieve iteration count on S2KSpecifier Type {self._type!r}") + if self._count is None: + raise ValueError(f"S2KSpecifier iteration count is unset") + return self._count + + @staticmethod + def _convert_iteration_count_to_byte(count: int) -> bytes: + if count < 1: + raise ValueError("Cannot set S2K iteration count below 1") + exponent: int = min(21, max(6, math.floor(math.log2(count)) - 4)) + mantissa: int = min(31, max(16, count >> exponent)) + val = (mantissa - 16) | ((exponent - 6) << 4) + return bytes([val]) + + @staticmethod + def _convert_iteration_byte_to_count(octet: Union[bytes, bytearray]) -> int: + if len(octet) != 1: + raise ValueError("expected a single byte") + mantissa: int = (octet[0] & 0x0f) + 16 + exponent: int = (octet[0] >> 4) + 6 + return mantissa << exponent + + @iteration_count.register + def iteration_count_int(self, val: int) -> None: + if self._type is not String2KeyType.Iterated: + raise ValueError(f"Cannot set iteration count on S2KSpecifier type {self._type!r}") + f = self._convert_iteration_byte_to_count(self._convert_iteration_count_to_byte(val)) + if f != val: + warn(f"Could not select S2K iteration count {val}, using {f} instead") + self._count = f + + @iteration_count.register + def iteration_count_octet(self, val: Union[bytes, bytearray]) -> None: + self._count = self._convert_iteration_byte_to_count(val) @sdproperty - def gnuext(self) -> S2KGNUExtension: - return self._gnuext - - @gnuext.register - def gnuext_int(self, val: int) -> None: - if isinstance(val, S2KGNUExtension): - self._gnuext: S2KGNUExtension = val - else: - self._gnuext = S2KGNUExtension(val) + def iteration_octet(self) -> Optional[bytes]: + if self._type is not String2KeyType.Iterated or self._count is None: + return None + return self._convert_iteration_count_to_byte(self._count) @sdproperty def halg(self) -> HashAlgorithm: return self._halg @halg.register - def halg_int(self, val: int) -> None: - if isinstance(val, HashAlgorithm): - self._halg = val - else: - self._halg = HashAlgorithm(val) + def halg_set(self, val: Union[HashAlgorithm, int]) -> None: + self._halg = HashAlgorithm(val) @sdproperty def salt(self) -> bytes: - if self._specifier.salt_length == 0: + if self._type.salt_length == 0: return b'' if self._salt is None: - self._salt: Optional[bytes] = os.urandom(self._specifier.salt_length) + self._salt = os.urandom(self._type.salt_length) return self._salt @salt.register def salt_bytes(self, val: Union[bytes, bytearray]) -> None: - if self._specifier.salt_length == 0: - raise ValueError(f"salt cannnot be set for String2KeyType {self._specifier!r}") - if len(val) != self._specifier.salt_length: - raise ValueError(f"salt for String2KeyType {self._specifier!r} should be {self._specifier.salt_length}, not {len(val)}") + if self._type.salt_length == 0: + raise ValueError(f"salt cannnot be set for String2KeyType {self._type!r}") + if len(val) != self._type.salt_length: + raise ValueError(f"salt for String2KeyType {self._type!r} should be {self._type.salt_length}, not {len(val)}") self._salt = bytes(val) + @property + def gnuext(self) -> Optional[S2KGNUExtension]: + return self._gnupg_extension + + @sdproperty + def smartcard_serial(self) -> Optional[bytes]: + if self._type is not String2KeyType.GNUExtension or self._gnupg_extension is not S2KGNUExtension.Smartcard: + return None + return self._smartcard_serial + + @smartcard_serial.register + def smartcard_serial_bytes(self, val: Union[bytes, bytearray]) -> None: + if self._type is not String2KeyType.GNUExtension: + raise ValueError(f"smartcard serial number can only be set for String2KeyType GNUExtension, not {self._type!r}") + if self._gnupg_extension != S2KGNUExtension.Smartcard: + raise ValueError(f"smartcard serial number can only be set when S2KGNUExtension is Smartcard, not {self._gnupg_extension!r}") + if len(val) > 16: + raise ValueError(f"smartcard serial number can only be 16 octets maximum, not {len(val)}") + self._smartcard_serial = bytes(val) + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + if self._type is String2KeyType.Unknown: + _bytes.append(self._opaque_type) + else: + _bytes.append(self._type) + if self._type is String2KeyType.GNUExtension: + return self._gnu_bytearray(_bytes) + if self._type in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated}: + _bytes.append(self._halg) + _bytes += self.salt + if self._type is String2KeyType.Iterated: + _bytes += self.iteration_octet + return _bytes + + def __len__(self) -> int: + return len(self.__bytearray__()) + + def parse(self, packet) -> None: + self._type = String2KeyType(packet[0]) + if self._type is String2KeyType.Unknown: + self._opaque_type: int = packet[0] + del packet[0] + + if self._type is String2KeyType.GNUExtension: + return self._parse_gnu_extension(packet) + + if self._type in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated}: + self._halg = HashAlgorithm(packet[0]) + del packet[0] + + if self._type.salt_length > 0: + self._salt = bytes(packet[:self._type.salt_length]) + del packet[:self._type.salt_length] + + if self._type == String2KeyType.Iterated: + self.iteration_count = packet[:1] + del packet[:1] + + def _gnu_bytearray(self, _bytes): + if self._type is not String2KeyType.GNUExtension: + raise ValueError(f"This is not a GnuPG-extended S2K specifier ({self._type})") + if self._gnupg_extension is None: + raise ValueError(f"S2KGNUExtension is unset") + _bytes += b'\x00GNU' + _bytes.append(self._gnupg_extension) + if self._gnupg_extension == S2KGNUExtension.Smartcard: + if self._smartcard_serial is None: + _bytes.append(0) + else: + _bytes.append(len(self._smartcard_serial)) + _bytes += self._smartcard_serial + return _bytes + + def _parse_gnu_extension(self, packet) -> None: + """ + https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=3046523da62c576cf6a765a8b0829876cfdc6b3b;hb=b0f0791e4ade845b2a0e2a94dbda4f3bf1ceb039#l1346 + + GNU extensions to the S2K algorithm + + 1 octet - S2K Specifier: 101 + 4 octets - "\x00GNU" + 1 octet - GNU S2K Extension Number. + + If such a GNU extension is used neither an IV nor any kind of + checksum is used. The defined GNU S2K Extension Numbers are: + + - 1 :: Do not store the secret part at all. No specific data + follows. + + - 2 :: A stub to access smartcards. This data follows: + - One octet with the length of the following serial number. + - The serial number. Regardless of what the length octet + indicates no more than 16 octets are stored. + """ + if self._type != String2KeyType.GNUExtension: + raise ValueError(f"This is not a GnuPG-extended S2K specifier ({self._type!r})") + if packet[:4] != b'\x00GNU': + raise PGPError("Invalid S2K GNU extension magic value") + del packet[:4] + + self._gnupg_extension = S2KGNUExtension(packet[0]) + del packet[0] + + if self._gnupg_extension == S2KGNUExtension.Smartcard: + slen = min(packet[0], 16) + del packet[0] + self.smartcard_serial = packet[:slen] + del packet[:slen] + + def derive_key(self, passphrase: Union[str, bytes], keylen_bits: int) -> bytes: + if self._type not in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated}: + raise NotImplementedError(f"Cannot derive key from S2KSpecifier {self._type!r}") + + if not isinstance(passphrase, bytes): + passphrase = passphrase.encode('utf-8') + + hashlen = self._halg.digest_size * 8 + + ctx = int(math.ceil((keylen_bits / hashlen))) + + base_count = len(self.salt + passphrase) + count = base_count + if self._type is String2KeyType.Iterated and self._count > count: + count = self._count + + hcount = (count // base_count) + hleft = count - (hcount * base_count) + + h = [] + for i in range(0, ctx): + _h = self._halg.hasher + _h.update(b'\x00' * i + (self.salt + passphrase) * hcount + (self.salt + passphrase)[:hleft]) + h.append(_h) + + # and return the key! + return b''.join(hc.finalize() for hc in h)[:(keylen_bits // 8)] + + +class String2Key(Field): + """ + Used for secret key protection. + This contains an S2KUsage flag. Depending on the S2KUsage flag, it can also contain an S2KSpecifier, an encryption algorithm, and an IV. + """ + + @sdproperty + def encalg(self) -> SymmetricKeyAlgorithm: + return self._encalg + + @encalg.register + def encalg_int(self, val: int) -> None: + if isinstance(val, SymmetricKeyAlgorithm): + self._encalg: SymmetricKeyAlgorithm = val + else: + self._encalg = SymmetricKeyAlgorithm(val) + @property def _iv_length(self) -> int: if self.usage is S2KUsage.Unprotected: return 0 elif self.usage in [S2KUsage.MalleableCFB, S2KUsage.CFB]: - if not self.specifier.has_iv: + if not self._specifier._type.has_iv: # this is likely some sort of weird extension case return 0 return self.encalg.block_size // 8 @@ -887,69 +1108,23 @@ def iv_bytearray(self, val: Optional[Union[bytearray, bytes]]) -> None: val = bytes(val) self._iv = val - @sdproperty - def count(self) -> int: - return (16 + (self._count & 15)) << ((self._count >> 4) + 6) - - @count.register - def count_int(self, val: int) -> None: - if val < 0 or val > 255: # pragma: no cover - raise ValueError("count must be between 0 and 256") - self._count = val - def __init__(self) -> None: super().__init__() self.usage = S2KUsage.Unprotected - self._encalg = SymmetricKeyAlgorithm.Plaintext - self._specifier = String2KeyType.Unknown + self._encalg = SymmetricKeyAlgorithm.AES256 + self._specifier = S2KSpecifier() self._iv = None - # specifier-specific fields - # simple, salted, iterated - self._halg = HashAlgorithm.Unknown - - # salted, iterated - self._salt = None - - # iterated - self.count = 0 - - # GNU extension default type: ignored if specifier != GNUExtension - self.gnuext = 1 - - # GNU extension smartcard - self.scserial: Optional[bytearray] = None - def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes.append(self.usage) if bool(self): _bytes.append(self.encalg) - if self.specifier is String2KeyType.Unknown: - _bytes.append(self._opaque_specifier) - else: - _bytes.append(self.specifier) - if self.specifier == String2KeyType.GNUExtension: - return self._experimental_bytearray(_bytes) - if self.specifier >= String2KeyType.Simple: - _bytes.append(self.halg) - if self.specifier >= String2KeyType.Salted: - _bytes += self.salt - if self.specifier == String2KeyType.Iterated: - _bytes.append(self._count) + _bytes += self._specifier.__bytearray__() if self.iv is not None: _bytes += self.iv return _bytes - def _experimental_bytearray(self, _bytes: bytearray) -> bytearray: - if self.specifier == String2KeyType.GNUExtension: - _bytes += b'\x00GNU' - _bytes.append(self.gnuext) - if self.scserial: - _bytes.append(len(self.scserial)) - _bytes += self.scserial - return _bytes - def __len__(self) -> int: return len(self.__bytearray__()) @@ -960,16 +1135,9 @@ def __copy__(self) -> 'String2Key': s2k = String2Key() s2k.usage = self.usage s2k.encalg = self.encalg - if bool(self) and self.specifier is String2KeyType.Unknown: - s2k.specifier = self._opaque_specifier - else: - s2k.specifier = self.specifier - s2k.gnuext = self.gnuext + s2k._specifier = copy.copy(self._specifier) + s2k.iv = self.iv - s2k.halg = self.halg - s2k.salt = copy.copy(self.salt) - s2k.count = self._count - s2k.scserial = self.scserial return s2k def parse(self, packet: bytearray, iv: bool = True) -> None: @@ -980,108 +1148,18 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: self.encalg = SymmetricKeyAlgorithm(packet[0]) del packet[0] - self.specifier = packet[0] - del packet[0] - - if self.specifier is String2KeyType.GNUExtension: - return self._experimental_parse(packet, iv) - - if self.specifier >= String2KeyType.Simple: - # this will always be true - self.halg = packet[0] - del packet[0] - - if self.specifier >= String2KeyType.Salted: - self.salt = packet[:8] - del packet[:8] - - if self.specifier is String2KeyType.Iterated: - self.count = packet[0] - del packet[0] - - if iv: + self._specifier.parse(packet) + if self.encalg is not SymmetricKeyAlgorithm.Plaintext and iv: ivlen = self._iv_length if ivlen: self.iv = packet[:(ivlen)] del packet[:(ivlen)] - def _experimental_parse(self, packet: bytearray, iv: bool = True) -> None: - """ - https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=3046523da62c576cf6a765a8b0829876cfdc6b3b;hb=b0f0791e4ade845b2a0e2a94dbda4f3bf1ceb039#l1346 - - GNU extensions to the S2K algorithm - - 1 octet - S2K Usage: either 254 or 255. - 1 octet - S2K Cipher Algo: 0 - 1 octet - S2K Specifier: 101 - 4 octets - "\x00GNU" - 1 octet - GNU S2K Extension Number. - - If such a GNU extension is used neither an IV nor any kind of - checksum is used. The defined GNU S2K Extension Numbers are: - - - 1 :: Do not store the secret part at all. No specific data - follows. - - - 2 :: A stub to access smartcards. This data follows: - - One octet with the length of the following serial number. - - The serial number. Regardless of what the length octet - indicates no more than 16 octets are stored. - """ - if self.specifier == String2KeyType.GNUExtension: - if packet[:4] != b'\x00GNU': - raise PGPError("Invalid S2K GNU extension magic value") - del packet[:4] - self.gnuext = packet[0] - del packet[0] - - if self.gnuext == S2KGNUExtension.Smartcard: - slen = min(packet[0], 16) - del packet[0] - self.scserial = packet[:slen] - del packet[:slen] - - def derive_key(self, passphrase: Union[str, bytes]) -> bytes: - ##TODO: raise an exception if self.usage is not 254 or 255 - keylen = self.encalg.key_size - hashlen = self.halg.digest_size * 8 - - ctx = int(math.ceil(keylen / hashlen)) - - # Simple S2K - always done - hsalt = b'' - if isinstance(passphrase, bytes): - hpass = passphrase - else: - hpass = passphrase.encode('utf-8') - - # salted, iterated S2K - if self.specifier >= String2KeyType.Salted: - hsalt = bytes(self.salt) - - count = len(hsalt + hpass) - if self.specifier == String2KeyType.Iterated and self.count > len(hsalt + hpass): - count = self.count - - hcount = (count // len(hsalt + hpass)) - hleft = count - (hcount * len(hsalt + hpass)) - - hashdata = ((hsalt + hpass) * hcount) + (hsalt + hpass)[:hleft] - - h = [] - for i in range(0, ctx): - _h = self.halg.hasher - _h.update(b'\x00' * i) - _h.update(hashdata) - h.append(_h) - - # GC some stuff - del hsalt - del hpass - del hashdata - - # and return the key! - return b''.join(hc.finalize() for hc in h)[:(keylen // 8)] + def derive_key(self, passphrase) -> bytes: + derivable = {S2KUsage.MalleableCFB, S2KUsage.CFB} + if self.usage not in derivable: + raise ValueError(f"can only derive key from String2Key object when usage octet is {derivable}, not {self.usage}") + return self._specifier.derive_key(passphrase, self.encalg.key_size) class ECKDF(Field): diff --git a/tests/test_01_packetfields.py b/tests/test_01_packetfields.py index d9437a08..7b8d64e6 100644 --- a/tests/test_01_packetfields.py +++ b/tests/test_01_packetfields.py @@ -329,9 +329,9 @@ def test_simple_string2key(self, sis2k): assert s.__bytes__() == b assert bool(s) - assert s.halg in HashAlgorithm + assert s._specifier.halg in HashAlgorithm assert s.encalg in SymmetricKeyAlgorithm - assert s.specifier == String2KeyType.Simple + assert s._specifier._type is String2KeyType.Simple assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('sas2k', sas2ks) @@ -345,10 +345,10 @@ def test_salted_string2key(self, sas2k): assert s.__bytes__() == b assert bool(s) - assert s.halg in HashAlgorithm + assert s._specifier.halg in HashAlgorithm assert s.encalg in SymmetricKeyAlgorithm - assert s.specifier == String2KeyType.Salted - assert s.salt == _salt + assert s._specifier._type is String2KeyType.Salted + assert s._specifier.salt == _salt assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('is2k', is2ks) @@ -362,11 +362,11 @@ def test_iterated_string2key(self, is2k): assert s.__bytes__() == b assert bool(s) - assert s.halg in HashAlgorithm + assert s._specifier.halg in HashAlgorithm assert s.encalg in SymmetricKeyAlgorithm - assert s.specifier == String2KeyType.Iterated - assert s.salt == _salt - assert s.count == 2048 + assert s._specifier._type is String2KeyType.Iterated + assert s._specifier.salt == _salt + assert s._specifier.iteration_count == 2048 assert s.iv == _iv[:s.encalg.block_size//8] @pytest.mark.parametrize('gnus2k', gnus2ks) @@ -381,7 +381,7 @@ def test_gnu_extension_string2key(self, gnus2k): assert bool(s) assert s.encalg == SymmetricKeyAlgorithm.Plaintext - assert s.specifier == String2KeyType.GNUExtension - assert s.gnuext in S2KGNUExtension - if s.gnuext == S2KGNUExtension.Smartcard: - assert s.scserial is not None and len(s.scserial) <= 16 + assert s._specifier._type is String2KeyType.GNUExtension + assert s._specifier.gnuext in S2KGNUExtension + if s._specifier.gnuext is S2KGNUExtension.Smartcard: + assert s._specifier.smartcard_serial is not None and len(s._specifier.smartcard_serial) <= 16 diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index e66ae531..06452db5 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -461,7 +461,7 @@ def test_change_passphrase(self, pkspec): key = self.keys[pkspec] with key.unlock('There Are Many Like It, But This Key Is Mine') as ukey: - ukey.protect('This Password Has Been Changed', ukey._key.keymaterial.s2k.encalg, ukey._key.keymaterial.s2k.halg) + ukey.protect('This Password Has Been Changed', ukey._key.keymaterial.s2k.encalg, ukey._key.keymaterial.s2k._specifier.halg) @pytest.mark.order(after='test_change_passphrase') @pytest.mark.parametrize('pkspec', pkeyspecs, ids=[str(a) for a, s in pkeyspecs]) From 1950a976a9bccd3e7293dac072b13b63e57938f6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 12:03:05 +0900 Subject: [PATCH 163/287] PGPKey.protect: do not require explicit algorithm specification users shouldn't need to choose an algorithm, there should just be sane choices. We allow the user to provide arguments to avoid an API change. --- docs/source/changelog.rst | 5 +++++ pgpy/packet/packets.py | 7 ++++++- pgpy/pgp.py | 27 +++++++++++++++++++-------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 4c1a570d..7903b1a5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -67,6 +67,11 @@ only for Secret Key protection, and it now contains an explicit S2KSpecifier member, rather than containing all S2K parameters directly. +PGPKey.protect() now no longer requires you to specify a choice of +algorithms. PGPy will make good decisions by default, and you should +not indicate specific algorithms unless you have a very clear reason +to do so. + v0.6.0 ====== diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 0eee1bcd..239ec62e 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -22,6 +22,7 @@ from .fields import CipherText from .fields import Signature as SignatureField from .fields import PubKey as PubKeyField +from .fields import PrivKey as PrivKeyField from .fields import OpaquePubKey from .fields import OpaquePrivKey from .fields import OpaqueSignature @@ -1001,7 +1002,11 @@ def unlocked(self): return 0 not in list(self.keymaterial) return True # pragma: no cover - def protect(self, passphrase, enc_alg, hash_alg): + def protect(self, passphrase: str, + enc_alg: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, + hash_alg: HashAlgorithm = HashAlgorithm.SHA256) -> None: + if not isinstance(self.keymaterial, PrivKeyField): + raise TypeError("Key material is not a private key, cannot protect") self.keymaterial.encrypt_keyblob(passphrase, enc_alg, hash_alg) del passphrase self.update_hlen() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 6c73dac5..f40f99c5 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1789,7 +1789,8 @@ def __copy__(self): return key - def protect(self, passphrase, enc_alg, hash_alg): + def protect(self, passphrase: str, + *posargs, **prefs) -> None: """ Add a passphrase to a private key. If the key is already passphrase protected, it should be unlocked before a new passphrase can be specified. @@ -1797,13 +1798,14 @@ def protect(self, passphrase, enc_alg, hash_alg): Has no effect on public keys. :param passphrase: A passphrase to protect the key with - :type passphrase: ``str``, ``unicode`` - :param enc_alg: Symmetric encryption algorithm to use to protect the key - :type enc_alg: :py:obj:`~constants.SymmetricKeyAlgorithm` - :param hash_alg: Hash algorithm to use in the String-to-Key specifier - :type hash_alg: :py:obj:`~constants.HashAlgorithm` + :type passphrase: ``str`` + + Additional keyword arguments can be passed that will be passed through to the protect() + method on the underlying PrivKey objects. Normal use should not need to supply any + additional arguments. + + Positional arguments are accepted for backward compatibility. """ - ##TODO: specify strong defaults for enc_alg and hash_alg if self.is_public: # we can't protect public keys because only private key material is ever protected warnings.warn("Public keys cannot be passphrase-protected", stacklevel=2) @@ -1815,8 +1817,17 @@ def protect(self, passphrase, enc_alg, hash_alg): "please unlock it before attempting to specify a new passphrase", stacklevel=2) return + # backward compatibility: + # old API had a SymmetricKeyAlgorithm ("enc_alg") followed by a HashAlgorithm ("hash_alg") + if len(posargs) not in [0, 2]: + raise TypeError(f"PGPKey.protect should take a single argument (passphrase)") + if len(posargs) == 2: + prefs['enc_alg'] = posargs[0] + prefs['hash_alg'] = posargs[1] + for sk in itertools.chain([self], self.subkeys.values()): - sk._key.protect(passphrase, enc_alg, hash_alg) + if sk._key is not None: + sk._key.protect(passphrase, **prefs) del passphrase From 349f6b62610632b976c651cf57a417302e82bc92 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 27 Mar 2023 13:07:47 +0900 Subject: [PATCH 164/287] PGPKey.protect: Drop hard-coded algorithm choices. --- tests/test_05_actions.py | 3 +-- tests/test_10_exceptions.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 06452db5..6c95c4b2 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -426,8 +426,7 @@ def test_protect(self, pkspec): key = self.keys[pkspec] assert key.is_protected is False - key.protect('There Are Many Like It, But This Key Is Mine', - SymmetricKeyAlgorithm.AES256, HashAlgorithm.SHA256) + key.protect('There Are Many Like It, But This Key Is Mine') assert key.is_protected assert key.is_unlocked is False diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 2d5f3c68..4e5d7175 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -165,13 +165,13 @@ def test_unlock_not_protected(self, rsa_sec, recwarn): assert w.filename == __file__ def test_protect_pubkey(self, rsa_pub, recwarn): - rsa_pub.protect('QwertyUiop', SymmetricKeyAlgorithm.CAST5, HashAlgorithm.SHA1) + rsa_pub.protect('QwertyUiop') w = recwarn.pop(UserWarning) assert str(w.message) == "Public keys cannot be passphrase-protected" assert w.filename == __file__ def test_protect_protected_key(self, rsa_enc, recwarn): - rsa_enc.protect('QwertyUiop', SymmetricKeyAlgorithm.CAST5, HashAlgorithm.SHA1) + rsa_enc.protect('QwertyUiop') warning = "This key is already protected with a passphrase - please unlock it before attempting to specify a new passphrase" msgs = list(filter(lambda x: str(x.message) == warning, recwarn)) From b47fe1c34338025dd08d0316de35f7084cac9b8f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 15:29:24 +0900 Subject: [PATCH 165/287] Allow passing through a full S2K when protecting a secret key This gives the user more control if they want to control how the encrypted blob works, while deferring to the S2KSpecifier interface. We copy the S2K Specifier upon reciept, so that if the s2k specifier changes later, it doesn't sneakily get changed here too. --- pgpy/packet/fields.py | 21 ++++++++++++++++----- pgpy/packet/packets.py | 17 ++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 4638dc32..35a22efb 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1313,15 +1313,26 @@ def _compute_chksum(self): def publen(self) -> int: return super().__len__() - def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): + def encrypt_keyblob(self, passphrase: str, + enc_alg: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, + hash_alg: Optional[HashAlgorithm] = None, + s2kspec: Optional[S2KSpecifier] = None) -> None: # PGPy will only ever use iterated and salted S2k mode self.s2k.usage = S2KUsage.CFB self.s2k.encalg = enc_alg - self.s2k.specifier = String2KeyType.Iterated + passed_s2kspec: bool + if s2kspec is not None: + passed_s2kspec = True + else: + passed_s2kspec = False + s2kspec = S2KSpecifier() + if hash_alg is not None: + if hash_alg != s2kspec.halg: + if passed_s2kspec: + warn(f"Passed S2K specifier with hash algorithm {s2kspec.halg!r} but also passed hash algorithm {hash_alg!r}, going with {hash_alg!r}") + s2kspec.halg = hash_alg + self.s2k._specifier = copy.copy(s2kspec) self.s2k.gen_iv() - self.s2k.halg = hash_alg - self.s2k.salt = bytearray(os.urandom(8)) - self.s2k.count = 255 # now that String-to-Key is ready to go, derive sessionkey from passphrase # and then unreference passphrase diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 239ec62e..99447617 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -28,6 +28,7 @@ from .fields import OpaqueSignature from .fields import RSACipherText, RSAPriv, RSAPub, RSASignature from .fields import String2Key +from .fields import S2KSpecifier from .fields import SubPackets from .fields import UserAttributeSubPackets @@ -816,6 +817,13 @@ class PrivKey(VersionedPacket, Primary, Private): __typeid__ = PacketTag.SecretKey __ver__ = 0 + @abc.abstractmethod + def protect(self, passphrase: str, + enc_alg: Optional[SymmetricKeyAlgorithm] = None, + hash_alg: Optional[HashAlgorithm] = None, + s2kspec: Optional[S2KSpecifier] = None) -> None: + '''Protect the secret key''' + class PubKey(VersionedPacket, Primary, Public): __typeid__ = PacketTag.PublicKey @@ -1003,11 +1011,14 @@ def unlocked(self): return True # pragma: no cover def protect(self, passphrase: str, - enc_alg: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, - hash_alg: HashAlgorithm = HashAlgorithm.SHA256) -> None: + enc_alg: Optional[SymmetricKeyAlgorithm] = None, + hash_alg: Optional[HashAlgorithm] = None, + s2kspec: Optional[S2KSpecifier] = None) -> None: + if enc_alg is None: + enc_alg = SymmetricKeyAlgorithm.AES256 if not isinstance(self.keymaterial, PrivKeyField): raise TypeError("Key material is not a private key, cannot protect") - self.keymaterial.encrypt_keyblob(passphrase, enc_alg, hash_alg) + self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec) del passphrase self.update_hlen() From dacb09ba865893be82d3938b4d1ae9b2e34e5277 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 27 Mar 2023 05:20:30 +0900 Subject: [PATCH 166/287] PGPKey.protect: allow user to pass in list of IVs enables creation of reproducible results, not for standard use. --- pgpy/packet/fields.py | 5 ++++- pgpy/packet/packets.py | 8 +++++--- pgpy/pgp.py | 8 ++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 35a22efb..d195b261 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1316,7 +1316,8 @@ def publen(self) -> int: def encrypt_keyblob(self, passphrase: str, enc_alg: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, hash_alg: Optional[HashAlgorithm] = None, - s2kspec: Optional[S2KSpecifier] = None) -> None: + s2kspec: Optional[S2KSpecifier] = None, + iv: Optional[bytes] = None) -> None: # PGPy will only ever use iterated and salted S2k mode self.s2k.usage = S2KUsage.CFB self.s2k.encalg = enc_alg @@ -1326,6 +1327,8 @@ def encrypt_keyblob(self, passphrase: str, else: passed_s2kspec = False s2kspec = S2KSpecifier() + if iv is not None: + self.s2k.iv = iv if hash_alg is not None: if hash_alg != s2kspec.halg: if passed_s2kspec: diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 99447617..3999c321 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -821,7 +821,8 @@ class PrivKey(VersionedPacket, Primary, Private): def protect(self, passphrase: str, enc_alg: Optional[SymmetricKeyAlgorithm] = None, hash_alg: Optional[HashAlgorithm] = None, - s2kspec: Optional[S2KSpecifier] = None) -> None: + s2kspec: Optional[S2KSpecifier] = None, + iv: Optional[bytes] = None) -> None: '''Protect the secret key''' @@ -1013,12 +1014,13 @@ def unlocked(self): def protect(self, passphrase: str, enc_alg: Optional[SymmetricKeyAlgorithm] = None, hash_alg: Optional[HashAlgorithm] = None, - s2kspec: Optional[S2KSpecifier] = None) -> None: + s2kspec: Optional[S2KSpecifier] = None, + iv: Optional[bytes] = None) -> None: if enc_alg is None: enc_alg = SymmetricKeyAlgorithm.AES256 if not isinstance(self.keymaterial, PrivKeyField): raise TypeError("Key material is not a private key, cannot protect") - self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec) + self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec, iv=iv) del passphrase self.update_hlen() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f40f99c5..2bb47e74 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1825,7 +1825,15 @@ def protect(self, passphrase: str, prefs['enc_alg'] = posargs[0] prefs['hash_alg'] = posargs[1] + # allow the user to pass in a list of initialization vectors + # (this is suitable for trying to create reproducible objects, but should not normally be used) + ivs: List[bytes] = prefs.pop('ivs', []) + for sk in itertools.chain([self], self.subkeys.values()): + if 'iv' in prefs: + del prefs['iv'] + if ivs: + prefs['iv'] = ivs.pop(0) if sk._key is not None: sk._key.protect(passphrase, **prefs) From 596eb3e98e21a7de9cf1090da3b3ecd4d5c5ee7d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Mar 2023 13:25:12 +0900 Subject: [PATCH 167/287] Normalize searching for preferences (API change) - a selfsig *without* a preferences subpacket (no preference stated) is different from a selfsig with an empty preferences subpacket (a stated preference for the MTI algorithm) - given the complications around deciding between multiple selfsigs with different preferences, we might want to skip a selfsig with no preference stated, but terminate on a selfsig with a stated preference, even if it is empty - use a generator to walk the list of selfsigs that might have legitimately-stated preferences This includes a subtle API change (preferences properties can now return None as distinct from an empty list), but i think it's necessary to do the right thing here. Tests are adjusted to match the new behavior as well. --- docs/source/changelog.rst | 5 +- pgpy/pgp.py | 117 +++++++++++++++++++++++++++++--------- tests/test_03_armor.py | 6 +- 3 files changed, 98 insertions(+), 30 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 7903b1a5..198c054b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -41,8 +41,11 @@ finalize() instead of digest(). The following properties of PGPSignature now return None if the corresponding subpacket is not present (they used to return an empty -string in that case): +list or string in that case): +* cipherprefs +* compprefs +* hashprefs * keyserver * policy_uri * signer diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 2bb47e74..d32021f3 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -17,7 +17,7 @@ from datetime import datetime, timezone -from typing import Any, Deque, List, Mapping, Optional, Set, Tuple, Union +from typing import Any, Deque, List, Iterator, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -97,22 +97,22 @@ def __sig__(self): return self._signature.signature.__sig__() @property - def cipherprefs(self): + def cipherprefs(self) -> Optional[List[SymmetricKeyAlgorithm]]: """ - A ``list`` of preferred symmetric algorithms specified in this signature, if any. Otherwise, an empty ``list``. + A ``list`` of preferred symmetric algorithms specified in this signature, if any. Otherwise, return None. """ if 'PreferredSymmetricAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredSymmetricAlgorithms'])).flags - return [] + return None @property - def compprefs(self): + def compprefs(self) -> Optional[List[CompressionAlgorithm]]: """ - A ``list`` of preferred compression algorithms specified in this signature, if any. Otherwise, an empty ``list``. + A ``list`` of preferred compression algorithms specified in this signature, if any. Otherwise, return None. """ if 'PreferredCompressionAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredCompressionAlgorithms'])).flags - return [] + return None @property def created(self): @@ -160,13 +160,13 @@ def hash2(self): return self._signature.hash2 @property - def hashprefs(self): + def hashprefs(self) -> Optional[List[HashAlgorithm]]: """ - A ``list`` of preferred hash algorithms specified in this signature, if any. Otherwise, an empty ``list``. + A ``list`` of preferred hash algorithms specified in this signature, if any. Otherwise, return None """ if 'PreferredHashAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredHashAlgorithms'])).flags - return [] + return None @property def hash_algorithm(self) -> HashAlgorithm: @@ -2003,15 +2003,17 @@ def _sign(self, subject, sig, **prefs): if user is not None: uid = self.get_uid(user) - else: - uid = next(iter(self.userids), None) - if uid is None and self.parent is not None: - uid = next(iter(self.parent.userids), None) + default_halg = HashAlgorithm.SHA256 + prefsig: Optional[PGPSignature] = None + for prefsig in self.search_pref_sigs(uid=user): + if prefsig.hashprefs is not None: + default_halg = next((h for h in prefsig.hashprefs if h.is_supported), default_halg) + break if sig.hash_algorithm is None: - sig._signature.halg = next((h for h in uid.selfsig.hashprefs if h.is_supported), HashAlgorithm.SHA256) + sig._signature.halg = default_halg - if uid is not None and sig.hash_algorithm not in uid.selfsig.hashprefs: + if prefsig is not None and prefsig.hashprefs is not None and sig.hash_algorithm not in prefsig.hashprefs: warnings.warn("Selected hash algorithm not in key preferences", stacklevel=4) # signature options that can be applied at any level @@ -2465,6 +2467,55 @@ def _do_self_signatures_verification(self): self._self_verified = None raise + def search_pref_sigs(self, uid: Optional[str] = None) -> Iterator[PGPSignature]: + '''Iterate over valid PGPSignature self-sigs where preferences might be found + + If uid is supplied, prefer preferences from self-sigs over the given User ID. + + If this is called on a subkey, the subkey binding signature preferences will be prioritized + ''' + # FIXME: how should we disambiguate? see https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/103#note_1317448098 + if self.is_primary: + primary = self + else: + primary = self.parent + + when = datetime.now(timezone.utc) + + # use the most recent self-sig for the preferred uid + # FIXME: do not yield if uid is revoked + if uid is not None: + userid: Optional[PGPUID] = primary.get_uid(uid) + if userid is None: + raise PGPError(f"No User ID matching {uid}") + if userid.selfsig is not None: + yield userid.selfsig + + sig: PGPSignature + # if we're called on a subkey, use most recent subkey binding signature that is not in the future: + # FIXME: do not yield if subkey is revoked + if not self.is_primary: + for sig in self._signatures: + if sig.created <= when and sig.type == SignatureType.Subkey_Binding: + yield sig + break + + # use the most recent direct key signature + # FIXME: do not yield if the key is revoked + for sig in primary._signatures: + if sig.created <= when and sig.type == SignatureType.DirectlyOnKey: + yield sig + break + + # FIXME: prioritize primary UIDs first + # FIXME: do not yield if the userid is revoked + for userid in primary.userids: + maybesig: Optional[PGPSignature] = userid.selfsig + if maybesig is not None: + sig = maybesig + if sig.created <= when: + yield sig + @property def self_verified(self): warnings.warn("TODO: Self-sigs verification is not yet working because self-sigs are not parsed!!!") @@ -2625,20 +2676,34 @@ def encrypt(self, preference defaults and selection validation. :type user: ``str``, ``unicode`` """ - uid = None - if user is not None: - uid = self.get_uid(user) - else: - uid = next(iter(self.userids), None) - if uid is None and self.parent is not None: - uid = next(iter(self.parent.userids), None) - pref_cipher = next((c for c in uid.selfsig.cipherprefs if c.is_supported), SymmetricKeyAlgorithm.TripleDES) + cipherprefs: Optional[List[SymmetricKeyAlgorithm]] = None + compprefs: Optional[List[CompressionAlgorithm]] = None + sig: PGPSignature + + # line up a list of iterators of self-signatures: walk + # through them in order until we have both cipherprefs and + # compprefs: + + for sig in self.search_pref_sigs(uid=user): + if sig.cipherprefs is not None: + cipherprefs = sig.cipherprefs + if sig.compprefs is not None: + compprefs = sig.compprefs + if cipherprefs is not None and compprefs is not None: + break + + if cipherprefs is None: + cipherprefs = [] + if compprefs is None: + compprefs = [] + + pref_cipher = next((c for c in cipherprefs if c.is_supported), SymmetricKeyAlgorithm.TripleDES) cipher_algo = cipher if cipher is not None else pref_cipher - if cipher_algo not in uid.selfsig.cipherprefs: + if cipher_algo not in cipherprefs: warnings.warn("Selected symmetric algorithm not in key preferences", stacklevel=3) - if message.is_compressed and message._compression not in uid.selfsig.compprefs: + if message.is_compressed and message._compression not in compprefs: warnings.warn("Selected compression algorithm not in key preferences", stacklevel=3) if sessionkey is None: diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index c217018e..7b2df8cd 100644 --- a/tests/test_03_armor.py +++ b/tests/test_03_armor.py @@ -187,14 +187,14 @@ b'\x81\x87\x36\x1a\xa6\x5c\x79\x98\xfe\xdb\xdd\x23\x54\x69\x92\x2f\x0b\xc4\xee\x2a\x61' b'\x77\x35\x59\x6e\xb2\xe2\x1b\x80\x61\xaf\x2d\x7a\x64\x38\xfe\xe3\x95\xcc\xe8\xa4\x05' b'\x55\x5d'), - ('cipherprefs', []), - ('compprefs', []), + ('cipherprefs', None), + ('compprefs', None), ('created', datetime.fromtimestamp(1402615373, timezone.utc)), ('embedded', False), ('exportable', True), ('features', None), ('hash2', b'\xc4\x24'), - ('hashprefs', []), + ('hashprefs', None), ('hash_algorithm', HashAlgorithm.SHA512), ('is_expired', False), ('key_algorithm', PubKeyAlgorithm.RSAEncryptOrSign), From c771653a6b3b7143b586e237c1bafa1d7cbeef06 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 20 Jun 2023 09:45:24 -0400 Subject: [PATCH 168/287] Minor type-checking improvements --- pgpy/pgp.py | 7 +++++-- pgpy/sopgpy.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index d32021f3..2d6dbbbc 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1643,7 +1643,7 @@ def revocation_keys(self): yield sig.revocation_key @classmethod - def new(cls, key_algorithm, key_size, created=None): + def new(cls, key_algorithm: PubKeyAlgorithm, key_size: Union[int, EllipticCurveOID], created: Optional[datetime] = None) -> 'PGPKey': """ Generate a new PGP key @@ -1991,13 +1991,16 @@ def _get_key_flags(self, user=None): return next(self.self_signatures).key_flags - def _sign(self, subject, sig, **prefs): + def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: """ The actual signing magic happens here. :param subject: The subject to sign :param sig: The :py:obj:`PGPSignature` object the new signature is to be encapsulated within :returns: ``sig``, after the signature is added to it. """ + if self._key is None: + raise PGPError('Internal implementation error: PGPKey._key should not be None') + user = prefs.pop('user', None) uid = None if user is not None: diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 7c04c137..5d7681f3 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -193,7 +193,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], raise sop.SOPPasswordNotHumanReadable(f'Key password was not UTF-8') keypassword = pstring.strip().encode(encoding='utf-8') - primary.protect(keypassword, + primary.protect(keypassword.decode('utf-8'), pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.HashAlgorithm.SHA512) return self._maybe_armor(armor, primary) From b9b1c1f6d39dc7e5d2efe68d69d943450b0a5edb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 14 Mar 2023 10:08:02 -0400 Subject: [PATCH 169/287] Enable explicitly setting the IV when encrypting SEIPDv1 This makes it easier to generate deterministic test vectors from PGPy, but *SHOULD NOT* be used in regular operations. --- pgpy/packet/packets.py | 5 +++-- pgpy/pgp.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 3999c321..1f4a4e12 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1643,8 +1643,9 @@ def parse(self, packet): self.ct = packet[:self.header.length - 1] del packet[:self.header.length - 1] - def encrypt(self, key, alg, data): - iv = alg.gen_iv() + def encrypt(self, key, alg, data, iv: Optional[bytes] = None): + if iv is None: + iv = alg.gen_iv() data = iv + iv[-2:] + data mdc = MDC() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 2d6dbbbc..d78f6a93 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1284,7 +1284,8 @@ def encrypt(self, passphrase, sessionkey=None, **prefs): if not self.is_encrypted: skedata = IntegrityProtectedSKEDataV1() - skedata.encrypt(sessionkey, cipher_algo, self.__bytes__()) + iv = prefs.pop('iv', None) + skedata.encrypt(sessionkey, cipher_algo, self.__bytes__(), iv = iv) msg |= skedata else: From 6c0ed22587813301fdac6bee79d08af92b96c8a5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 7 Apr 2023 16:35:48 -0400 Subject: [PATCH 170/287] Allow user to set salts explicitly for each S2K when protecting a key without this, the first use of the S2K will internally generate a salt and then subsequent uses of it will reuse the salt, probably not a desired outcome. --- pgpy/pgp.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index d78f6a93..edc03861 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1830,11 +1830,23 @@ def protect(self, passphrase: str, # (this is suitable for trying to create reproducible objects, but should not normally be used) ivs: List[bytes] = prefs.pop('ivs', []) + # allow the user to pass in a list of salts for the S2K + # (this is suitable for trying to create reproducible objects, but should not normally be used) + salts: List[bytes] = prefs.pop('salts', []) + for sk in itertools.chain([self], self.subkeys.values()): if 'iv' in prefs: del prefs['iv'] if ivs: prefs['iv'] = ivs.pop(0) + + if 's2kspec' in prefs: + if salts: + prefs['s2kspec'].salt = salts.pop(0) + else: + # reset the salt for each key + prefs['s2kspec']._salt = None + if sk._key is not None: sk._key.protect(passphrase, **prefs) From 6dc82a98fd1d9290621466e9135fcce1583f7404 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 17 May 2023 11:40:50 -0400 Subject: [PATCH 171/287] PGPSignature.new: use Fingerprint for signer --- docs/source/changelog.rst | 5 +++++ pgpy/pgp.py | 17 ++++++++++------- tests/test_99_regressions.py | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 198c054b..bcf0c777 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -75,6 +75,11 @@ algorithms. PGPy will make good decisions by default, and you should not indicate specific algorithms unless you have a very clear reason to do so. +PGPSignature.new's "signer" argument should be a Fingerprint object +(it used to accept an Key ID-length string). This is generally not +used externally anyway (most users will use e.g. PGPKey.sign to +produce a PGPSignature object). + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index edc03861..b9f9194a 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -326,16 +326,19 @@ def type(self): return self._signature.sigtype @classmethod - def new(cls, sigtype, pkalg, halg, signer, created=None): + def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Fingerprint, created=None) -> "PGPSignature": sig = PGPSignature() + sigpkt: Signature if created is None: created = datetime.now(timezone.utc) sigpkt = SignatureV4() sigpkt.header.tag = 2 sigpkt.header.version = 4 sigpkt.subpackets.addnew('CreationTime', critical=True, hashed=True, created=created) - sigpkt.subpackets.addnew('Issuer', _issuer=signer) + if signer.version <= 4: + sigpkt.subpackets.addnew('Issuer', _issuer=signer.keyid) + sigpkt.subpackets.addnew('IssuerFingerprint', issuer_fingerprint=signer) sigpkt.sigtype = sigtype sigpkt.pubalg = pkalg @@ -2148,7 +2151,7 @@ def sign(self, subject, **prefs): subject = subject.message - sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None)) + sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) return self._sign(subject, sig, **prefs) @@ -2224,7 +2227,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): if isinstance(subject, PGPKey): sig_type = SignatureType.DirectlyOnKey - sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None)) + sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) # signature options that only make sense in certifications usage = prefs.pop('usage', None) @@ -2357,7 +2360,7 @@ def revoke(self, target, **prefs): else: # pragma: no cover raise TypeError - sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None)) + sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) # signature options that only make sense when revoking reason = prefs.pop('reason', RevocationReason.NotSpecified) @@ -2386,7 +2389,7 @@ def revoker(self, revoker, **prefs): """ hash_algo = prefs.pop('hash', None) - sig = PGPSignature.new(SignatureType.DirectlyOnKey, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None)) + sig = PGPSignature.new(SignatureType.DirectlyOnKey, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) # signature options that only make sense when adding a revocation key sensitive = prefs.pop('sensitive', False) @@ -2427,7 +2430,7 @@ def bind(self, key, **prefs): else: # pragma: no cover raise PGPError - sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None)) + sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) if sig_type == SignatureType.Subkey_Binding: # signature options that only make sense in subkey binding signatures diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index 9d0dd26f..cb5e6b1a 100644 --- a/tests/test_99_regressions.py +++ b/tests/test_99_regressions.py @@ -133,7 +133,7 @@ def test_reg_bug_56(): sigsubject = bytearray(b"Hello!I'm a test document.I'm going to get signed a bunch of times.KBYE!") sig = PGPSignature.new(SignatureType.BinaryDocument, PubKeyAlgorithm.RSAEncryptOrSign, HashAlgorithm.SHA512, - sk.fingerprint.keyid) + sk.fingerprint) sig._signature.subpackets['h_CreationTime'][-1].created = datetime(2014, 8, 6, 23, 28, 51, tzinfo=timezone.utc) sig._signature.subpackets.update_hlen() hdata = sig.hashdata(sigsubject) From e7a313d71e9f3ced5fbc064fd709cb89efa4103b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 17 May 2023 11:43:47 -0400 Subject: [PATCH 172/287] PGPKey: filter self-sigs based on fingerprints, not just key IDs --- pgpy/pgp.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b9f9194a..7c784c25 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1600,13 +1600,16 @@ def pubkey(self, pubkey): pubkey._sibling = weakref.ref(self) @property - def self_signatures(self): - keyid, keytype = (self.fingerprint.keyid, SignatureType.DirectlyOnKey) if self.is_primary \ - else (self.parent.fingerprint.keyid, SignatureType.Subkey_Binding) + def self_signatures(self) -> Iterator[PGPSignature]: + fpr, keytype = (self.fingerprint, SignatureType.DirectlyOnKey) if self.is_primary \ + else (self.parent.fingerprint, SignatureType.Subkey_Binding) ##TODO: filter out revoked signatures as well - yield from iter(sig for sig in self._signatures - if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])) + for sig in self._signatures: + if all([sig.type == keytype, + (sig.signer is not None and fpr == sig.signer) or (sig.signer_fingerprint is not None and fpr == sig.signer_fingerprint), + not sig.is_expired]): + yield sig @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: From df785037774ad011bd6bd9bc5e258cc0f54fa1eb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 17 May 2023 11:44:17 -0400 Subject: [PATCH 173/287] PGPKey.signers: return fingerprints where possible, not just key IDs --- docs/source/changelog.rst | 3 +++ pgpy/pgp.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index bcf0c777..1df8c5e5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -80,6 +80,9 @@ PGPSignature.new's "signer" argument should be a Fingerprint object used externally anyway (most users will use e.g. PGPKey.sign to produce a PGPSignature object). +PGPUID.signers, PGPKey.signers, and PGPMessage.signers will now return +Fingerprints, not just Key IDs. + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 7c784c25..022f86de 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1614,7 +1614,8 @@ def self_signatures(self) -> Iterator[PGPSignature]: @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` of key ids of keys that were used to sign this key""" - return set(sig.signer for sig in self.__sig__) | set(sig.signer_fingerprint for sig in self.__sig__ if sig.signer_fingerprint is not None) + return set(sig.signer for sig in self.__sig__ if sig.signer is not None) | \ + set(sig.signer_fingerprint for sig in self.__sig__ if sig.signer_fingerprint is not None) @property def revocation_signatures(self): From 372097c172e627fa6939792d5e7cde31a2887089 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 17 May 2023 11:47:35 -0400 Subject: [PATCH 174/287] SignatureV4: handle signer being None. This means cleaning up how we handle OnePassSignature generation as well. And, PGPUID.signers now returns Fingerprints instead of KeyIDs, so we fix up the tests too. --- pgpy/packet/packets.py | 8 ++++++-- pgpy/pgp.py | 6 +++--- tests/test_05_actions.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 1f4a4e12..cbbc720b 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -429,8 +429,12 @@ def signature(self, val): self._signature = val @property - def signer(self) -> KeyID: - return self.subpackets['Issuer'][-1].issuer + def signer(self) -> Optional[Union[KeyID, Fingerprint]]: + if 'IssuerFingerprint' in self.subpackets: + return self.subpackets['IssuerFingerprint'][-1].issuer_fingerprint + elif 'Issuer' in self.subpackets: + return self.subpackets['Issuer'][-1].issuer + return None def __init__(self): super(Signature, self).__init__() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 022f86de..b27ee53a 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -753,7 +753,7 @@ def selfsig(self) -> Optional[PGPSignature]: @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: """ - This will be a set of all of the key ids which have signed this User ID or Attribute. + This will be a set of all of the key ids or fingerprints of the keys that signed this User ID or Attribute. """ return {s.signer for s in self.__sig__} | {s.signer_fingerprint for s in self.__sig__ if s.signer_fingerprint is not None} @@ -994,7 +994,7 @@ def signatures(self): @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: - """A ``set`` containing all key ids (if any) which have signed this message.""" + """A ``set`` containing key ids or fingerprints of the keys (if any) which have signed this message.""" return {m.signer for m in self._signatures} | {m.signer_fingerprint for m in self._signatures if m.signer_fingerprint is not None} @property @@ -1613,7 +1613,7 @@ def self_signatures(self) -> Iterator[PGPSignature]: @property def signers(self) -> Set[Union[KeyID, Fingerprint]]: - """A ``set`` of key ids of keys that were used to sign this key""" + """A ``set`` of key ids or fingerprints of keys that were used to sign this key""" return set(sig.signer for sig in self.__sig__ if sig.signer is not None) | \ set(sig.signer_fingerprint for sig in self.__sig__ if sig.signer_fingerprint is not None) diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 6c95c4b2..2ca5773b 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -850,7 +850,7 @@ def test_certify_uid(self, sec, abe): assert sig.type == SignatureType.Casual_Cert assert sig.exportable - assert ({sec.fingerprint.keyid} | set(sec.subkeys)) & userid.signers + assert ({sec.fingerprint.keyid, sec.fingerprint} | set(sec.subkeys)) & userid.signers @pytest.mark.order(after='test_certify_uid') @pytest.mark.parametrize('pub', pubkeys, From f6887f63af6106733b3f76d3cfe3bf62b7dc8d7b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 18 May 2023 18:45:51 -0400 Subject: [PATCH 175/287] crosssig should have same time as subkey binding sig --- pgpy/pgp.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b27ee53a..8f632133 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2410,7 +2410,7 @@ def revoker(self, revoker, **prefs): return self._sign(self, sig, **prefs) @KeyAction(is_unlocked=True, is_public=False) - def bind(self, key, **prefs): + def bind(self, key: 'PGPKey', **prefs) -> PGPSignature: """ Bind a subkey to this key. @@ -2434,7 +2434,13 @@ def bind(self, key, **prefs): else: # pragma: no cover raise PGPError - sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) + created: Optional[datetime] = prefs.pop('created', None) + + key_algo = self.key_algorithm + if key_algo is None: + raise PGPError('Uninitialized or unknown PGPKey cannot bind subkeys') + + sig = PGPSignature.new(sig_type, key_algo, hash_algo, self.fingerprint, created=created) if sig_type == SignatureType.Subkey_Binding: # signature options that only make sense in subkey binding signatures @@ -2445,14 +2451,14 @@ def bind(self, key, **prefs): crosssig = None # if possible, have the subkey create a primary key binding signature - if key.key_algorithm.can_sign and prefs.pop('crosssign', True): + if key.key_algorithm is not None and key.key_algorithm.can_sign and prefs.pop('crosssign', True): subkey_fpr = key.fingerprint if not key.is_public: - crosssig = key.bind(self) + crosssig = key.bind(self, created=created) elif subkey_fpr in self._children: # pragma: no cover - crosssig = self._children[subkey_fpr].bind(self) + crosssig = self._children[subkey_fpr].bind(self, created=created) if crosssig is None: if usage is None: From 3daa0dbe96a01a530231f6a7564446bcf4b0687d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 21 Jun 2023 15:36:07 -0400 Subject: [PATCH 176/287] OnePassSignatureV3: add type signatures --- pgpy/packet/packets.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index cbbc720b..b555ecf1 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -735,36 +735,40 @@ class OnePassSignatureV3(OnePassSignature): __ver__ = 3 @sdproperty - def sigtype(self): + def sigtype(self) -> Optional[SignatureType]: return self._sigtype - @sigtype.register(int) - @sigtype.register(SignatureType) - def sigtype_int(self, val): - self._sigtype = SignatureType(val) + @sigtype.register + def sigtype_int(self, val: int) -> None: + if isinstance(val, SignatureType): + self._sigtype: Optional[SignatureType] = None + else: + self._sigtype = SignatureType(val) @sdproperty - def pubalg(self): + def pubalg(self) -> Optional[PubKeyAlgorithm]: return self._pubalg - @pubalg.register(int) - @pubalg.register(PubKeyAlgorithm) - def pubalg_int(self, val): - self._pubalg = PubKeyAlgorithm(val) - if self._pubalg in [PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign]: - self.signature = RSASignature() + @pubalg.register + def pubalg_int(self, val: int): + if isinstance(val, PubKeyAlgorithm): + self._pubalg: Optional[PubKeyAlgorithm] = val + else: + self._pubalg = PubKeyAlgorithm(val) + if self._pubalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: + self.signature: SignatureField = RSASignature() elif self._pubalg == PubKeyAlgorithm.DSA: self.signature = DSASignature() @sdproperty - def halg(self) -> HashAlgorithm: + def halg(self) -> Optional[HashAlgorithm]: return self._halg @halg.register def halg_int(self, val: int) -> None: if isinstance(val, HashAlgorithm): - self._halg = val + self._halg: Optional[HashAlgorithm] = val else: self._halg = HashAlgorithm(val) if self._halg is HashAlgorithm.Unknown: @@ -775,10 +779,10 @@ def signer(self) -> KeyID: return self._signer @signer.register - def signer_bin(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]): + def signer_bin(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: self._signer = KeyID(val) - def __init__(self): + def __init__(self) -> None: super().__init__() self._sigtype = None self._halg = None @@ -786,7 +790,7 @@ def __init__(self): self._signer = KeyID(b'\x00' * 8) self.nested = False - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += super().__bytearray__() _bytes += bytearray([self.sigtype]) @@ -799,7 +803,7 @@ def __bytearray__(self): _bytes += bytearray([int(not self.nested)]) return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.sigtype = packet[0] del packet[0] From 748bb5987a37cc80abcbf23078b80f6f09def4cf Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 21 Jun 2023 15:42:50 -0400 Subject: [PATCH 177/287] OnePassSignatureV3: move common members to OnePassSignature --- pgpy/packet/packets.py | 97 ++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index b555ecf1..fbb81751 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -691,9 +691,60 @@ def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: class OnePassSignature(VersionedPacket): + '''Holds common members of various OPS packet versions''' __typeid__ = PacketTag.OnePassSignature __ver__ = 0 + def __init__(self) -> None: + super().__init__() + self._sigtype: Optional[SignatureType] = None + self._halg: Optional[HashAlgorithm] = None + self._pubalg: Optional[PubKeyAlgorithm] = None + self.nested: bool = False + + @sdproperty + def sigtype(self) -> Optional[SignatureType]: + return self._sigtype + + @sigtype.register + def sigtype_int(self, val: int) -> None: + if isinstance(val, SignatureType): + self._sigtype = val + else: + self._sigtype = SignatureType(val) + + @sdproperty + def pubalg(self) -> Optional[PubKeyAlgorithm]: + return self._pubalg + + @pubalg.register + def pubalg_int(self, val: int): + if isinstance(val, PubKeyAlgorithm): + self._pubalg = val + else: + self._pubalg = PubKeyAlgorithm(val) + + @sdproperty + def halg(self) -> Optional[HashAlgorithm]: + return self._halg + + @halg.register + def halg_int(self, val: int) -> None: + if isinstance(val, HashAlgorithm): + self._halg = val + else: + self._halg = HashAlgorithm(val) + if self._halg is HashAlgorithm.Unknown: + self._opaque_halg: int = val + + @abc.abstractproperty + def signer(self) -> Union[KeyID, Fingerprint]: + raise NotImplementedError() + + @abc.abstractmethod + def signer_set(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: + pass + class OnePassSignatureV3(OnePassSignature): """ @@ -734,61 +785,17 @@ class OnePassSignatureV3(OnePassSignature): """ __ver__ = 3 - @sdproperty - def sigtype(self) -> Optional[SignatureType]: - return self._sigtype - - @sigtype.register - def sigtype_int(self, val: int) -> None: - if isinstance(val, SignatureType): - self._sigtype: Optional[SignatureType] = None - else: - self._sigtype = SignatureType(val) - - @sdproperty - def pubalg(self) -> Optional[PubKeyAlgorithm]: - return self._pubalg - - @pubalg.register - def pubalg_int(self, val: int): - if isinstance(val, PubKeyAlgorithm): - self._pubalg: Optional[PubKeyAlgorithm] = val - else: - self._pubalg = PubKeyAlgorithm(val) - if self._pubalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: - self.signature: SignatureField = RSASignature() - - elif self._pubalg == PubKeyAlgorithm.DSA: - self.signature = DSASignature() - - @sdproperty - def halg(self) -> Optional[HashAlgorithm]: - return self._halg - - @halg.register - def halg_int(self, val: int) -> None: - if isinstance(val, HashAlgorithm): - self._halg: Optional[HashAlgorithm] = val - else: - self._halg = HashAlgorithm(val) - if self._halg is HashAlgorithm.Unknown: - self._opaque_halg: int = val - @sdproperty def signer(self) -> KeyID: return self._signer @signer.register - def signer_bin(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: + def signer_set(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: self._signer = KeyID(val) def __init__(self) -> None: super().__init__() - self._sigtype = None - self._halg = None - self._pubalg = None self._signer = KeyID(b'\x00' * 8) - self.nested = False def __bytearray__(self) -> bytearray: _bytes = bytearray() From 1cfaf3d2ec45c4c114d14c464ede58dd97d07874 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 21 Jun 2023 17:08:30 -0400 Subject: [PATCH 178/287] OnePassSignatureV3.__bytearray__: simplify appending one-octet fields --- pgpy/packet/packets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index fbb81751..db8e9ffb 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -800,14 +800,14 @@ def __init__(self) -> None: def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += super().__bytearray__() - _bytes += bytearray([self.sigtype]) + _bytes.append(self.sigtype) if self.halg is HashAlgorithm.Unknown: _bytes.append(self._opaque_halg) else: _bytes.append(self.halg) - _bytes += bytearray([self.pubalg]) + _bytes.append(self.pubalg) _bytes += bytes(self.signer) - _bytes += bytearray([int(not self.nested)]) + _bytes.append(int(not self.nested)) return _bytes def parse(self, packet: bytearray) -> None: From ba1ec450d233370257563826746bb73bedae6073 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 21 Jun 2023 15:53:35 -0400 Subject: [PATCH 179/287] packets.Signature: consolidate shared functionality from SignatureV4 --- pgpy/packet/packets.py | 144 ++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index db8e9ffb..d5ed312f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -319,6 +319,72 @@ class Signature(VersionedPacket): __typeid__ = PacketTag.Signature __ver__ = 0 + def __init__(self) -> None: + super().__init__() + self._sigtype: Optional[SignatureType] = None + self._pubalg: Optional[PubKeyAlgorithm] = None + self._halg: Optional[HashAlgorithm] = None + self.subpackets = SubPackets() + self.hash2 = bytearray(2) + self._signature: SignatureField = OpaqueSignature() + + @sdproperty + def sigtype(self) -> Optional[SignatureType]: + return self._sigtype + + @sigtype.register + def sigtype_int(self, val: int) -> None: + self._sigtype = SignatureType(val) + + @sdproperty + def pubalg(self) -> Optional[PubKeyAlgorithm]: + return self._pubalg + + @pubalg.register + def pubalg_int(self, val: int) -> None: + if isinstance(val, PubKeyAlgorithm): + self._pubalg = val + else: + self._pubalg = PubKeyAlgorithm(val) + if self._pubalg is PubKeyAlgorithm.Unknown: + self._opaque_pubalg: int = val + + if self.pubalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSASign}: + self.signature = RSASignature() + elif self.pubalg is PubKeyAlgorithm.DSA: + self.signature = DSASignature() + elif self.pubalg is PubKeyAlgorithm.ECDSA: + self.signature = ECDSASignature() + elif self.pubalg is PubKeyAlgorithm.EdDSA: + self.signature = EdDSASignature() + else: + self.signature = OpaqueSignature() + + @sdproperty + def halg(self) -> Optional[HashAlgorithm]: + return self._halg + + @halg.register + def halg_int(self, val: int) -> None: + if isinstance(val, HashAlgorithm): + self._halg = val + else: + self._halg = HashAlgorithm(val) + if self._halg is HashAlgorithm.Unknown: + self._opaque_halg = val + + @property + def signature(self) -> SignatureField: + return self._signature + + @signature.setter + def signature(self, val: SignatureField) -> None: + self._signature = val + + def update_hlen(self): + self.subpackets.update_hlen() + super().update_hlen() + @abc.abstractmethod def make_onepass(self) -> 'OnePassSignature': raise NotImplementedError() @@ -375,59 +441,6 @@ class SignatureV4(Signature): """ __ver__ = 4 - @sdproperty - def sigtype(self) -> SignatureType: - return self._sigtype - - @sigtype.register - def sigtype_int(self, val: int) -> None: - self._sigtype = SignatureType(val) - - @sdproperty - def pubalg(self) -> PubKeyAlgorithm: - return self._pubalg - - @pubalg.register - def pubalg_int(self, val: int) -> None: - if isinstance(val, PubKeyAlgorithm): - self._pubalg: PubKeyAlgorithm = val - else: - self._pubalg = PubKeyAlgorithm(val) - if self._pubalg is PubKeyAlgorithm.Unknown: - self._opaque_pubalg: int = val - - if self.pubalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSASign}: - self.signature: SignatureField = RSASignature() - elif self.pubalg is PubKeyAlgorithm.DSA: - self.signature = DSASignature() - elif self.pubalg is PubKeyAlgorithm.ECDSA: - self.signature = ECDSASignature() - elif self.pubalg is PubKeyAlgorithm.EdDSA: - self.signature = EdDSASignature() - else: - self.signature = OpaqueSignature() - - @sdproperty - def halg(self) -> HashAlgorithm: - return self._halg - - @halg.register - def halg_int(self, val: int) -> None: - if isinstance(val, HashAlgorithm): - self._halg = val - else: - self._halg = HashAlgorithm(val) - if self._halg is HashAlgorithm.Unknown: - self._opaque_halg = val - - @property - def signature(self): - return self._signature - - @signature.setter - def signature(self, val): - self._signature = val - @property def signer(self) -> Optional[Union[KeyID, Fingerprint]]: if 'IssuerFingerprint' in self.subpackets: @@ -436,18 +449,9 @@ def signer(self) -> Optional[Union[KeyID, Fingerprint]]: return self.subpackets['Issuer'][-1].issuer return None - def __init__(self): - super(Signature, self).__init__() - self._sigtype = None - self._pubalg = None - self._halg = None - self.subpackets = SubPackets() - self.hash2 = bytearray(2) - self.signature = None - - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() - _bytes += super(Signature, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.int_to_bytes(self.sigtype) if self.pubalg is PubKeyAlgorithm.Unknown: _bytes.append(self._opaque_pubalg) @@ -463,7 +467,7 @@ def __bytearray__(self): return _bytes - def canonical_bytes(self): + def canonical_bytes(self) -> bytearray: '''Returns a bytearray that is the way the signature packet should be represented if it is itself being signed. @@ -498,7 +502,7 @@ def canonical_bytes(self): _hdr += self.int_to_bytes(len(_body), minlen=4) return _hdr + _body - def __copy__(self): + def __copy__(self) -> 'SignatureV4': spkt = SignatureV4() spkt.header = copy.copy(self.header) spkt._sigtype = self._sigtype @@ -515,12 +519,8 @@ def __copy__(self): return spkt - def update_hlen(self): - self.subpackets.update_hlen() - super().update_hlen() - - def parse(self, packet): - super(Signature, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.sigtype = packet[0] del packet[0] From 6c54fb90d0afc92337e948349149955a491e175e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 19 May 2023 18:43:31 -0400 Subject: [PATCH 180/287] Consolidate methods into PrivKey and PubKey from PrivKeyv4 and PubKeyV4 This moves code shared by v6 keys and v4 keys into common base classes We also adjust the inheritance tree for PrivSubKey: this should cause no functional change, but it makes the expanded API surface for PubSubKey more visible directly in PrivSubKey, which in turn will make type annotations clearer when subkeys are invoked. --- pgpy/packet/packets.py | 155 +++++++++++++++++++++-------------------- pgpy/pgp.py | 19 +++-- 2 files changed, 94 insertions(+), 80 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index d5ed312f..98855ad6 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -828,48 +828,36 @@ def parse(self, packet: bytearray) -> None: del packet[0] -class PrivKey(VersionedPacket, Primary, Private): - __typeid__ = PacketTag.SecretKey - __ver__ = 0 - - @abc.abstractmethod - def protect(self, passphrase: str, - enc_alg: Optional[SymmetricKeyAlgorithm] = None, - hash_alg: Optional[HashAlgorithm] = None, - s2kspec: Optional[S2KSpecifier] = None, - iv: Optional[bytes] = None) -> None: - '''Protect the secret key''' - - class PubKey(VersionedPacket, Primary, Public): __typeid__ = PacketTag.PublicKey __ver__ = 0 + def __init__(self) -> None: + super().__init__() + self.created = datetime.now(timezone.utc) + self.pkalg = 0 + self.keymaterial: Optional[PubKeyField] = None + @abc.abstractproperty def fingerprint(self) -> Fingerprint: """compute and return the fingerprint of the key""" - -class PubKeyV4(PubKey): - __ver__ = 4 - @sdproperty - def created(self): + def created(self) -> datetime: return self._created - @created.register(datetime) - def created_datetime(self, val): + @created.register + def created_datetime(self, val: datetime) -> None: if val.tzinfo is None: warnings.warn("Passing TZ-naive datetime object to PubKeyV4 packet") self._created = val - @created.register(int) - def created_int(self, val): + @created.register + def created_int(self, val: int) -> None: self.created = datetime.fromtimestamp(val, timezone.utc) - @created.register(bytes) - @created.register(bytearray) - def created_bin(self, val): + @created.register + def created_bin(self, val: Union[bytes, bytearray]) -> None: self.created = self.bytes_to_int(val) @sdproperty @@ -886,7 +874,7 @@ def pkalg_int(self, val: int) -> None: self._opaque_pkalg: int = val if self.pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: - self.keymaterial: PubKeyField = (RSAPub if self.public else RSAPriv)() + self.keymaterial = (RSAPub if self.public else RSAPriv)() elif self.pkalg is PubKeyAlgorithm.DSA: self.keymaterial = (DSAPub if self.public else DSAPriv)() elif self.pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: @@ -901,9 +889,28 @@ def pkalg_int(self, val: int) -> None: self.keymaterial = (OpaquePubKey if self.public else OpaquePrivKey)() @property - def public(self): + def public(self) -> bool: return isinstance(self, PubKey) and not isinstance(self, PrivKey) + def __copy__(self) -> 'PubKey': + pk = self.__class__() + pk.header = copy.copy(self.header) + pk.created = self.created + if self.pkalg is PubKeyAlgorithm.Unknown: + pk.pkalg = self._opaque_pkalg + else: + pk.pkalg = self.pkalg + pk.keymaterial = copy.copy(self.keymaterial) + + return pk + + def verify(self, subj, sigbytes, hash_alg): + return self.keymaterial.verify(subj, sigbytes, hash_alg) + + +class PubKeyV4(PubKey): + __ver__ = 4 + @property def fingerprint(self): # A V4 fingerprint is the 160-bit SHA-1 hash of the octet 0x99, followed by the two-octet packet length, @@ -950,21 +957,6 @@ def __bytearray__(self): _bytes += self.keymaterial.__bytearray__() return _bytes - def __copy__(self): - pk = self.__class__() - pk.header = copy.copy(self.header) - pk.created = self.created - if self.pkalg is PubKeyAlgorithm.Unknown: - pk.pkalg = self._opaque_pkalg - else: - pk.pkalg = self.pkalg - pk.keymaterial = copy.copy(self.keymaterial) - - return pk - - def verify(self, subj, sigbytes, hash_alg): - return self.keymaterial.verify(subj, sigbytes, hash_alg) - def parse(self, packet): super().parse(packet) @@ -980,16 +972,61 @@ def parse(self, packet): del packet[:pend] +class PrivKey(PubKey, Private): + __typeid__ = PacketTag.SecretKey + __ver__ = 0 + + @property + def protected(self) -> bool: + if not isinstance(self.keymaterial, PrivKeyField): + return False + return bool(self.keymaterial.s2k) + + @property + def unlocked(self) -> bool: + if self.keymaterial is None: + return True + if self.protected: + return 0 not in list(self.keymaterial) + return True # pragma: no cover + + def protect(self, passphrase: str, + enc_alg: Optional[SymmetricKeyAlgorithm] = None, + hash_alg: Optional[HashAlgorithm] = None, + s2kspec: Optional[S2KSpecifier] = None, + iv: Optional[bytes] = None) -> None: + if enc_alg is None: + enc_alg = SymmetricKeyAlgorithm.AES256 + if not isinstance(self.keymaterial, PrivKeyField): + raise TypeError("Key material is not a private key, cannot protect") + self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec, iv=iv) + del passphrase + self.update_hlen() + + def unprotect(self, passphrase: str) -> None: + if not isinstance(self.keymaterial, PrivKeyField): + raise TypeError("Key material is not a private key, cannot unprotect") + self.keymaterial.decrypt_keyblob(passphrase) + del passphrase + + def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: + if not isinstance(self.keymaterial, PrivKeyField): + raise TypeError("Key material is not a private key, cannot sign") + return self.keymaterial.sign(sigdata, hash_alg) + + class PrivKeyV4(PrivKey, PubKeyV4): __ver__ = 4 @classmethod - def new(cls, key_algorithm, key_size, created=None): + def new(cls, key_algorithm, key_size, created=None) -> 'PrivKeyV4': # build a key packet pk = PrivKeyV4() pk.pkalg = key_algorithm if pk.keymaterial is None: raise NotImplementedError(key_algorithm) + if not isinstance(pk.keymaterial, PrivKeyField): + raise TypeError("Key material is not a private key") pk.keymaterial._generate(key_size) if created is not None: pk.created = created @@ -1016,38 +1053,8 @@ def pubkey(self): pk.update_hlen() return pk - @property - def protected(self): - return bool(self.keymaterial.s2k) - - @property - def unlocked(self): - if self.protected: - return 0 not in list(self.keymaterial) - return True # pragma: no cover - - def protect(self, passphrase: str, - enc_alg: Optional[SymmetricKeyAlgorithm] = None, - hash_alg: Optional[HashAlgorithm] = None, - s2kspec: Optional[S2KSpecifier] = None, - iv: Optional[bytes] = None) -> None: - if enc_alg is None: - enc_alg = SymmetricKeyAlgorithm.AES256 - if not isinstance(self.keymaterial, PrivKeyField): - raise TypeError("Key material is not a private key, cannot protect") - self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec, iv=iv) - del passphrase - self.update_hlen() - - def unprotect(self, passphrase): - self.keymaterial.decrypt_keyblob(passphrase) - del passphrase - - def sign(self, sigdata, hash_alg): - return self.keymaterial.sign(sigdata, hash_alg) - -class PrivSubKey(VersionedPacket, Sub, Private): +class PrivSubKey(PrivKey, Sub): __typeid__ = PacketTag.SecretSubKey __ver__ = 0 diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 8f632133..7e29650f 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -47,7 +47,10 @@ from .packet import Packet from .packet import Primary from .packet import Private +from .packet import PubKey from .packet import PubKeyV4 +from .packet import PrivKey +from .packet import PrivSubKey from .packet import PrivKeyV4 from .packet import PrivSubKeyV4 from .packet import Public @@ -70,6 +73,8 @@ from .packet.packets import SKESessionKey from .packet.packets import SKESessionKeyV4 +from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub + from .packet.types import Opaque from .types import Armorable @@ -1530,8 +1535,10 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: """ if self._key is None: return None - if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA}: + if isinstance(self._key.keymaterial, (ECDSAPub, EdDSAPub, ECDHPub)): return self._key.keymaterial.oid + if self._key.keymaterial is None: + return None # check if keymaterial is not an Opaque class containing a bytearray param = next(iter(self._key.keymaterial)) if isinstance(param, bytearray): @@ -1691,7 +1698,7 @@ def __init__(self) -> None: of either of those methods. """ super().__init__() - self._key = None + self._key: Optional[PubKey] = None self._children = FingerprintDict["PGPKey"]() self._signatures = SorteDeque() self._uids: Deque[PGPUID] = SorteDeque() @@ -1854,7 +1861,7 @@ def protect(self, passphrase: str, # reset the salt for each key prefs['s2kspec']._salt = None - if sk._key is not None: + if isinstance(sk._key, PrivKey): sk._key.protect(passphrase, **prefs) del passphrase @@ -1981,7 +1988,7 @@ def add_subkey(self, key, **prefs): raise PGPError("Cannot add a key that already has subkeys as a subkey!") # convert key into a subkey - npk = PrivSubKeyV4() + npk: PrivSubKey = PrivSubKeyV4() npk.pkalg = key._key.pkalg npk.created = key._key.created npk.keymaterial = key._key.keymaterial @@ -2018,8 +2025,8 @@ def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: :param sig: The :py:obj:`PGPSignature` object the new signature is to be encapsulated within :returns: ``sig``, after the signature is added to it. """ - if self._key is None: - raise PGPError('Internal implementation error: PGPKey._key should not be None') + if not isinstance(self._key, PrivKey): + raise PGPError('Internal implementation error: PGPKey._key must be a private key to be able to sign') user = prefs.pop('user', None) uid = None From 428986a0cc26cc539110451bb9a98e6e44af9240 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 21 Jun 2023 18:48:37 -0400 Subject: [PATCH 181/287] more type annotation cleanup --- setup.py | 2 +- tests/conftest.py | 2 +- tests/test_06_compatibility.py | 2 ++ tests/test_99_regressions.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 60684932..eb1ac740 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ -from setuptools import setup +from setuptools import setup # type: ignore setup() diff --git a/tests/conftest.py b/tests/conftest.py index d16db53c..c3081a44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import glob try: - import gpg + import gpg # type: ignore except ImportError: gpg = None gpg_ver = 'unknown' diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index 8ffec7a1..e7905da6 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -1,6 +1,8 @@ # coding=utf-8 """ ensure that we don't crash on surprising messages """ +from typing import Optional + import pytest from pgpy import PGPKey, PGPMessage, PGPSignatures diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index cb5e6b1a..af0a6f4e 100644 --- a/tests/test_99_regressions.py +++ b/tests/test_99_regressions.py @@ -3,7 +3,7 @@ from conftest import gpg_ver, gnupghome try: - import gpg + import gpg # type: ignore except ImportError: gpg = None import os From 9fa615a8f637bebacc42d077063d186b44628e29 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 10:27:29 -0400 Subject: [PATCH 182/287] SED and SEIPD: type signatures for decrypt --- pgpy/packet/packets.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 98855ad6..b3d63f74 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1199,7 +1199,9 @@ def parse(self, packet): self.ct = packet[:self.header.length] del packet[:self.header.length] - def decrypt(self, key, alg): # pragma: no cover + def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray: # pragma: no cover + if alg is None: + raise TypeError("SED cannot decrypt without knowing the symmetric algorithm") block_size_bytes = alg.block_size // 8 pt_prefix = _cfb_decrypt(bytes(self.ct[:block_size_bytes + 2]), bytes(key), alg) @@ -1542,6 +1544,10 @@ class IntegrityProtectedSKEData(VersionedPacket): __typeid__ = PacketTag.SymmetricallyEncryptedIntegrityProtectedData __ver__ = 0 + @abc.abstractmethod + def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray: + raise NotImplementedError() + class IntegrityProtectedSKEDataV1(IntegrityProtectedSKEData): """ @@ -1678,7 +1684,9 @@ def encrypt(self, key, alg, data, iv: Optional[bytes] = None): self.ct = _cfb_encrypt(data, key, alg) self.update_hlen() - def decrypt(self, key, alg): + def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray: + if alg is None: + raise TypeError("SEIPDv1 cannot decrypt without knowing the symmetric algorithm") # iv, ivl2, pt = super(IntegrityProtectedSKEDataV1, self).decrypt(key, alg) pt = _cfb_decrypt(bytes(self.ct), bytes(key), alg) From 5cf4ca2794eb2a0db62baabd74f603c5732d7a4d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 10:28:37 -0400 Subject: [PATCH 183/287] PGPMessage: add type signatures, make some keyword arguments explicit If the session key in encrypt_sk is an int, we need to convert it manually to a bytes object. --- pgpy/pgp.py | 121 ++++++++++++++++++++---------------- tests/test_10_exceptions.py | 2 +- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 7e29650f..cd053835 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -17,7 +17,7 @@ from datetime import datetime, timezone -from typing import Any, Deque, List, Iterator, Mapping, Optional, Set, Tuple, Union +from typing import Any, ByteString, Deque, Literal, List, Iterator, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -927,11 +927,11 @@ def __format__(self, format_spec): class PGPMessage(Armorable, PGPObject): @staticmethod - def dash_unescape(text): + def dash_unescape(text: str) -> str: return re.subn(r'^- ', '', text, flags=re.MULTILINE)[0] @staticmethod - def dash_escape(text): + def dash_escape(text: str) -> str: return re.subn(r'^-', '- -', text, flags=re.MULTILINE)[0] @property @@ -940,29 +940,29 @@ def encrypters(self) -> Set[Union[KeyID, Fingerprint]]: return {m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey) and m.encrypter is not None} @property - def filename(self): - """If applicable, returns the original filename of the message. Otherwise, returns an empty string.""" - if self.type == 'literal': + def filename(self) -> Optional[str]: + """If applicable, returns the original filename of the message. Otherwise, returns None.""" + if self.type == 'literal' and isinstance(self._message, LiteralData): return self._message.filename - return '' + return None @property - def is_compressed(self): + def is_compressed(self) -> bool: """``True`` if this message will be compressed when exported""" return self._compression != CompressionAlgorithm.Uncompressed @property - def is_encrypted(self): + def is_encrypted(self) -> bool: """``True`` if this message is encrypted; otherwise, ``False``""" return isinstance(self._message, (SKEData, IntegrityProtectedSKEData)) @property - def is_sensitive(self): + def is_sensitive(self) -> bool: """``True`` if this message is marked sensitive; otherwise ``False``""" - return self.type == 'literal' and self._message.filename == '_CONSOLE' + return self.type == 'literal' and isinstance(self._message, LiteralData) and self._message.filename == '_CONSOLE' @property - def is_signed(self): + def is_signed(self) -> bool: """ ``True`` if this message is signed; otherwise, ``False``. Should always be ``False`` if the message is encrypted. @@ -975,25 +975,26 @@ def issuers(self) -> Set[Union[KeyID, Fingerprint]]: return self.encrypters | self.signers @property - def magic(self): + def magic(self) -> str: if self.type == 'cleartext': return "SIGNATURE" return "MESSAGE" @property - def message(self): + def message(self) -> Union[str, bytes, SKEData, IntegrityProtectedSKEData]: """The message contents""" if self.type == 'cleartext': return self.bytes_to_text(self._message) - if self.type == 'literal': + if isinstance(self._message, LiteralData): return self._message.contents - if self.type == 'encrypted': + if isinstance(self._message, (SKEData, IntegrityProtectedSKEData)): return self._message + raise ValueError(f'PGPMessage had unexpected type {self.type}') @property - def signatures(self): + def signatures(self) -> List[PGPSignature]: """A ``set`` containing all key ids (if any) which have signed this message.""" return list(self._signatures) @@ -1003,7 +1004,7 @@ def signers(self) -> Set[Union[KeyID, Fingerprint]]: return {m.signer for m in self._signatures} | {m.signer_fingerprint for m in self._signatures if m.signer_fingerprint is not None} @property - def type(self): + def type(self) -> Literal['cleartext', 'literal', 'encrypted']: ##TODO: it might be better to use an Enum for the output of this if isinstance(self._message, (str, bytes, bytearray)): return 'cleartext' @@ -1016,7 +1017,7 @@ def type(self): raise NotImplementedError - def __init__(self): + def __init__(self) -> None: """ PGPMessage objects represent OpenPGP message compositions. @@ -1030,13 +1031,13 @@ def __init__(self): of either of those methods. """ super().__init__() - self._compression = CompressionAlgorithm.Uncompressed - self._message = None - self._mdc = None - self._signatures = SorteDeque() - self._sessionkeys = [] + self._compression: CompressionAlgorithm = CompressionAlgorithm.Uncompressed + self._message: Optional[Union[str, bytes, bytearray, LiteralData, SKEData, IntegrityProtectedSKEData]] = None + self._mdc: Optional[MDC] = None + self._signatures: SorteDeque = SorteDeque() + self._sessionkeys: List[Union[PKESessionKey, SKESessionKey]] = [] - def __bytearray__(self): + def __bytearray__(self) -> bytearray: if self.is_compressed: comp = CompressedData() comp.calg = self._compression @@ -1049,7 +1050,7 @@ def __bytearray__(self): _bytes += pkt.__bytearray__() return _bytes - def __str__(self): + def __str__(self) -> str: if self.type == 'cleartext': tmpl = "-----BEGIN PGP SIGNED MESSAGE-----\n" \ "{hhdr:s}\n" \ @@ -1092,7 +1093,7 @@ def __iter__(self): for sig in self._signatures: yield sig - def __or__(self, other): + def __or__(self, other) -> 'PGPMessage': if isinstance(other, Marker): return self @@ -1146,7 +1147,7 @@ def __or__(self, other): raise NotImplementedError(str(type(other))) - def __copy__(self): + def __copy__(self) -> 'PGPMessage': msg = super().__copy__() msg._compression = self._compression msg._message = copy.copy(self._message) @@ -1161,7 +1162,13 @@ def __copy__(self): return msg @classmethod - def new(cls, message, **kwargs): + def new(cls, message: Union[str, bytes, bytearray], + cleartext: bool = False, + format: Optional[str] = None, + sensitive: bool = False, + compression: CompressionAlgorithm = CompressionAlgorithm.ZIP, + file: bool = False, + encoding: Optional[str] = None) -> 'PGPMessage': """ Create a new PGPMessage object. @@ -1187,27 +1194,24 @@ def new(cls, message, **kwargs): :type encoding: ``str`` representing a valid codec in codecs """ # TODO: have 'codecs' above (in :type encoding:) link to python documentation page on codecs - cleartext = kwargs.pop('cleartext', False) - format = kwargs.pop('format', None) - sensitive = kwargs.pop('sensitive', False) - compression = kwargs.pop('compression', CompressionAlgorithm.ZIP) - file = kwargs.pop('file', False) - charset = kwargs.pop('encoding', None) filename = '' mtime = datetime.now(timezone.utc) msg = PGPMessage() - if charset: - msg.charset = charset + if encoding: + msg.charset = encoding # if format in 'tu' and isinstance(message, (bytes, bytearray)): # # if message format is text or unicode and we got binary data, we'll need to transcode it to UTF-8 # message = if file and os.path.isfile(message): - filename = message + if not isinstance(message, str): + filename = message.decode() + else: + filename = message message = bytearray(os.path.getsize(filename)) mtime = datetime.fromtimestamp(os.path.getmtime(filename), timezone.utc) @@ -1230,7 +1234,7 @@ def new(cls, message, **kwargs): # if message is a binary type and we're building a textual message, we need to transcode the bytes to UTF-8 if isinstance(message, (bytes, bytearray)) and (cleartext or format in 'tu'): - message = message.decode(charset or 'utf-8') + message = message.decode(encoding or 'utf-8') if cleartext: msg |= message @@ -1250,7 +1254,11 @@ def new(cls, message, **kwargs): return msg - def encrypt(self, passphrase, sessionkey=None, **prefs): + def encrypt(self, passphrase: Union[str, bytes], + sessionkey: Optional[Union[int, ByteString]] = None, + cipher: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, + hash: HashAlgorithm = HashAlgorithm.SHA256, + iv: Optional[bytes] = None) -> 'PGPMessage': """ encrypt(passphrase, [sessionkey=None,] **prefs) @@ -1272,19 +1280,18 @@ def encrypt(self, passphrase, sessionkey=None, **prefs): :raises: :py:exc:`~errors.PGPEncryptionError` :returns: A new :py:obj:`PGPMessage` containing the encrypted contents of this message. """ - cipher_algo = prefs.pop('cipher', SymmetricKeyAlgorithm.AES256) - hash_algo = prefs.pop('hash', HashAlgorithm.SHA256) - # set up a new SKESessionKeyV4 skesk = SKESessionKeyV4() skesk.s2k.usage = 255 skesk.s2k.specifier = 3 - skesk.s2k.halg = hash_algo - skesk.s2k.encalg = cipher_algo + skesk.s2k.halg = hash + skesk.s2k.encalg = cipher skesk.s2k.count = 255 if sessionkey is None: - sessionkey = cipher_algo.gen_key() + sessionkey = cipher.gen_key() + elif isinstance(sessionkey, int): + sessionkey = self.int_to_bytes(sessionkey) skesk.encrypt_sk(passphrase, sessionkey) del passphrase @@ -1292,8 +1299,7 @@ def encrypt(self, passphrase, sessionkey=None, **prefs): if not self.is_encrypted: skedata = IntegrityProtectedSKEDataV1() - iv = prefs.pop('iv', None) - skedata.encrypt(sessionkey, cipher_algo, self.__bytes__(), iv = iv) + skedata.encrypt(sessionkey, cipher, self.__bytes__(), iv = iv) msg |= skedata else: @@ -1301,7 +1307,7 @@ def encrypt(self, passphrase, sessionkey=None, **prefs): return msg - def decrypt(self, passphrase): + def decrypt(self, passphrase: Union[str, bytes]) -> 'PGPMessage': """ Attempt to decrypt this message using a passphrase. @@ -1310,14 +1316,14 @@ def decrypt(self, passphrase): :raises: :py:exc:`~errors.PGPDecryptionError` if decryption failed for any reason. :returns: A new :py:obj:`PGPMessage` containing the decrypted contents of this message """ - if not self.is_encrypted: + if not isinstance(self._message, (SKEData, IntegrityProtectedSKEData)): raise PGPError("This message is not encrypted!") for skesk in iter(sk for sk in self._sessionkeys if isinstance(sk, SKESessionKey)): try: symalg, key = skesk.decrypt_sk(passphrase) decmsg = PGPMessage() - decmsg.parse(self.message.decrypt(key, symalg)) + decmsg.parse(self._message.decrypt(key, symalg)) except (TypeError, ValueError, NotImplementedError, PGPDecryptionError): continue @@ -1331,9 +1337,11 @@ def decrypt(self, passphrase): return decmsg - def parse(self, packet): + def parse(self, packet: bytearray) -> None: unarmored = self.ascii_unarmor(packet) data = unarmored['body'] + if not isinstance(data, bytearray): + raise TypeError(f"Expected data to be a bytearray, not {type(data)}") if unarmored['magic'] is not None and unarmored['magic'] not in ['MESSAGE', 'SIGNATURE']: raise ValueError('Expected: MESSAGE. Got: {}'.format(str(unarmored['magic']))) @@ -1345,9 +1353,12 @@ def parse(self, packet): if unarmored['magic'] == 'SIGNATURE': # the composition for this will be the 'cleartext' as a str, # followed by one or more signatures (each one loaded into a PGPSignature) - self |= self.dash_unescape(unarmored['cleartext']) + cleartext = unarmored['cleartext'] + if not isinstance(cleartext, str): + raise TypeError(f"Expected cleartext to be str, not {type(cleartext)}") + self |= self.dash_unescape(cleartext) while len(data) > 0: - pkt = Packet(data) + pkt = Packet(data) # type: ignore[abstract] if not isinstance(pkt, Signature): # pragma: no cover warnings.warn("Discarded unexpected packet: {:s}".format(pkt.__class__.__name__), stacklevel=2) continue @@ -1355,7 +1366,7 @@ def parse(self, packet): else: while len(data) > 0: - self |= Packet(data) + self |= Packet(data) # type: ignore[abstract] class PGPKey(Armorable, ParentRef, PGPObject): diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 4e5d7175..9655995a 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -381,7 +381,7 @@ def test_encrypt_insecure_cipher(self): def test_encrypt_sessionkey_wrongtype(self): msg = PGPMessage.new('asdf') - with pytest.raises(TypeError): + with pytest.raises(ValueError): msg.encrypt('asdf', sessionkey=0xabdf1234abdf1234, cipher=SymmetricKeyAlgorithm.AES128) def test_parse_wrong_magic(self): From e05f0268ae9072b2219c08725c95219bf5b38c20 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 10:30:01 -0400 Subject: [PATCH 184/287] sopgpy: rely on PGPMessage type annotations --- pgpy/sopgpy.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 5d7681f3..463bdd2b 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -413,7 +413,6 @@ def decrypt(self, encmsg: pgpy.PGPMessage = pgpy.PGPMessage.from_blob(data) msg: pgpy.PGPMessage ret: Optional[bytes] = None - out: Union[str, bytes] for handle, seckey in seckeys.items(): try: if seckey.is_protected: @@ -431,7 +430,7 @@ def decrypt(self, if isinstance(out, str): ret = out.encode('utf8') else: - ret = out + ret = bytes(out) break except pgpy.errors.PGPDecryptionError as e: logging.warning(f'could not decrypt with {seckey.fingerprint}') @@ -468,7 +467,7 @@ def decrypt(self, if isinstance(out, str): ret = out.encode('utf8') else: - ret = out + ret = bytes(out) break except pgpy.errors.PGPDecryptionError: pass @@ -543,7 +542,7 @@ def inline_detach(self, self.raise_on_unknown_options(**kwargs) msg: pgpy.PGPMessage msg = pgpy.PGPMessage.from_blob(clearsigned) - body: Union[bytes, bytearray, str] = msg.message + body = msg.message if isinstance(body, str): body = body.encode('utf-8') return (bytes(body), self._maybe_armor(armor, pgpy.PGPSignatures(msg.signatures))) @@ -609,10 +608,10 @@ def inline_verify(self, data: bytes, sigresults: List[sop.SOPSigResult] = self._check_sigs(certs, msg, None, start, end) if not sigresults: raise sop.SOPNoSignature("No good signature found") - outmsg: Union[bytes, str] = msg.message + outmsg = msg.message if isinstance(outmsg, str): outmsg = outmsg.encode("utf-8") - return (outmsg, sigresults) + return (bytes(outmsg), sigresults) def main() -> None: From 247c10f7d95fd5234d7ba7cdd8b9febb7ab211c9 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 11:30:32 -0400 Subject: [PATCH 185/287] PGPObject: add type signatures Note that bytes_to_text and text_to_bytes no longer accept None as input. When they would accept None as input, that meant that their output could also be None which in turn made it harder to vet the type of the output when the input was clearly not None. --- docs/source/changelog.rst | 3 +++ pgpy/packet/packets.py | 4 +++- pgpy/pgp.py | 4 ++-- pgpy/types.py | 23 ++++++++++------------- tests/test_01_types.py | 6 ------ 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 1df8c5e5..5701f424 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -83,6 +83,9 @@ produce a PGPSignature object). PGPUID.signers, PGPKey.signers, and PGPMessage.signers will now return Fingerprints, not just Key IDs. +Passing None to PGPObject.text_to_bytes or PGPObject.bytes_to_text is +now an error. + v0.6.0 ====== diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index b3d63f74..ae18cc9e 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -283,6 +283,8 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: return (symalg, symkey) def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: + if symalg is None: + raise ValueError('PKESKv3: must pass a symmetric key algorithm explicitly when encrypting') m = bytearray(self.int_to_bytes(symalg) + symkey) m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) @@ -684,7 +686,7 @@ def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: # symmetric algorithm for the following SED or SEIPD packet. # This is a reasonable simplification for generation, but it # won't always be the same when parsing - self.ct = _cfb_encrypt(self.int_to_bytes(self.symalg) + sk, esk, self.symalg) + self.ct = _cfb_encrypt(self.int_to_bytes(self.symalg) + bytes(sk), esk, self.symalg) # update header length and return sk self.update_hlen() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index cd053835..c888530d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -983,7 +983,7 @@ def magic(self) -> str: @property def message(self) -> Union[str, bytes, SKEData, IntegrityProtectedSKEData]: """The message contents""" - if self.type == 'cleartext': + if isinstance(self._message, (str, bytes, bytearray)): return self.bytes_to_text(self._message) if isinstance(self._message, LiteralData): @@ -1051,7 +1051,7 @@ def __bytearray__(self) -> bytearray: return _bytes def __str__(self) -> str: - if self.type == 'cleartext': + if isinstance(self._message, (bytes, bytearray, str)): tmpl = "-----BEGIN PGP SIGNED MESSAGE-----\n" \ "{hhdr:s}\n" \ "{cleartext:s}\n" \ diff --git a/pgpy/types.py b/pgpy/types.py index 52139e49..0438e4ab 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -15,7 +15,7 @@ from enum import IntEnum -from typing import Optional, Dict, List, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic +from typing import Optional, Dict, List, Literal, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic from .decorators import sdproperty @@ -266,27 +266,24 @@ def __init__(self): class PGPObject(metaclass=abc.ABCMeta): @staticmethod - def int_byte_len(i): + def int_byte_len(i: int) -> int: return (i.bit_length() + 7) // 8 @staticmethod - def bytes_to_int(b, order='big'): # pragma: no cover + def bytes_to_int(b: bytes, order: Literal['little', 'big'] = 'big') -> int: # pragma: no cover """convert bytes to integer""" return int.from_bytes(b, order) @staticmethod - def int_to_bytes(i, minlen=1, order='big'): # pragma: no cover + def int_to_bytes(i: int, minlen: int = 1, order: Literal['little', 'big'] = 'big') -> bytes: # pragma: no cover """convert integer to bytes""" blen = max(minlen, PGPObject.int_byte_len(i), 1) return i.to_bytes(blen, order) @staticmethod - def text_to_bytes(text): - if text is None: - return text - + def text_to_bytes(text: Union[str, bytes, bytearray]) -> Union[bytes, bytearray]: # if we got bytes, just return it if isinstance(text, (bytearray, bytes)): return text @@ -296,24 +293,24 @@ def text_to_bytes(text): return text.encode('utf-8') @staticmethod - def bytes_to_text(text): - if text is None or isinstance(text, str): + def bytes_to_text(text: Union[str, bytes, bytearray]) -> str: + if isinstance(text, str): return text return text.decode('utf-8') @abc.abstractmethod - def parse(self, packet): + def parse(self, packet: bytearray) -> None: """this method is too abstract to understand""" @abc.abstractmethod - def __bytearray__(self): + def __bytearray__(self) -> bytearray: """ Returns the contents of concrete subclasses in a binary format that can be understood by other OpenPGP implementations """ - def __bytes__(self): + def __bytes__(self) -> bytes: """ Return the contents of concrete subclasses in a binary format that can be understood by other OpenPGP implementations diff --git a/tests/test_01_types.py b/tests/test_01_types.py index e3a4c679..d666c7f0 100644 --- a/tests/test_01_types.py +++ b/tests/test_01_types.py @@ -62,11 +62,5 @@ def test_text_to_bytes_encodings(self, encoded_text): with pytest.raises(UnicodeDecodeError): pgpo.data.decode('utf-8') - def test_text_to_bytes_none(self): - assert PGPObject.text_to_bytes(None) is None - - def test_bytes_to_text_none(self): - assert PGPObject.bytes_to_text(None) is None - def test_bytes_to_text_text(self): assert PGPObject.bytes_to_text('asdf') == 'asdf' From 8497be3635a18edc9f3327548fce5e5aa530a057 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 11:35:52 -0400 Subject: [PATCH 186/287] Use Literal type for format byte for LiteralData --- pgpy/pgp.py | 3 ++- pgpy/sopgpy.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index c888530d..9d9425a1 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1036,6 +1036,7 @@ def __init__(self) -> None: self._mdc: Optional[MDC] = None self._signatures: SorteDeque = SorteDeque() self._sessionkeys: List[Union[PKESessionKey, SKESessionKey]] = [] + self.format: Literal['t', 'u', 'b', 'm'] = 'b' def __bytearray__(self) -> bytearray: if self.is_compressed: @@ -1164,7 +1165,7 @@ def __copy__(self) -> 'PGPMessage': @classmethod def new(cls, message: Union[str, bytes, bytearray], cleartext: bool = False, - format: Optional[str] = None, + format: Optional[Literal['t', 'u', 'b', 'm']] = None, sensitive: bool = False, compression: CompressionAlgorithm = CompressionAlgorithm.ZIP, file: bool = False, diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 463bdd2b..8a9c6493 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -40,7 +40,7 @@ from importlib import metadata from datetime import datetime, timezone -from typing import List, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable +from typing import List, Literal, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable from argparse import Namespace, _SubParsersAction, ArgumentParser from cryptography.hazmat.backends import openssl @@ -285,7 +285,7 @@ def encrypt(self, handle: str keys: MutableMapping[str, pgpy.PGPKey] = {} pws: MutableMapping[str, str] = {} - format_octet: str + format_octet: Literal['t', 'u', 'b', 'm'] if literaltype is sop.SOPLiteralDataType.text: format_octet = 'u' From dfdae47ecaf41b695bc4c2439d641c5b21fe2a16 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 11:46:32 -0400 Subject: [PATCH 187/287] PKESK: encrypt_sk and decrypt_sk need a proper corresponding PubKey or PrivKey packet In addition to the extra type safety, this also corrects an erroneous comment about them. --- pgpy/packet/packets.py | 14 ++++++++------ pgpy/pgp.py | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index ae18cc9e..33ec7156 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -90,14 +90,12 @@ class PKESessionKey(VersionedPacket): __typeid__ = PacketTag.PublicKeyEncryptedSessionKey __ver__ = 0 - # note that we don't have a good type signature for pk: it should be PrivKey, but from .fields, not the PrivKey in this file. @abc.abstractmethod - def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() - # note that we don't have a good type signature for pk: it should be PubKey, but from .fields, not the PubKey in this file. @abc.abstractmethod - def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: + def encrypt_sk(self, pk: 'PubKey', symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: raise NotImplementedError() # a PKESK should return a pointer to the recipient, or None @@ -237,8 +235,10 @@ def __copy__(self): return sk - def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: if isinstance(self.ct, RSACipherText): + if not isinstance(pk.keymaterial, PrivKeyField): + raise TypeError(f"Private key key material was {type(pk.keymaterial)}, should have been PrivKeyField") # pad up ct with null bytes if necessary ct = self.ct.me_mod_n.to_mpibytes()[2:] ct = b'\x00' * ((pk.keymaterial.__privkey__().key_size // 8) - len(ct)) + ct @@ -282,9 +282,11 @@ def decrypt_sk(self, pk) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: return (symalg, symkey) - def encrypt_sk(self, pk, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: + def encrypt_sk(self, pk: 'PubKey', symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: if symalg is None: raise ValueError('PKESKv3: must pass a symmetric key algorithm explicitly when encrypting') + if pk.keymaterial is None: + raise ValueError('PKESKv3: public key material must be instantiated') m = bytearray(self.int_to_bytes(symalg) + symkey) m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 9d9425a1..284c3690 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2724,6 +2724,8 @@ def encrypt(self, preference defaults and selection validation. :type user: ``str``, ``unicode`` """ + if self._key is None: + raise PGPError("PGPKey: cannot encrypt with incomplete key material") cipherprefs: Optional[List[SymmetricKeyAlgorithm]] = None compprefs: Optional[List[CompressionAlgorithm]] = None sig: PGPSignature From dc9314bbb471a4e4a5f460d1df650980934fa0a0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 12:09:51 -0400 Subject: [PATCH 188/287] More basic type annotations Note that we change types.Header._lenfmt from an int to a bool, and rename it _openpgp_format. this is in keeping with the crypto-refresh, which explicitly names the two headr formats "OpenPGP format" and "Legacy Format". --- pgpy/packet/types.py | 14 +++++------ pgpy/types.py | 60 +++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 2d59f640..c8ea1c42 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -39,7 +39,7 @@ def tag(self): @tag.register(int) @tag.register(PacketTag) def tag_int(self, val): - _tag = (val & 0x3F) if self._lenfmt else ((val & 0x3C) >> 2) + _tag = (val & 0x3F) if self._openpgp_format else ((val & 0x3C) >> 2) try: self._tag = PacketTag(_tag) @@ -55,11 +55,11 @@ def __init__(self): self.tag = 0x00 def __bytearray__(self): - tag = 0x80 | (self._lenfmt << 6) - tag |= (self.tag) if self._lenfmt else ((self.tag << 2) | {1: 0, 2: 1, 4: 2, 0: 3}[self.llen]) + tag = 0x80 | (0x40 if self._openpgp_format else 0x00) + tag |= (self.tag) if self._openpgp_format else ((self.tag << 2) | {1: 0, 2: 1, 4: 2, 0: 3}[self.llen]) _bytes = bytearray(self.int_to_bytes(tag)) - _bytes += self.encode_length(self.length, self._lenfmt, self.llen) + _bytes += self.encode_length(self.length, self._openpgp_format, self.llen) return _bytes def __len__(self): @@ -103,13 +103,13 @@ def parse(self, packet): :param packet: raw packet bytes """ - self._lenfmt = ((packet[0] & 0x40) >> 6) + self._openpgp_format = bool(packet[0] & 0x40) self.tag = packet[0] - if self._lenfmt == 0: + if not self._openpgp_format: self.llen = (packet[0] & 0x03) del packet[0] - if (self._lenfmt == 0 and self.llen > 0) or self._lenfmt == 1: + if (not self._openpgp_format and self.llen > 0) or self._openpgp_format: self.length = packet else: diff --git a/pgpy/types.py b/pgpy/types.py index 0438e4ab..02cc2500 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -15,7 +15,7 @@ from enum import IntEnum -from typing import Optional, Dict, List, Literal, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic +from typing import ByteString, Optional, Dict, List, Literal, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic from .decorators import sdproperty @@ -154,13 +154,15 @@ def ascii_unarmor(text: Union[str, bytes, bytearray]) -> Dict[str, Optional[Unio if m['crc'] is not None: m['crc'] = Header.bytes_to_int(base64.b64decode(m['crc'].encode())) - if Armorable.crc24(m['body']) != m['crc']: + if not isinstance(m['body'], ByteString): + warnings.warn(f"Armored body was not a ByteString ({type(m['body'])})") + elif Armorable.crc24(m['body']) != m['crc']: warnings.warn('Incorrect crc24', stacklevel=3) return m @staticmethod - def crc24(data): + def crc24(data: ByteString) -> int: # CRC24 computation, as described in the RFC 4880 section on Radix-64 Conversions # # The checksum is a 24-bit Cyclic Redundancy Check (CRC) converted to @@ -171,9 +173,6 @@ def crc24(data): # radix-64, rather than on the converted data. crc = Armorable.__crc24_init - if not isinstance(data, bytearray): - data = iter(data) - for b in data: crc ^= b << 16 @@ -220,7 +219,7 @@ def __init__(self): super().__init__() self.ascii_headers = collections.OrderedDict() - def __str__(self): + def __str__(self) -> str: payload = base64.b64encode(self.__bytes__()).decode('latin-1') payload = '\n'.join(payload[i:(i + 64)] for i in range(0, len(payload), 64)) @@ -321,14 +320,14 @@ def __bytes__(self) -> bytes: class Field(PGPObject): @abc.abstractmethod - def __len__(self): + def __len__(self) -> int: """Return the length of the output of __bytes__""" class Header(Field): @staticmethod - def encode_length(length, nhf=True, llen=1): - def _new_length(nl): + def encode_length(length: int, nhf: bool = True, llen: int = 1) -> bytes: + def _new_length(nl: int) -> bytes: if 192 > nl: return Header.int_to_bytes(nl) @@ -338,24 +337,23 @@ def _new_length(nl): return b'\xFF' + Header.int_to_bytes(nl, 4) - def _old_length(nl, llen): + def _old_length(nl: int, llen: int): return Header.int_to_bytes(nl, llen) if llen > 0 else b'' return _new_length(length) if nhf else _old_length(length, llen) @sdproperty - def length(self): + def length(self) -> int: return self._len - @length.register(int) - def length_int(self, val): - self._len = val + @length.register + def length_int(self, val: int) -> None: + self._len: int = val - @length.register(bytes) - @length.register(bytearray) - def length_bin(self, val): - def _new_len(b): - def _parse_len(a, offset=0): + @length.register + def length_bin(self, val: bytearray) -> None: + def _new_len(b: bytearray): + def _parse_len(a: Union[bytes, bytearray], offset: int = 0): # returns (the parsed length, size of length field, whether the length was of partial type) fo = a[offset] @@ -389,7 +387,7 @@ def _parse_len(a, offset=0): else: self._len = part_len - def _old_len(b): + def _old_len(b: bytearray) -> None: if self.llen > 0: self._len = self.bytes_to_int(b[:self.llen]) del b[:self.llen] @@ -397,13 +395,11 @@ def _old_len(b): else: # pragma: no cover self._len = 0 - _new_len(val) if self._lenfmt == 1 else _old_len(val) + _new_len(val) if self._openpgp_format else _old_len(val) @sdproperty - def llen(self): - lf = self._lenfmt - - if lf == 1: + def llen(self) -> int: + if self._openpgp_format: # new-format length if 192 > self.length: return 1 @@ -419,17 +415,17 @@ def llen(self): ##TODO: what if _llen needs to be (re)computed? return self._llen - @llen.register(int) - def llen_int(self, val): - if self._lenfmt == 0: + @llen.register + def llen_int(self, val: int) -> None: + if not self._openpgp_format: self._llen = {0: 1, 1: 2, 2: 4, 3: 0}[val] - def __init__(self): + def __init__(self) -> None: super().__init__() self._len = 1 self._llen = 1 - self._lenfmt = 1 - self._partial = False + self._openpgp_format: bool = True + self._partial: bool = False class DispatchGuidance(IntEnum): From 24d71d538463974d52b78b7072bd8bccba80fe7c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 12:42:37 -0400 Subject: [PATCH 189/287] declare OpenPGP armor header classes as explicit literals --- pgpy/pgp.py | 17 +++++++++++------ pgpy/types.py | 4 +++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 284c3690..bf44ea70 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -85,6 +85,7 @@ from .types import PGPObject from .types import SignatureVerification from .types import SorteDeque +from .types import PGPMagicClass __all__ = ['PGPSignature', 'PGPSignatures', @@ -238,7 +239,7 @@ def keyserverprefs(self) -> Optional[KeyServerPreferences]: return None @property - def magic(self): + def magic(self) -> PGPMagicClass: return "SIGNATURE" @property @@ -618,7 +619,7 @@ def __iter__(self) -> collections.abc.Iterator[PGPSignature]: yield sig @property - def magic(self) -> str: + def magic(self) -> PGPMagicClass: return "SIGNATURE" def __bytearray__(self) -> bytearray: @@ -975,7 +976,7 @@ def issuers(self) -> Set[Union[KeyID, Fingerprint]]: return self.encrypters | self.signers @property - def magic(self) -> str: + def magic(self) -> PGPMagicClass: if self.type == 'cleartext': return "SIGNATURE" return "MESSAGE" @@ -1558,9 +1559,13 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: return param.bit_length() @property - def magic(self) -> str: - return '{:s} KEY BLOCK'.format('PUBLIC' if (isinstance(self._key, Public) and not isinstance(self._key, Private)) else - 'PRIVATE' if isinstance(self._key, Private) else '') + def magic(self) -> PGPMagicClass: + if isinstance(self._key, Private): + return 'PRIVATE KEY BLOCK' + elif isinstance(self._key, Public): + return 'PUBLIC KEY BLOCK' + else: + raise TypeError(f'PGPKey has no appropriate type: {type(self._key)}') @property def pubkey(self): diff --git a/pgpy/types.py b/pgpy/types.py index 02cc2500..fa910a13 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -36,6 +36,8 @@ 'SignatureVerification', 'SorteDeque'] +PGPMagicClass = Literal['SIGNATURE', 'MESSAGE', 'PUBLIC KEY BLOCK', 'PRIVATE KEY BLOCK'] + class Armorable(metaclass=abc.ABCMeta): __crc24_init = 0x0B704CE @@ -184,7 +186,7 @@ def crc24(data: ByteString) -> int: return crc & 0xFFFFFF @abc.abstractproperty - def magic(self): + def magic(self) -> PGPMagicClass: """The magic string identifier for the current PGP type""" @classmethod From 17e5e752ec54580f277810245a17023fe878f977 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 14:59:53 -0400 Subject: [PATCH 190/287] tests: type annotation and PEP-8 cleanup --- tests/test_05_actions.py | 12 +++++++----- tests/test_06_compatibility.py | 14 ++++++++------ tests/test_10_exceptions.py | 4 +++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 2ca5773b..36fad262 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -7,7 +7,7 @@ import copy import glob try: - import gpg + import gpg # type: ignore except ImportError: gpg = None import itertools @@ -16,6 +16,8 @@ import warnings from datetime import datetime, timedelta, timezone +from typing import Dict, Tuple, Union + from pgpy import PGPKey from pgpy import PGPMessage from pgpy import PGPSignature @@ -34,7 +36,7 @@ from pgpy.packet import Packet from pgpy.packet.packets import PrivKeyV4 from pgpy.packet.packets import PrivSubKeyV4 - +from pgpy.types import KeyID, Fingerprint enc_msgs = [ PGPMessage.from_file(f) for f in sorted(glob.glob('tests/testdata/messages/message*.pass*.asc')) ] @@ -242,7 +244,7 @@ class TestPGPKey_Management: # - adding/removing UIDs # - adding/removing signatures # - protecting/unlocking - keys = {} + keys: Dict[Tuple[PubKeyAlgorithm, Union[int, EllipticCurveOID]], PGPKey] = {} def gpg_verify_key(self, key): with gpg.Context(offline=True) as c: @@ -622,8 +624,8 @@ def targette_sec(): symalgos = sorted(filter(lambda x: x is not SymmetricKeyAlgorithm.Plaintext, sorted(SymmetricKeyAlgorithm))) class TestPGPKey_Actions: - sigs = {} - msgs = {} + sigs: Dict[Union[str, Tuple[KeyID, str]], PGPSignature] = {} + msgs: Dict[Tuple[Fingerprint, SymmetricKeyAlgorithm], PGPMessage] = {} def gpg_verify(self, subject, sig=None, pubkey=None): # verify with GnuPG diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py index e7905da6..63b8668f 100644 --- a/tests/test_06_compatibility.py +++ b/tests/test_06_compatibility.py @@ -6,6 +6,7 @@ import pytest from pgpy import PGPKey, PGPMessage, PGPSignatures +from pgpy.types import SignatureVerification from pgpy.constants import SecurityIssues import glob @@ -28,10 +29,10 @@ def test_bob_sig_from_multisig(self, sig:str)-> None: (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob.pgp') msg = 'Hello World :)' sigs = PGPSignatures.from_file(f'tests/testdata/compatibility/{sig}') - verif:Optional[pgpy.SignatureVerification] = None - for sig in sigs: - if sig.signer == k.fingerprint.keyid: - verif = k.verify(msg, sig) + verif:Optional[SignatureVerification] = None + for asig in sigs: + if asig.signer == k.fingerprint.keyid: + verif = k.verify(msg, asig) assert verif is not None def test_cert_unknown_algo(self) -> None: @@ -54,7 +55,8 @@ def test_cert_unknown_curve(self, flavor:str) -> None: def test_unknown_message(self, msg:str)-> None: k:PGPKey (k, _) = PGPKey.from_file('tests/testdata/compatibility/bob-key.pgp') - msg:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') - cleartext:PGPMessage = k.decrypt(msg) + msgobj:PGPMessage = PGPMessage.from_file(f'tests/testdata/compatibility/{msg}') + cleartext:PGPMessage = k.decrypt(msgobj) assert not cleartext.is_encrypted + assert isinstance(cleartext.message, (bytes, bytearray)) assert cleartext.message.startswith(b'Encrypted') diff --git a/tests/test_10_exceptions.py b/tests/test_10_exceptions.py index 9655995a..28aa82dd 100644 --- a/tests/test_10_exceptions.py +++ b/tests/test_10_exceptions.py @@ -5,6 +5,8 @@ import glob import warnings +from typing import Dict, List, Union + from pgpy import PGPKey from pgpy import PGPKeyring from pgpy import PGPMessage @@ -76,7 +78,7 @@ def temp_key(): key_algs_unim = [ pka for pka in PubKeyAlgorithm if not pka.can_gen and not pka.deprecated ] key_algs_rsa_depr = [ pka for pka in PubKeyAlgorithm if pka.deprecated and pka is not PubKeyAlgorithm.FormerlyElGamalEncryptOrSign ] -key_algs_badsizes = { +key_algs_badsizes: Dict[PubKeyAlgorithm, List[Union[int, EllipticCurveOID]]] = { PubKeyAlgorithm.RSAEncryptOrSign: [256], PubKeyAlgorithm.DSA: [512], PubKeyAlgorithm.ECDSA: [curve for curve in EllipticCurveOID if not curve.can_gen], From 0c816b7e6e4cb07d0707742c79d1151236659978 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 15:14:52 -0400 Subject: [PATCH 191/287] Every Armorable is a PGPObject. Make that inheritance explicit. --- pgpy/pgp.py | 8 ++-- pgpy/types.py | 114 +++++++++++++++++++++++++------------------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index bf44ea70..b74bc81d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -95,7 +95,7 @@ 'PGPKeyring'] -class PGPSignature(Armorable, ParentRef, PGPObject): +class PGPSignature(Armorable, ParentRef): ReasonForRevocation = collections.namedtuple('ReasonForRevocation', ['code', 'comment']) @property @@ -599,7 +599,7 @@ def parse(self, packet): raise ValueError('Expected: Signature. Got: {:s}'.format(pkt.__class__.__name__)) -class PGPSignatures(collections.abc.Container, collections.abc.Iterable, collections.abc.Sized, Armorable, PGPObject): +class PGPSignatures(collections.abc.Container, collections.abc.Iterable, collections.abc.Sized, Armorable): '''OpenPGP detached signatures can often contain more than one signature in them.''' def __init__(self, signatures: List[PGPSignature] = []) -> None: @@ -926,7 +926,7 @@ def __format__(self, format_spec): raise NotImplementedError -class PGPMessage(Armorable, PGPObject): +class PGPMessage(Armorable): @staticmethod def dash_unescape(text: str) -> str: return re.subn(r'^- ', '', text, flags=re.MULTILINE)[0] @@ -1371,7 +1371,7 @@ def parse(self, packet: bytearray) -> None: self |= Packet(data) # type: ignore[abstract] -class PGPKey(Armorable, ParentRef, PGPObject): +class PGPKey(Armorable, ParentRef): """ 11.1. Transferable Public Keys diff --git a/pgpy/types.py b/pgpy/types.py index fa910a13..558fc147 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -39,7 +39,63 @@ PGPMagicClass = Literal['SIGNATURE', 'MESSAGE', 'PUBLIC KEY BLOCK', 'PRIVATE KEY BLOCK'] -class Armorable(metaclass=abc.ABCMeta): +class PGPObject(metaclass=abc.ABCMeta): + + @staticmethod + def int_byte_len(i: int) -> int: + return (i.bit_length() + 7) // 8 + + @staticmethod + def bytes_to_int(b: bytes, order: Literal['little', 'big'] = 'big') -> int: # pragma: no cover + """convert bytes to integer""" + + return int.from_bytes(b, order) + + @staticmethod + def int_to_bytes(i: int, minlen: int = 1, order: Literal['little', 'big'] = 'big') -> bytes: # pragma: no cover + """convert integer to bytes""" + blen = max(minlen, PGPObject.int_byte_len(i), 1) + + return i.to_bytes(blen, order) + + @staticmethod + def text_to_bytes(text: Union[str, bytes, bytearray]) -> Union[bytes, bytearray]: + # if we got bytes, just return it + if isinstance(text, (bytearray, bytes)): + return text + + # if we were given a unicode string, or if we translated the string into utf-8, + # we know that Python already has it in utf-8 encoding, so we can now just encode it to bytes + return text.encode('utf-8') + + @staticmethod + def bytes_to_text(text: Union[str, bytes, bytearray]) -> str: + if isinstance(text, str): + return text + + return text.decode('utf-8') + + @abc.abstractmethod + def parse(self, packet: bytearray) -> None: + """this method is too abstract to understand""" + + @abc.abstractmethod + def __bytearray__(self) -> bytearray: + """ + Returns the contents of concrete subclasses in a binary format that can be understood by other OpenPGP + implementations + """ + + def __bytes__(self) -> bytes: + """ + Return the contents of concrete subclasses in a binary format that can be understood by other OpenPGP + implementations + """ + # this is what all subclasses will do anyway, so doing this here we can reduce code duplication significantly + return bytes(self.__bytearray__()) + + +class Armorable(PGPObject, metaclass=abc.ABCMeta): __crc24_init = 0x0B704CE __crc24_poly = 0x1864CFB @@ -264,62 +320,6 @@ def __init__(self): self._parent = None -class PGPObject(metaclass=abc.ABCMeta): - - @staticmethod - def int_byte_len(i: int) -> int: - return (i.bit_length() + 7) // 8 - - @staticmethod - def bytes_to_int(b: bytes, order: Literal['little', 'big'] = 'big') -> int: # pragma: no cover - """convert bytes to integer""" - - return int.from_bytes(b, order) - - @staticmethod - def int_to_bytes(i: int, minlen: int = 1, order: Literal['little', 'big'] = 'big') -> bytes: # pragma: no cover - """convert integer to bytes""" - blen = max(minlen, PGPObject.int_byte_len(i), 1) - - return i.to_bytes(blen, order) - - @staticmethod - def text_to_bytes(text: Union[str, bytes, bytearray]) -> Union[bytes, bytearray]: - # if we got bytes, just return it - if isinstance(text, (bytearray, bytes)): - return text - - # if we were given a unicode string, or if we translated the string into utf-8, - # we know that Python already has it in utf-8 encoding, so we can now just encode it to bytes - return text.encode('utf-8') - - @staticmethod - def bytes_to_text(text: Union[str, bytes, bytearray]) -> str: - if isinstance(text, str): - return text - - return text.decode('utf-8') - - @abc.abstractmethod - def parse(self, packet: bytearray) -> None: - """this method is too abstract to understand""" - - @abc.abstractmethod - def __bytearray__(self) -> bytearray: - """ - Returns the contents of concrete subclasses in a binary format that can be understood by other OpenPGP - implementations - """ - - def __bytes__(self) -> bytes: - """ - Return the contents of concrete subclasses in a binary format that can be understood by other OpenPGP - implementations - """ - # this is what all subclasses will do anyway, so doing this here we can reduce code duplication significantly - return bytes(self.__bytearray__()) - - class Field(PGPObject): @abc.abstractmethod def __len__(self) -> int: From 8aeb86f7c0d6f9d556f14984a94bf35be183282c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 15:52:45 -0400 Subject: [PATCH 192/287] PGPUID: add type annotations Note that you used to be able to use the or operator to append UserID and UserAttribute packets indefinitely, but only the first packet would take effect. all the other ones would be silently ignored. the current code is simpler, but instead ignores all the previous packets, and uses the last one. --- pgpy/pgp.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b74bc81d..3c8c34d7 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -712,7 +712,7 @@ def userid(self) -> Optional[str]: return self._uid.uid if isinstance(self._uid, UserID) else None @property - def image(self): + def image(self) -> Optional[bytearray]: """ If this is a User Attribute, this will be the stored image. If this is not a User Attribute, this will be ``None``. """ @@ -764,15 +764,16 @@ def signers(self) -> Set[Union[KeyID, Fingerprint]]: return {s.signer for s in self.__sig__} | {s.signer_fingerprint for s in self.__sig__ if s.signer_fingerprint is not None} @property - def hashdata(self): + def hashdata(self) -> bytearray: if self.is_uid: return self._uid.__bytearray__()[len(self._uid.header):] - if self.is_ua: + if isinstance(self._uid, UserAttribute): return self._uid.subpackets.__bytearray__() + return bytearray(b'') @property - def third_party_certifications(self): + def third_party_certifications(self) -> Iterator[PGPSignature]: ''' A generator returning all third-party certifications ''' @@ -784,7 +785,7 @@ def third_party_certifications(self): if (sig.signer_fingerprint != '' and fpr != sig.signer_fingerprint) or (sig.signer != keyid): yield sig - def attested_to(self, certifications): + def attested_to(self, certifications: Iterator[PGPSignature]) -> Iterator[PGPSignature]: '''filter certifications, only returning those that have been attested to by the first party''' # first find the set of the most recent valid Attestation Key Signatures: if self.parent is None: @@ -811,7 +812,7 @@ def attested_to(self, certifications): yield certification @property - def attested_third_party_certifications(self): + def attested_third_party_certifications(self) -> Iterator[PGPSignature]: ''' A generator that provides a list of all third-party certifications attested to by the primary key. @@ -819,7 +820,7 @@ def attested_third_party_certifications(self): return self.attested_to(self.third_party_certifications) @classmethod - def new(cls, pn, comment="", email=""): + def new(cls, pn: Union[bytearray, str], comment: Optional[str] = None, email: Optional[str] = None) -> 'PGPUID': """ Create a new User ID or photo. @@ -851,7 +852,7 @@ def new(cls, pn, comment="", email=""): return uid - def __init__(self): + def __init__(self) -> None: """ PGPUID objects represent User IDs and User Attributes for keys. @@ -859,15 +860,15 @@ def __init__(self): 'name (comment) ', leaving out any comment or email fields that are not present. """ super().__init__() - self._uid = None + self._uid: Union[UserID, UserAttribute] self._signatures = SorteDeque() - def __repr__(self): + def __repr__(self) -> str: if self.selfsig is not None: return "".format(self._uid.__class__.__name__, self.selfsig.created, id(self)) return "".format(self._uid.__class__.__name__, id(self)) - def __lt__(self, other): # pragma: no cover + def __lt__(self, other: 'PGPUID') -> bool: # pragma: no cover if self.is_uid == other.is_uid: if self.is_primary == other.is_primary: mysig = self.selfsig @@ -889,7 +890,9 @@ def __lt__(self, other): # pragma: no cover if self.is_ua and other.is_uid: return False - def __or__(self, other): + raise ValueError("should not have reached here!") + + def __or__(self, other: Union[PGPSignature, UserID, UserAttribute]) -> 'PGPUID': if isinstance(other, PGPSignature): self._signatures.insort(other) if self.parent is not None and self in self.parent._uids: @@ -897,18 +900,18 @@ def __or__(self, other): return self - if isinstance(other, UserID) and self._uid is None: + if isinstance(other, UserID): self._uid = other return self - if isinstance(other, UserAttribute) and self._uid is None: + if isinstance(other, UserAttribute): self._uid = other return self raise TypeError("unsupported operand type(s) for |: '{:s}' and '{:s}'" "".format(self.__class__.__name__, other.__class__.__name__)) - def __copy__(self): + def __copy__(self) -> 'PGPUID': # because the default shallow copy isn't actually all that useful, # and deepcopy does too much work uid = PGPUID() @@ -917,7 +920,7 @@ def __copy__(self): uid |= copy.copy(sig) return uid - def __format__(self, format_spec): + def __format__(self, format_spec: str) -> str: if self.is_uid: comment = "" if self.comment == "" else " ({:s})".format(self.comment) email = "" if self.email == "" else " <{:s}>".format(self.email) From 96c422a4069597f1b309115d32d023a3d6fd0f94 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 18:59:00 -0400 Subject: [PATCH 193/287] SymmetricKeyAlgorithm: simplify mapping to cryptography module further --- pgpy/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index 852172c5..b6a700d2 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm from .decorators import classproperty From d46439d784d4043d55feba4295f844082026a881 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 22 Jun 2023 21:36:06 -0400 Subject: [PATCH 194/287] PGPSignature: add more type signatures --- pgpy/pgp.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 3c8c34d7..fd1fd5a6 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -181,14 +181,14 @@ def hash_algorithm(self) -> HashAlgorithm: """ return self._signature.halg - def check_primitives(self): + def check_primitives(self) -> SecurityIssues: return self.hash_algorithm.is_considered_secure - def check_soundness(self): + def check_soundness(self) -> SecurityIssues: return self.check_primitives() @property - def is_expired(self): + def is_expired(self) -> bool: """ ``True`` if the signature has an expiration date, and is expired. Otherwise, ``False`` """ @@ -199,14 +199,14 @@ def is_expired(self): return False @property - def key_algorithm(self): + def key_algorithm(self) -> PubKeyAlgorithm: """ The :py:obj:`~constants.PubKeyAlgorithm` of the key that generated this signature. """ return self._signature.pubalg @property - def key_expiration(self): + def key_expiration(self) -> Optional[datetime]: if 'KeyExpirationTime' in self._signature.subpackets: return next(iter(self._signature.subpackets['KeyExpirationTime'])).expires return None @@ -325,7 +325,7 @@ def target_signature(self): return NotImplemented @property - def type(self): + def type(self) -> SignatureType: """ The :py:obj:`~constants.SignatureType` of this signature. """ @@ -407,7 +407,7 @@ def attests_to(self, othersig): h.update(othersig._signature.canonical_bytes()) return h.finalize() in self.attested_certifications - def hashdata(self, subject): + def hashdata(self, subject) -> bytes: _data = bytearray() if isinstance(subject, str): @@ -577,7 +577,7 @@ def hashdata(self, subject): def make_onepass(self) -> OnePassSignature: return self._signature.make_onepass() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: unarmored = self.ascii_unarmor(packet) data = unarmored['body'] @@ -588,7 +588,7 @@ def parse(self, packet): self.ascii_headers = unarmored['headers'] # load *one* packet from data - pkt = Packet(data) + pkt = Packet(data) # type: ignore if pkt.header.tag == PacketTag.Signature: if isinstance(pkt, Opaque): # this is an unrecognized version. From 94c3f7c1eff189ca31bcb5b3851249dc98e02894 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 23 Jun 2023 17:20:09 -0400 Subject: [PATCH 195/287] PGPUID: improve string parsing, return None when fields are missing This also avoids parsing and then re-assembling the user ID during __format__ operations, which in the worst case can actually modify the string returned. --- docs/source/changelog.rst | 6 ++++ pgpy/pgp.py | 68 +++++++++++++++++++++--------------- tests/test_04_PGP_objects.py | 6 ++-- tests/test_11_userids.py | 37 ++++++++++++++++++++ 4 files changed, 85 insertions(+), 32 deletions(-) create mode 100644 tests/test_11_userids.py diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 5701f424..0427b13f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -86,6 +86,12 @@ Fingerprints, not just Key IDs. Passing None to PGPObject.text_to_bytes or PGPObject.bytes_to_text is now an error. +PGPUID's name, email, and comment members all return None if the field +in question doesn't exist in the User ID string, rather than returning +the empty string. User ID string parsing is also improved, to better +handle raw e-mail addresses (without angle-brackets) and other subtle +variations. + v0.6.0 ====== diff --git a/pgpy/pgp.py b/pgpy/pgp.py index fd1fd5a6..918ae4f6 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -658,49 +658,61 @@ class PGPUID(ParentRef): def __sig__(self): return list(self._signatures) - def _splitstring(self) -> Tuple[str, str, str]: + def _splitstring(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: '''returns name, comment email from User ID string''' if not isinstance(self._uid, UserID): - return "", "", "" + return (None, None, None) if self._uid.uid == "": - return "", "", "" - output = re.match(r"""^ - # name should always match something - (?P.+?) - # comment *optionally* matches text in parens following name - # this should never come after email and must be followed immediately by - # either the email field, or the end of the packet. - (\ \((?P.+?)\)(?=(\ <|$)))? - # email *optionally* matches text in angle brackets following name or comment - # this should never come before a comment, if comment exists, - # but can immediately follow name if comment does not exist - (\ <(?P.+)>)? - $ - """, self._uid.uid, flags=re.VERBOSE) + return (None, None, None) + + specials = r'[()<>\[\]:;@\\,."]' + atext = "[-A-Za-z0-9!#$%&'*+/=?^_`{|}~\x80-\U0010ffff]" + dot_atom_text = atext + r"+(?:\." + atext + "+)*" + pgp_addr_spec = dot_atom_text + "@" + dot_atom_text + pgp_uid_prefix_char = "(?:" + atext + "|" + specials + "| )" + addr_spec_raw = "(?P" + pgp_addr_spec + ")" + pgp_comment = r'(?: *\((?P' + pgp_uid_prefix_char + r'*)\) *)?' + pgp_name = '(?P' + pgp_uid_prefix_char + r'*?)' + pgp_email = r'<(?P' + pgp_addr_spec + ")>" + addr_spec_wrapped = pgp_name + pgp_comment + pgp_email + pgp_uid_convention = "^(?:" + addr_spec_raw + "|" + addr_spec_wrapped + ")$" + + pgp_uid_convention_re = re.compile(pgp_uid_convention, re.UNICODE) + + output = pgp_uid_convention_re.match(self._uid.uid) if output is None: - raise ValueError("the standard User ID regex should have always matched something!") - rfc2822 = output.groupdict() + return self._uid.uid, None, None - return (rfc2822['name'], rfc2822['comment'] or "", rfc2822['email'] or "") + name = output['name'] + if name is not None: + name = name.strip() + if name == '': + name = None + + comment = output['comment'] + if comment is not None and comment == '': + comment = None + + return (name, comment, output['addr_spec_wrapped'] or output['addr_spec_raw']) @property - def name(self) -> str: - """If this is a User ID, the stored name. If this is not a User ID, this will be an empty string.""" + def name(self) -> Optional[str]: + """If this is a User ID, the stored name. If this is not a User ID, or if the User ID only contains an e-mail address, this will be None.""" return self._splitstring()[0] @property - def comment(self) -> str: + def comment(self) -> Optional[str]: """ If this is a User ID, this will be the stored comment. If this is not a User ID, or there is no stored comment, - this will be an empty string., + this will be None. """ return self._splitstring()[1] @property - def email(self) -> str: + def email(self) -> Optional[str]: """ If this is a User ID, this will be the stored email address. If this is not a User ID, or there is no stored - email address, this will be an empty string. + email address, this will be None. """ return self._splitstring()[2] @@ -921,10 +933,8 @@ def __copy__(self) -> 'PGPUID': return uid def __format__(self, format_spec: str) -> str: - if self.is_uid: - comment = "" if self.comment == "" else " ({:s})".format(self.comment) - email = "" if self.email == "" else " <{:s}>".format(self.email) - return "{:s}{:s}{:s}".format(self.name, comment, email) + if isinstance(self._uid, UserID): + return self._uid.uid raise NotImplementedError diff --git a/tests/test_04_PGP_objects.py b/tests/test_04_PGP_objects.py index 04216eb5..d5421718 100644 --- a/tests/test_04_PGP_objects.py +++ b/tests/test_04_PGP_objects.py @@ -71,9 +71,9 @@ def test_userid(self, abe): assert abe.image is None def test_userphoto(self, abe_image): - assert abe_image.name == "" - assert abe_image.comment == "" - assert abe_image.email == "" + assert abe_image.name is None + assert abe_image.comment is None + assert abe_image.email is None with open('tests/testdata/abe.jpg', 'rb') as abef: abebytes = bytearray(os.path.getsize('tests/testdata/abe.jpg')) abef.readinto(abebytes) diff --git a/tests/test_11_userids.py b/tests/test_11_userids.py new file mode 100644 index 00000000..208a0ab4 --- /dev/null +++ b/tests/test_11_userids.py @@ -0,0 +1,37 @@ +# coding=utf-8 +""" verify that User ID parsing aligns with expected behavior + +See also https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/23 for more discussion +""" + +from typing import Dict, Optional, Tuple + +import pytest + +from pgpy import PGPUID + +uids: Dict[str, Tuple[Optional[str], Optional[str], Optional[str]]] = { + 'Alice Lovelace ': ('Alice Lovelace', None, 'alice@example.org'), + '': (None, None, 'alice@example.org'), + ' ': (None, None, 'alice@example.org'), + 'Alice Lovelace (j. random hacker) ': ('Alice Lovelace', 'j. random hacker', 'alice@example.org'), + 'alice@example.org': (None, None, 'alice@example.org'), + 'Alice Lovelace': ('Alice Lovelace', None, None), +} + + +class TestPGP_UserIDs(object): + @pytest.mark.parametrize('uid', uids.keys()) + def test_uid_name(self, uid: str): + pgpuid = PGPUID.new(uid) + assert pgpuid.name == uids[uid][0] + + @pytest.mark.parametrize('uid', uids.keys()) + def test_uid_comment(self, uid: str): + pgpuid = PGPUID.new(uid) + assert pgpuid.comment == uids[uid][1] + + @pytest.mark.parametrize('uid', uids.keys()) + def test_uid_email(self, uid: str): + pgpuid = PGPUID.new(uid) + assert pgpuid.email == uids[uid][2] From e10e64fdfb8ef6fdf628510b9382cbcfa6316e18 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 27 Jun 2023 23:55:28 -0400 Subject: [PATCH 196/287] PKESessionKeyV3: move initialization, pubkey algo, and ciphertext to superclass --- pgpy/packet/packets.py | 49 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 33ec7156..289dd74f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -90,6 +90,12 @@ class PKESessionKey(VersionedPacket): __typeid__ = PacketTag.PublicKeyEncryptedSessionKey __ver__ = 0 + def __init__(self) -> None: + super().__init__() + self._pkalg: PubKeyAlgorithm = PubKeyAlgorithm.Unknown + self._opaque_pkalg: int = 0 + self.ct: Optional[CipherText] = None + @abc.abstractmethod def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() @@ -103,6 +109,26 @@ def encrypt_sk(self, pk: 'PubKey', symalg: Optional[SymmetricKeyAlgorithm], symk def encrypter(self) -> Optional[Union[KeyID, Fingerprint]]: raise NotImplementedError() + @sdproperty + def pkalg(self): + return self._pkalg + + @pkalg.register + def pkalg_int(self, val: int) -> None: + if isinstance(val, PubKeyAlgorithm): + self._pkalg = val + else: + self._pkalg = PubKeyAlgorithm(val) + if self._pkalg is PubKeyAlgorithm.Invalid: + self._opaque_pkalg = val + + if self._pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt}: + self.ct = RSACipherText() + elif self._pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: + self.ct = ElGCipherText() + elif self._pkalg is PubKeyAlgorithm.ECDH: + self.ct = ECDHCipherText() + class PKESessionKeyV3(PKESessionKey): """ @@ -182,32 +208,9 @@ def encrypter_bin(self, val: Union[bytearray, KeyID]) -> None: else: self._encrypter = KeyID(val) - @sdproperty - def pkalg(self): - return self._pkalg - - @pkalg.register - def pkalg_int(self, val: int) -> None: - if isinstance(val, PubKeyAlgorithm): - self._pkalg = val - else: - self._pkalg = PubKeyAlgorithm(val) - if self._pkalg is PubKeyAlgorithm.Invalid: - self._opaque_pkalg: int = val - - self.ct: Optional[CipherText] = None - if self._pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt}: - self.ct = RSACipherText() - elif self._pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: - self.ct = ElGCipherText() - elif self._pkalg is PubKeyAlgorithm.ECDH: - self.ct = ECDHCipherText() - def __init__(self) -> None: super().__init__() self._encrypter = None - self.pkalg = 0 - self.ct = None def __bytearray__(self): _bytes = bytearray() From d9bd771e2e1b006668e199e22c67446aeb2bcb3f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 16 Nov 2022 17:45:10 -0500 Subject: [PATCH 197/287] Prepare SubPackets object to have different length binary representations in v6 keys, the wire format for the subpacket sections contains 4-octet length fields instead of 2. Make the rest of the code flexible enough to prepare for that (without changing it) --- pgpy/packet/fields.py | 34 ++++++++++++++++++++-------------- pgpy/packet/packets.py | 3 ++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index d195b261..99b15db5 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -40,6 +40,8 @@ from .subpackets import signature from .subpackets import userattribute +from .subpackets.types import SubPacket + from .types import MPI from .types import MPIs @@ -101,32 +103,36 @@ class SubPackets(collections.abc.MutableMapping, Field): _spmodule = signature - def __init__(self): + def __init__(self, width: int = 2) -> None: super().__init__() - self._hashed_sp = collections.OrderedDict() - self._unhashed_sp = collections.OrderedDict() + self._hashed_sp: collections.OrderedDict[str, SubPacket] = collections.OrderedDict() + self._unhashed_sp: collections.OrderedDict[str, SubPacket] = collections.OrderedDict() + # self._width represents how wide the size field is when these + # subpackets are put on the wire. v4 subpackets use a width + # of 2. newer subpackets use a width of 4. + self._width = width - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += self.__hashbytearray__() _bytes += self.__unhashbytearray__() return _bytes - def __hashbytearray__(self): + def __hashbytearray__(self) -> bytearray: _bytes = bytearray() - _bytes += self.int_to_bytes(sum(len(sp) for sp in self._hashed_sp.values()), 2) + _bytes += self.int_to_bytes(sum(len(sp) for sp in self._hashed_sp.values()), self._width) for hsp in self._hashed_sp.values(): _bytes += hsp.__bytearray__() return _bytes - def __unhashbytearray__(self): + def __unhashbytearray__(self) -> bytearray: _bytes = bytearray() - _bytes += self.int_to_bytes(sum(len(sp) for sp in self._unhashed_sp.values()), 2) + _bytes += self.int_to_bytes(sum(len(sp) for sp in self._unhashed_sp.values()), self._width) for uhsp in self._unhashed_sp.values(): _bytes += uhsp.__bytearray__() return _bytes - def __len__(self): # pragma: no cover + def __len__(self) -> int: # pragma: no cover return sum(sp.header.length for sp in itertools.chain(self._hashed_sp.values(), self._unhashed_sp.values())) + 4 def __iter__(self): @@ -171,7 +177,7 @@ def __contains__(self, key): return key in {k for k, _ in itertools.chain(self._hashed_sp, self._unhashed_sp)} def __copy__(self): - sp = SubPackets() + sp = SubPackets(self._width) sp._hashed_sp = self._hashed_sp.copy() sp._unhashed_sp = self._unhashed_sp.copy() @@ -209,8 +215,8 @@ def _normalize(self) -> None: self._unhashed_sp = collections.OrderedDict(sorted(self._unhashed_sp.items(), key=lambda x: (x[1].__typeid__, x[0][1]))) def parse(self, packet): - hl = self.bytes_to_int(packet[:2]) - del packet[:2] + hl = self.bytes_to_int(packet[:self._width]) + del packet[:self._width] # we do it this way because we can't ensure that subpacket headers are sized appropriately # for their contents, but we can at least output that correctly @@ -220,8 +226,8 @@ def parse(self, packet): sp = SignatureSP(packet) self['h_' + sp.__class__.__name__] = sp - uhl = self.bytes_to_int(packet[:2]) - del packet[:2] + uhl = self.bytes_to_int(packet[:self._width]) + del packet[:self._width] plen = len(packet) while plen - len(packet) < uhl: diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 289dd74f..b12ba79f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -325,13 +325,14 @@ def parse(self, packet): class Signature(VersionedPacket): __typeid__ = PacketTag.Signature __ver__ = 0 + __subpacket_width__ = 2 def __init__(self) -> None: super().__init__() self._sigtype: Optional[SignatureType] = None self._pubalg: Optional[PubKeyAlgorithm] = None self._halg: Optional[HashAlgorithm] = None - self.subpackets = SubPackets() + self.subpackets = SubPackets(self.__subpacket_width__) self.hash2 = bytearray(2) self._signature: SignatureField = OpaqueSignature() From 27fbae91559a53127558ca15a6c17b4840c7df0a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 28 Jun 2023 15:21:59 -0400 Subject: [PATCH 198/287] PubKeyV4: more type signatures --- pgpy/packet/packets.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index b12ba79f..8f0a7f5b 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -920,7 +920,10 @@ class PubKeyV4(PubKey): __ver__ = 4 @property - def fingerprint(self): + def fingerprint(self) -> Fingerprint: + if self.keymaterial is None: + raise TypeError("Key material is not present, cannot calculate fingerprint") + # A V4 fingerprint is the 160-bit SHA-1 hash of the octet 0x99, followed by the two-octet packet length, # followed by the entire Public-Key packet starting with the version field. The Key ID is the # low-order 64 bits of the fingerprint. @@ -948,13 +951,13 @@ def fingerprint(self): # and return the digest return Fingerprint(fp.finalize()) - def __init__(self): + def __init__(self) -> None: super().__init__() self.created = datetime.now(timezone.utc) - self.pkalg = 0 - self.keymaterial = None - def __bytearray__(self): + def __bytearray__(self) -> bytearray: + if self.keymaterial is None: + raise TypeError("Key Material is missing, cannot produce bytearray") _bytes = bytearray() _bytes += super().__bytearray__() _bytes += self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4) @@ -965,7 +968,7 @@ def __bytearray__(self): _bytes += self.keymaterial.__bytearray__() return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.created = packet[:4] @@ -976,7 +979,8 @@ def parse(self, packet): # bound keymaterial to the remaining length of the packet pend = self.header.length - 6 - self.keymaterial.parse(packet[:pend]) + if self.keymaterial is not None: + self.keymaterial.parse(packet[:pend]) del packet[:pend] From b5e6635d22bf5f1c0146627799c4a36b154a2100 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 28 Jun 2023 18:17:11 -0400 Subject: [PATCH 199/287] Fields: add type signatures for __bytearray__ and parse --- pgpy/packet/fields.py | 80 +++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 99b15db5..5bfb0dcc 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -214,7 +214,7 @@ def _normalize(self) -> None: self._hashed_sp = collections.OrderedDict(sorted(self._hashed_sp.items(), key=lambda x: (x[1].__typeid__, x[0][1]))) self._unhashed_sp = collections.OrderedDict(sorted(self._unhashed_sp.items(), key=lambda x: (x[1].__typeid__, x[0][1]))) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: hl = self.bytes_to_int(packet[:self._width]) del packet[:self._width] @@ -243,29 +243,29 @@ class UserAttributeSubPackets(SubPackets): """ _spmodule = userattribute - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() for uhsp in self._unhashed_sp.values(): _bytes += uhsp.__bytearray__() return _bytes - def __len__(self): # pragma: no cover + def __len__(self) -> int: # pragma: no cover return sum(len(sp) for sp in self._unhashed_sp.values()) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: # parse just one packet and add it to the unhashed subpacket ordereddict # I actually have yet to come across a User Attribute packet with more than one subpacket # which makes sense, given that there is only one defined subpacket - sp = UserAttribute(packet) + sp = UserAttribute(packet) # type: ignore[abstract] self[sp.__class__.__name__] = sp class Signature(MPIs): - def __init__(self): + def __init__(self) -> None: for i in self.__mpis__: setattr(self, i, MPI(0)) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() for i in self: _bytes += i.to_mpibytes() @@ -281,17 +281,17 @@ def from_signer(self, sig): class OpaqueSignature(Signature): - def __init__(self): + def __init__(self) -> None: super().__init__() self.data = bytearray() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return self.data def __sig__(self): return self.data - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.data = packet def from_signer(self, sig): @@ -304,7 +304,7 @@ class RSASignature(Signature): def __sig__(self): return self.md_mod_n.to_mpibytes()[2:] - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.md_mod_n = MPI(packet) def from_signer(self, sig): @@ -397,10 +397,10 @@ def __iter__(self): def __pubkey__(self): return NotImplemented - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return self.data - def parse(self, packet): + def parse(self, packet: bytearray) -> None: ##TODO: this needs to be length-bounded to the end of the packet self.data = packet @@ -421,7 +421,7 @@ def verify(self, subj, sigbytes, hash_alg): return False return True - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.n = MPI(packet) self.e = MPI(packet) @@ -441,7 +441,7 @@ def verify(self, subj, sigbytes, hash_alg): return False return True - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.p = MPI(packet) self.q = MPI(packet) self.g = MPI(packet) @@ -455,7 +455,7 @@ class ElGPub(PubKey): def __pubkey__(self): raise NotImplementedError() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.p = MPI(packet) self.g = MPI(packet) self.y = MPI(packet) @@ -539,7 +539,7 @@ def __len__(self): def __pubkey__(self): return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _b = bytearray() _b += bytes(self.oid) _b += self.p.to_mpibytes() @@ -557,7 +557,7 @@ def verify(self, subj, sigbytes, hash_alg): return False return True - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.oid = EllipticCurveOID.parse(packet) if isinstance(self.oid, EllipticCurveOID): @@ -576,10 +576,10 @@ def __init__(self): super().__init__() self.oid = None - def __len__(self): + def __len__(self) -> int: return len(self.p) + len(self.oid) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _b = bytearray() _b += bytes(self.oid) _b += self.p.to_mpibytes() @@ -588,7 +588,7 @@ def __bytearray__(self): def __pubkey__(self): return ed25519.Ed25519PublicKey.from_public_bytes(self.p.x) - def __copy__(self): + def __copy__(self) -> 'EdDSAPub': pkt = super().__copy__() pkt.oid = self.oid return pkt @@ -605,7 +605,7 @@ def verify(self, subj, sigbytes, hash_alg): return False return True - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.oid = EllipticCurveOID.parse(packet) if isinstance(self.oid, EllipticCurveOID): @@ -647,7 +647,7 @@ def __copy__(self): pkt.kdf = copy.copy(self.kdf) return pkt - def parse(self, packet): + def parse(self, packet: bytearray) -> None: """ Algorithm-Specific Fields for ECDH keys: @@ -958,7 +958,7 @@ def __bytearray__(self) -> bytearray: def __len__(self) -> int: return len(self.__bytearray__()) - def parse(self, packet) -> None: + def parse(self, packet: bytearray) -> None: self._type = String2KeyType(packet[0]) if self._type is String2KeyType.Unknown: self._opaque_type: int = packet[0] @@ -1207,7 +1207,7 @@ def __init__(self): self.halg = 0 self.encalg = 0 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes.append(len(self) - 1) _bytes.append(0x01) @@ -1218,7 +1218,7 @@ def __bytearray__(self): def __len__(self): return 4 - def parse(self, packet): + def parse(self, packet: bytearray) -> None: # packet[0] should always be 3 # packet[1] should always be 1 # TODO: this assert is likely not necessary, but we should raise some kind of exception @@ -1462,7 +1462,7 @@ def _generate(self, key_size: Optional[Union[int, EllipticCurveOID]]) -> None: self._compute_chksum() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.s2k.parse(packet) @@ -1534,7 +1534,7 @@ def _generate(self, key_size: Optional[Union[int, EllipticCurveOID]]) -> None: self._compute_chksum() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.s2k.parse(packet) @@ -1575,7 +1575,7 @@ def _compute_chksum(self): def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: raise NotImplementedError(PubKeyAlgorithm.ElGamal) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.s2k.parse(packet) @@ -1632,7 +1632,7 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: self.s = MPI(pk.private_numbers().private_value) self._compute_chksum() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.s2k.parse(packet) @@ -1693,7 +1693,7 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: ))) self._compute_chksum() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.s2k.parse(packet) @@ -1721,7 +1721,7 @@ def sign(self, sigdata, hash_alg): class ECDHPriv(ECDSAPriv, ECDHPub): - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _b = ECDHPub.__bytearray__(self) _b += self.s2k.__bytearray__() if not self.s2k: @@ -1732,7 +1732,7 @@ def __bytearray__(self): _b += self.encbytes return _b - def __len__(self): + def __len__(self) -> int: nbytes = ECDHPub.__len__(self) + len(self.s2k) + len(self.chksum) if self.s2k: nbytes += len(self.encbytes) @@ -1780,7 +1780,7 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: def publen(self): return ECDHPub.__len__(self) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: ECDHPub.parse(self, packet) self.s2k.parse(packet) @@ -1812,7 +1812,7 @@ def encrypt(cls, encfn, *args): def decrypt(self, decfn, *args): """decrypt the ciphertext contained in this CipherText instance""" - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() for i in self: _bytes += i.to_mpibytes() @@ -1831,7 +1831,7 @@ def encrypt(cls, encfn, *args): def decrypt(self, decfn, *args): return decfn(*args) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.me_mod_n = MPI(packet) @@ -1845,7 +1845,7 @@ def encrypt(cls, encfn, *args): def decrypt(self, decfn, *args): raise NotImplementedError() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.gk_mod_p = MPI(packet) self.myk_mod_p = MPI(packet) @@ -1931,18 +1931,18 @@ def decrypt(self, pk, *args): padder = PKCS7(64).unpadder() return padder.update(_m) + padder.finalize() - def __init__(self): + def __init__(self) -> None: super().__init__() self.c = bytearray(0) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += self.p.to_mpibytes() _bytes.append(len(self.c)) _bytes += self.c return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: # read ephemeral public key self.p = ECPoint(packet) # read signature value From 1c7dd7917c2d48f5868726faa40513d2bc065a7d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 11:35:53 -0400 Subject: [PATCH 200/287] Packet, VersionedPacket type signatures --- pgpy/packet/types.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index c8ea1c42..a7ffb0c7 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -4,7 +4,7 @@ import abc import copy -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Type, Union from ..constants import PacketTag @@ -146,29 +146,29 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): __typeid__: Optional[Union[PacketTag, DispatchGuidance]] = None - __headercls__ = Header + __headercls__: Type[Header] = Header - def __init__(self, _=None): + def __init__(self, _=None) -> None: super().__init__() self.header = self.__headercls__() if isinstance(self.__typeid__, int): self.header.tag = self.__typeid__ @abc.abstractmethod - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return self.header.__bytearray__() - def __len__(self): + def __len__(self) -> int: return len(self.header) + self.header.length - def __repr__(self): + def __repr__(self) -> str: return "<{cls:s} [tag {tag:02d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, id=id(self)) - def update_hlen(self): + def update_hlen(self) -> None: self.header.length = len(self.__bytearray__()) - len(self.header) @abc.abstractmethod - def parse(self, packet): + def parse(self, packet: bytearray) -> None: if self.header.tag == 0: self.header.parse(packet) @@ -177,12 +177,12 @@ class VersionedPacket(Packet): __typeid__: Union[PacketTag, DispatchGuidance] = DispatchGuidance.NoDispatch __headercls__ = VersionedHeader - def __init__(self): + def __init__(self) -> None: super().__init__() if isinstance(self.__ver__, int): self.header.version = self.__ver__ - def __repr__(self): + def __repr__(self) -> str: return "<{cls:s} [tag {tag:02d}][v{ver:d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, ver=self.header.version, id=id(self)) From ca370d3663ed47c2f061eae2775d2b0e5dff81dc Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 11:36:57 -0400 Subject: [PATCH 201/287] clean up type signature fixes for Packet() instantiation (MetaDispatchable) --- pgpy/pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 918ae4f6..30e00893 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -588,7 +588,7 @@ def parse(self, packet: bytearray) -> None: self.ascii_headers = unarmored['headers'] # load *one* packet from data - pkt = Packet(data) # type: ignore + pkt = Packet(data) # type: ignore[abstract] if pkt.header.tag == PacketTag.Signature: if isinstance(pkt, Opaque): # this is an unrecognized version. From f325c6616998398c0fe8cb0978b2f745785b3400 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:06:05 -0400 Subject: [PATCH 202/287] More type signatures --- pgpy/packet/fields.py | 45 ++++++++++++++++++++++++------------------ pgpy/packet/packets.py | 3 +++ pgpy/packet/types.py | 33 ++++++++++++++++--------------- pgpy/pgp.py | 9 ++++++++- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 5bfb0dcc..e10468bb 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -491,7 +491,7 @@ def from_values(cls, bitlen, pform, x, y=None): ct.y = y return ct - def __len__(self): + def __len__(self) -> int: """ Returns length of MPI encoded point """ if self.format == ECPointFormat.Standard: return 2 * self.bytelen + 3 @@ -500,7 +500,7 @@ def __len__(self): else: raise NotImplementedError("No curve is supposed to use only X or Y coordinates") - def to_mpibytes(self): + def to_mpibytes(self) -> bytes: """ Returns MPI encoded point as it should be written in packet """ b = bytearray() b.append(self.format) @@ -513,10 +513,10 @@ def to_mpibytes(self): raise NotImplementedError("No curve is supposed to use only X or Y coordinates") return MPI(MPIs.bytes_to_int(b)).to_mpibytes() - def __bytearray__(self): - return self.to_mpibytes() + def __bytearray__(self) -> bytearray: + return bytearray(self.to_mpibytes()) - def __copy__(self): + def __copy__(self) -> 'ECPoint': pk = self.__class__() pk.bytelen = self.bytelen pk.format = self.format @@ -529,11 +529,11 @@ class ECDSAPub(PubKey): __pubfields__ = ('p',) __pubkey_algo__ = PubKeyAlgorithm.ECDSA - def __init__(self): + def __init__(self) -> None: super().__init__() - self.oid = None + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.NIST_P256 - def __len__(self): + def __len__(self) -> int: return len(self.p) + len(self.oid) def __pubkey__(self): @@ -545,8 +545,10 @@ def __bytearray__(self) -> bytearray: _b += self.p.to_mpibytes() return _b - def __copy__(self): + def __copy__(self) -> 'ECDSAPub': pkt = super().__copy__() + if not isinstance(pkt, ECDSAPub): + raise TypeError(f"Failed to create ECDSAPub when copying, got {type(pkt)}") pkt.oid = self.oid return pkt @@ -561,7 +563,7 @@ def parse(self, packet: bytearray) -> None: self.oid = EllipticCurveOID.parse(packet) if isinstance(self.oid, EllipticCurveOID): - self.p = ECPoint(packet) + self.p: Union[ECPoint, MPI] = ECPoint(packet) if self.p.format != ECPointFormat.Standard: raise PGPIncompatibleECPointFormatError("Only Standard format is valid for ECDSA") else: @@ -572,9 +574,9 @@ class EdDSAPub(PubKey): __pubfields__ = ('p', ) __pubkey_algo__ = PubKeyAlgorithm.EdDSA - def __init__(self): + def __init__(self) -> None: super().__init__() - self.oid = None + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.Ed25519 def __len__(self) -> int: return len(self.p) + len(self.oid) @@ -590,6 +592,8 @@ def __pubkey__(self): def __copy__(self) -> 'EdDSAPub': pkt = super().__copy__() + if not isinstance(pkt, EdDSAPub): + raise TypeError(f"Failed to create EdDSAPub when copying, got {type(pkt)}") pkt.oid = self.oid return pkt @@ -609,7 +613,7 @@ def parse(self, packet: bytearray) -> None: self.oid = EllipticCurveOID.parse(packet) if isinstance(self.oid, EllipticCurveOID): - self.p = ECPoint(packet) + self.p: Union[ECPoint, MPI] = ECPoint(packet) if self.p.format != ECPointFormat.Native: raise PGPIncompatibleECPointFormatError("Only Native format is valid for EdDSA") else: @@ -620,9 +624,9 @@ class ECDHPub(PubKey): __pubfields__ = ('p',) __pubkey_algo__ = PubKeyAlgorithm.ECDH - def __init__(self): + def __init__(self) -> None: super().__init__() - self.oid = None + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.NIST_P256 self.kdf = ECKDF() def __len__(self): @@ -641,8 +645,10 @@ def __bytearray__(self) -> bytearray: _b += self.kdf.__bytearray__() return _b - def __copy__(self): + def __copy__(self) -> 'ECDHPub': pkt = super().__copy__() + if not isinstance(pkt, ECDHPub): + raise TypeError(f"Failed to create ECDHAPub when copying, got {type(pkt)}") pkt.oid = self.oid pkt.kdf = copy.copy(self.kdf) return pkt @@ -679,7 +685,7 @@ def parse(self, packet: bytearray) -> None: self.oid = EllipticCurveOID.parse(packet) if isinstance(self.oid, EllipticCurveOID): - self.p = ECPoint(packet) + self.p: Union[ECPoint, MPI] = ECPoint(packet) if self.oid == EllipticCurveOID.Curve25519: if self.p.format != ECPointFormat.Native: raise PGPIncompatibleECPointFormatError("Only Native format is valid for Curve25519") @@ -1774,8 +1780,9 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: self._compute_chksum() else: ECDSAPriv._generate(self, _oid) - self.kdf.halg = self.oid.kdf_halg - self.kdf.encalg = self.oid.kek_alg + if isinstance(self.oid, EllipticCurveOID): + self.kdf.halg = self.oid.kdf_halg + self.kdf.encalg = self.oid.kek_alg def publen(self): return ECDHPub.__len__(self) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 8f0a7f5b..9b84ba7f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -38,6 +38,7 @@ from .types import Public from .types import Sub from .types import VersionedPacket +from .types import VersionedHeader from ..constants import PacketTag from ..constants import CompressionAlgorithm @@ -490,6 +491,8 @@ def canonical_bytes(self) -> bytearray: the unhashed subpacket data length value is set to zero. ''' _body = bytearray() + if not isinstance(self.header, VersionedHeader): + raise TypeError(f"SignatureV4 should have VersionedHeader, had {type(self.header)}") _body += self.int_to_bytes(self.header.version) _body += self.int_to_bytes(self.sigtype) if self.pubalg is PubKeyAlgorithm.Unknown: diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index a7ffb0c7..61c6aa29 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -4,7 +4,7 @@ import abc import copy -from typing import Optional, Tuple, Type, Union +from typing import Iterator, Optional, Tuple, Type, Union from ..constants import PacketTag @@ -179,10 +179,12 @@ class VersionedPacket(Packet): def __init__(self) -> None: super().__init__() - if isinstance(self.__ver__, int): + if isinstance(self.__ver__, int) and isinstance(self.header, VersionedHeader): self.header.version = self.__ver__ def __repr__(self) -> str: + if not isinstance(self.header, VersionedHeader): + raise TypeError(f"VersionedPacket should have VersionedHeader, instead it has {type(self.header)}") return "<{cls:s} [tag {tag:02d}][v{ver:d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, ver=self.header.version, id=id(self)) @@ -191,24 +193,23 @@ class Opaque(Packet): __typeid__ = None @sdproperty - def payload(self): + def payload(self) -> Union[bytes, bytearray]: return self._payload - @payload.register(bytearray) - @payload.register(bytes) - def payload_bin(self, val): + @payload.register + def payload_bin(self, val: Union[bytes, bytearray]) -> None: self._payload = val - def __init__(self): + def __init__(self) -> None: super().__init__() self.payload = b'' - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.payload return _bytes - def parse(self, packet): # pragma: no cover + def parse(self, packet: bytearray) -> None: # pragma: no cover super().parse(packet) pend = self.header.length if hasattr(self.header, 'version'): @@ -271,30 +272,30 @@ def __new__(cls, num): return super().__new__(cls, mpi) - def byte_length(self): + def byte_length(self) -> int: return ((self.bit_length() + 7) // 8) - def to_mpibytes(self): + def to_mpibytes(self) -> bytes: return MPIs.int_to_bytes(self.bit_length(), 2) + MPIs.int_to_bytes(self, self.byte_length()) - def __len__(self): + def __len__(self) -> int: return self.byte_length() + 2 class MPIs(Field): # this differs from MPI in that it's subclasses hold/parse several MPI fields # and, in the case of v4 private keys, also a String2Key specifier/information. - __mpis__: Tuple = () + __mpis__: Tuple[str, ...] = () - def __len__(self): + def __len__(self) -> int: return sum(len(i) for i in self) - def __iter__(self): + def __iter__(self) -> Iterator[MPI]: """yield all components of an MPI so it can be iterated over""" for i in self.__mpis__: yield getattr(self, i) - def __copy__(self): + def __copy__(self) -> 'MPIs': pk = self.__class__() for m in self.__mpis__: setattr(pk, m, copy.copy(getattr(self, m))) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 30e00893..3b92c40d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -76,6 +76,7 @@ from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub from .packet.types import Opaque +from .packet.types import VersionedHeader from .types import Armorable from .types import Fingerprint @@ -340,6 +341,8 @@ def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Finge created = datetime.now(timezone.utc) sigpkt = SignatureV4() sigpkt.header.tag = 2 + if not isinstance(sigpkt.header, VersionedHeader): + raise TypeError(f"Signature packet should have VersionedHeader, had {type(sigpkt.header)}") sigpkt.header.version = 4 sigpkt.subpackets.addnew('CreationTime', critical=True, hashed=True, created=created) if signer.version <= 4: @@ -1562,7 +1565,11 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: if self._key is None: return None if isinstance(self._key.keymaterial, (ECDSAPub, EdDSAPub, ECDHPub)): - return self._key.keymaterial.oid + if isinstance(self._key.keymaterial.oid, EllipticCurveOID): + return self._key.keymaterial.oid + else: + # this is an unknown elliptic curve + return 0 if self._key.keymaterial is None: return None # check if keymaterial is not an Opaque class containing a bytearray From 5f470cad987c7997560b1c340b1762a9abb5e77c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:06:39 -0400 Subject: [PATCH 203/287] Drop long/int conflation from python 2. python 3 just uses int. --- pgpy/packet/types.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 61c6aa29..f3455c11 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -252,11 +252,7 @@ class Sub(Key): pass -# This is required for class MPI to work in both Python 2 and 3 -long = int - - -class MPI(long): +class MPI(int): def __new__(cls, num): mpi = num From 15481fbc562256b39d0e757d237de3dd228e3318 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:07:08 -0400 Subject: [PATCH 204/287] More type-checker workarounds for MetaDispatchable --- pgpy/packet/fields.py | 4 ++-- pgpy/packet/subpackets/types.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index e10468bb..45e3c14e 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -223,7 +223,7 @@ def parse(self, packet: bytearray) -> None: # so instead of tracking how many bytes we can now output, we track how many bytes have we parsed so far plen = len(packet) while plen - len(packet) < hl: - sp = SignatureSP(packet) + sp = SignatureSP(packet) # type: ignore[abstract] self['h_' + sp.__class__.__name__] = sp uhl = self.bytes_to_int(packet[:self._width]) @@ -231,7 +231,7 @@ def parse(self, packet: bytearray) -> None: plen = len(packet) while plen - len(packet) < uhl: - sp = SignatureSP(packet) + sp = SignatureSP(packet) # type: ignore[abstract] self[sp.__class__.__name__] = sp diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index a802a924..b97f8006 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -109,10 +109,18 @@ def parse(self, packet): # pragma: no cover class Signature(SubPacket): __typeid__: Optional[SigSubpacketType] = None + # allow one parameter for MetaDispatchable initialization: + def __init__(self, _: Optional[bytes] = None) -> None: + super().__init__() + class UserAttribute(SubPacket): __typeid__: Optional[AttributeType] = None + # allow one parameter for MetaDispatchable initialization: + def __init__(self, _: Optional[bytes] = None) -> None: + super().__init__() + class Opaque(Signature, UserAttribute): __typeid__ = None From b4f0fed4a4729cbf60a0cb8f5d83c3cbe390d928 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:08:26 -0400 Subject: [PATCH 205/287] Type checker gets confused by __copy__ overloading with multiple inheritance This shouldn't be a problem because __copy__ will actually work correctly thanks to the multiple inheritance itself. There could be a better way to annotate this that i don't know about, but this should do. --- pgpy/packet/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 45e3c14e..878f4e8f 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1726,7 +1726,7 @@ def sign(self, sigdata, hash_alg): return self.__privkey__().sign(sigdata) -class ECDHPriv(ECDSAPriv, ECDHPub): +class ECDHPriv(ECDSAPriv, ECDHPub): # type: ignore[misc] # (definition of __copy__ in base classes ECDHPub and ECDSAPub differs) def __bytearray__(self) -> bytearray: _b = ECDHPub.__bytearray__(self) _b += self.s2k.__bytearray__() From 6e983205b933c892441936ddb78af77e4068abc0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:14:04 -0400 Subject: [PATCH 206/287] PGPKeyRing.unload: simplify, clarify --- pgpy/pgp.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 3b92c40d..ba5b330a 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -3092,7 +3092,7 @@ def fingerprints(self, keyhalf='any', keytype='any'): if pk.is_public in [True if keyhalf in ['public', 'any'] else None, False if keyhalf in ['private', 'any'] else None]} - def unload(self, key): + def unload(self, key) -> None: """ Unload a loaded key and its subkeys. @@ -3108,7 +3108,10 @@ def unload(self, key): pkid = id(key) if pkid in self._keys: # remove references - [ kd.remove(pkid) for kd in [self._pubkeys, self._privkeys] if pkid in kd ] + if pkid in self._pubkeys: + self._pubkeys.remove(pkid) + if pkid in self._privkeys: + self._privkeys.remove(pkid) # remove the key self._keys.pop(pkid) @@ -3121,4 +3124,5 @@ def unload(self, key): # if key is a primary key, unload its subkeys as well if key.is_primary: - [ self.unload(sk) for sk in key.subkeys.values() ] + for sk in key.subkeys.values(): + self.unload(sk) From 30d6c2b7c7e941c2ac646d2bf242b77f02781d44 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 14:27:09 -0400 Subject: [PATCH 207/287] Rename PacketTag to PacketType While the term "Tag" is present in the spec, it's ambiguous and confusing, and the identifier is also referred to as the type of packet. See discussion over on https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/319 --- docs/source/changelog.rst | 6 +++ pgpy/constants.py | 11 ++++- pgpy/packet/fields.py | 2 +- pgpy/packet/packets.py | 36 ++++++++-------- pgpy/packet/subpackets/types.py | 17 ++++---- pgpy/packet/types.py | 73 +++++++++++++++++---------------- pgpy/pgp.py | 12 +++--- tests/test_01_packetfields.py | 3 +- tests/test_02_packets.py | 8 ++-- 9 files changed, 92 insertions(+), 76 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0427b13f..970881a8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -92,6 +92,12 @@ the empty string. User ID string parsing is also improved, to better handle raw e-mail addresses (without angle-brackets) and other subtle variations. +pgpy.constants.PacketTag has been renamed to PacketType. Similarly, +Header objects (both for Packet and Subpacket) use the "typeid" +property. Packet Header objects no longer expose a "tag" alias. The +term "Tag" was used ambiguously in the OpenPGP specifications, so we +avoid it. + v0.6.0 ====== diff --git a/pgpy/constants.py b/pgpy/constants.py index b6a700d2..466c00d8 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -25,7 +25,7 @@ 'ECFields', 'EllipticCurveOID', 'ECPointFormat', - 'PacketTag', + 'PacketType', 'SymmetricKeyAlgorithm', 'PubKeyAlgorithm', 'CompressionAlgorithm', @@ -61,7 +61,8 @@ class ECPointFormat(IntEnum): OnlyY = 0x42 -class PacketTag(IntEnum): +class PacketType(IntEnum): + Unknown = -1 Invalid = 0 PublicKeyEncryptedSessionKey = 1 Signature = 2 @@ -81,6 +82,12 @@ class PacketTag(IntEnum): SymmetricallyEncryptedIntegrityProtectedData = 18 ModificationDetectionCode = 19 + @classmethod + def _missing_(cls, val: object) -> 'PacketType': + if not isinstance(val, int): + raise TypeError(f"cannot look up PacketType by non-int {type(val)}") + return cls.Unknown + class SymmetricKeyAlgorithm(IntEnum): """Supported symmetric key algorithms.""" diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 878f4e8f..2fd2145e 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -205,7 +205,7 @@ def update_hlen(self): sp.update_hlen() def _normalize(self) -> None: - '''Order subpackets by subpacket tag number + '''Order subpackets by subpacket ID This private interface must only be called a Subpackets object before it is signed, otherwise it will break the signature diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 9b84ba7f..07f30b18 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -40,7 +40,7 @@ from .types import VersionedPacket from .types import VersionedHeader -from ..constants import PacketTag +from ..constants import PacketType from ..constants import CompressionAlgorithm from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm @@ -88,7 +88,7 @@ class PKESessionKey(VersionedPacket): - __typeid__ = PacketTag.PublicKeyEncryptedSessionKey + __typeid__ = PacketType.PublicKeyEncryptedSessionKey __ver__ = 0 def __init__(self) -> None: @@ -324,7 +324,7 @@ def parse(self, packet): class Signature(VersionedPacket): - __typeid__ = PacketTag.Signature + __typeid__ = PacketType.Signature __ver__ = 0 __subpacket_width__ = 2 @@ -566,7 +566,7 @@ def make_onepass(self) -> 'OnePassSignatureV3': class SKESessionKey(VersionedPacket): - __typeid__ = PacketTag.SymmetricKeyEncryptedSessionKey + __typeid__ = PacketType.SymmetricKeyEncryptedSessionKey __ver__ = 0 # FIXME: the type signature for this function is awkward because @@ -703,7 +703,7 @@ def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: class OnePassSignature(VersionedPacket): '''Holds common members of various OPS packet versions''' - __typeid__ = PacketTag.OnePassSignature + __typeid__ = PacketType.OnePassSignature __ver__ = 0 def __init__(self) -> None: @@ -840,7 +840,7 @@ def parse(self, packet: bytearray) -> None: class PubKey(VersionedPacket, Primary, Public): - __typeid__ = PacketTag.PublicKey + __typeid__ = PacketType.PublicKey __ver__ = 0 def __init__(self) -> None: @@ -988,7 +988,7 @@ def parse(self, packet: bytearray) -> None: class PrivKey(PubKey, Private): - __typeid__ = PacketTag.SecretKey + __typeid__ = PacketType.SecretKey __ver__ = 0 @property @@ -1070,7 +1070,7 @@ def pubkey(self): class PrivSubKey(PrivKey, Sub): - __typeid__ = PacketTag.SecretSubKey + __typeid__ = PacketType.SecretSubKey __ver__ = 0 @@ -1108,7 +1108,7 @@ class CompressedData(Packet): BZip2-compressed packets are compressed using the BZip2 [BZ2] algorithm. """ - __typeid__ = PacketTag.CompressedData + __typeid__ = PacketType.CompressedData @sdproperty def calg(self): @@ -1192,7 +1192,7 @@ class SKEData(Packet): incorrect. See the "Security Considerations" section for hints on the proper use of this "quick check". """ - __typeid__ = PacketTag.SymmetricallyEncryptedData + __typeid__ = PacketType.SymmetricallyEncryptedData def __init__(self): super().__init__() @@ -1238,7 +1238,7 @@ def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray class Marker(Packet): - __typeid__ = PacketTag.Marker + __typeid__ = PacketType.Marker def __init__(self): super().__init__() @@ -1302,7 +1302,7 @@ class LiteralData(Packet): normal line endings). These should be converted to native line endings by the receiving software. """ - __typeid__ = PacketTag.LiteralData + __typeid__ = PacketType.LiteralData @sdproperty def mtime(self): @@ -1393,7 +1393,7 @@ class Trust(Packet): transferred to other users, and they SHOULD be ignored on any input other than local keyring files. """ - __typeid__ = PacketTag.Trust + __typeid__ = PacketType.Trust @sdproperty def trustlevel(self): @@ -1449,7 +1449,7 @@ class UserID(Packet): restrictions on its content. The packet length in the header specifies the length of the User ID. """ - __typeid__ = PacketTag.UserID + __typeid__ = PacketType.UserID def __init__(self, uid=""): super().__init__() @@ -1484,7 +1484,7 @@ def parse(self, packet): class PubSubKey(VersionedPacket, Sub, Public): - __typeid__ = PacketTag.PublicSubKey + __typeid__ = PacketType.PublicSubKey __ver__ = 0 @@ -1525,7 +1525,7 @@ class UserAttribute(Packet): not recognize. Subpacket types 100 through 110 are reserved for private or experimental use. """ - __typeid__ = PacketTag.UserAttribute + __typeid__ = PacketType.UserAttribute @property def image(self): @@ -1556,7 +1556,7 @@ def update_hlen(self): class IntegrityProtectedSKEData(VersionedPacket): - __typeid__ = PacketTag.SymmetricallyEncryptedIntegrityProtectedData + __typeid__ = PacketType.SymmetricallyEncryptedIntegrityProtectedData __ver__ = 0 @abc.abstractmethod @@ -1749,7 +1749,7 @@ class MDC(Packet): in the data hash. While this is a bit restrictive, it reduces complexity. """ - __typeid__ = PacketTag.ModificationDetectionCode + __typeid__ = PacketType.ModificationDetectionCode def __init__(self): super().__init__() diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index b97f8006..7e163cbc 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -4,6 +4,7 @@ from typing import Optional, Union +from ...constants import PacketType from ...constants import SigSubpacketType from ...constants import AttributeType @@ -67,11 +68,11 @@ def __bytearray__(self): class EmbeddedSignatureHeader(VersionedHeader): - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return bytearray([self.version]) - def parse(self, packet): - self.tag = 2 + def parse(self, packet: bytearray) -> None: + self.typeid = PacketType.Signature super().parse(packet) @@ -88,20 +89,20 @@ def __init__(self): ): self.header.typeid = self.__typeid__ - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return self.header.__bytearray__() - def __len__(self): + def __len__(self) -> int: return (self.header.llen + self.header.length) - def __repr__(self): + def __repr__(self) -> str: return "<{} [0x{:02x}] {}at 0x{:x}>".format(self.__class__.__name__, self.header.typeid, 'critical! ' if self.header.critical else '', id(self)) - def update_hlen(self): + def update_hlen(self) -> None: self.header.length = (len(self.__bytearray__()) - len(self.header)) + 1 @abc.abstractmethod - def parse(self, packet): # pragma: no cover + def parse(self, packet: bytearray) -> None: # pragma: no cover if self.header._typeid == -1: self.header.parse(packet) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index f3455c11..7547bf23 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -6,7 +6,7 @@ from typing import Iterator, Optional, Tuple, Type, Union -from ..constants import PacketTag +from ..constants import PacketType from ..decorators import sdproperty @@ -33,39 +33,41 @@ class Header(_Header): @sdproperty - def tag(self): - return self._tag + def typeid(self) -> PacketType: + return self._typeid - @tag.register(int) - @tag.register(PacketTag) - def tag_int(self, val): - _tag = (val & 0x3F) if self._openpgp_format else ((val & 0x3C) >> 2) - try: - self._tag = PacketTag(_tag) + @typeid.register + def typeid_int(self, val: int) -> None: + if isinstance(val, PacketType): + self._typeid = val + return - except ValueError: # pragma: no cover - self._tag = _tag + if self._openpgp_format: + type_id = (val & 0x3F) + else: + type_id = ((val & 0x3C) >> 2) - @property - def typeid(self): - return self.tag + self._typeid = PacketType(type_id) + if self._typeid is PacketType.Unknown: + self._opaque_typeid = type_id - def __init__(self): + def __init__(self) -> None: super().__init__() - self.tag = 0x00 + self._typeid = PacketType.Invalid - def __bytearray__(self): + def __bytearray__(self) -> bytearray: tag = 0x80 | (0x40 if self._openpgp_format else 0x00) - tag |= (self.tag) if self._openpgp_format else ((self.tag << 2) | {1: 0, 2: 1, 4: 2, 0: 3}[self.llen]) + tval: int = self._opaque_typeid if self._typeid is PacketType.Unknown else self._typeid + tag |= (tval) if self._openpgp_format else ((tval << 2) | {1: 0, 2: 1, 4: 2, 0: 3}[self.llen]) - _bytes = bytearray(self.int_to_bytes(tag)) + _bytes = bytearray([tag]) _bytes += self.encode_length(self.length, self._openpgp_format, self.llen) return _bytes - def __len__(self): + def __len__(self) -> int: return 1 + self.llen - def parse(self, packet): + def parse(self, packet: bytearray) -> None: """ There are two formats for headers @@ -104,7 +106,7 @@ def parse(self, packet): :param packet: raw packet bytes """ self._openpgp_format = bool(packet[0] & 0x40) - self.tag = packet[0] + self.typeid = packet[0] if not self._openpgp_format: self.llen = (packet[0] & 0x03) del packet[0] @@ -119,24 +121,24 @@ def parse(self, packet): class VersionedHeader(Header): @sdproperty - def version(self): + def version(self) -> int: return self._version - @version.register(int) - def version_int(self, val): + @version.register + def version_int(self, val: int) -> None: self._version = val - def __init__(self): + def __init__(self) -> None: super().__init__() self.version = 0 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray(super().__bytearray__()) _bytes += bytearray([self.version]) return _bytes - def parse(self, packet): # pragma: no cover - if self.tag == 0: + def parse(self, packet: bytearray) -> None: # pragma: no cover + if self.typeid is PacketType.Invalid: super().parse(packet) if self.version == 0: @@ -145,14 +147,14 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): - __typeid__: Optional[Union[PacketTag, DispatchGuidance]] = None + __typeid__: Optional[Union[PacketType, DispatchGuidance]] = None __headercls__: Type[Header] = Header def __init__(self, _=None) -> None: super().__init__() self.header = self.__headercls__() if isinstance(self.__typeid__, int): - self.header.tag = self.__typeid__ + self.header.typeid = self.__typeid__ @abc.abstractmethod def __bytearray__(self) -> bytearray: @@ -162,19 +164,19 @@ def __len__(self) -> int: return len(self.header) + self.header.length def __repr__(self) -> str: - return "<{cls:s} [tag {tag:02d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, id=id(self)) + return f'<{self.__class__.__name__} [type {self.header.typeid:02}] at 0x{id(self):x}>' def update_hlen(self) -> None: self.header.length = len(self.__bytearray__()) - len(self.header) @abc.abstractmethod def parse(self, packet: bytearray) -> None: - if self.header.tag == 0: + if self.header.typeid is PacketType.Invalid: self.header.parse(packet) class VersionedPacket(Packet): - __typeid__: Union[PacketTag, DispatchGuidance] = DispatchGuidance.NoDispatch + __typeid__: Union[PacketType, DispatchGuidance] = DispatchGuidance.NoDispatch __headercls__ = VersionedHeader def __init__(self) -> None: @@ -185,8 +187,7 @@ def __init__(self) -> None: def __repr__(self) -> str: if not isinstance(self.header, VersionedHeader): raise TypeError(f"VersionedPacket should have VersionedHeader, instead it has {type(self.header)}") - return "<{cls:s} [tag {tag:02d}][v{ver:d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, - ver=self.header.version, id=id(self)) + return f"<{self.__class__.__name__} [type {self.header.typeid:02}][v{self.header.version}] at 0x{id(self):x}>" class Opaque(Packet): diff --git a/pgpy/pgp.py b/pgpy/pgp.py index ba5b330a..4eadf26e 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -29,7 +29,7 @@ from .constants import KeyFlags from .constants import KeyServerPreferences from .constants import NotationDataFlags -from .constants import PacketTag +from .constants import PacketType from .constants import PubKeyAlgorithm from .constants import RevocationKeyClass from .constants import RevocationReason @@ -340,7 +340,7 @@ def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Finge if created is None: created = datetime.now(timezone.utc) sigpkt = SignatureV4() - sigpkt.header.tag = 2 + sigpkt.header.typeid = PacketType.Signature if not isinstance(sigpkt.header, VersionedHeader): raise TypeError(f"Signature packet should have VersionedHeader, had {type(sigpkt.header)}") sigpkt.header.version = 4 @@ -592,7 +592,7 @@ def parse(self, packet: bytearray) -> None: # load *one* packet from data pkt = Packet(data) # type: ignore[abstract] - if pkt.header.tag == PacketTag.Signature: + if pkt.header.typeid is PacketType.Signature: if isinstance(pkt, Opaque): # this is an unrecognized version. pass @@ -644,7 +644,7 @@ def parse(self, packet: bytes) -> None: while data: # this is safe to do because of how MetaDispatchable works: pkt = Packet(data) # type: ignore[abstract] - if pkt.header.tag == PacketTag.Signature: + if pkt.header.typeid is PacketType.Signature: if isinstance(pkt, Opaque): # skip unrecognized version. pass @@ -2858,7 +2858,7 @@ def parse(self, data): def _getpkt(d): return Packet(d) if d else None # some packets are filtered out - getpkt = filter(lambda p: p.header.tag != PacketTag.Trust, iter(functools.partial(_getpkt, data), None)) + getpkt = filter(lambda p: p.header.typeid is not PacketType.Trust, iter(functools.partial(_getpkt, data), None)) def pktgrouper(): class PktGrouper: @@ -2866,7 +2866,7 @@ def __init__(self): self.last = None def __call__(self, pkt): - if pkt.header.tag != PacketTag.Signature: + if pkt.header.typeid is not PacketType.Signature: self.last = '{:02X}_{:s}'.format(id(pkt), pkt.__class__.__name__) return self.last return PktGrouper() diff --git a/tests/test_01_packetfields.py b/tests/test_01_packetfields.py index 7b8d64e6..df0842fd 100644 --- a/tests/test_01_packetfields.py +++ b/tests/test_01_packetfields.py @@ -5,6 +5,7 @@ import itertools from pgpy.constants import HashAlgorithm +from pgpy.constants import PacketType from pgpy.constants import String2KeyType from pgpy.constants import SymmetricKeyAlgorithm from pgpy.constants import S2KGNUExtension @@ -58,7 +59,7 @@ def test_packet_header(self, pheader): h = Header() h.parse(pheader) - assert h.tag == 0x02 + assert h.typeid is PacketType.Signature assert h.length == len(pheader) - len(_trailer) assert pheader[h.length:] == _trailer assert len(h) == len(b) - len(pheader) diff --git a/tests/test_02_packets.py b/tests/test_02_packets.py index 61e82aa4..353da5c4 100644 --- a/tests/test_02_packets.py +++ b/tests/test_02_packets.py @@ -63,13 +63,13 @@ def test_load(self, packet): assert p.__bytes__() == b[:-len(_trailer)] # instantiated class is what we expected - if hasattr(p.header, 'version') and (p.header.tag, p.header.version) in _pclasses: + if hasattr(p.header, 'version') and (p.header.typeid, p.header.version) in _pclasses: # versioned packet - assert p.__class__.__name__ == _pclasses[(p.header.tag, p.header.version)] + assert p.__class__.__name__ == _pclasses[(p.header.typeid, p.header.version)] - elif (not hasattr(p.header, 'version')) and p.header.tag in _pclasses: + elif (not hasattr(p.header, 'version')) and p.header.typeid in _pclasses: # unversioned packet - assert p.__class__.__name__ in _pclasses[p.header.tag] + assert p.__class__.__name__ in _pclasses[p.header.typeid] else: # fallback to opaque From 8ff5062857e0716a9e9accbc8f4c7c0e01fe0ace Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 16:51:26 -0400 Subject: [PATCH 208/287] Add SecurityIssues.AlgorithmUnknown --- docs/source/changelog.rst | 2 ++ pgpy/constants.py | 2 ++ pgpy/pgp.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 970881a8..9b3613bc 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -24,6 +24,8 @@ PGPSignatures represents a detached signature, which can contain more than a single signature. It is a simple sequence of individual PGPSignature objects. +New SecurityIssues flag: AlgorithmUnknown + API changes ----------- diff --git a/pgpy/constants.py b/pgpy/constants.py index 466c00d8..a15ebb6e 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -763,6 +763,7 @@ class SecurityIssues(IntFlag): AsymmetricKeyLengthIsTooShort = (1 << 8) InsecureCurve = (1 << 9) NoSelfSignature = (1 << 10) + AlgorithmUnknown = (1 << 11) @property def causes_signature_verify_to_fail(self) -> bool: @@ -772,6 +773,7 @@ def causes_signature_verify_to_fail(self) -> bool: SecurityIssues.Disabled, SecurityIssues.Invalid, SecurityIssues.NoSelfSignature, + SecurityIssues.AlgorithmUnknown, } diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 4eadf26e..31d91ffe 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2600,6 +2600,8 @@ def self_verified(self): return self._self_verified def check_primitives(self): + if self.key_algorithm is None: + return SecurityIssues.AlgorithmUnknown return self.key_algorithm.validate_params(self.key_size) def check_management(self, self_verifying=False): From 4bf03a5df93f1fe91a04f6b853e72f58731a1966 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 16:52:58 -0400 Subject: [PATCH 209/287] More type signatures --- pgpy/packet/fields.py | 2 +- pgpy/pgp.py | 24 ++++++++++++------------ pgpy/types.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 2fd2145e..35613258 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -100,7 +100,7 @@ 'ECDHCipherText', ] -class SubPackets(collections.abc.MutableMapping, Field): +class SubPackets(collections.abc.MutableMapping[str, SubPacket], Field): _spmodule = signature def __init__(self, width: int = 2) -> None: diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 31d91ffe..08b267c5 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1740,7 +1740,7 @@ def __init__(self) -> None: self._signatures = SorteDeque() self._uids: Deque[PGPUID] = SorteDeque() self._sibling = None - self._self_verified = None + self._self_verified: Optional[SecurityIssues] = None self._require_usage_flags = True def __bytearray__(self): @@ -2514,14 +2514,14 @@ def bind(self, key: 'PGPKey', **prefs) -> PGPSignature: return self._sign(key, sig, **prefs) - def is_considered_insecure(self, self_verifying=False): + def is_considered_insecure(self, self_verifying=False) -> SecurityIssues: res = self.check_soundness(self_verifying=self_verifying) for sk in self.subkeys.values(): res |= sk.check_soundness(self_verifying=self_verifying) return res - def self_verify(self): + def self_verify(self) -> SecurityIssues: self_sigs = list(self.self_signatures) res = SecurityIssues.OK if self_sigs: @@ -2533,7 +2533,7 @@ def self_verify(self): return SecurityIssues.NoSelfSignature return res - def _do_self_signatures_verification(self): + def _do_self_signatures_verification(self) -> None: try: self._self_verified = self.self_verify() except Exception: @@ -2590,7 +2590,7 @@ def search_pref_sigs(self, uid: Optional[str] = None) -> Iterator[PGPSignature]: yield sig @property - def self_verified(self): + def self_verified(self) -> SecurityIssues: warnings.warn("TODO: Self-sigs verification is not yet working because self-sigs are not parsed!!!") return SecurityIssues.OK @@ -2599,12 +2599,12 @@ def self_verified(self): return self._self_verified - def check_primitives(self): + def check_primitives(self) -> SecurityIssues: if self.key_algorithm is None: return SecurityIssues.AlgorithmUnknown return self.key_algorithm.validate_params(self.key_size) - def check_management(self, self_verifying=False): + def check_management(self, self_verifying: bool = False) -> SecurityIssues: res = self.self_verified if self.is_expired: warnings.warn('Key {} has expired at {:s}'.format(repr(self), self.expires_at)) @@ -2615,7 +2615,7 @@ def check_management(self, self_verifying=False): res |= int(bool(list(self.revocation_signatures))) * SecurityIssues.Revoked return res - def check_soundness(self, self_verifying=False): + def check_soundness(self, self_verifying: bool = False) -> SecurityIssues: return self.check_management(self_verifying) | self.check_primitives() def issuer_matches(self, sig: PGPSignature) -> bool: @@ -2864,12 +2864,12 @@ def _getpkt(d): def pktgrouper(): class PktGrouper: - def __init__(self): - self.last = None + def __init__(self) -> None: + self.last: Optional[str] = None - def __call__(self, pkt): + def __call__(self, pkt) -> Optional[str]: if pkt.header.typeid is not PacketType.Signature: - self.last = '{:02X}_{:s}'.format(id(pkt), pkt.__class__.__name__) + self.last = f'{id(pkt):02X}_{pkt.__class__.__name__}' return self.last return PktGrouper() diff --git a/pgpy/types.py b/pgpy/types.py index 558fc147..73ea9597 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -339,7 +339,7 @@ def _new_length(nl: int) -> bytes: return b'\xFF' + Header.int_to_bytes(nl, 4) - def _old_length(nl: int, llen: int): + def _old_length(nl: int, llen: int) -> bytes: return Header.int_to_bytes(nl, llen) if llen > 0 else b'' return _new_length(length) if nhf else _old_length(length, llen) From f3e0ced1eeef95fd332f6277d3435b234ac6385a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 19:38:39 -0400 Subject: [PATCH 210/287] SKESK: use S2KSpecifier instead of String2Key This means that we can also simplify parsing the String2Key (no need to pass along whether we need to parse out an iv). --- pgpy/packet/fields.py | 4 ++-- pgpy/packet/packets.py | 33 ++++++++++++++++----------------- pgpy/pgp.py | 8 +++----- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 35613258..c7d88f0f 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1152,7 +1152,7 @@ def __copy__(self) -> 'String2Key': s2k.iv = self.iv return s2k - def parse(self, packet: bytearray, iv: bool = True) -> None: + def parse(self, packet: bytearray) -> None: self.usage = S2KUsage(packet[0]) del packet[0] @@ -1161,7 +1161,7 @@ def parse(self, packet: bytearray, iv: bool = True) -> None: del packet[0] self._specifier.parse(packet) - if self.encalg is not SymmetricKeyAlgorithm.Plaintext and iv: + if self.encalg is not SymmetricKeyAlgorithm.Plaintext: ivlen = self._iv_length if ivlen: self.iv = packet[:(ivlen)] diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 07f30b18..320adf67 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -634,43 +634,42 @@ class SKESessionKeyV4(SKESessionKey): """ __ver__ = 4 - @property - def symalg(self): - return self.s2k.encalg - - def __init__(self): + def __init__(self) -> None: super().__init__() - self.s2k = String2Key() + self.symalg = SymmetricKeyAlgorithm.AES256 + self.s2kspec = S2KSpecifier() self.ct = bytearray() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += super().__bytearray__() - _bytes += self.s2k.__bytearray__()[1:] + _bytes.append(self.symalg) + _bytes += self.s2kspec.__bytearray__() _bytes += self.ct return _bytes - def __copy__(self): + def __copy__(self) -> 'SKESessionKeyV4': sk = self.__class__() sk.header = copy.copy(self.header) - sk.s2k = copy.copy(self.s2k) + sk.s2kspec = copy.copy(self.s2kspec) sk.ct = self.ct[:] return sk - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) - # prepend a valid usage identifier so this parses correctly - packet.insert(0, 255) - self.s2k.parse(packet, iv=False) + self.symalg = SymmetricKeyAlgorithm(packet[0]) + del packet[0] + self.s2kspec.parse(packet) - ctend = self.header.length - len(self.s2k) + ctend = self.header.length - (2 + len(self.s2kspec)) self.ct = packet[:ctend] del packet[:ctend] def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: # derive the first session key from our passphrase - sk = self.s2k.derive_key(passphrase) + + sk = self.s2kspec.derive_key(passphrase, self.symalg.key_size) del passphrase # if there is no ciphertext, then the first session key is the session key being used @@ -688,7 +687,7 @@ def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricK def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: # derive the key to encrypt sk with from it (salt will be generated automatically if it is not yet set) - esk = self.s2k.derive_key(passphrase) + esk = self.s2kspec.derive_key(passphrase, self.symalg.key_size) del passphrase # note that by default, we assume that we're using same diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 08b267c5..ea576654 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1300,11 +1300,9 @@ def encrypt(self, passphrase: Union[str, bytes], """ # set up a new SKESessionKeyV4 skesk = SKESessionKeyV4() - skesk.s2k.usage = 255 - skesk.s2k.specifier = 3 - skesk.s2k.halg = hash - skesk.s2k.encalg = cipher - skesk.s2k.count = 255 + skesk.symalg = cipher + skesk.s2kspec.halg = hash + skesk.s2kspec.iteration_count = b'\xff' if sessionkey is None: sessionkey = cipher.gen_key() From 2ad0f3361574fb83dca3353592206bb96c69c55d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 20:00:02 -0400 Subject: [PATCH 211/287] fields.PrivKey: learn about the version of the key This will become useful as newer versions of secret key packets have different wire formats for the key material. --- pgpy/packet/fields.py | 4 +++- pgpy/packet/packets.py | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c7d88f0f..38726c56 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1263,9 +1263,10 @@ def __mpis__(self): yield from super().__mpis__ yield from self.__privfields__ - def __init__(self): + def __init__(self, key_version: int = 4) -> None: super().__init__() + self.key_version = key_version self.s2k = String2Key() self.encbytes = bytearray() self.chksum = bytearray() @@ -1306,6 +1307,7 @@ def __len__(self): def __copy__(self): pk = super().__copy__() + pk.key_version = self.key_version pk.s2k = copy.copy(self.s2k) pk.encbytes = copy.copy(self.encbytes) pk.chksum = copy.copy(self.chksum) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 320adf67..066e907b 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -884,19 +884,19 @@ def pkalg_int(self, val: int) -> None: self._opaque_pkalg: int = val if self.pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: - self.keymaterial = (RSAPub if self.public else RSAPriv)() + self.keymaterial = RSAPub() if self.public else RSAPriv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.DSA: - self.keymaterial = (DSAPub if self.public else DSAPriv)() + self.keymaterial = DSAPub() if self.public else DSAPriv(self.__ver__) elif self.pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: - self.keymaterial = (ElGPub if self.public else ElGPriv)() + self.keymaterial = ElGPub() if self.public else ElGPriv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.ECDSA: - self.keymaterial = (ECDSAPub if self.public else ECDSAPriv)() + self.keymaterial = ECDSAPub() if self.public else ECDSAPriv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.ECDH: - self.keymaterial = (ECDHPub if self.public else ECDHPriv)() + self.keymaterial = ECDHPub() if self.public else ECDHPriv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.EdDSA: - self.keymaterial = (EdDSAPub if self.public else EdDSAPriv)() + self.keymaterial = EdDSAPub() if self.public else EdDSAPriv(self.__ver__) else: - self.keymaterial = (OpaquePubKey if self.public else OpaquePrivKey)() + self.keymaterial = OpaquePubKey() if self.public else OpaquePrivKey(self.__ver__) @property def public(self) -> bool: From 6a288eb88a692e27a47ad788483d34777cbe1215 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 29 Jun 2023 20:05:46 -0400 Subject: [PATCH 212/287] String2Key: make key_version explicit This will become useful because when String2Key is placed on the wire in a Secret Key packet, it may be represented differently depending on the version of the secret key packet. --- pgpy/packet/fields.py | 7 ++++--- tests/test_01_packetfields.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 38726c56..16d66fe9 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1120,8 +1120,9 @@ def iv_bytearray(self, val: Optional[Union[bytearray, bytes]]) -> None: val = bytes(val) self._iv = val - def __init__(self) -> None: + def __init__(self, key_version: int) -> None: super().__init__() + self.key_version = key_version self.usage = S2KUsage.Unprotected self._encalg = SymmetricKeyAlgorithm.AES256 self._specifier = S2KSpecifier() @@ -1144,7 +1145,7 @@ def __bool__(self) -> bool: return self.usage in [S2KUsage.CFB, S2KUsage.MalleableCFB] def __copy__(self) -> 'String2Key': - s2k = String2Key() + s2k = String2Key(self.key_version) s2k.usage = self.usage s2k.encalg = self.encalg s2k._specifier = copy.copy(self._specifier) @@ -1267,7 +1268,7 @@ def __init__(self, key_version: int = 4) -> None: super().__init__() self.key_version = key_version - self.s2k = String2Key() + self.s2k = String2Key(key_version) self.encbytes = bytearray() self.chksum = bytearray() diff --git a/tests/test_01_packetfields.py b/tests/test_01_packetfields.py index df0842fd..b6be65a3 100644 --- a/tests/test_01_packetfields.py +++ b/tests/test_01_packetfields.py @@ -322,7 +322,7 @@ class TestString2Key: @pytest.mark.parametrize('sis2k', sis2ks) def test_simple_string2key(self, sis2k): b = sis2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(sis2k) assert len(sis2k) == 0 @@ -338,7 +338,7 @@ def test_simple_string2key(self, sis2k): @pytest.mark.parametrize('sas2k', sas2ks) def test_salted_string2key(self, sas2k): b = sas2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(sas2k) assert len(sas2k) == 0 @@ -355,7 +355,7 @@ def test_salted_string2key(self, sas2k): @pytest.mark.parametrize('is2k', is2ks) def test_iterated_string2key(self, is2k): b = is2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(is2k) assert len(is2k) == 0 @@ -373,7 +373,7 @@ def test_iterated_string2key(self, is2k): @pytest.mark.parametrize('gnus2k', gnus2ks) def test_gnu_extension_string2key(self, gnus2k): b = gnus2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(gnus2k) assert len(gnus2k) == 0 From 7d0848529541a051da95926f110dbb5d7210475e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 11:01:12 -0400 Subject: [PATCH 213/287] more type signatures --- pgpy/packet/subpackets/signature.py | 21 +++++++++------ pgpy/packet/subpackets/types.py | 42 ++++++++++++++--------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 4205ce16..7c50185d 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -17,6 +17,8 @@ from .types import EmbeddedSignatureHeader from .types import Signature +from ..types import VersionedHeader + from ...constants import CompressionAlgorithm from ...constants import Features as _Features from ...constants import HashAlgorithm @@ -27,6 +29,7 @@ from ...constants import RevocationKeyClass from ...constants import RevocationReason from ...constants import SigSubpacketType +from ...constants import SignatureType from ...constants import SymmetricKeyAlgorithm from ...decorators import sdproperty @@ -868,23 +871,25 @@ def _sig(self): return self._sigpkt @_sig.setter - def _sig_set(self, val): + def _sig_set(self, val) -> None: esh = EmbeddedSignatureHeader() + if not isinstance(val.header, VersionedHeader): + raise TypeError(f"Signature packet should have had a versioned header, got {type(val.header)}") esh.version = val.header.version val.header = esh val.update_hlen() self._sigpkt = val @property - def sigtype(self): + def sigtype(self) -> SignatureType: return self._sig.sigtype @property - def pubalg(self): + def pubalg(self) -> PubKeyAlgorithm: return self._sig.pubalg @property - def halg(self): + def halg(self) -> HashAlgorithm: return self._sig.halg @property @@ -900,19 +905,19 @@ def signature(self): return self._sig.signature @property - def signer(self): + def signer(self) -> Optional[Union[KeyID, Fingerprint]]: return self._sig.signer - def __init__(self): + def __init__(self) -> None: super().__init__() from ..packets import SignatureV4 self._sigpkt = SignatureV4() self._sigpkt.header = EmbeddedSignatureHeader() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return super().__bytearray__() + self._sigpkt.__bytearray__() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self._sig.parse(packet) diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 7e163cbc..ce966399 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -25,43 +25,42 @@ class Header(_Header): @sdproperty - def critical(self): + def critical(self) -> bool: return self._critical - @critical.register(bool) - def critical_bool(self, val): + @critical.register + def critical_bool(self, val: bool) -> None: self._critical = val @sdproperty - def typeid(self): + def typeid(self) -> int: return self._typeid - @typeid.register(int) - def typeid_int(self, val): + @typeid.register + def typeid_int(self, val: int) -> None: self._typeid = val & 0x7f - @typeid.register(bytes) - @typeid.register(bytearray) - def typeid_bin(self, val): + @typeid.register + def typeid_bin(self, val: Union[bytes, bytearray]) -> None: v = self.bytes_to_int(val) self.typeid = v self.critical = bool(v & 0x80) - def __init__(self): + def __init__(self) -> None: super().__init__() self._typeid = -1 self.critical = False - def parse(self, packet): + def parse(self, packet: bytearray) -> None: self.length = packet self.typeid = packet[:1] del packet[:1] - def __len__(self): + def __len__(self) -> int: return self.llen + 1 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray(self.encode_length(self.length)) _bytes += self.int_to_bytes((int(self.critical) << 7) + self.typeid) return _bytes @@ -79,7 +78,7 @@ def parse(self, packet: bytearray) -> None: class SubPacket(Dispatchable): __headercls__ = Header - def __init__(self): + def __init__(self) -> None: super().__init__() self.header = Header() @@ -127,24 +126,23 @@ class Opaque(Signature, UserAttribute): __typeid__ = None @sdproperty - def payload(self): + def payload(self) -> bytearray: return self._payload - @payload.register(bytes) - @payload.register(bytearray) - def payload_bin(self, val): + @payload.register + def payload_bin(self, val: Union[bytes, bytearray]) -> None: self._payload = bytearray(val) - def __init__(self): + def __init__(self) -> None: super().__init__() - self.payload = b'' + self.payload = bytearray(b'') - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.payload return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.payload = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] From 6f1959001aef72c7b8f20c80b85128927e8517b7 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 12:44:58 -0400 Subject: [PATCH 214/287] PrivKeyV4.pubkey: move most of implementation to base class This prepares us for a simplified switch for v6 keys. Improve the type signatures for the superclass here, since pubkey() is a method, not a property. --- pgpy/packet/packets.py | 41 +++++++++++++++++++++++++---------------- pgpy/packet/types.py | 2 +- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 066e907b..5f7c6ca6 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1028,6 +1028,29 @@ def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: raise TypeError("Key material is not a private key, cannot sign") return self.keymaterial.sign(sigdata, hash_alg) + def _extract_pubkey(self, pk: PubKey) -> None: + pk.created = self.created + pk.pkalg = self.pkalg + + if self.keymaterial is not None: + if pk.keymaterial is None: + raise TypeError(f"pubkey material for {type(self.keymaterial)} was missing") + # copy over MPIs + for pm in self.keymaterial.__pubfields__: + setattr(pk.keymaterial, pm, copy.copy(getattr(self.keymaterial, pm))) + + if isinstance(self.keymaterial, (ECDSAPub, EdDSAPub, ECDHPub)): + if not isinstance(pk.keymaterial, (ECDSAPub, EdDSAPub, ECDHPub)): + raise TypeError(f"Expected Elliptic Curve, got {type(pk.keymaterial)} instead") + pk.keymaterial.oid = self.keymaterial.oid + + if isinstance(self.keymaterial, ECDHPub): + if not isinstance(pk.keymaterial, ECDHPub): + raise TypeError(f"Expected ECDH, got {type(pk.keymaterial)} instead") + pk.keymaterial.kdf = copy.copy(self.keymaterial.kdf) + + pk.update_hlen() + class PrivKeyV4(PrivKey, PubKeyV4): __ver__ = 4 @@ -1047,24 +1070,10 @@ def new(cls, key_algorithm, key_size, created=None) -> 'PrivKeyV4': pk.update_hlen() return pk - def pubkey(self): + def pubkey(self) -> Public: # return a copy of ourselves, but just the public half pk = PubKeyV4() if not isinstance(self, PrivSubKeyV4) else PubSubKeyV4() - pk.created = self.created - pk.pkalg = self.pkalg - - # copy over MPIs - for pm in self.keymaterial.__pubfields__: - setattr(pk.keymaterial, pm, copy.copy(getattr(self.keymaterial, pm))) - - if self.pkalg in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA}: - pk.keymaterial.oid = self.keymaterial.oid - - if self.pkalg == PubKeyAlgorithm.ECDH: - pk.keymaterial.oid = self.keymaterial.oid - pk.keymaterial.kdf = copy.copy(self.keymaterial.kdf) - - pk.update_hlen() + self._extract_pubkey(pk) return pk diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 7547bf23..a4e38d60 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -232,7 +232,7 @@ class Public(Key): class Private(Key): - @abc.abstractproperty + @abc.abstractmethod def pubkey(self) -> Public: """compute and return the fingerprint of the key""" From 43ca39c8e48d13fc28f93fd5a3593e0efe27945e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 13:51:37 -0400 Subject: [PATCH 215/287] PGPSignature: more type annotations --- pgpy/packet/packets.py | 4 +++ pgpy/pgp.py | 82 +++++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 5f7c6ca6..458cce11 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -398,6 +398,10 @@ def update_hlen(self): def make_onepass(self) -> 'OnePassSignature': raise NotImplementedError() + @abc.abstractproperty + def signer(self) -> Optional[Union[KeyID, Fingerprint]]: + ... + class SignatureV4(Signature): """ diff --git a/pgpy/pgp.py b/pgpy/pgp.py index ea576654..72fd8772 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -108,6 +108,8 @@ def cipherprefs(self) -> Optional[List[SymmetricKeyAlgorithm]]: """ A ``list`` of preferred symmetric algorithms specified in this signature, if any. Otherwise, return None. """ + if self._signature is None: + return None if 'PreferredSymmetricAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredSymmetricAlgorithms'])).flags return None @@ -117,6 +119,8 @@ def compprefs(self) -> Optional[List[CompressionAlgorithm]]: """ A ``list`` of preferred compression algorithms specified in this signature, if any. Otherwise, return None. """ + if self._signature is None: + return None if 'PreferredCompressionAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredCompressionAlgorithms'])).flags return None @@ -126,6 +130,8 @@ def created(self): """ A :py:obj:`~datetime.datetime` of when this signature was created. """ + if self._signature is None: + return None return self._signature.subpackets['h_CreationTime'][-1].created @property @@ -138,6 +144,8 @@ def expires_at(self) -> Optional[datetime]: A :py:obj:`~datetime.datetime` of when this signature expires, if a signature expiration date is specified. Otherwise, ``None`` """ + if self._signature is None: + return None if 'SignatureExpirationTime' in self._signature.subpackets: expd = next(iter(self._signature.subpackets['SignatureExpirationTime'])).expires return self.created + expd @@ -148,6 +156,8 @@ def exportable(self) -> bool: """ ``False`` if this signature is marked as being not exportable. Otherwise, ``True``. """ + if self._signature is None: + return True if 'ExportableCertification' in self._signature.subpackets: return bool(next(iter(self._signature.subpackets['ExportableCertification']))) @@ -158,6 +168,8 @@ def features(self) -> Optional[Features]: """ The implementation Features specified in this signature, if any. Otherwise, None """ + if self._signature is None: + return None if 'Features' in self._signature.subpackets: return next(iter(self._signature.subpackets['Features'])).flags return None @@ -171,6 +183,8 @@ def hashprefs(self) -> Optional[List[HashAlgorithm]]: """ A ``list`` of preferred hash algorithms specified in this signature, if any. Otherwise, return None """ + if self._signature is None: + return None if 'PreferredHashAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredHashAlgorithms'])).flags return None @@ -180,6 +194,8 @@ def hash_algorithm(self) -> HashAlgorithm: """ The :py:obj:`~constants.HashAlgorithm` used when computing this signature. """ + if self._signature is None: + raise ValueError("wanted hash_algorithm of uninitizalized PGPSignature") return self._signature.halg def check_primitives(self) -> SecurityIssues: @@ -204,10 +220,14 @@ def key_algorithm(self) -> PubKeyAlgorithm: """ The :py:obj:`~constants.PubKeyAlgorithm` of the key that generated this signature. """ + if self._signature is None: + raise ValueError("wanted key_algorithm of uninitizalized PGPSignature") return self._signature.pubalg @property def key_expiration(self) -> Optional[datetime]: + if self._signature is None: + return None if 'KeyExpirationTime' in self._signature.subpackets: return next(iter(self._signature.subpackets['KeyExpirationTime'])).expires return None @@ -217,6 +237,8 @@ def key_flags(self) -> Optional[KeyFlags]: """ The KeyFlags specified in this signature, if any. Otherwise, None. """ + if self._signature is None: + return None if 'KeyFlags' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyFlags'])).flags return None @@ -226,6 +248,8 @@ def keyserver(self) -> Optional[str]: """ The preferred key server specified in this signature, if any. Otherwise, None. """ + if self._signature is None: + return None if 'PreferredKeyServer' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredKeyServer'])).uri return None @@ -235,6 +259,8 @@ def keyserverprefs(self) -> Optional[KeyServerPreferences]: """ The KeyServerPreferences` in this signature, if any. Otherwise, None. """ + if self._signature is None: + return None if 'KeyServerPreferences' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyServerPreferences'])).flags return None @@ -248,6 +274,8 @@ def notation(self): """ A ``dict`` of notation data in this signature, if any. Otherwise, an empty ``dict``. """ + if self._signature is None: + return {} return {nd.name: nd.value for nd in self._signature.subpackets['NotationData']} @property @@ -255,6 +283,8 @@ def policy_uri(self) -> Optional[str]: """ The policy URI specified in this signature, if any. Otherwise, None. """ + if self._signature is None: + return None if 'Policy' in self._signature.subpackets: return next(iter(self._signature.subpackets['Policy'])).uri return None @@ -264,18 +294,24 @@ def revocable(self) -> bool: """ ``False`` if this signature is marked as being not revocable. Otherwise, ``True``. """ + if self._signature is None: + return True if 'Revocable' in self._signature.subpackets: return bool(next(iter(self._signature.subpackets['Revocable']))) return True @property def revocation_key(self): + if self._signature is None: + return None if 'RevocationKey' in self._signature.subpackets: raise NotImplementedError() return None @property def revocation_reason(self) -> Optional[ReasonForRevocation]: + if self._signature is None: + return None if 'ReasonForRevocation' in self._signature.subpackets: subpacket = next(iter(self._signature.subpackets['ReasonForRevocation'])) return self.ReasonForRevocation(subpacket.code, subpacket.string) @@ -299,10 +335,12 @@ def attested_certifications(self): return ret @property - def signer(self) -> Optional[KeyID]: + def signer(self) -> Optional[Union[KeyID, Fingerprint]]: """ The 16-character Key ID of the key that generated this signature. """ + if self._signature is None: + return None return self._signature.signer @property @@ -310,6 +348,8 @@ def signer_fingerprint(self) -> Optional[Fingerprint]: """ The fingerprint of the key that generated this signature, if it contained. Otherwise, None. """ + if self._signature is None: + return None if 'IssuerFingerprint' in self._signature.subpackets: return next(iter(self._signature.subpackets['IssuerFingerprint'])).issuer_fingerprint return None @@ -330,6 +370,8 @@ def type(self) -> SignatureType: """ The :py:obj:`~constants.SignatureType` of this signature. """ + if self._signature is None: + raise ValueError("wanted signature type of uninitizalized PGPSignature") return self._signature.sigtype @classmethod @@ -358,7 +400,7 @@ def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Finge sig._signature = sigpkt return sig - def __init__(self): + def __init__(self) -> None: """ PGPSignature objects represent OpenPGP compliant signatures. @@ -369,15 +411,19 @@ def __init__(self): OpenPGP-compliant binary format. """ super().__init__() - self._signature = None + self._signature: Optional[Signature] = None - def __bytearray__(self): + def __bytearray__(self) -> bytearray: + if self._signature is None: + return bytearray() return self._signature.__bytearray__() - def __repr__(self): - return "".format(self.type.name, id(self)) + def __repr__(self) -> str: + return f"" - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: + if not isinstance(other, PGPSignature): + raise TypeError(f"tried to compare PGPSignature to {type(other)}") return self.created < other.created def __or__(self, other): @@ -411,6 +457,8 @@ def attests_to(self, othersig): return h.finalize() in self.attested_certifications def hashdata(self, subject) -> bytes: + if self._signature is None: + raise TypeError("called hashdata on uninitializaed PGPSignature") _data = bytearray() if isinstance(subject, str): @@ -578,6 +626,8 @@ def hashdata(self, subject) -> bytes: return bytes(_data) def make_onepass(self) -> OnePassSignature: + if self._signature is None: + raise TypeError("called make_onepass on an uninitialized PGPSignature") return self._signature.make_onepass() def parse(self, packet: bytearray) -> None: @@ -596,10 +646,12 @@ def parse(self, packet: bytearray) -> None: if isinstance(pkt, Opaque): # this is an unrecognized version. pass - else: + elif isinstance(pkt, Signature): self._signature = pkt + else: + raise TypeError(f"Expected Signature, got {type(pkt)}") else: - raise ValueError('Expected: Signature. Got: {:s}'.format(pkt.__class__.__name__)) + raise ValueError(f'Expected: Signature. Got: {type(pkt)}') class PGPSignatures(collections.abc.Container, collections.abc.Iterable, collections.abc.Sized, Armorable): @@ -648,12 +700,14 @@ def parse(self, packet: bytes) -> None: if isinstance(pkt, Opaque): # skip unrecognized version. pass - else: + elif isinstance(pkt, Signature): sig = PGPSignature() sig._signature = pkt self._sigs.append(sig) + else: + raise TypeError(f"Expected Signature. Got: {type(pkt)}") else: - raise ValueError(f"Expected: Signature. Got: {format(pkt.__class__.__name__)}") + raise ValueError(f"Expected: Signature. Got: {type(pkt)}") class PGPUID(ParentRef): @@ -738,7 +792,7 @@ def is_primary(self) -> bool: """ If the most recent, valid self-signature specifies this as being primary, this will be True. Otherwise, False. """ - if self.selfsig is not None: + if self.selfsig is not None and self.selfsig._signature is not None: return bool(next(iter(self.selfsig._signature.subpackets['h_PrimaryUserID']), False)) return False @@ -2087,6 +2141,8 @@ def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: revocable = prefs.pop('revocable', True) policy_uri = prefs.pop('policy_uri', None) intended_recipients = prefs.pop('intended_recipients', []) + if sig._signature is None: + raise ValueError("PGPSignature should have been initialized already here") for intended_recipient in intended_recipients: if isinstance(intended_recipient, PGPKey) and isinstance(intended_recipient._key, PubKeyV4): @@ -2483,6 +2539,8 @@ def bind(self, key: 'PGPKey', **prefs) -> PGPSignature: raise PGPError('Uninitialized or unknown PGPKey cannot bind subkeys') sig = PGPSignature.new(sig_type, key_algo, hash_algo, self.fingerprint, created=created) + if sig._signature is None: + raise PGPError("PGPSignature should have been initialized here") if sig_type == SignatureType.Subkey_Binding: # signature options that only make sense in subkey binding signatures From cdc784fcba1c792f6c1b9a71b341384a6aa51554 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 13:52:44 -0400 Subject: [PATCH 216/287] append embedded signatures as signature packets, not subpackets --- pgpy/pgp.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 72fd8772..50d22caa 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -426,18 +426,13 @@ def __lt__(self, other: Any) -> bool: raise TypeError(f"tried to compare PGPSignature to {type(other)}") return self.created < other.created - def __or__(self, other): + def __or__(self, other: Signature) -> 'PGPSignature': if isinstance(other, Signature): if self._signature is None: self._signature = other return self - ##TODO: this is not a great way to do this - if other.__class__.__name__ == 'EmbeddedSignature': - self._signature = other - return self - - raise TypeError + raise TypeError(f"should be Signature, got {type(other)}") def __copy__(self): # because the default shallow copy isn't actually all that useful, @@ -614,7 +609,9 @@ def hashdata(self, subject) -> bytes: """ hcontext = bytearray() - hcontext.append(self._signature.header.version if not self.embedded else self._signature._sig.header.version) + if not isinstance(self._signature.header, VersionedHeader): + raise TypeError(f"Expected a VersionedHeader for signature, got {type(self._signature.header)}") + hcontext.append(self._signature.header.version) hcontext.append(self.type) hcontext.append(self.key_algorithm) hcontext.append(self.hash_algorithm) @@ -1850,7 +1847,7 @@ def __or__(self, other, from_sib=False): # if this is a subkey binding signature that has embedded primary key binding signatures, add them to parent if other.type == SignatureType.Subkey_Binding: for es in iter(pkb for pkb in other._signature.subpackets['EmbeddedSignature']): - esig = PGPSignature() | es + esig = PGPSignature() | es._sig esig._parent = other self._signatures.insort(esig) From 0908d949798e3ab2c840ce907206b2d4ad4fc4ab Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 13:55:05 -0400 Subject: [PATCH 217/287] Keys and Fingerprints know their own version, no need to interpolate. --- pgpy/pgp.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 50d22caa..b75aa9e9 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2142,12 +2142,11 @@ def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: raise ValueError("PGPSignature should have been initialized already here") for intended_recipient in intended_recipients: - if isinstance(intended_recipient, PGPKey) and isinstance(intended_recipient._key, PubKeyV4): - sig._signature.subpackets.addnew('IntendedRecipient', hashed=True, version=4, + if isinstance(intended_recipient, PGPKey): + sig._signature.subpackets.addnew('IntendedRecipient', hashed=True, intended_recipient=intended_recipient.fingerprint) elif isinstance(intended_recipient, Fingerprint): - # FIXME: what if it's not a v4 fingerprint? - sig._signature.subpackets.addnew('IntendedRecipient', hashed=True, version=4, + sig._signature.subpackets.addnew('IntendedRecipient', hashed=True, intended_recipient=intended_recipient) else: warnings.warn("Intended Recipient is not a PGPKey, ignoring") From fd89df7de7a312f641eb63eacbf04ed81e53ebb1 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 1 Jul 2023 11:44:04 -0400 Subject: [PATCH 218/287] Test and support python 3.11 --- README.rst | 2 +- setup.cfg | 2 ++ tox.ini | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9699c437..08395df9 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Requirements - Python >= 3.6 - Tested with: 3.10, 3.9, 3.8, 3.7, 3.6 + Tested with: 3.11, 3.10, 3.9, 3.8, 3.7, 3.6 - `Cryptography `_ diff --git a/setup.cfg b/setup.cfg index 28139442..04da686b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,8 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.7 diff --git a/tox.ini b/tox.ini index 70f142ad..02d85fc2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{36,37,38,39,310}{,-setup}, pypy3, pep8 +envlist = py{36,37,38,39,310,311}{,-setup}, pypy3, pep8 skipsdist = True [pytest] @@ -36,7 +36,7 @@ install_command = pip install {opts} --no-cache-dir {packages} commands = py.test --cov pgpy --cov-report term-missing tests/ -[testenv:py{36,37,38,39,310}-setup] +[testenv:py{36,37,38,39,310,311}-setup] recreate = True allowlist_externals = /usr/bin/rm From 3068a0c4110f7764510046e1987595d8bbe2f04b Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 1 Jul 2023 16:29:04 -0400 Subject: [PATCH 219/287] We no longer support Python 3.5, so we can switch to standard pytest-order --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 02d85fc2..ac09c574 100644 --- a/tox.ini +++ b/tox.ini @@ -29,8 +29,7 @@ deps = gpg==1.10.0 pytest pytest-cov - # We need a patched version of pytest-order to run on 3.5 and handle parameterized tests - git+https://github.com/SecurityInnovation/pytest-order.git@07ceb36233fb083275f34d5c8abbd3e35cd00158#egg=pytest-order + pytest-order install_command = pip install {opts} --no-cache-dir {packages} commands = From 0c2cfd1589a52171663e3df79294f659e14bc2fe Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 20:57:43 -0400 Subject: [PATCH 220/287] from __future__ import annotations This lets us use forward-declared type annotations without manually noting them as strings. --- pgpy/constants.py | 20 +++++++++++--------- pgpy/packet/fields.py | 13 +++++++------ pgpy/packet/packets.py | 22 ++++++++++++---------- pgpy/packet/types.py | 3 ++- pgpy/pgp.py | 28 +++++++++++++++------------- pgpy/types.py | 9 +++++---- 6 files changed, 52 insertions(+), 43 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index a15ebb6e..b833f7a4 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -1,5 +1,7 @@ """ constants.py """ +from __future__ import annotations + import bz2 import os import zlib @@ -83,7 +85,7 @@ class PacketType(IntEnum): ModificationDetectionCode = 19 @classmethod - def _missing_(cls, val: object) -> 'PacketType': + def _missing_(cls, val: object) -> PacketType: if not isinstance(val, int): raise TypeError(f"cannot look up PacketType by non-int {type(val)}") return cls.Unknown @@ -205,7 +207,7 @@ class PubKeyAlgorithm(IntEnum): EdDSA = 0x16 # https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04 @classmethod - def _missing_(cls, val: object) -> 'PubKeyAlgorithm': + def _missing_(cls, val: object) -> PubKeyAlgorithm: if not isinstance(val, int): raise TypeError(f"cannot look up PubKeyAlgorithm by non-int {type(val)}") return cls.Unknown @@ -232,7 +234,7 @@ def deprecated(self) -> bool: PubKeyAlgorithm.RSASign, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign} - def validate_params(self, size) -> 'SecurityIssues': + def validate_params(self, size) -> SecurityIssues: min_size = MINIMUM_ASYMMETRIC_KEY_LENGTHS.get(self) if min_size is not None: if isinstance(min_size, set): @@ -338,7 +340,7 @@ class HashAlgorithm(IntEnum): #SHA3_512 = 15 @classmethod - def _missing_(cls, val: object) -> 'HashAlgorithm': + def _missing_(cls, val: object) -> HashAlgorithm: if not isinstance(val, int): raise TypeError(f"cannot look up HashAlgorithm by non-int {type(val)}") return cls.Unknown @@ -364,7 +366,7 @@ def is_collision_resistant(self) -> bool: return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512} @property - def is_considered_secure(self) -> 'SecurityIssues': + def is_considered_secure(self) -> SecurityIssues: if self.is_collision_resistant: return SecurityIssues.OK @@ -429,7 +431,7 @@ class EllipticCurveOID(Enum): SECP256K1 = (ec.SECP256K1, '1.3.132.0.10', b'\x2b\x81\x04\x00\x0a') - def __new__(cls, impl_cls: Type, oid: str, oid_der: bytes, name: Optional[str] = None, key_size_bits: Optional[int] = None) -> "EllipticCurveOID": + def __new__(cls, impl_cls: Type, oid: str, oid_der: bytes, name: Optional[str] = None, key_size_bits: Optional[int] = None) -> EllipticCurveOID: # preprocessing stage for enum members: # - set enum_member.value to ObjectIdentifier(oid) # - if curve is not None and curve.name is in ec._CURVE_TYPES, set enum_member.curve to curve @@ -564,7 +566,7 @@ class ImageEncoding(IntEnum): JPEG = 0x01 @classmethod - def encodingof(cls, imagebytes: bytes) -> 'ImageEncoding': + def encodingof(cls, imagebytes: bytes) -> ImageEncoding: if imagebytes[6:10] in (b'JFIF', b'Exif') or imagebytes[:4] == b'\xff\xd8\xff\xdb': return ImageEncoding.JPEG return ImageEncoding.Unknown # pragma: no cover @@ -664,7 +666,7 @@ class String2KeyType(IntEnum): GNUExtension = 101 @classmethod - def _missing_(cls, val: object) -> 'String2KeyType': + def _missing_(cls, val: object) -> String2KeyType: if not isinstance(val, int): raise TypeError(f"cannot look up String2KeyType by non-int {type(val)}") return cls.Unknown @@ -730,7 +732,7 @@ class Features(IntFlag): UnknownFeature80 = 0x80 @classproperty - def pgpy_features(cls) -> 'Features': + def pgpy_features(cls) -> Features: return Features.ModificationDetection diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 16d66fe9..e57e0ac1 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1,5 +1,6 @@ """ fields.py """ +from __future__ import annotations import abc import binascii @@ -516,7 +517,7 @@ def to_mpibytes(self) -> bytes: def __bytearray__(self) -> bytearray: return bytearray(self.to_mpibytes()) - def __copy__(self) -> 'ECPoint': + def __copy__(self) -> ECPoint: pk = self.__class__() pk.bytelen = self.bytelen pk.format = self.format @@ -545,7 +546,7 @@ def __bytearray__(self) -> bytearray: _b += self.p.to_mpibytes() return _b - def __copy__(self) -> 'ECDSAPub': + def __copy__(self) -> ECDSAPub: pkt = super().__copy__() if not isinstance(pkt, ECDSAPub): raise TypeError(f"Failed to create ECDSAPub when copying, got {type(pkt)}") @@ -590,7 +591,7 @@ def __bytearray__(self) -> bytearray: def __pubkey__(self): return ed25519.Ed25519PublicKey.from_public_bytes(self.p.x) - def __copy__(self) -> 'EdDSAPub': + def __copy__(self) -> EdDSAPub: pkt = super().__copy__() if not isinstance(pkt, EdDSAPub): raise TypeError(f"Failed to create EdDSAPub when copying, got {type(pkt)}") @@ -645,7 +646,7 @@ def __bytearray__(self) -> bytearray: _b += self.kdf.__bytearray__() return _b - def __copy__(self) -> 'ECDHPub': + def __copy__(self) -> ECDHPub: pkt = super().__copy__() if not isinstance(pkt, ECDHPub): raise TypeError(f"Failed to create ECDHAPub when copying, got {type(pkt)}") @@ -843,7 +844,7 @@ def __init__(self, if smartcard_serial is not None: self.smartcard_serial = bytes(smartcard_serial) - def __copy__(self) -> "S2KSpecifier": + def __copy__(self) -> S2KSpecifier: s2k = S2KSpecifier() s2k._type = self._type if self._type is String2KeyType.Unknown: @@ -1144,7 +1145,7 @@ def __len__(self) -> int: def __bool__(self) -> bool: return self.usage in [S2KUsage.CFB, S2KUsage.MalleableCFB] - def __copy__(self) -> 'String2Key': + def __copy__(self) -> String2Key: s2k = String2Key(self.key_version) s2k.usage = self.usage s2k.encalg = self.encalg diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 458cce11..f197b413 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1,5 +1,7 @@ """ packet.py """ +from __future__ import annotations + import abc import binascii import calendar @@ -98,11 +100,11 @@ def __init__(self) -> None: self.ct: Optional[CipherText] = None @abc.abstractmethod - def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() @abc.abstractmethod - def encrypt_sk(self, pk: 'PubKey', symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: + def encrypt_sk(self, pk: PubKey, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: raise NotImplementedError() # a PKESK should return a pointer to the recipient, or None @@ -239,7 +241,7 @@ def __copy__(self): return sk - def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: if isinstance(self.ct, RSACipherText): if not isinstance(pk.keymaterial, PrivKeyField): raise TypeError(f"Private key key material was {type(pk.keymaterial)}, should have been PrivKeyField") @@ -286,7 +288,7 @@ def decrypt_sk(self, pk: 'PrivKey') -> Tuple[Optional[SymmetricKeyAlgorithm], by return (symalg, symkey) - def encrypt_sk(self, pk: 'PubKey', symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: + def encrypt_sk(self, pk: PubKey, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: if symalg is None: raise ValueError('PKESKv3: must pass a symmetric key algorithm explicitly when encrypting') if pk.keymaterial is None: @@ -395,7 +397,7 @@ def update_hlen(self): super().update_hlen() @abc.abstractmethod - def make_onepass(self) -> 'OnePassSignature': + def make_onepass(self) -> OnePassSignature: raise NotImplementedError() @abc.abstractproperty @@ -517,7 +519,7 @@ def canonical_bytes(self) -> bytearray: _hdr += self.int_to_bytes(len(_body), minlen=4) return _hdr + _body - def __copy__(self) -> 'SignatureV4': + def __copy__(self) -> SignatureV4: spkt = SignatureV4() spkt.header = copy.copy(self.header) spkt._sigtype = self._sigtype @@ -552,7 +554,7 @@ def parse(self, packet: bytearray) -> None: self.signature.parse(packet) - def make_onepass(self) -> 'OnePassSignatureV3': + def make_onepass(self) -> OnePassSignatureV3: signer = self.signer if signer is None: raise ValueError("Cannot make a one-pass signature without knowledge of who the signer is") @@ -652,7 +654,7 @@ def __bytearray__(self) -> bytearray: _bytes += self.ct return _bytes - def __copy__(self) -> 'SKESessionKeyV4': + def __copy__(self) -> SKESessionKeyV4: sk = self.__class__() sk.header = copy.copy(self.header) sk.s2kspec = copy.copy(self.s2kspec) @@ -906,7 +908,7 @@ def pkalg_int(self, val: int) -> None: def public(self) -> bool: return isinstance(self, PubKey) and not isinstance(self, PrivKey) - def __copy__(self) -> 'PubKey': + def __copy__(self) -> PubKey: pk = self.__class__() pk.header = copy.copy(self.header) pk.created = self.created @@ -1060,7 +1062,7 @@ class PrivKeyV4(PrivKey, PubKeyV4): __ver__ = 4 @classmethod - def new(cls, key_algorithm, key_size, created=None) -> 'PrivKeyV4': + def new(cls, key_algorithm, key_size, created=None) -> PrivKeyV4: # build a key packet pk = PrivKeyV4() pk.pkalg = key_algorithm diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index a4e38d60..afa8d288 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -1,5 +1,6 @@ """ types.py """ +from __future__ import annotations import abc import copy @@ -292,7 +293,7 @@ def __iter__(self) -> Iterator[MPI]: for i in self.__mpis__: yield getattr(self, i) - def __copy__(self) -> 'MPIs': + def __copy__(self) -> MPIs: pk = self.__class__() for m in self.__mpis__: setattr(pk, m, copy.copy(getattr(self, m))) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b75aa9e9..a63cc5fb 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2,6 +2,8 @@ this is where the armorable PGP block objects live """ +from __future__ import annotations + import binascii import collections import collections.abc @@ -375,7 +377,7 @@ def type(self) -> SignatureType: return self._signature.sigtype @classmethod - def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Fingerprint, created=None) -> "PGPSignature": + def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Fingerprint, created=None) -> PGPSignature: sig = PGPSignature() sigpkt: Signature @@ -426,7 +428,7 @@ def __lt__(self, other: Any) -> bool: raise TypeError(f"tried to compare PGPSignature to {type(other)}") return self.created < other.created - def __or__(self, other: Signature) -> 'PGPSignature': + def __or__(self, other: Signature) -> PGPSignature: if isinstance(other, Signature): if self._signature is None: self._signature = other @@ -886,7 +888,7 @@ def attested_third_party_certifications(self) -> Iterator[PGPSignature]: return self.attested_to(self.third_party_certifications) @classmethod - def new(cls, pn: Union[bytearray, str], comment: Optional[str] = None, email: Optional[str] = None) -> 'PGPUID': + def new(cls, pn: Union[bytearray, str], comment: Optional[str] = None, email: Optional[str] = None) -> PGPUID: """ Create a new User ID or photo. @@ -934,7 +936,7 @@ def __repr__(self) -> str: return "".format(self._uid.__class__.__name__, self.selfsig.created, id(self)) return "".format(self._uid.__class__.__name__, id(self)) - def __lt__(self, other: 'PGPUID') -> bool: # pragma: no cover + def __lt__(self, other: PGPUID) -> bool: # pragma: no cover if self.is_uid == other.is_uid: if self.is_primary == other.is_primary: mysig = self.selfsig @@ -958,7 +960,7 @@ def __lt__(self, other: 'PGPUID') -> bool: # pragma: no cover raise ValueError("should not have reached here!") - def __or__(self, other: Union[PGPSignature, UserID, UserAttribute]) -> 'PGPUID': + def __or__(self, other: Union[PGPSignature, UserID, UserAttribute]) -> PGPUID: if isinstance(other, PGPSignature): self._signatures.insort(other) if self.parent is not None and self in self.parent._uids: @@ -977,7 +979,7 @@ def __or__(self, other: Union[PGPSignature, UserID, UserAttribute]) -> 'PGPUID': raise TypeError("unsupported operand type(s) for |: '{:s}' and '{:s}'" "".format(self.__class__.__name__, other.__class__.__name__)) - def __copy__(self) -> 'PGPUID': + def __copy__(self) -> PGPUID: # because the default shallow copy isn't actually all that useful, # and deepcopy does too much work uid = PGPUID() @@ -1162,7 +1164,7 @@ def __iter__(self): for sig in self._signatures: yield sig - def __or__(self, other) -> 'PGPMessage': + def __or__(self, other) -> PGPMessage: if isinstance(other, Marker): return self @@ -1216,7 +1218,7 @@ def __or__(self, other) -> 'PGPMessage': raise NotImplementedError(str(type(other))) - def __copy__(self) -> 'PGPMessage': + def __copy__(self) -> PGPMessage: msg = super().__copy__() msg._compression = self._compression msg._message = copy.copy(self._message) @@ -1237,7 +1239,7 @@ def new(cls, message: Union[str, bytes, bytearray], sensitive: bool = False, compression: CompressionAlgorithm = CompressionAlgorithm.ZIP, file: bool = False, - encoding: Optional[str] = None) -> 'PGPMessage': + encoding: Optional[str] = None) -> PGPMessage: """ Create a new PGPMessage object. @@ -1327,7 +1329,7 @@ def encrypt(self, passphrase: Union[str, bytes], sessionkey: Optional[Union[int, ByteString]] = None, cipher: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, hash: HashAlgorithm = HashAlgorithm.SHA256, - iv: Optional[bytes] = None) -> 'PGPMessage': + iv: Optional[bytes] = None) -> PGPMessage: """ encrypt(passphrase, [sessionkey=None,] **prefs) @@ -1374,7 +1376,7 @@ def encrypt(self, passphrase: Union[str, bytes], return msg - def decrypt(self, passphrase: Union[str, bytes]) -> 'PGPMessage': + def decrypt(self, passphrase: Union[str, bytes]) -> PGPMessage: """ Attempt to decrypt this message using a passphrase. @@ -1744,7 +1746,7 @@ def revocation_keys(self): yield sig.revocation_key @classmethod - def new(cls, key_algorithm: PubKeyAlgorithm, key_size: Union[int, EllipticCurveOID], created: Optional[datetime] = None) -> 'PGPKey': + def new(cls, key_algorithm: PubKeyAlgorithm, key_size: Union[int, EllipticCurveOID], created: Optional[datetime] = None) -> PGPKey: """ Generate a new PGP key @@ -2504,7 +2506,7 @@ def revoker(self, revoker, **prefs): return self._sign(self, sig, **prefs) @KeyAction(is_unlocked=True, is_public=False) - def bind(self, key: 'PGPKey', **prefs) -> PGPSignature: + def bind(self, key: PGPKey, **prefs) -> PGPSignature: """ Bind a subkey to this key. diff --git a/pgpy/types.py b/pgpy/types.py index 73ea9597..38d4f030 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -1,5 +1,6 @@ """ types.py """ +from __future__ import annotations import abc import base64 @@ -665,7 +666,7 @@ class KeyID(str): If a uint64 basic type existed in the stdlib, it would have been beter to use that instead. ''' - def __new__(cls, content: Union[str, bytes, bytearray]) -> "KeyID": + def __new__(cls, content: Union[str, bytes, bytearray]) -> KeyID: if isinstance(content, str): if not re.match(r'^[0-9A-F]{16}$', content): raise ValueError(f'Initializing a KeyID from a string requires it to be 16 uppercase hex digits, not "{content}"') @@ -678,11 +679,11 @@ def __new__(cls, content: Union[str, bytes, bytearray]) -> "KeyID": raise TypeError(f'cannot initialize a KeyID from {type(content)}') @classmethod - def from_bytes(cls, b: bytes) -> "KeyID": + def from_bytes(cls, b: bytes) -> KeyID: return cls(binascii.b2a_hex(b).decode('latin-1').upper()) @classmethod - def parse(cls, b: bytearray) -> "KeyID": + def parse(cls, b: bytearray) -> KeyID: 'read a Key ID off the wire and consume the series of bytes that represent the Key ID. Produces a new KeyID object' ret = cls.from_bytes(b[:8]) del b[:8] @@ -741,7 +742,7 @@ def version(self) -> int: def version_int(self, version: int) -> None: self._version: int = version - def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> "Fingerprint": + def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> Fingerprint: if isinstance(content, Fingerprint): return content From 7ed5edf6edd2b2d1368fb13309a703c36c409da5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 21:15:33 -0400 Subject: [PATCH 221/287] Add PGPSubject --- docs/source/changelog.rst | 3 +++ pgpy/pgp.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 9b3613bc..7074d40b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -24,6 +24,9 @@ PGPSignatures represents a detached signature, which can contain more than a single signature. It is a simple sequence of individual PGPSignature objects. +PGPSubject is a simple type that collects all the possible things that +could be signed in OpenPGP. It is useful for type annotations. + New SecurityIssues flag: AlgorithmUnknown API changes diff --git a/pgpy/pgp.py b/pgpy/pgp.py index a63cc5fb..8fe804eb 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -3182,3 +3182,11 @@ def unload(self, key) -> None: if key.is_primary: for sk in key.subkeys.values(): self.unload(sk) + + +# things that can be signed in OpenPGP: +PGPSubject = Union[None, # nothing (i.e., a standalone or timestamp signature) + str, bytes, bytearray, PGPMessage, # a message (i.e., a binary or canonical text document signature) + PGPUID, # a User ID with its parent primary key (i.e., a certification) + PGPKey, # a subkey (i.e., a subkey binding signature) + ] From e6fa7eff531cb2881715afd1a8e6a8daf96da3d3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 7 Jul 2023 23:53:30 -0400 Subject: [PATCH 222/287] explicitly document the ability to sign encrypted messages I'm deeply skeptical about the sense or utility of this, but it is at least documenting the current behavior more adequately. The resulting messages (when signed) form this packet sequence: SIG PKESK SEIPD This is technically valid in the formal OpenPGP message grammar but it's unclear what it is supposed to mean. --- pgpy/pgp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 8fe804eb..d3a7060d 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -3189,4 +3189,7 @@ def unload(self, key) -> None: str, bytes, bytearray, PGPMessage, # a message (i.e., a binary or canonical text document signature) PGPUID, # a User ID with its parent primary key (i.e., a certification) PGPKey, # a subkey (i.e., a subkey binding signature) + SKEData, IntegrityProtectedSKEData, # signing an encrypted message (i.e., binary document signature) + # this last one seems dubious, as signatures outside encryption have a very different set of semantics + # compared to signatures inside encryption, and the API is unclear how the user can distinguish them) ] From 8460756772030ed44f45024e41fa550b7c005a61 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 21:17:41 -0400 Subject: [PATCH 223/287] Declare SignatureVerifications.SigSubj with NamedTuple (instead of sigsubj (with namedtuple)) This makes it easier to see type-annotations. Since the name of the type was never fully exported anyway, and it keeps all of its standard members, this isn't really an API change. --- pgpy/sopgpy.py | 2 +- pgpy/types.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 8a9c6493..d9fb8af6 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -365,7 +365,7 @@ def _check_sigs(self, for signer, cert in certs.items(): try: verif: pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) - goodsig: pgpy.types.SignatureVerification.sigsubj + goodsig: pgpy.types.SignatureVerification.SigSubj for goodsig in verif.good_signatures: sigtime = goodsig.signature.created # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: diff --git a/pgpy/types.py b/pgpy/types.py index 38d4f030..a4eb5b8a 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -16,12 +16,18 @@ from enum import IntEnum -from typing import ByteString, Optional, Dict, List, Literal, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic +from typing import ByteString, Optional, Dict, List, Literal, NamedTuple, Set, Tuple, Type, Union, OrderedDict, TypeVar, Generic from .decorators import sdproperty +from .constants import SecurityIssues from .errors import PGPError +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .pgp import PGPSubject, PGPSignature, PGPKey + __all__ = ['Armorable', 'ParentRef', 'PGPObject', @@ -574,7 +580,12 @@ def __headercls__(self): # pragma: no cover class SignatureVerification: __slots__ = ("_subjects",) - sigsubj = collections.namedtuple('sigsubj', ['issues', 'by', 'signature', 'subject']) + + class SigSubj(NamedTuple): + issues: SecurityIssues + by: PGPKey + signature: PGPSignature + subject: PGPSubject @property def good_signatures(self): From c39c873cdb4537722d2e9e14e144b49d12ac5f90 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 21:18:08 -0400 Subject: [PATCH 224/287] type annotations for PGPKey.verify --- pgpy/pgp.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index d3a7060d..52d9083e 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -19,7 +19,7 @@ from datetime import datetime, timezone -from typing import Any, ByteString, Deque, Literal, List, Iterator, Mapping, Optional, Set, Tuple, Union +from typing import Any, ByteString, Deque, Literal, List, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -2690,7 +2690,8 @@ def signing_subkey(self, sig: PGPSignature) -> Optional["PGPKey"]: return self._children[sig.signer] return None - def verify(self, subject, signature=None): + def verify(self, subject: PGPSubject, + signature: Optional[PGPSignature] = None) -> SignatureVerification: """ Verify a subject with a signature using this key. @@ -2700,7 +2701,7 @@ def verify(self, subject, signature=None): :type signature: :py:obj:`PGPSignature` :returns: :py:obj:`~pgpy.types.SignatureVerification` """ - sspairs = [] + sspairs: List[Tuple[PGPSignature, PGPSubject]] = [] # some type checking if not isinstance(subject, (type(None), PGPMessage, PGPKey, PGPUID, PGPSignature, str, bytes, bytearray)): @@ -2708,7 +2709,7 @@ def verify(self, subject, signature=None): if not isinstance(signature, (type(None), PGPSignature)): raise TypeError("Unexpected signature value: {:s}".format(str(type(signature)))) - def _filter_sigs(sigs): + def _filter_sigs(sigs: Iterable[PGPSignature]) -> Iterator[PGPSignature]: for sig in sigs: if self.issuer_matches(sig): yield sig @@ -2716,6 +2717,8 @@ def _filter_sigs(sigs): # collect signature(s) if signature is None: if isinstance(subject, PGPMessage): + if not isinstance(subject.message, (str, bytes, bytearray)): + raise TypeError("Cannot verify encrypted message without decrypting it first") for sig in _filter_sigs(subject.signatures): sspairs.append((sig, subject.message)) @@ -2751,6 +2754,8 @@ def _filter_sigs(sigs): sigv &= signing_subkey.verify(subj, sig) else: + if self._key is None: + raise PGPError("Tried to verify using a key with no key material!") if isinstance(subj, PGPKey): self_verifying = sig.signer == subj.fingerprint else: From ffb540a5b12a53cde3f2e6b13bfc7e07e8c76125 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 21:18:27 -0400 Subject: [PATCH 225/287] type annotations and string cleanup for SignatureVerification --- pgpy/types.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index a4eb5b8a..6dcb4f29 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -628,22 +628,22 @@ def bad_signatures(self): # pragma: no cover if sigsub.issues and sigsub.issues.causes_signature_verify_to_fail ) - def __init__(self): + def __init__(self) -> None: """ Returned by :py:meth:`.PGPKey.verify` Can be compared directly as a boolean to determine whether or not the specified signature verified. """ super().__init__() - self._subjects = [] + self._subjects: List[SignatureVerification.SigSubj] = [] - def __contains__(self, item): + def __contains__(self, item: PGPSubject) -> bool: return item in {ii for i in self._subjects for ii in [i.signature, i.subject]} - def __len__(self): + def __len__(self) -> int: return len(self._subjects) - def __bool__(self): + def __bool__(self) -> bool: from .constants import SecurityIssues return all( sigsub.issues is SecurityIssues.OK @@ -651,24 +651,23 @@ def __bool__(self): for sigsub in self._subjects ) - def __and__(self, other): + def __and__(self, other) -> SignatureVerification: if not isinstance(other, SignatureVerification): raise TypeError(type(other)) self._subjects += other._subjects return self - def __repr__(self): - return '<{classname}({val})>'.format( - classname=self.__class__.__name__, - val=bool(self) - ) + def __repr__(self) -> str: + return f'<{self.__class__.__name__}({bool(self)})>' - def add_sigsubj(self, signature, by, subject=None, issues=None): + def add_sigsubj(self, signature: PGPSignature, by: PGPKey, + subject: PGPSubject = None, + issues: Optional[SecurityIssues] = None) -> None: if issues is None: from .constants import SecurityIssues issues = SecurityIssues(0xFF) - self._subjects.append(self.sigsubj(issues, by, signature, subject)) + self._subjects.append(self.SigSubj(issues, by, signature, subject)) class KeyID(str): From d5dad20573438de66b66d340c029c23297fdc513 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 6 Jul 2023 23:08:52 -0400 Subject: [PATCH 226/287] PGPKey: add more type annotations --- pgpy/packet/packets.py | 4 ++++ pgpy/pgp.py | 32 +++++++++++++++++++++++--------- pgpy/sopgpy.py | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index f197b413..102887d8 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -404,6 +404,10 @@ def make_onepass(self) -> OnePassSignature: def signer(self) -> Optional[Union[KeyID, Fingerprint]]: ... + @abc.abstractmethod + def canonical_bytes(self) -> bytearray: + ... + class SignatureV4(Signature): """ diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 52d9083e..1a09de78 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2009,7 +2009,7 @@ def unlock(self, passphrase): for sk in itertools.chain([self], self.subkeys.values()): sk._key.keymaterial.clear() - def add_uid(self, uid, selfsign=True, **prefs): + def add_uid(self, uid: PGPUID, selfsign: bool = True, **prefs) -> None: """ Add a User ID to this key. @@ -2027,7 +2027,7 @@ def add_uid(self, uid, selfsign=True, **prefs): self |= uid - def get_uid(self, search): + def get_uid(self, search: str) -> Optional[PGPUID]: """ Find and return a User ID that matches the search string given. @@ -2039,7 +2039,7 @@ def get_uid(self, search): return next((u for u in self._uids if search in filter(lambda a: a is not None, (u.name, u.comment, u.email))), None) return self.parent.get_uid(search) - def del_uid(self, search): + def del_uid(self, search: str) -> None: """ Find and remove a user id that matches the search string given. This method does not modify the corresponding :py:obj:`~pgpy.PGPUID` object; it only removes it from the list of user ids on the key. @@ -2055,7 +2055,7 @@ def del_uid(self, search): u._parent = None self._uids.remove(u) - def add_subkey(self, key, **prefs): + def add_subkey(self, key: PGPKey, **prefs) -> None: """ Add a key as a subkey to this key. @@ -2074,9 +2074,13 @@ def add_subkey(self, key, **prefs): if key.is_primary: if len(key._children) > 0: raise PGPError("Cannot add a key that already has subkeys as a subkey!") + if key._key is None: + raise PGPError("Cannot add a PGPKey as a subkey when it has no proper key object") # convert key into a subkey npk: PrivSubKey = PrivSubKeyV4() + if key._key is None: + raise PGPError("Cannot add a subkey with an unknown algorithm") npk.pkalg = key._key.pkalg npk.created = key._key.created npk.keymaterial = key._key.keymaterial @@ -2090,13 +2094,13 @@ def add_subkey(self, key, **prefs): bsig = self.bind(key, **prefs) key |= bsig - def _get_key_flags(self, user=None): + def _get_key_flags(self, user=None) -> Optional[KeyFlags]: if self.is_primary: if user is not None: user = self.get_uid(user) elif len(self._uids) == 0: - return {KeyFlags.Certify} + return KeyFlags.Certify else: user = next(iter(self.userids)) @@ -2106,7 +2110,7 @@ def _get_key_flags(self, user=None): return next(self.self_signatures).key_flags - def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: + def _sign(self, subject: PGPSubject, sig: PGPSignature, **prefs) -> PGPSignature: """ The actual signing magic happens here. :param subject: The subject to sign @@ -2205,7 +2209,7 @@ def _sign(self, subject, sig: PGPSignature, **prefs) -> PGPSignature: return sig @KeyAction(KeyFlags.Sign, is_unlocked=True, is_public=False) - def sign(self, subject, **prefs): + def sign(self, subject: PGPSubject, **prefs) -> PGPSignature: """ Sign text, a message, or a timestamp using this key. @@ -2239,6 +2243,9 @@ def sign(self, subject, **prefs): (only for v4 keys, defaults to True) :type include_issuer_fingerprint: ``bool`` """ + if self.key_algorithm is None: + raise PGPError("PGPKey: cannot sign with an unknown algorithm") + sig_type = SignatureType.BinaryDocument hash_algo = prefs.pop('hash', None) @@ -2256,7 +2263,7 @@ def sign(self, subject, **prefs): return self._sign(subject, sig, **prefs) @KeyAction(KeyFlags.Certify, is_unlocked=True, is_public=False) - def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): + def certify(self, subject: PGPSubject, level: SignatureType = SignatureType.Generic_Cert, **prefs) -> PGPSignature: """ certify(subject, level=SignatureType.Generic_Cert, **prefs) @@ -2322,12 +2329,17 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): :keyword exportable: Whether this certification is exportable or not. :type exportable: ``bool`` """ + if self.key_algorithm is None: + raise PGPError("PGPKey: cannot certify with an unknown algorithm") + hash_algo = prefs.pop('hash', None) sig_type = level if isinstance(subject, PGPKey): sig_type = SignatureType.DirectlyOnKey sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint, created=prefs.pop('created', None)) + if sig._signature is None: + raise PGPError(f"Failed to create usable PGPSignature using {self.key_algorithm!r} and {hash_algo!r}") # signature options that only make sense in certifications usage = prefs.pop('usage', None) @@ -2398,6 +2410,8 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): attestations = set() for attestation in attested_certifications: if isinstance(attestation, PGPSignature) and attestation.type in cert_sigtypes: + if attestation._signature is None: + raise PGPError("Attestation PGPSignature is missing packet!") h = sig.hash_algorithm.hasher h.update(attestation._signature.canonical_bytes()) attestations.add(h.finalize()) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d9fb8af6..daa428a9 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -173,7 +173,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], } for uid in uids: - primary.add_uid(pgpy.PGPUID.new(uid), **uidoptions) + primary.add_uid(pgpy.PGPUID.new(uid), selfsign=True, **uidoptions) if 'primary' in uidoptions: # only first User ID is Primary del uidoptions['primary'] From 217c84e9361fe6a67d96c9680af1f36f536683c5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 8 Jul 2023 00:40:29 -0400 Subject: [PATCH 227/287] PGPSignature: add more type signatures --- pgpy/pgp.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 1a09de78..b2ebac69 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -436,7 +436,7 @@ def __or__(self, other: Signature) -> PGPSignature: raise TypeError(f"should be Signature, got {type(other)}") - def __copy__(self): + def __copy__(self) -> PGPSignature: # because the default shallow copy isn't actually all that useful, # and deepcopy does too much work sig = super().__copy__() @@ -445,24 +445,27 @@ def __copy__(self): sig |= copy.copy(self._signature) return sig - def attests_to(self, othersig): + def attests_to(self, othersig: PGPSignature) -> bool: 'returns True if this signature attests to othersig (acknolwedges it for redistribution)' if not isinstance(othersig, PGPSignature): raise TypeError h = self.hash_algorithm.hasher + if othersig._signature is None: + raise TypeError("PGPSignature.attests_to() other sig had no underlying signature packet") h.update(othersig._signature.canonical_bytes()) return h.finalize() in self.attested_certifications - def hashdata(self, subject) -> bytes: + def hashdata(self, subject: PGPSubject) -> bytes: if self._signature is None: raise TypeError("called hashdata on uninitializaed PGPSignature") _data = bytearray() if isinstance(subject, str): try: - subject = subject.encode('utf-8') + subject_bytes = subject.encode('utf-8') except UnicodeEncodeError: - subject = subject.encode('charmap') + subject_bytes = subject.encode('charmap') + subject = subject_bytes """ All signatures are formed by producing a hash over the signature @@ -476,8 +479,10 @@ def hashdata(self, subject) -> bytes: """ if isinstance(subject, (SKEData, IntegrityProtectedSKEData)): _data += subject.__bytearray__() - else: + elif isinstance(subject, (bytes, bytearray)): _data += bytearray(subject) + else: + raise TypeError(f"Tried to make binary signature over {type(subject)}") if self.type == SignatureType.CanonicalDocument: """ @@ -485,6 +490,8 @@ def hashdata(self, subject) -> bytes: document is canonicalized by converting line endings to , and the resulting data is hashed. """ + if not isinstance(subject, (bytes, bytearray)): + raise TypeError(f'Tried to make canonical text signature over {type(subject)}') _data += re.subn(br'\r?\n', b'\r\n', subject)[0] if self.type in {SignatureType.Generic_Cert, SignatureType.Persona_Cert, SignatureType.Casual_Cert, @@ -518,7 +525,12 @@ def hashdata(self, subject) -> bytes: the subkey using the same format as the main key (also using 0x99 as the first octet). """ + if not isinstance(subject, PGPKey): + raise TypeError(f'Tried to make {self.type!r} over {type(subject)}, expected PGPKey') + if subject.is_primary: + if self.signer is None: + raise TypeError(f"Cannot make signature over primary key ({self.type!r}) without identifying the signer") _s = subject.subkeys[self.signer].hashdata else: @@ -554,6 +566,9 @@ def hashdata(self, subject) -> bytes: about the key itself, rather than the binding between a key and a name. """ + if not isinstance(subject, PGPKey): + raise TypeError(f'Tried to make {self.type!r} over {type(subject)}, expected PGPKey') + if self.type == SignatureType.SubkeyRevocation: # hash the primary key first if this is a Subkey Revocation signature _s = subject.parent.hashdata @@ -580,6 +595,8 @@ def hashdata(self, subject) -> bytes: revokes, and should have a later creation date than that certificate. """ + if not isinstance(subject, PGPUID): + raise TypeError(f'Tried to make {self.type!r} over {type(subject)}, expected PGPUID') _s = subject.hashdata if subject.is_uid: From 36a13903c783f04a3528de1b3837cf5743d23ad2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 8 Jul 2023 01:35:22 -0400 Subject: [PATCH 228/287] sopgpy: correct handling for inline-signed messages This had been broken with the introduction of PGPSignatures, but it should now work again. --- pgpy/sopgpy.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index daa428a9..33345f01 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -353,6 +353,28 @@ def encrypt(self, del sessionkey return self._maybe_armor(armor, msg) + def _convert_sig_verification(self, + cert: pgpy.PGPKey, + verif: pgpy.types.SignatureVerification, + start: Optional[datetime], + end: Optional[datetime]) -> List[sop.SOPSigResult]: + results: List[sop.SOPSigResult] = [] + goodsig: pgpy.types.SignatureVerification.SigSubj + for goodsig in verif.good_signatures: + sigtime = goodsig.signature.created + # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: + # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects + if sigtime.tzinfo is None: + sigtime = sigtime.replace(tzinfo=timezone.utc) + # PGPy before 0.6.0 included a "verified" boolean in sigsubj: + if ('issues' in goodsig._fields and goodsig.issues == 0) or \ + ('verified' in goodsig._fields and goodsig.verified): # type: ignore[attr-defined] + if start is None or sigtime >= start: + if end is None or sigtime <= end: + results += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, + primary_fpr=cert.fingerprint, moreinfo=goodsig.signature.__repr__())] + return results + def _check_sigs(self, certs: MutableMapping[str, pgpy.PGPKey], msg: Union[pgpy.PGPMessage, bytes], @@ -365,22 +387,16 @@ def _check_sigs(self, for signer, cert in certs.items(): try: verif: pgpy.types.SignatureVerification = cert.verify(msg, signature=sig) - goodsig: pgpy.types.SignatureVerification.SigSubj - for goodsig in verif.good_signatures: - sigtime = goodsig.signature.created - # some versions of pgpy return tz-naive objects, even though all timestamps are in UTC: - # see https://docs.python.org/3/library/datetime.html#aware-and-naive-objects - if sigtime.tzinfo is None: - sigtime = sigtime.replace(tzinfo=timezone.utc) - # PGPy before 0.6.0 included a "verified" boolean in sigsubj: - if ('issues' in goodsig._fields and goodsig.issues == 0) or \ - ('verified' in goodsig._fields and goodsig.verified): # type: ignore[attr-defined] - if start is None or sigtime >= start: - if end is None or sigtime <= end: - results += [sop.SOPSigResult(when=goodsig.signature.created, signing_fpr=goodsig.by.fingerprint, - primary_fpr=cert.fingerprint, moreinfo=goodsig.signature.__repr__())] + results += self._convert_sig_verification(cert, verif, start, end) except: pass + else: + for signer, cert in certs.items(): + try: + verif = cert.verify(msg) + results += self._convert_sig_verification(cert, verif, start, end) + except: + pass return results def decrypt(self, From ca2b4ac49bb6b9d59617ed10a7a2331d253f7d72 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 8 Jul 2023 02:12:52 -0400 Subject: [PATCH 229/287] clean up regex for cleartext signing framework newlines near the top of a CSF message can be handled more delicately. - if the Hash: header is missing, we want the trailing newline to appear - there's no need to have a non-capturing grouping places where the group is not used. --- pgpy/types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index 6dcb4f29..ba4d84f0 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -116,8 +116,9 @@ class Armorable(PGPObject, metaclass=abc.ABCMeta): # - whitespace is ignored except when in a character class or escaped # - anything after a '#' that is not escaped or in a character class is ignored, allowing for comments __armor_regex = re.compile(r"""# This capture group is optional because it will only be present in signed cleartext messages - (^-{5}BEGIN\ PGP\ SIGNED\ MESSAGE-{5}(?:\r?\n) - (Hash:\ (?P[A-Za-z0-9\-,]+)(?:\r?\n){2})? + (^-{5}BEGIN\ PGP\ SIGNED\ MESSAGE-{5}\r?\n + (Hash:\ (?P[A-Za-z0-9\-,]+)\r?\n)? + \r?\n (?P(.*\r?\n)*(.*(?=\r?\n-{5})))(?:\r?\n) )? # armor header line; capture the variable part of the magic text From b9b64b5b23d63e3c7989d2cd04004ba477afdcfd Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 10 Jul 2023 21:29:44 -0400 Subject: [PATCH 230/287] ImageEncoding: use Unknown the same way as PacketType and PubKeyAlgorithm I don't expect new image encoding types to be defined. Indeed, the version itself could just go away and we could probably retcon UserAttribute 1 as JPEG (see https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/162) with a weird 16-octet header. But it's better to have the code normalized here. --- pgpy/constants.py | 9 +++++++- pgpy/packet/subpackets/userattribute.py | 28 +++++++++++++------------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index b833f7a4..4a16fd5a 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -562,7 +562,8 @@ class AttributeType(IntEnum): class ImageEncoding(IntEnum): - Unknown = 0x00 + Unknown = -1 + Invalid = 0x00 JPEG = 0x01 @classmethod @@ -571,6 +572,12 @@ def encodingof(cls, imagebytes: bytes) -> ImageEncoding: return ImageEncoding.JPEG return ImageEncoding.Unknown # pragma: no cover + @classmethod + def _missing_(cls, val: object) -> ImageEncoding: + if not isinstance(val, int): + raise TypeError(f"cannot look up ImageEncoding by non-int {type(val)}") + return cls.Unknown + class SignatureType(IntEnum): """Types of signatures that can be found in a Signature packet.""" diff --git a/pgpy/packet/subpackets/userattribute.py b/pgpy/packet/subpackets/userattribute.py index 72ff8c31..90b22981 100644 --- a/pgpy/packet/subpackets/userattribute.py +++ b/pgpy/packet/subpackets/userattribute.py @@ -58,17 +58,15 @@ def version_int(self, val): self._version = val @sdproperty - def iencoding(self): + def iencoding(self) -> ImageEncoding: return self._iencoding - @iencoding.register(int) - @iencoding.register(ImageEncoding) - def iencoding_int(self, val): - try: - self._iencoding = ImageEncoding(val) + @iencoding.register + def iencoding_int(self, val: int) -> None: + self._iencoding = ImageEncoding(val) - except ValueError: # pragma: no cover - self._iencoding = val + if self._iencoding is ImageEncoding.Unknown: + self._opaque_iencoding = val @sdproperty def image(self): @@ -79,19 +77,23 @@ def image(self): def image_bin(self, val): self._image = bytearray(val) - def __init__(self): + def __init__(self) -> None: super().__init__() - self.version = 1 - self.iencoding = 1 + self.version: int = 1 + self.iencoding: ImageEncoding = ImageEncoding.JPEG self.image = bytearray() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() if self.version == 1: + if self.iencoding is ImageEncoding.Unknown: + encoding: int = self._opaque_iencoding + else: + encoding = self.iencoding # v1 image header length is always 16 bytes # and stored little-endian due to an 'historical accident' - _bytes += struct.pack(' Date: Mon, 10 Jul 2023 21:31:03 -0400 Subject: [PATCH 231/287] More type annotations --- pgpy/packet/fields.py | 8 +- pgpy/packet/subpackets/signature.py | 118 ++++++++++++------------ pgpy/packet/subpackets/userattribute.py | 17 ++-- pgpy/packet/types.py | 2 +- 4 files changed, 73 insertions(+), 72 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index e57e0ac1..8af6ed2a 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -357,7 +357,7 @@ class PubKey(MPIs): def __mpis__(self): yield from self.__pubfields__ - def __init__(self): + def __init__(self) -> None: super().__init__() for field in self.__pubfields__: if isinstance(field, tuple): # pragma: no cover @@ -370,17 +370,17 @@ def __init__(self): def __pubkey__(self): """return the requisite *PublicKey class from the cryptography library""" - def __len__(self): + def __len__(self) -> int: return sum(len(getattr(self, i)) for i in self.__pubfields__) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() for field in self.__pubfields__: _bytes += getattr(self, field).to_mpibytes() return _bytes - def publen(self): + def publen(self) -> int: return len(self) def verify(self, subj, sigbytes, hash_alg): diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 7c50185d..a93339bd 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -72,27 +72,27 @@ class URI(Signature): @sdproperty - def uri(self): + def uri(self) -> str: return self._uri - @uri.register(str) - def uri_str(self, val): + @uri.register + def uri_str(self, val: str) -> None: self._uri = val - @uri.register(bytearray) - def uri_bytearray(self, val): + @uri.register + def uri_bytearray(self, val: bytearray) -> None: self.uri = val.decode('latin-1') - def __init__(self): + def __init__(self) -> None: super().__init__() self.uri = "" - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.uri.encode() return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.uri = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] @@ -180,30 +180,30 @@ def parse(self, packet): class Boolean(Signature): @sdproperty - def bflag(self): + def bflag(self) -> bool: return self._bool - @bflag.register(bool) - def bflag_bool(self, val): + @bflag.register + def bflag_bool(self, val: bool) -> None: self._bool = val - @bflag.register(bytearray) - def bflag_bytearray(self, val): + @bflag.register + def bflag_bytearray(self, val: bytearray) -> None: self.bool = bool(self.bytes_to_int(val)) - def __init__(self): + def __init__(self) -> None: super().__init__() self.bflag = False - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() - _bytes += self.int_to_bytes(int(self.bflag)) + _bytes.append(int(self.bflag)) return _bytes - def __bool__(self): + def __bool__(self) -> bool: return self.bflag - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.bflag = packet[:1] del packet[:1] @@ -258,33 +258,33 @@ class CreationTime(Signature): __typeid__ = SigSubpacketType.CreationTime @sdproperty - def created(self): + def created(self) -> datetime: return self._created - @created.register(datetime) - def created_datetime(self, val): + @created.register + def created_datetime(self, val: datetime) -> None: if val.tzinfo is None: warnings.warn("Passing TZ-naive datetime object to CreationTime subpacket") self._created = val - @created.register(int) - def created_int(self, val): + @created.register + def created_int(self, val: int) -> None: self.created = datetime.fromtimestamp(val, timezone.utc) - @created.register(bytearray) - def created_bytearray(self, val): + @created.register + def created_bytearray(self, val: bytearray) -> None: self.created = self.bytes_to_int(val) - def __init__(self): + def __init__(self) -> None: super().__init__() self.created = datetime.now(timezone.utc) - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.int_to_bytes(calendar.timegm(self.created.utctimetuple()), 4) return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.created = packet[:4] del packet[:4] @@ -303,31 +303,31 @@ class SignatureExpirationTime(Signature): __typeid__ = SigSubpacketType.SigExpirationTime @sdproperty - def expires(self): + def expires(self) -> timedelta: return self._expires - @expires.register(timedelta) - def expires_timedelta(self, val): + @expires.register + def expires_timedelta(self, val: timedelta) -> None: self._expires = val - @expires.register(int) - def expires_int(self, val): + @expires.register + def expires_int(self, val: int) -> None: self.expires = timedelta(seconds=val) - @expires.register(bytearray) - def expires_bytearray(self, val): + @expires.register + def expires_bytearray(self, val: bytearray) -> None: self.expires = self.bytes_to_int(val) - def __init__(self): + def __init__(self) -> None: super().__init__() self.expires = 0 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.int_to_bytes(int(self.expires.total_seconds()), 4) return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.expires = packet[:4] del packet[:4] @@ -388,42 +388,42 @@ class TrustSignature(Signature): __typeid__ = SigSubpacketType.TrustSignature @sdproperty - def level(self): + def level(self) -> int: return self._level - @level.register(int) - def level_int(self, val): + @level.register + def level_int(self, val: int) -> None: self._level = val - @level.register(bytearray) - def level_bytearray(self, val): + @level.register + def level_bytearray(self, val: bytearray) -> None: self.level = self.bytes_to_int(val) @sdproperty - def amount(self): + def amount(self) -> int: return self._amount - @amount.register(int) - def amount_int(self, val): + @amount.register + def amount_int(self, val: int) -> None: # clamp 'val' to the range 0-255 self._amount = max(0, min(val, 255)) - @amount.register(bytearray) - def amount_bytearray(self, val): + @amount.register + def amount_bytearray(self, val: bytearray) -> None: self.amount = self.bytes_to_int(val) - def __init__(self): + def __init__(self) -> None: super().__init__() self.level = 0 self.amount = 0 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.int_to_bytes(self.level) _bytes += self.int_to_bytes(self.amount) return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.level = packet[:1] del packet[:1] @@ -448,27 +448,27 @@ class RegularExpression(Signature): __typeid__ = SigSubpacketType.RegularExpression @sdproperty - def regex(self): + def regex(self) -> str: return self._regex - @regex.register(str) - def regex_str(self, val): + @regex.register + def regex_str(self, val: str) -> None: self._regex = val - @regex.register(bytearray) - def regex_bytearray(self, val): + @regex.register + def regex_bytearray(self, val: bytearray) -> None: self.regex = val.decode('latin-1') - def __init__(self): + def __init__(self) -> None: super().__init__() self.regex = r'' - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = super().__bytearray__() _bytes += self.regex.encode() return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) self.regex = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] diff --git a/pgpy/packet/subpackets/userattribute.py b/pgpy/packet/subpackets/userattribute.py index 90b22981..5620d0e6 100644 --- a/pgpy/packet/subpackets/userattribute.py +++ b/pgpy/packet/subpackets/userattribute.py @@ -2,6 +2,8 @@ """ import struct +from typing import Union + from .types import UserAttribute from ...constants import AttributeType @@ -50,11 +52,11 @@ class Image(UserAttribute): __typeid__ = AttributeType.Image @sdproperty - def version(self): + def version(self) -> int: return self._version - @version.register(int) - def version_int(self, val): + @version.register + def version_int(self, val: int) -> None: self._version = val @sdproperty @@ -69,12 +71,11 @@ def iencoding_int(self, val: int) -> None: self._opaque_iencoding = val @sdproperty - def image(self): + def image(self) -> bytearray: return self._image - @image.register(bytes) - @image.register(bytearray) - def image_bin(self, val): + @image.register + def image_bin(self, val: Union[bytes, bytearray]) -> None: self._image = bytearray(val) def __init__(self) -> None: @@ -98,7 +99,7 @@ def __bytearray__(self) -> bytearray: _bytes += self.image return _bytes - def parse(self, packet): + def parse(self, packet: bytearray) -> None: super().parse(packet) with memoryview(packet) as _head: diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index afa8d288..16fbcc89 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -255,7 +255,7 @@ class Sub(Key): class MPI(int): - def __new__(cls, num): + def __new__(cls, num: Union[bytes, bytearray, int]) -> MPI: mpi = num if isinstance(num, (bytes, bytearray)): From 98b62a7ad470be11d7e0a198c45ff14c6df67dac Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 14:14:39 -0400 Subject: [PATCH 232/287] PGPKey: Improve type signatures for unprotect, related keyblob decryption, etc --- pgpy/packet/fields.py | 44 +++++++++++++++++++++++++++--------------- pgpy/packet/packets.py | 2 +- pgpy/pgp.py | 32 +++++++++++++++++------------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 8af6ed2a..711ab4c9 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1372,10 +1372,13 @@ def encrypt_keyblob(self, passphrase: str, self.clear() @abc.abstractmethod - def decrypt_keyblob(self, passphrase): + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + ... + + def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes]) -> Optional[bytearray]: if not self.s2k: # pragma: no cover # not encrypted - return + return None # Encryption/decryption of the secret data is done in CFB mode using # the key created from the passphrase and the Initial Vector from the @@ -1408,7 +1411,7 @@ def decrypt_keyblob(self, passphrase): def sign(self, sigdata, hash_alg): return NotImplemented # pragma: no cover - def clear(self): + def clear(self) -> None: """delete and re-initialize all private components to zero""" for field in self.__privfields__: delattr(self, field) @@ -1420,11 +1423,10 @@ def __privkey__(self): return NotImplemented def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: - # return NotImplemented raise NotImplementedError() - def decrypt_keyblob(self, passphrase): - return NotImplemented + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + raise NotImplementedError() class RSAPriv(PrivKey, RSAPub): @@ -1490,9 +1492,11 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = self._decrypt_keyblob_helper(passphrase) del passphrase + if kb is None: + return self.d = MPI(kb) self.p = MPI(kb) @@ -1558,9 +1562,11 @@ def parse(self, packet: bytearray) -> None: self.chksum = packet[:2] del packet[:2] - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = self._decrypt_keyblob_helper(passphrase) del passphrase + if kb is None: + return self.x = MPI(kb) @@ -1599,9 +1605,11 @@ def parse(self, packet: bytearray) -> None: self.chksum = packet[:2] del packet[:2] - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = self._decrypt_keyblob_helper(passphrase) del passphrase + if kb is None: + return self.x = MPI(kb) @@ -1656,9 +1664,11 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = self._decrypt_keyblob_helper(passphrase) del passphrase + if kb is None: + return self.s = MPI(kb) def sign(self, sigdata, hash_alg): @@ -1716,9 +1726,11 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = self._decrypt_keyblob_helper(passphrase) del passphrase + if kb is None: + return self.s = MPI(kb) def sign(self, sigdata, hash_alg): diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 102887d8..db321656 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1027,7 +1027,7 @@ def protect(self, passphrase: str, del passphrase self.update_hlen() - def unprotect(self, passphrase: str) -> None: + def unprotect(self, passphrase: Union[str, bytes]) -> None: if not isinstance(self.keymaterial, PrivKeyField): raise TypeError("Key material is not a private key, cannot unprotect") self.keymaterial.decrypt_keyblob(passphrase) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b2ebac69..3271997f 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -19,7 +19,7 @@ from datetime import datetime, timezone -from typing import Any, ByteString, Deque, Literal, List, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union +from typing import Any, ByteString, Generator, Literal, List, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes @@ -76,6 +76,7 @@ from .packet.packets import SKESessionKeyV4 from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub +from .packet.fields import PrivKey as field_PrivKey from .packet.types import Opaque from .packet.types import VersionedHeader @@ -1806,13 +1807,15 @@ def __init__(self) -> None: self._key: Optional[PubKey] = None self._children = FingerprintDict["PGPKey"]() self._signatures = SorteDeque() - self._uids: Deque[PGPUID] = SorteDeque() + self._uids: collections.deque[PGPUID] = SorteDeque() self._sibling = None self._self_verified: Optional[SecurityIssues] = None self._require_usage_flags = True - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() + if self._key is None: + raise ValueError("PGPKey: tried to get bytearray but this is uninitializes") # us _bytes += self._key.__bytearray__() # our signatures; ignore embedded signatures @@ -1829,7 +1832,7 @@ def __bytearray__(self): return _bytes - def __repr__(self): + def __repr__(self) -> str: if self._key is not None: return "" \ "".format(self._key.__class__.__name__, self.fingerprint.keyid, id(self)) @@ -1837,7 +1840,7 @@ def __repr__(self): return "" \ "".format(id(self)) - def __contains__(self, item): + def __contains__(self, item) -> bool: if isinstance(item, PGPKey): # pragma: no cover return item.fingerprint in self._children @@ -1852,8 +1855,8 @@ def __contains__(self, item): raise TypeError - def __or__(self, other, from_sib=False): - if isinstance(other, Key) and self._key is None: + def __or__(self, other: Any, from_sib: bool = False) -> PGPKey: + if isinstance(other, PubKey) and self._key is None: self._key = other elif isinstance(other, PGPKey) and not other.is_primary and other.is_public == self.is_public: @@ -1864,7 +1867,7 @@ def __or__(self, other, from_sib=False): self._signatures.insort(other) # if this is a subkey binding signature that has embedded primary key binding signatures, add them to parent - if other.type == SignatureType.Subkey_Binding: + if other.type is SignatureType.Subkey_Binding and other._signature is not None: for es in iter(pkb for pkb in other._signature.subpackets['EmbeddedSignature']): esig = PGPSignature() | es._sig esig._parent = other @@ -1872,7 +1875,8 @@ def __or__(self, other, from_sib=False): elif isinstance(other, PGPUID): other._parent = weakref.ref(self) - self._uids.insort(other) + if isinstance(self._uids, SorteDeque): # should not be necessary, but satisfies the type checker. + self._uids.insort(other) else: raise TypeError( @@ -1890,7 +1894,7 @@ def __or__(self, other, from_sib=False): return self - def __copy__(self): + def __copy__(self) -> PGPKey: key = super().__copy__() key._key = copy.copy(self._key) @@ -1972,7 +1976,7 @@ def protect(self, passphrase: str, del passphrase @contextlib.contextmanager - def unlock(self, passphrase): + def unlock(self, passphrase: Union[str, bytes]) -> Generator[PGPKey, None, None]: """ Context manager method for unlocking passphrase-protected private keys. Has no effect if the key is not both private and passphrase-protected. @@ -2017,14 +2021,16 @@ def unlock(self, passphrase): try: for sk in itertools.chain([self], self.subkeys.values()): - sk._key.unprotect(passphrase) + if isinstance(sk._key, PrivKey): + sk._key.unprotect(passphrase) del passphrase yield self finally: # clean up here by deleting the previously decrypted secret key material for sk in itertools.chain([self], self.subkeys.values()): - sk._key.keymaterial.clear() + if isinstance(sk._key, PrivKey) and isinstance(sk._key.keymaterial, field_PrivKey): + sk._key.keymaterial.clear() def add_uid(self, uid: PGPUID, selfsign: bool = True, **prefs) -> None: """ From e467d8e7e77cb390128c356a4bfd4e627aadcf95 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 16 Nov 2022 17:42:54 -0500 Subject: [PATCH 233/287] ASCII armoring should not require the CRC line See the rationale from the upcoming standard: https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-optional-checksum --- pgpy/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index ba4d84f0..944760d1 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -129,8 +129,8 @@ class Armorable(PGPObject, metaclass=abc.ABCMeta): # capture all lines of the body, up to 76 characters long, # including the newline, and the pad character(s) (?P([A-Za-z0-9+/]{1,76}={,2}(?:\r?\n))+) - # capture the armored CRC24 value - ^=(?P[A-Za-z0-9+/]{4})(?:\r?\n) + # optionally capture the armored CRC24 value + (?:^=(?P[A-Za-z0-9+/]{4})(?:\r?\n))? # finally, capture the armor tail line, which must match the armor header line ^-{5}END\ PGP\ (?P=magic)-{5}(?:\r?\n)? """, flags=re.MULTILINE | re.VERBOSE) From cc34a73e204045773bbcc2882d55e39562004215 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 10 Jul 2023 21:48:17 -0400 Subject: [PATCH 234/287] add test vectors from draft-ietf-openpgp-crypto-refresh-10 --- tests/test_12_crypto_refresh.py | 72 +++++++++++++++++++ .../cleartext-signed-message.txt | 15 ++++ .../crypto-refresh/inline-signed-message.pgp | 10 +++ .../v4-ed25519-pubkey-packet.key | 5 ++ .../v4-ed25519-signature-over-OpenPGP.sig | 5 ++ .../crypto-refresh/v4skesk-argon2-aes128.pgp | 8 +++ .../crypto-refresh/v4skesk-argon2-aes192.pgp | 9 +++ .../crypto-refresh/v4skesk-argon2-aes256.pgp | 9 +++ .../crypto-refresh/v6-minimal-cert.key | 12 ++++ .../v6-minimal-secret-locked.key | 16 +++++ .../crypto-refresh/v6-minimal-secret.key | 14 ++++ .../crypto-refresh/v6pkesk-aes128-ocb.pgp | 8 +++ .../crypto-refresh/v6skesk-aes128-eax.pgp | 7 ++ .../crypto-refresh/v6skesk-aes128-gcm.pgp | 7 ++ .../crypto-refresh/v6skesk-aes128-ocb.pgp | 7 ++ 15 files changed, 204 insertions(+) create mode 100644 tests/test_12_crypto_refresh.py create mode 100644 tests/testdata/crypto-refresh/cleartext-signed-message.txt create mode 100644 tests/testdata/crypto-refresh/inline-signed-message.pgp create mode 100644 tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key create mode 100644 tests/testdata/crypto-refresh/v4-ed25519-signature-over-OpenPGP.sig create mode 100644 tests/testdata/crypto-refresh/v4skesk-argon2-aes128.pgp create mode 100644 tests/testdata/crypto-refresh/v4skesk-argon2-aes192.pgp create mode 100644 tests/testdata/crypto-refresh/v4skesk-argon2-aes256.pgp create mode 100644 tests/testdata/crypto-refresh/v6-minimal-cert.key create mode 100644 tests/testdata/crypto-refresh/v6-minimal-secret-locked.key create mode 100644 tests/testdata/crypto-refresh/v6-minimal-secret.key create mode 100644 tests/testdata/crypto-refresh/v6pkesk-aes128-ocb.pgp create mode 100644 tests/testdata/crypto-refresh/v6skesk-aes128-eax.pgp create mode 100644 tests/testdata/crypto-refresh/v6skesk-aes128-gcm.pgp create mode 100644 tests/testdata/crypto-refresh/v6skesk-aes128-ocb.pgp diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py new file mode 100644 index 00000000..cbdbf0a4 --- /dev/null +++ b/tests/test_12_crypto_refresh.py @@ -0,0 +1,72 @@ +# coding=utf-8 +""" Verify samples from draft-ietf-openpgp-crypto-refresh-10 +""" + +from typing import Dict, Optional, Tuple + +import pytest + +from warnings import warn + +from pgpy import PGPKey, PGPSignature, PGPMessage +from pgpy.errors import PGPDecryptionError + +class TestPGP_CryptoRefresh(object): + def test_v4_sigs(self) -> None: + (k, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key') + s = PGPSignature.from_file('tests/testdata/crypto-refresh/v4-ed25519-signature-over-OpenPGP.sig') + assert k.verify('OpenPGP', s) + assert not k.verify('Banana', s) + + @pytest.mark.parametrize('cipher', {'aes128', 'aes192', 'aes256'}) + def test_v4_skesk_argon2(self, cipher: str) -> None: + msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v4skesk-argon2-{cipher}.pgp') + assert msg.is_encrypted + pytest.xfail('Argon2 is not supported') + unlocked = msg.decrypt('password') + assert not unlocked.is_encrypted + assert unlocked.message == b'Hello, world!' + + @pytest.mark.parametrize('aead', {'ocb', 'eax', 'gcm'}) + def test_v6_skesk(self, aead: str) -> None: + msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v6skesk-aes128-{aead}.pgp') + pytest.xfail('v2 SEIPD is not supported') + assert msg.is_encrypted + unlocked = msg.decrypt('password') + assert not unlocked.is_encrypted + assert unlocked.message == b'Hello, world!' + + @pytest.mark.parametrize('msg', {'inline-signed-message.pgp', 'cleartext-signed-message.txt'}) + def test_v6_signed_messages(self, msg: str) -> None: + (cert, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-cert.key') + pytest.xfail('v6 keys are not supported') + assert cert.is_public + pgpmsg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/{msg}') + assert not pgpmsg.is_encrypted + assert pgpmsg.message == 'What we need from the grocery store:\n\n- tofu\n- vegetables\n- noodles\n' + assert cert.verify(pgpmsg) + + def test_v6_key(self): + (cert, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-cert.key') + pytest.xfail('v6 keys are not supported') + assert cert.is_public + (key, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-secret.key') + assert not key.is_public + assert not key.is_protected + assert key.is_unlocked + msg = PGPMessage.from_file('tests/testdata/crypto-refresh/v6pkesk-aes128-ocb.pgp') + assert msg.is_encrypted + clearmsg = key.decrypt(msg) + assert not clearmsg.is_encrypted + assert clearmsg.message == b'Hello, world!' + + # verify decryption with unprotected key + (locked, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-secret-locked.key') + assert not locked.is_public + assert locked.is_protected + assert not locked.is_unlocked + with locked.unlock("correct horse battery staple"): + assert locked.is_unlocked + clearmsg2 = locked.decrypt(msg) + assert not clearmsg2.is_encrypted + assert clearmsg2.message == b'Hello, world!' diff --git a/tests/testdata/crypto-refresh/cleartext-signed-message.txt b/tests/testdata/crypto-refresh/cleartext-signed-message.txt new file mode 100644 index 00000000..958bc382 --- /dev/null +++ b/tests/testdata/crypto-refresh/cleartext-signed-message.txt @@ -0,0 +1,15 @@ +-----BEGIN PGP SIGNED MESSAGE----- + +What we need from the grocery store: + +- - tofu +- - vegetables +- - noodles + +-----BEGIN PGP SIGNATURE----- + +wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo +/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr +NK2ay45cX1IVAQ== +-----END PGP SIGNATURE----- diff --git a/tests/testdata/crypto-refresh/inline-signed-message.pgp b/tests/testdata/crypto-refresh/inline-signed-message.pgp new file mode 100644 index 00000000..627a98a7 --- /dev/null +++ b/tests/testdata/crypto-refresh/inline-signed-message.pgp @@ -0,0 +1,10 @@ +-----BEGIN PGP MESSAGE----- + +xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk +1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv +bSB0aGUgZ3JvY2VyeSBzdG9yZToKCi0gdG9mdQotIHZlZ2V0YWJsZXMKLSBub29k +bGVzCsKYBgEbCgAAACkFgmOYo2MiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9l +JewnutmsyQAAAABpNiB2SV9QIYiQ9/Xi7jwYIlFPcFAPVR2G5ckh5ATjSlP7rCfQ +b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj +FtCgazStmsuOXF9SFQE= +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key b/tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key new file mode 100644 index 00000000..1c270a07 --- /dev/null +++ b/tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key @@ -0,0 +1,5 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEU/NfCxYJKwYBBAHaRw8BAQdAPwmJlL3ZFu1AUxl5NOSofIBzOhKA1i+AEJku +Q+47JAY= +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/crypto-refresh/v4-ed25519-signature-over-OpenPGP.sig b/tests/testdata/crypto-refresh/v4-ed25519-signature-over-OpenPGP.sig new file mode 100644 index 00000000..4f90407f --- /dev/null +++ b/tests/testdata/crypto-refresh/v4-ed25519-signature-over-OpenPGP.sig @@ -0,0 +1,5 @@ +-----BEGIN PGP SIGNATURE----- + +iF4EABYIAAYFAlX5X5UACgkQjP3hIZeWWpr2IgD/VvkMypjiECY3vZg/2xbBMd/S +ftgr9N3lYG4NdWrtM2YBANCcT6EVJ/A44PV/IgHYLy6iyQMyZfps60iehUuuYbQE +-----END PGP SIGNATURE----- diff --git a/tests/testdata/crypto-refresh/v4skesk-argon2-aes128.pgp b/tests/testdata/crypto-refresh/v4skesk-argon2-aes128.pgp new file mode 100644 index 00000000..45d53639 --- /dev/null +++ b/tests/testdata/crypto-refresh/v4skesk-argon2-aes128.pgp @@ -0,0 +1,8 @@ +-----BEGIN PGP MESSAGE----- +Comment: Encrypted using AES with 128-bit key +Comment: Session key: 01FE16BBACFD1E7B78EF3B865187374F + +wycEBwScUvg8J/leUNU1RA7N/zE2AQQVnlL8rSLPP5VlQsunlO+ECxHSPgGYGKY+ +YJz4u6F+DDlDBOr5NRQXt/KJIf4m4mOlKyC/uqLbpnLJZMnTq3o79GxBTdIdOzhH +XfA3pqV4mTzF +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v4skesk-argon2-aes192.pgp b/tests/testdata/crypto-refresh/v4skesk-argon2-aes192.pgp new file mode 100644 index 00000000..3e5ef8b0 --- /dev/null +++ b/tests/testdata/crypto-refresh/v4skesk-argon2-aes192.pgp @@ -0,0 +1,9 @@ +-----BEGIN PGP MESSAGE----- +Comment: Encrypted using AES with 192-bit key +Comment: Session key: 27006DAE68E509022CE45A14E569E91001C2955... +Comment: Session key: ...AF8DFE194 + +wy8ECAThTKxHFTRZGKli3KNH4UP4AQQVhzLJ2va3FG8/pmpIPd/H/mdoVS5VBLLw +F9I+AdJ1Sw56PRYiKZjCvHg+2bnq02s33AJJoyBexBI4QKATFRkyez2gldJldRys +LVg77Mwwfgl2n/d572WciAM= +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v4skesk-argon2-aes256.pgp b/tests/testdata/crypto-refresh/v4skesk-argon2-aes256.pgp new file mode 100644 index 00000000..f5247915 --- /dev/null +++ b/tests/testdata/crypto-refresh/v4skesk-argon2-aes256.pgp @@ -0,0 +1,9 @@ +-----BEGIN PGP MESSAGE----- +Comment: Encrypted using AES with 256-bit key +Comment: Session key: BBEDA55B9AAE63DAC45D4F49D89DACF4AF37FEF... +Comment: Session key: ...C13BAB2F1F8E18FB74580D8B0 + +wzcECQS4eJUgIG/3mcaILEJFpmJ8AQQVnZ9l7KtagdClm9UaQ/Z6M/5roklSGpGu +623YmaXezGj80j4B+Ku1sgTdJo87X1Wrup7l0wJypZls21Uwd67m9koF60eefH/K +95D1usliXOEm8ayQJQmZrjf6K6v9PWwqMQ== +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v6-minimal-cert.key b/tests/testdata/crypto-refresh/v6-minimal-cert.key new file mode 100644 index 00000000..7cdbdda5 --- /dev/null +++ b/tests/testdata/crypto-refresh/v6-minimal-cert.key @@ -0,0 +1,12 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf +GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy +KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw +gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE +QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn ++eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh +BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8 +j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805 +I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/crypto-refresh/v6-minimal-secret-locked.key b/tests/testdata/crypto-refresh/v6-minimal-secret-locked.key new file mode 100644 index 00000000..a29cc2ce --- /dev/null +++ b/tests/testdata/crypto-refresh/v6-minimal-secret-locked.key @@ -0,0 +1,16 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC +FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS +3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC +Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW +cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin +7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/ +0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0 +gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf +9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR +v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr +DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki +Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt +ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/testdata/crypto-refresh/v6-minimal-secret.key b/tests/testdata/crypto-refresh/v6-minimal-secret.key new file mode 100644 index 00000000..976b460c --- /dev/null +++ b/tests/testdata/crypto-refresh/v6-minimal-secret.key @@ -0,0 +1,14 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB +exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ +BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh +RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe +7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/ +LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG +GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE +M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr +k0mXubZvyl4GBg== +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/testdata/crypto-refresh/v6pkesk-aes128-ocb.pgp b/tests/testdata/crypto-refresh/v6pkesk-aes128-ocb.pgp new file mode 100644 index 00000000..51e43e80 --- /dev/null +++ b/tests/testdata/crypto-refresh/v6pkesk-aes128-ocb.pgp @@ -0,0 +1,8 @@ +-----BEGIN PGP MESSAGE----- + +wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO +WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS +aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l +yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo +bhF30A+IitsxxA== +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v6skesk-aes128-eax.pgp b/tests/testdata/crypto-refresh/v6skesk-aes128-eax.pgp new file mode 100644 index 00000000..fdb5b18e --- /dev/null +++ b/tests/testdata/crypto-refresh/v6skesk-aes128-eax.pgp @@ -0,0 +1,7 @@ +-----BEGIN PGP MESSAGE----- + +w0AGHgcBCwMIpa5XnR/F2Cv/aSJPkZmTs1Bvo7WaanPP+MXvxfQcV/tU4cImgV14 +KPX5LEVOtl6+AKtZhsaObnxV0mkCBwEGn/kOOzIZZPOkKRPI3MZhkyUBUifvt+rq +pJ8EwuZ0F11KPSJu1q/LnKmsEiwUcOEcY9TAqyQcapOK1Iv5mlqZuQu6gyXeYQR1 +QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o= +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v6skesk-aes128-gcm.pgp b/tests/testdata/crypto-refresh/v6skesk-aes128-gcm.pgp new file mode 100644 index 00000000..be593ad7 --- /dev/null +++ b/tests/testdata/crypto-refresh/v6skesk-aes128-gcm.pgp @@ -0,0 +1,7 @@ +-----BEGIN PGP MESSAGE----- + +wzwGGgcDCwMI6dOXhbIHAAj/tC58SD70iERXyzcmubPbn/d25fTZpAlS4kRymIUa +v/91Jt8t1VRBdXmneZ/SaQIHAwb8uUSQvLmLvcnRBsYJAmaUD3LontwhtVlrFXax +Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS ++pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q== +-----END PGP MESSAGE----- diff --git a/tests/testdata/crypto-refresh/v6skesk-aes128-ocb.pgp b/tests/testdata/crypto-refresh/v6skesk-aes128-ocb.pgp new file mode 100644 index 00000000..f11e5cf8 --- /dev/null +++ b/tests/testdata/crypto-refresh/v6skesk-aes128-ocb.pgp @@ -0,0 +1,7 @@ +-----BEGIN PGP MESSAGE----- + +wz8GHQcCCwMIVqKY0vXjZFP/z8xcEWZO2520JZDX3EawckG2EsOBLP/76gDyNHsl +ZBEj+IeuYNT9YU4IN9gZ02zSaQIHAgYgpmH3MfyaMDK1YjMmAn46XY21dI6+/wsM +WRDQns3WQf+f04VidYA1vEl1TOG/P/+n2tCjuBBPUTPPQqQQCoPu9MobSAGohGv0 +K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw== +-----END PGP MESSAGE----- From 2d53b9a70e39e92c370e90539f2b8f51c83f2602 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 19:07:37 -0400 Subject: [PATCH 235/287] sopgpy: use default algorithms when protecting secret key material --- pgpy/sopgpy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 33345f01..18529076 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -193,9 +193,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], raise sop.SOPPasswordNotHumanReadable(f'Key password was not UTF-8') keypassword = pstring.strip().encode(encoding='utf-8') - primary.protect(keypassword.decode('utf-8'), - pgpy.constants.SymmetricKeyAlgorithm.AES256, - pgpy.constants.HashAlgorithm.SHA512) + primary.protect(keypassword.decode('utf-8')) return self._maybe_armor(armor, primary) def extract_cert(self, From ee92d8d2abdb58bb4c93f56b1895a7469aa800aa Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 19:16:23 -0400 Subject: [PATCH 236/287] PGPKey.new() no longer requires a key_size option. The implementation will select a reasonable option for the indicated pubkey algorithm if you don't supply it. --- pgpy/pgp.py | 4 +++- pgpy/sopgpy.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 3271997f..1d9d9a40 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1764,7 +1764,9 @@ def revocation_keys(self): yield sig.revocation_key @classmethod - def new(cls, key_algorithm: PubKeyAlgorithm, key_size: Union[int, EllipticCurveOID], created: Optional[datetime] = None) -> PGPKey: + def new(cls, key_algorithm: PubKeyAlgorithm, + key_size: Optional[Union[int, EllipticCurveOID]] = None, + created: Optional[datetime] = None) -> PGPKey: """ Generate a new PGP key diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 18529076..ebbc18c6 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -150,10 +150,9 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) if profile is not None and profile.name == 'rfc4880': - primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 3072) + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) else: - primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA, - pgpy.constants.EllipticCurveOID.Ed25519) + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA) primaryflags: Set[int] = set() primaryflags.add(pgpy.constants.KeyFlags.Certify) primaryflags.add(pgpy.constants.KeyFlags.Sign) @@ -178,10 +177,9 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], del uidoptions['primary'] if profile is not None and profile.name == 'rfc4880': - subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 3072) + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) else: - subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH, - pgpy.constants.EllipticCurveOID.Curve25519) + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH) subflags: Set[int] = set() subflags.add(pgpy.constants.KeyFlags.EncryptCommunications) subflags.add(pgpy.constants.KeyFlags.EncryptStorage) From f799d4b1ed7e5cc3cc5bc89340b7e533269f770f Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 23:13:54 -0400 Subject: [PATCH 237/287] sopgpy: add a Direct Key Signature to the default key creation This could be done only during the --profile draft-ietf-openpgp-crypto-refresh-10, but there's nothing in the earlier specifications that would indicate a problem or incompatibility to include such a signature. --- pgpy/sopgpy.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index ebbc18c6..c039ebb1 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -157,13 +157,17 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], primaryflags.add(pgpy.constants.KeyFlags.Certify) primaryflags.add(pgpy.constants.KeyFlags.Sign) first: bool = True - uidoptions = { + + hashes: List[pgpy.constants.HashAlgorithm] = [] + + hashes += [pgpy.constants.HashAlgorithm.SHA512, + pgpy.constants.HashAlgorithm.SHA384, + pgpy.constants.HashAlgorithm.SHA256, + pgpy.constants.HashAlgorithm.SHA224] + + prefs = { 'usage': primaryflags, - 'primary': True, - 'hashes': [pgpy.constants.HashAlgorithm.SHA512, - pgpy.constants.HashAlgorithm.SHA384, - pgpy.constants.HashAlgorithm.SHA256, - pgpy.constants.HashAlgorithm.SHA224], + 'hashes': hashes, 'ciphers': [pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.SymmetricKeyAlgorithm.AES192, pgpy.constants.SymmetricKeyAlgorithm.AES128], @@ -171,10 +175,15 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], 'keyserver_flags': [pgpy.constants.KeyServerPreferences.NoModify] } + # make a direct key signature with prefs: + direct_key_sig = primary.certify(primary, **prefs) + primary |= direct_key_sig + + prefs['primary'] = True for uid in uids: - primary.add_uid(pgpy.PGPUID.new(uid), selfsign=True, **uidoptions) - if 'primary' in uidoptions: # only first User ID is Primary - del uidoptions['primary'] + primary.add_uid(pgpy.PGPUID.new(uid), selfsign=True, **prefs) + if 'primary' in prefs: # only first User ID is Primary + del prefs['primary'] if profile is not None and profile.name == 'rfc4880': subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) From 6fb08f2e076e620bf95673fda7db9da91949f7aa Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 00:23:26 -0400 Subject: [PATCH 238/287] sopgpy: generate-key: improve type signatures --- pgpy/sopgpy.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index c039ebb1..12db1c88 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -153,26 +153,33 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) else: primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA) - primaryflags: Set[int] = set() - primaryflags.add(pgpy.constants.KeyFlags.Certify) - primaryflags.add(pgpy.constants.KeyFlags.Sign) + primaryflags = pgpy.constants.KeyFlags.Certify | pgpy.constants.KeyFlags.Sign first: bool = True + features = pgpy.constants.Features.ModificationDetection hashes: List[pgpy.constants.HashAlgorithm] = [] hashes += [pgpy.constants.HashAlgorithm.SHA512, - pgpy.constants.HashAlgorithm.SHA384, - pgpy.constants.HashAlgorithm.SHA256, - pgpy.constants.HashAlgorithm.SHA224] - - prefs = { - 'usage': primaryflags, - 'hashes': hashes, - 'ciphers': [pgpy.constants.SymmetricKeyAlgorithm.AES256, - pgpy.constants.SymmetricKeyAlgorithm.AES192, - pgpy.constants.SymmetricKeyAlgorithm.AES128], - 'compression': [pgpy.constants.CompressionAlgorithm.Uncompressed], - 'keyserver_flags': [pgpy.constants.KeyServerPreferences.NoModify] + pgpy.constants.HashAlgorithm.SHA384, + pgpy.constants.HashAlgorithm.SHA256, + pgpy.constants.HashAlgorithm.SHA224] + + prefs: Dict[str, Union[List[pgpy.constants.SymmetricKeyAlgorithm], + List[pgpy.constants.HashAlgorithm], + List[pgpy.constants.CompressionAlgorithm], + bool, + pgpy.constants.KeyFlags, + pgpy.constants.Features, + pgpy.constants.KeyServerPreferences, + ]] = { + 'usage': primaryflags, + 'hashes': hashes, + 'ciphers': [pgpy.constants.SymmetricKeyAlgorithm.AES256, + pgpy.constants.SymmetricKeyAlgorithm.AES192, + pgpy.constants.SymmetricKeyAlgorithm.AES128], + 'compression': [pgpy.constants.CompressionAlgorithm.Uncompressed], + 'keyserver_flags': pgpy.constants.KeyServerPreferences.NoModify, + 'features': features, } # make a direct key signature with prefs: From 11d641c35465f50003b0eb65ced580fa47059ac3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 00:24:39 -0400 Subject: [PATCH 239/287] Armoring: prepare to not emit CRC in some cases. --- pgpy/types.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pgpy/types.py b/pgpy/types.py index 944760d1..7254c592 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -109,7 +109,7 @@ class Armorable(PGPObject, metaclass=abc.ABCMeta): __armor_fmt = '-----BEGIN PGP {block_type}-----\n' \ '{headers}\n' \ '{packet}\n' \ - '={crc}\n' \ + '{crc}' \ '-----END PGP {block_type}-----\n' # the re.VERBOSE flag allows for: @@ -143,6 +143,10 @@ def charset(self) -> str: def charset(self, encoding: str) -> None: self.ascii_headers['Charset'] = codecs.lookup(encoding).name + @property + def emit_crc(self) -> bool: + return True + @staticmethod def is_utf8(text: Union[str, bytes, bytearray]) -> bool: if isinstance(text, str): @@ -289,11 +293,15 @@ def __str__(self) -> str: payload = base64.b64encode(self.__bytes__()).decode('latin-1') payload = '\n'.join(payload[i:(i + 64)] for i in range(0, len(payload), 64)) + crc = '' + if self.emit_crc: + crc = '=' + base64.b64encode(PGPObject.int_to_bytes(self.crc24(self.__bytes__()), 3)).decode('latin-1') + '\n' + return self.__armor_fmt.format( block_type=self.magic, headers=''.join('{key}: {val}\n'.format(key=key, val=val) for key, val in self.ascii_headers.items()), packet=payload, - crc=base64.b64encode(PGPObject.int_to_bytes(self.crc24(self.__bytes__()), 3)).decode('latin-1') + crc=crc ) def __copy__(self): From 08dd35ccc26ce522338720bdf26050b5e1f4d378 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 19:15:13 -0400 Subject: [PATCH 240/287] Look for primary key usage flags with same search path as preferences --- pgpy/pgp.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 1d9d9a40..c11e1d51 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2119,19 +2119,16 @@ def add_subkey(self, key: PGPKey, **prefs) -> None: bsig = self.bind(key, **prefs) key |= bsig - def _get_key_flags(self, user=None) -> Optional[KeyFlags]: + def _get_key_flags(self, user: Optional[str] = None) -> Optional[KeyFlags]: if self.is_primary: - if user is not None: - user = self.get_uid(user) - - elif len(self._uids) == 0: - return KeyFlags.Certify - - else: - user = next(iter(self.userids)) - # RFC 4880 says that primary keys *must* be capable of certification - return KeyFlags.Certify | (user.selfsig.key_flags if user.selfsig and user.selfsig.key_flags is not None else KeyFlags(0)) + key_flags: KeyFlags = KeyFlags.Certify + for prefsig in self.search_pref_sigs(uid=user): + if prefsig.key_flags is not None: + key_flags |= prefsig.key_flags + break + + return key_flags return next(self.self_signatures).key_flags From 35b00e808d908385694599bf7fea377e3f00ce83 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 21:30:29 -0400 Subject: [PATCH 241/287] SKESK: put common functionality (symmetric cipher, S2K specifier) into base class --- pgpy/packet/packets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index db321656..6f103c73 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -579,6 +579,11 @@ class SKESessionKey(VersionedPacket): __typeid__ = PacketType.SymmetricKeyEncryptedSessionKey __ver__ = 0 + def __init__(self) -> None: + super().__init__() + self.symalg = SymmetricKeyAlgorithm.AES256 + self.s2kspec = S2KSpecifier() + # FIXME: the type signature for this function is awkward because # the symmetric algorithm used by the following SEIPDv2 packet is # not encoded in the SKESKv6: @@ -646,8 +651,6 @@ class SKESessionKeyV4(SKESessionKey): def __init__(self) -> None: super().__init__() - self.symalg = SymmetricKeyAlgorithm.AES256 - self.s2kspec = S2KSpecifier() self.ct = bytearray() def __bytearray__(self) -> bytearray: From 18a557ce9b9e301690235910d101a7b7e1b8f9cb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 13 Jul 2023 13:03:01 -0400 Subject: [PATCH 242/287] PGPKey.new: add version parameter (only supports v4 at the moment) --- pgpy/pgp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index c11e1d51..4a17ec7f 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1766,7 +1766,8 @@ def revocation_keys(self): @classmethod def new(cls, key_algorithm: PubKeyAlgorithm, key_size: Optional[Union[int, EllipticCurveOID]] = None, - created: Optional[datetime] = None) -> PGPKey: + created: Optional[datetime] = None, + version: int = 4) -> PGPKey: """ Generate a new PGP key @@ -1780,6 +1781,8 @@ def new(cls, key_algorithm: PubKeyAlgorithm, :type created: :py:obj:`~datetime.datetime` or ``None`` :return: A newly generated :py:obj:`PGPKey` """ + if version != 4: + raise ValueError(f"PGPKey.new requested key version {version}, but can only generate v4 keys.") # new private key shell first key = PGPKey() From 4ab37e4355478c97f8322f95ab1b6e23706c40b5 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 13 Jul 2023 17:11:57 -0400 Subject: [PATCH 243/287] compare EllipticCurveOID values with "is" -- they are enums --- pgpy/packet/fields.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 711ab4c9..659e27df 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -634,7 +634,7 @@ def __len__(self): return len(self.p) + len(self.kdf) + len(self.oid) def __pubkey__(self): - if self.oid == EllipticCurveOID.Curve25519: + if self.oid is EllipticCurveOID.Curve25519: return x25519.X25519PublicKey.from_public_bytes(self.p.x) else: return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key() @@ -687,7 +687,7 @@ def parse(self, packet: bytearray) -> None: if isinstance(self.oid, EllipticCurveOID): self.p: Union[ECPoint, MPI] = ECPoint(packet) - if self.oid == EllipticCurveOID.Curve25519: + if self.oid is EllipticCurveOID.Curve25519: if self.p.format != ECPointFormat.Native: raise PGPIncompatibleECPointFormatError("Only Native format is valid for Curve25519") elif self.p.format != ECPointFormat.Standard: @@ -1700,7 +1700,7 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: else: self.oid = params - if self.oid != EllipticCurveOID.Ed25519: + if self.oid is not EllipticCurveOID.Ed25519: raise ValueError(f"EdDSA only supported with {EllipticCurveOID.Ed25519}, not {self.oid}") pk = ed25519.Ed25519PrivateKey.generate() @@ -1763,7 +1763,7 @@ def __len__(self) -> int: return nbytes def __privkey__(self): - if self.oid == EllipticCurveOID.Curve25519: + if self.oid is EllipticCurveOID.Curve25519: # NOTE: openssl and GPG don't use the same endianness for Curve25519 secret value s = self.int_to_bytes(self.s, (self.oid.key_size + 7) // 8, 'little') return x25519.X25519PrivateKey.from_private_bytes(s) @@ -1780,7 +1780,7 @@ def _generate(self, params: Optional[Union[int, EllipticCurveOID]]) -> None: else: _oid = params - if _oid == EllipticCurveOID.Curve25519: + if _oid is EllipticCurveOID.Curve25519: if any(c != 0 for c in self): # pragma: no cover raise PGPError("Key is already populated!") self.oid = _oid From 49c608313cd5a21aa7993694e17f12107473c1ed Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 13 Jul 2023 17:12:29 -0400 Subject: [PATCH 244/287] PubKey.verify should raise an exception, not return one. --- pgpy/packet/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 659e27df..c7adb0c1 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -384,7 +384,7 @@ def publen(self) -> int: return len(self) def verify(self, subj, sigbytes, hash_alg): - return NotImplemented # pragma: no cover + raise NotImplemented # pragma: no cover class OpaquePubKey(PubKey): # pragma: no cover From 77c05bbfb4edf1cf48af0b31c7f8ba7a86498fac Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 13 Jul 2023 17:56:08 -0400 Subject: [PATCH 245/287] Revise ciphertext API Move encrypt into fields.PubKey, and decrypt into fields.PrivKey. This is a simplifying change, and also prepares the codebase for the crypto-refresh, which has different subtle changes in the encoded pkesk, for different versions and different algorithms. The underlying cryptographic code gets pushed into fields.py (out of sight from higher-level functions) and lets us make more straightforward and explicit type definitions. --- pgpy/packet/fields.py | 251 ++++++++++++++++++++++++----------------- pgpy/packet/packets.py | 64 +---------- 2 files changed, 153 insertions(+), 162 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c7adb0c1..a92e6350 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -386,6 +386,16 @@ def publen(self) -> int: def verify(self, subj, sigbytes, hash_alg): raise NotImplemented # pragma: no cover + def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> CipherText: + raise NotImplemented + + def _encrypt_helper(self, symalg: Optional[SymmetricKeyAlgorithm], plaintext: bytes) -> bytes: + 'Common code for re-shaping session keys before storing in PKESK' + checksum = self.int_to_bytes(sum(plaintext) % 65536, 2) + if symalg is not None: + plaintext = bytes([symalg]) + plaintext + return plaintext + checksum + class OpaquePubKey(PubKey): # pragma: no cover def __init__(self): @@ -410,7 +420,7 @@ class RSAPub(PubKey): __pubfields__ = ('n', 'e') __pubkey_algo__ = PubKeyAlgorithm.RSAEncryptOrSign - def __pubkey__(self): + def __pubkey__(self) -> rsa.RSAPublicKey: return rsa.RSAPublicNumbers(self.e, self.n).public_key() def verify(self, subj, sigbytes, hash_alg): @@ -422,6 +432,11 @@ def verify(self, subj, sigbytes, hash_alg): return False return True + def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> RSACipherText: + ct = RSACipherText() + ct.from_raw_bytes(self.__pubkey__().encrypt(self._encrypt_helper(symalg, data), padding.PKCS1v15())) + return ct + def parse(self, packet: bytearray) -> None: self.n = MPI(packet) self.e = MPI(packet) @@ -697,6 +712,60 @@ def parse(self, packet: bytearray) -> None: self.kdf.parse(packet) + def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> ECDHCipherText: + """ + For convenience, the synopsis of the encoding method is given below; + however, this section, [NIST-SP800-56A], and [RFC3394] are the + normative sources of the definition. + + Obtain the authenticated recipient public key R + Generate an ephemeral key pair {v, V=vG} + Compute the shared point S = vR; + m = symm_alg_ID || session key || checksum || pkcs5_padding; + curve_OID_len = (byte)len(curve_OID); + Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 + || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap || "Anonymous + Sender " || recipient_fingerprint; + Z_len = the key size for the KEK_alg_ID used with AESKeyWrap + Compute Z = KDF( S, Z_len, Param ); + Compute C = AESKeyWrap( Z, m ) as per [RFC3394] + VB = convert point V to the octet string + Output (MPI(VB) || len(C) || C). + + The decryption is the inverse of the method given. Note that the + recipient obtains the shared secret by calculating + """ + if not isinstance(self.oid, EllipticCurveOID): + raise NotImplementedError(f"cannot encrypt to unknown curve ({self.oid!r})") + # m may need to be PKCS5-padded + padder = PKCS7(64).padder() + m = padder.update(self._encrypt_helper(symalg, data)) + padder.finalize() + + ct = ECDHCipherText() + + # generate ephemeral key pair and keep public key in ct + # use private key to compute the shared point "s" + if self.oid is EllipticCurveOID.Curve25519: + vx25519 = x25519.X25519PrivateKey.generate() + xcoord = vx25519.public_key().public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw) + ct.p = ECPoint.from_values(self.oid.key_size, ECPointFormat.Native, xcoord) + s = vx25519.exchange(self.__pubkey__()) + else: + vecdh = ec.generate_private_key(self.oid.curve()) + x = MPI(vecdh.public_key().public_numbers().x) + y = MPI(vecdh.public_key().public_numbers().y) + ct.p = ECPoint.from_values(self.oid.key_size, ECPointFormat.Standard, x, y) + s = vecdh.exchange(ec.ECDH(), self.__pubkey__()) + + # derive the wrapping key + z = self.kdf.derive_key(s, self.oid, PubKeyAlgorithm.ECDH, fpr) + + # compute C + ct.c = bytearray(aes_key_wrap(z, m)) + + return ct + class S2KSpecifier(Field): """ @@ -1411,6 +1480,45 @@ def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes]) -> Optional[byt def sign(self, sigdata, hash_alg): return NotImplemented # pragma: no cover + def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: + raise NotImplemented + + def _decrypt_helper(self, plaintext: bytes, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + """ + The value "m" in the above formulas is derived from the session key + as follows. First, the session key is prefixed with a one-octet + algorithm identifier that specifies the symmetric encryption + algorithm used to encrypt the following Symmetrically Encrypted Data + Packet. Then a two-octet checksum is appended, which is equal to the + sum of the preceding session key octets, not including the algorithm + identifier, modulo 65536. This value is then encoded as described in + PKCS#1 block encoding EME-PKCS1-v1_5 in Section 7.2.1 of [RFC3447] to + form the "m" value used in the formulas above. See Section 13.1 of + this document for notes on OpenPGP's use of PKCS#1. + """ + + m = bytearray(plaintext) + + symalg: Optional[SymmetricKeyAlgorithm] = None + keysize = len(m) - 2 + if get_symalg: + symalg = SymmetricKeyAlgorithm(m[0]) + del m[0] + keysize = symalg.key_size // 8 + + symkey = m[:keysize] + del m[:keysize] + + checksum = self.bytes_to_int(m[:2]) + del m[:2] + + if sum(symkey) % 65536 != checksum: # pragma: no cover + raise PGPDecryptionError(f"{self.__pubkey_algo__!r} decryption failed (sum: {sum(symkey)}, stored: {checksum}, length: {len(m)})") + if len(m) > 0: + raise PGPDecryptionError(f"{len(m)} bytes left unconsumed during {self.__pubkey_algo__!r} decryption") + + return (symalg, symkey) + def clear(self) -> None: """delete and re-initialize all private components to zero""" for field in self.__privfields__: @@ -1510,6 +1618,16 @@ def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: return self.__privkey__().sign(sigdata, padding.PKCS1v15(), hash_alg) + def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + if not isinstance(ct, RSACipherText): + raise TypeError(f"RSAPriv: cannot decrypt {type(ct)}") + + # pad up ct with null bytes if necessary + ciphertext = ct.me_mod_n.to_mpibytes()[2:] + ciphertext = b'\x00' * ((self.__privkey__().key_size // 8) - len(ciphertext)) + ciphertext + + return self._decrypt_helper(self.__privkey__().decrypt(ciphertext, padding.PKCS1v15()), True) + class DSAPriv(PrivKey, DSAPub): __privfields__ = ('x',) @@ -1819,22 +1937,38 @@ def parse(self, packet: bytearray) -> None: def sign(self, sigdata, hash_alg): raise PGPError("Cannot sign with an ECDH key") + def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + if not isinstance(ct, ECDHCipherText): + raise TypeError(f"ECDHPriv: cannot decrypt {type(ct)}") + + if not isinstance(self.oid, EllipticCurveOID): + raise TypeError(f"ECDH: Cannot decrypt with unknown curve({self.oid!r})") + + if self.oid is EllipticCurveOID.Curve25519: + vx25519 = x25519.X25519PublicKey.from_public_bytes(ct.p.x) + s = self.__privkey__().exchange(vx25519) + else: + # assemble the public component of ephemeral key v + vecdh = ec.EllipticCurvePublicNumbers(ct.p.x, ct.p.y, self.oid.curve()).public_key() + # compute s using the inverse of how it was derived during encryption + s = self.__privkey__().exchange(ec.ECDH(), vecdh) + + # derive the wrapping key + z = self.kdf.derive_key(s, self.oid, PubKeyAlgorithm.ECDH, fpr) + + # unwrap and unpad m + _m = aes_key_unwrap(z, ct.c) + + padder = PKCS7(64).unpadder() + return self._decrypt_helper(padder.update(_m) + padder.finalize(), get_symalg) + class CipherText(MPIs): - def __init__(self): + def __init__(self) -> None: super().__init__() for i in self.__mpis__: setattr(self, i, MPI(0)) - @classmethod - @abc.abstractmethod - def encrypt(cls, encfn, *args): - """create and populate a concrete CipherText class instance""" - - @abc.abstractmethod - def decrypt(self, decfn, *args): - """decrypt the ciphertext contained in this CipherText instance""" - def __bytearray__(self) -> bytearray: _bytes = bytearray() for i in self: @@ -1845,14 +1979,8 @@ def __bytearray__(self) -> bytearray: class RSACipherText(CipherText): __mpis__ = ('me_mod_n', ) - @classmethod - def encrypt(cls, encfn, *args): - ct = cls() - ct.me_mod_n = MPI(cls.bytes_to_int(encfn(*args))) - return ct - - def decrypt(self, decfn, *args): - return decfn(*args) + def from_raw_bytes(self, packet: bytes) -> None: + self.me_mod_n = MPI(self.bytes_to_int(packet)) def parse(self, packet: bytearray) -> None: self.me_mod_n = MPI(packet) @@ -1861,13 +1989,6 @@ def parse(self, packet: bytearray) -> None: class ElGCipherText(CipherText): __mpis__ = ('gk_mod_p', 'myk_mod_p') - @classmethod - def encrypt(cls, encfn, *args): - raise NotImplementedError() - - def decrypt(self, decfn, *args): - raise NotImplementedError() - def parse(self, packet: bytearray) -> None: self.gk_mod_p = MPI(packet) self.myk_mod_p = MPI(packet) @@ -1876,84 +1997,6 @@ def parse(self, packet: bytearray) -> None: class ECDHCipherText(CipherText): __mpis__ = ('p',) - @classmethod - def encrypt(cls, pk, *args): - """ - For convenience, the synopsis of the encoding method is given below; - however, this section, [NIST-SP800-56A], and [RFC3394] are the - normative sources of the definition. - - Obtain the authenticated recipient public key R - Generate an ephemeral key pair {v, V=vG} - Compute the shared point S = vR; - m = symm_alg_ID || session key || checksum || pkcs5_padding; - curve_OID_len = (byte)len(curve_OID); - Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 - || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap || "Anonymous - Sender " || recipient_fingerprint; - Z_len = the key size for the KEK_alg_ID used with AESKeyWrap - Compute Z = KDF( S, Z_len, Param ); - Compute C = AESKeyWrap( Z, m ) as per [RFC3394] - VB = convert point V to the octet string - Output (MPI(VB) || len(C) || C). - - The decryption is the inverse of the method given. Note that the - recipient obtains the shared secret by calculating - """ - # *args should be: - # - m - # - _m, = args - - # m may need to be PKCS5-padded - padder = PKCS7(64).padder() - m = padder.update(_m) + padder.finalize() - - km = pk.keymaterial - ct = cls() - - # generate ephemeral key pair and keep public key in ct - # use private key to compute the shared point "s" - if km.oid == EllipticCurveOID.Curve25519: - v = x25519.X25519PrivateKey.generate() - x = v.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) - ct.p = ECPoint.from_values(km.oid.key_size, ECPointFormat.Native, x) - s = v.exchange(km.__pubkey__()) - else: - v = ec.generate_private_key(km.oid.curve()) - x = MPI(v.public_key().public_numbers().x) - y = MPI(v.public_key().public_numbers().y) - ct.p = ECPoint.from_values(km.oid.key_size, ECPointFormat.Standard, x, y) - s = v.exchange(ec.ECDH(), km.__pubkey__()) - - # derive the wrapping key - z = km.kdf.derive_key(s, km.oid, PubKeyAlgorithm.ECDH, pk.fingerprint) - - # compute C - ct.c = aes_key_wrap(z, m) - - return ct - - def decrypt(self, pk, *args): - km = pk.keymaterial - if km.oid == EllipticCurveOID.Curve25519: - v = x25519.X25519PublicKey.from_public_bytes(self.p.x) - s = km.__privkey__().exchange(v) - else: - # assemble the public component of ephemeral key v - v = ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, km.oid.curve()).public_key() - # compute s using the inverse of how it was derived during encryption - s = km.__privkey__().exchange(ec.ECDH(), v) - - # derive the wrapping key - z = km.kdf.derive_key(s, km.oid, PubKeyAlgorithm.ECDH, pk.fingerprint) - - # unwrap and unpad m - _m = aes_key_unwrap(z, self.c) - - padder = PKCS7(64).unpadder() - return padder.update(_m) + padder.finalize() - def __init__(self) -> None: super().__init__() self.c = bytearray(0) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 6f103c73..2d37022f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -14,7 +14,6 @@ from typing import ByteString, Optional, Tuple, Union from cryptography.hazmat.primitives import constant_time -from cryptography.hazmat.primitives.asymmetric import padding from .fields import DSAPriv, DSAPub, DSASignature from .fields import ECDSAPub, ECDSAPriv, ECDSASignature @@ -242,72 +241,21 @@ def __copy__(self): return sk def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: - if isinstance(self.ct, RSACipherText): - if not isinstance(pk.keymaterial, PrivKeyField): - raise TypeError(f"Private key key material was {type(pk.keymaterial)}, should have been PrivKeyField") - # pad up ct with null bytes if necessary - ct = self.ct.me_mod_n.to_mpibytes()[2:] - ct = b'\x00' * ((pk.keymaterial.__privkey__().key_size // 8) - len(ct)) + ct - - decrypter = pk.keymaterial.__privkey__().decrypt - decargs = [ct, padding.PKCS1v15()] - - elif isinstance(self.ct, ECDHCipherText): - decrypter = pk - decargs = [] - - else: - raise NotImplementedError(self.pkalg) - - m = bytearray(self.ct.decrypt(decrypter, *decargs)) - - """ - The value "m" in the above formulas is derived from the session key - as follows. First, the session key is prefixed with a one-octet - algorithm identifier that specifies the symmetric encryption - algorithm used to encrypt the following Symmetrically Encrypted Data - Packet. Then a two-octet checksum is appended, which is equal to the - sum of the preceding session key octets, not including the algorithm - identifier, modulo 65536. This value is then encoded as described in - PKCS#1 block encoding EME-PKCS1-v1_5 in Section 7.2.1 of [RFC3447] to - form the "m" value used in the formulas above. See Section 13.1 of - this document for notes on OpenPGP's use of PKCS#1. - """ - - symalg = SymmetricKeyAlgorithm(m[0]) - del m[0] - - symkey = m[:symalg.key_size // 8] - del m[:symalg.key_size // 8] - - checksum = self.bytes_to_int(m[:2]) - del m[:2] - - if not sum(symkey) % 65536 == checksum: # pragma: no cover - raise PGPDecryptionError("{:s} decryption failed".format(self.pkalg.name)) + if not isinstance(pk.keymaterial, PrivKeyField): + raise TypeError(f"PKESKv3.decrypt_sk() expected private key material, got {type(pk.keymaterial)}") + if self.ct is None: + raise TypeError("PKESKv3.decrypt_sk() expected ciphertext, got None") - return (symalg, symkey) + return pk.keymaterial.decrypt(self.ct, pk.fingerprint, True) def encrypt_sk(self, pk: PubKey, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes) -> None: if symalg is None: raise ValueError('PKESKv3: must pass a symmetric key algorithm explicitly when encrypting') if pk.keymaterial is None: raise ValueError('PKESKv3: public key material must be instantiated') - m = bytearray(self.int_to_bytes(symalg) + symkey) - m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) - if isinstance(self.ct, RSACipherText): - encrypter = pk.keymaterial.__pubkey__().encrypt - encargs = [bytes(m), padding.PKCS1v15()] - - elif isinstance(self.ct, ECDHCipherText): - encrypter = pk - encargs = [bytes(m)] - - else: - raise NotImplementedError(self.pkalg) + self.ct = pk.keymaterial.encrypt(symalg, symkey, pk.fingerprint) - self.ct = self.ct.encrypt(encrypter, *encargs) self.update_hlen() def parse(self, packet): From a9d7a11d6a11009bf4afbcfee3f424db23d450ce Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 20 Jul 2023 12:53:04 -0400 Subject: [PATCH 246/287] Clean up NotImplemented/NotImplementedError - if something is not implemented, just raise NotImplementedError directly, rather than trying to sometimes return a NotImplemented object. Returning a NotImplemented object complicates the type signature without providing much of an advantage in terms of workflow. - PGPSignature.target_signature is a property that was never implemented or used. We can just drop this property entirely, implementing it only when it can actually be implemented. --- pgpy/packet/fields.py | 18 +++++++++--------- pgpy/pgp.py | 12 ++---------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index a92e6350..3c424209 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -172,7 +172,7 @@ def __getitem__(self, key): def __delitem__(self, key): ##TODO: this - raise NotImplementedError + raise NotImplementedError() def __contains__(self, key): return key in {k for k, _ in itertools.chain(self._hashed_sp, self._unhashed_sp)} @@ -384,10 +384,10 @@ def publen(self) -> int: return len(self) def verify(self, subj, sigbytes, hash_alg): - raise NotImplemented # pragma: no cover + raise NotImplementedError() # pragma: no cover def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> CipherText: - raise NotImplemented + raise NotImplementedError() def _encrypt_helper(self, symalg: Optional[SymmetricKeyAlgorithm], plaintext: bytes) -> bytes: 'Common code for re-shaping session keys before storing in PKESK' @@ -406,7 +406,7 @@ def __iter__(self): yield self.data def __pubkey__(self): - return NotImplemented + raise NotImplementedError() def __bytearray__(self) -> bytearray: return self.data @@ -1442,7 +1442,7 @@ def encrypt_keyblob(self, passphrase: str, @abc.abstractmethod def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - ... + raise NotImplementedError() def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes]) -> Optional[bytearray]: if not self.s2k: # pragma: no cover @@ -1478,10 +1478,10 @@ def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes]) -> Optional[byt return bytearray(pt) def sign(self, sigdata, hash_alg): - return NotImplemented # pragma: no cover + raise NotImplementedError() # pragma: no cover - def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm],bytes]: - raise NotImplemented + def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + raise NotImplementedError() def _decrypt_helper(self, plaintext: bytes, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: """ @@ -1528,7 +1528,7 @@ def clear(self) -> None: class OpaquePrivKey(PrivKey, OpaquePubKey): # pragma: no cover def __privkey__(self): - return NotImplemented + raise NotImplementedError() def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: raise NotImplementedError() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 4a17ec7f..555c8ed8 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -364,10 +364,6 @@ def intended_recipients(self): """ return map(lambda x: x.intended_recipient, self._signature.subpackets['IntendedRecipient']) - @property - def target_signature(self): - return NotImplemented - @property def type(self) -> SignatureType: """ @@ -1010,7 +1006,7 @@ def __format__(self, format_spec: str) -> str: if isinstance(self._uid, UserID): return self._uid.uid - raise NotImplementedError + raise NotImplementedError() class PGPMessage(Armorable): @@ -1103,7 +1099,7 @@ def type(self) -> Literal['cleartext', 'literal', 'encrypted']: if isinstance(self._message, (SKEData, IntegrityProtectedSKEData)): return 'encrypted' - raise NotImplementedError + raise NotImplementedError() def __init__(self) -> None: """ @@ -2225,8 +2221,6 @@ def _sign(self, subject: PGPSubject, sig: PGPSignature, **prefs) -> PGPSignature sig._signature.hash2 = bytearray(h2.finalize()[:2]) _sig = self._key.sign(sigdata, getattr(hashes, sig.hash_algorithm.name)()) - if _sig is NotImplemented: - raise NotImplementedError(self.key_algorithm) sig._signature.signature.from_signer(_sig) sig._signature.update_hlen() @@ -2811,8 +2805,6 @@ def _filter_sigs(sigs: Iterable[PGPSignature]) -> Iterator[PGPSignature]: sigv.add_sigsubj(sig, self, subj, issues) else: verified = self._key.verify(sig.hashdata(subj), sig.__sig__, getattr(hashes, sig.hash_algorithm.name)()) - if verified is NotImplemented: - raise NotImplementedError(sig.key_algorithm) sigv.add_sigsubj(sig, self, subj, SecurityIssues.WrongSig if not verified else SecurityIssues.OK) From 7dee6b0327060e494650172e42208135871dbbbe Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:36:47 -0400 Subject: [PATCH 247/287] correct comment about MTI symmetric key algorithm --- pgpy/sopgpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 12db1c88..e43f9b49 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -340,7 +340,7 @@ def encrypt(self, if c in ciphers: cipher = c break - # AES128 is MTI in RFC4880: + # AES128 is MTI in the upcoming revision to RFC 4880: if cipher is None: cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 sessionkey = cipher.gen_key() From 2e883d7d329c72dee685fa38ee7568ab4e4d1bd7 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:39:25 -0400 Subject: [PATCH 248/287] PGPKey: add features property --- pgpy/pgp.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 555c8ed8..5e996851 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2118,6 +2118,13 @@ def add_subkey(self, key: PGPKey, **prefs) -> None: bsig = self.bind(key, **prefs) key |= bsig + @property + def features(self) -> Optional[Features]: + for sig in self.search_pref_sigs(): + if sig.features is not None: + return sig.features + return None + def _get_key_flags(self, user: Optional[str] = None) -> Optional[KeyFlags]: if self.is_primary: # RFC 4880 says that primary keys *must* be capable of certification From a57714cc95f7fd7270963731a5e1872880167cbb Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:43:53 -0400 Subject: [PATCH 249/287] sopgpy: search for cipher preferences more cleanly use search_pref_sigs() instead of walking each user ID --- pgpy/sopgpy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index e43f9b49..d7e5a50f 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -330,12 +330,12 @@ def encrypt(self, ciphers = set(self._cipherprefs) for handle, cert in certs.items(): - keyciphers = set() - for uid in cert.userids: - if uid.selfsig and uid.selfsig.cipherprefs: - for cipher in uid.selfsig.cipherprefs: - keyciphers.add(cipher) - ciphers = ciphers.intersection(keyciphers) + keyciphers: Set[pgpy.constants.SymmetricKeyAlgorithm] = set() + for sig in cert.search_pref_sigs(): + if sig.cipherprefs is not None: + for c in sig.cipherprefs: + keyciphers.add(c) + ciphers &= keyciphers for c in self._cipherprefs: if c in ciphers: cipher = c From 51cefc77694c1c291a11493f8d7d7cc2a7f9e798 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:45:21 -0400 Subject: [PATCH 250/287] PGPKey.encrypt: add max_featureset option to limit version of SEIPD --- pgpy/pgp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 5e996851..134cc4ae 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2822,7 +2822,8 @@ def encrypt(self, message: PGPMessage, sessionkey: Optional[bytes] = None, user: Optional[str] = None, - cipher: Optional[SymmetricKeyAlgorithm] = None) -> PGPMessage: + cipher: Optional[SymmetricKeyAlgorithm] = None, + max_featureset: Optional[Features] = None) -> PGPMessage: """Encrypt a PGPMessage using this key. :param message: The message to encrypt. @@ -2871,6 +2872,9 @@ def encrypt(self, if compprefs is None: compprefs = [] + if max_featureset is not None: + features &= max_featureset + pref_cipher = next((c for c in cipherprefs if c.is_supported), SymmetricKeyAlgorithm.TripleDES) cipher_algo = cipher if cipher is not None else pref_cipher From d7137019fcf48796c290b17f4848d8fe86596381 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 18 Aug 2023 11:53:24 -0400 Subject: [PATCH 251/287] Have tox take the baseline requirements from requirements.txt This leaves only one place to list the requirements --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ac09c574..e6387bb9 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ passenv = LD_LIBRARY_PATH PATH deps = - cryptography>=2.6 + -rrequirements.txt gpg==1.10.0 pytest pytest-cov From f6cf609c52eded7a96f771699b8efc67b9a76403 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 18 Aug 2023 14:35:44 -0400 Subject: [PATCH 252/287] note dependency on `sop` as optional --- requirements.txt | 1 + setup.cfg | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6bc0dd57..4a173cdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ cryptography>=3.3.2 +sop>=0.5.1[sopgpy] diff --git a/setup.cfg b/setup.cfg index 04da686b..dbd37c8c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,13 +44,14 @@ packages = # TODO: fix support for cryptography >= 38.0.0 (https://github.com/SecurityInnovation/PGPy/issues/402) install_requires = cryptography>=3.3.2 - sop>=0.5.1 python_requires = >=3.6 # doc_requires = # sphinx # sphinx-better-theme +[options.extras_require] +sopgpy = sop>=0.5.1 [build_sphinx] source-dir = docs/source From 639b72b84ffba1c2d675f3895cee0338195ad107 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 23 Aug 2023 17:42:18 -0400 Subject: [PATCH 253/287] Parse embedded signature without assuming version This will be important if a non-v4 signature gets embedded. It might end up being an opaque signature, if PGPy can't read the version, but it shouldn't try to force a SignatureV4 if it is not actually version 4. This change also isolates the parsing of the embedded signature so that it can only consume bytes within the enclosing subpacket. --- pgpy/packet/subpackets/signature.py | 37 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index a93339bd..82807f93 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -2,6 +2,8 @@ Signature SubPackets """ +from __future__ import annotations + import binascii import calendar import warnings @@ -25,6 +27,7 @@ from ...constants import KeyFlags as _KeyFlags from ...constants import KeyServerPreferences as _KeyServerPreferences from ...constants import NotationDataFlags +from ...constants import PacketType from ...constants import PubKeyAlgorithm from ...constants import RevocationKeyClass from ...constants import RevocationReason @@ -37,6 +40,11 @@ from ...types import Fingerprint from ...types import KeyID +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..packets import Signature as SignaturePacket + __all__ = ['URI', 'FlagList', @@ -867,18 +875,23 @@ class EmbeddedSignature(Signature): __typeid__ = SigSubpacketType.EmbeddedSignature @sdproperty - def _sig(self): + def _sig(self) -> SignaturePacket: return self._sigpkt @_sig.setter - def _sig_set(self, val) -> None: + def _sig_set(self, val: SignaturePacket) -> None: + from ..packets import Signature as SignaturePacket + + if not isinstance(val, SignaturePacket): + raise TypeError(f"EmbeddedSignature._sig expects a SignaturePacket, not {type(val)}") + esh = EmbeddedSignatureHeader() if not isinstance(val.header, VersionedHeader): raise TypeError(f"Signature packet should have had a versioned header, got {type(val.header)}") esh.version = val.header.version val.header = esh val.update_hlen() - self._sigpkt = val + self._sigpkt: SignaturePacket = val @property def sigtype(self) -> SignatureType: @@ -918,8 +931,24 @@ def __bytearray__(self) -> bytearray: return super().__bytearray__() + self._sigpkt.__bytearray__() def parse(self, packet: bytearray) -> None: + from ..types import Packet + super().parse(packet) - self._sig.parse(packet) + # we know the length based on the size of the subpacket + synthetic_header = VersionedHeader() + synthetic_header.typeid = PacketType.Signature + synthetic_header.length = self.header.length + synthetic_header.version = packet[0] + del packet[0] + after_version_length = self.header.length - 2 + packet_remainder = bytes(packet[:after_version_length]) + synthetic_packet: bytearray = synthetic_header.__bytearray__() + packet_remainder + del packet[:after_version_length] + + pkt = Packet(synthetic_packet) # type: ignore[abstract] + if len(synthetic_packet) > 0: + warnings.warn(f"{len(synthetic_packet)} octets leftover when parsing EmbeddedSignature") + self._sig = pkt class IssuerFingerprint(FingerprintSubpacket): From e8104460c34ea6e39a1490d52720dbdef9d48c69 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 23 Aug 2023 18:21:15 -0400 Subject: [PATCH 254/287] Don't pass fingerprint version to FingerprintSubpacket This hasn't been necessary since fingerprints knew their own version, it was just a no-op. --- pgpy/pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 134cc4ae..8bf49cf3 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2217,7 +2217,7 @@ def _sign(self, subject: PGPSubject, sig: PGPSignature, **prefs) -> PGPSignature if prefs.pop('include_issuer_fingerprint', True): if isinstance(self._key, PrivKeyV4): - sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _version=4, _fpr=self.fingerprint) + sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _fpr=self.fingerprint) # place the subpackets in order by the subpacket type identifier octet sig._signature.subpackets._normalize() From 46d999573df899f9e64be614a6384ef94e1d3931 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 23 Aug 2023 18:24:28 -0400 Subject: [PATCH 255/287] SubPackets.addnew: warn when trying to set a non-existent attribute This is what caught the problem of passing _version to addnew --- pgpy/packet/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 3c424209..c80b1dd8 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -191,6 +191,8 @@ def addnew(self, spname: str, hashed: bool = False, critical: bool = False, **kw for p, v in kwargs.items(): if hasattr(nsp, p): setattr(nsp, p, v) + else: + warn(f"subpacket {spname} does not have attr {p}") nsp.update_hlen() if hashed: self['h_' + spname] = nsp From 89dee6aeaa6ef0776525d65c78033480619a96e4 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 17 Feb 2023 16:36:21 -0500 Subject: [PATCH 256/287] Implement Argon2 We rely on argon_cffi for the underlying implementation since python cryptography appears unlikely to grow support for argon2 any time soon: https://github.com/pyca/cryptography/issues/2643 --- README.rst | 2 ++ pgpy/constants.py | 2 ++ pgpy/packet/fields.py | 37 +++++++++++++++++++++++++++++++-- requirements.txt | 1 + setup.cfg | 1 + tests/test_12_crypto_refresh.py | 1 - 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 08395df9..b1eda839 100644 --- a/README.rst +++ b/README.rst @@ -61,6 +61,8 @@ Requirements - `Cryptography `_ +- `argon2_cffi `_ + To use `sopgpy` you'll also need: - `sop `_ >= 0.5.1 diff --git a/pgpy/constants.py b/pgpy/constants.py index 4a16fd5a..67782919 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -670,6 +670,7 @@ class String2KeyType(IntEnum): Salted = 1 Reserved = 2 Iterated = 3 + Argon2 = 4 GNUExtension = 101 @classmethod @@ -682,6 +683,7 @@ def _missing_(cls, val: object) -> String2KeyType: def salt_length(self) -> int: ks = {String2KeyType.Salted: 8, String2KeyType.Iterated: 8, + String2KeyType.Argon2: 16, } return ks.get(self, 0) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c80b1dd8..e3dc9657 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -16,6 +16,9 @@ from warnings import warn +from argon2.low_level import hash_secret_raw # type: ignore +from argon2 import Type as ArgonType # type: ignore + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes @@ -888,6 +891,9 @@ def __init__(self, iteration_count: int = 65011712, # default to maximum iterations gnupg_extension: S2KGNUExtension = S2KGNUExtension.NoSecret, smartcard_serial: Optional[bytes] = None, + argon2_time: int = 1, + argon2_parallelism: int = 4, + argon2_memory_exp: int = 21, ): if salt is not None: if s2ktype.salt_length == 0: @@ -901,6 +907,16 @@ def __init__(self, raise ValueError(f"Smartcard serial number should only be specified with S2KGNUExtension Smartcard, not {gnupg_extension!r}") if len(smartcard_serial) > 16: raise ValueError(f"Smartcard serial number should be 16 octets or less, not {len(smartcard_serial)}") + if s2ktype is String2KeyType.Argon2: + if argon2_time < 1 or argon2_time > 255: + raise ValueError(f"Argon2 time parameter must be between 1 and 255, inclusive, not {argon2_time}") + if argon2_parallelism < 1 or argon2_parallelism > 255: + raise ValueError(f"Argon2 parallelism parameter must be between 1 and 255, inclusive, not {argon2_time}") + if argon2_memory_exp > 255: + raise ValueError(f"Argon2 memory size exponent (2^m KiB) must be at most 255, not m={argon2_memory_exp}") + if (1 << argon2_memory_exp) < 8 * argon2_parallelism: + raise ValueError( + f"Argon2 memory size in KiB (m={argon2_memory_exp}, or {1 << argon2_memory_exp}KiB) should be at least parallelism({argon2_parallelism})*8") super().__init__() self._type: String2KeyType = s2ktype self._halg: HashAlgorithm = halg @@ -910,6 +926,9 @@ def __init__(self, self._count = 65011712 # the default! if s2ktype is String2KeyType.Iterated: self.iteration_count = iteration_count + self._a2_t = argon2_time + self._a2_p = argon2_parallelism + self._a2_m = argon2_memory_exp self._gnupg_extension: S2KGNUExtension = gnupg_extension self._smartcard_serial: Optional[bytes] = None if smartcard_serial is not None: @@ -926,6 +945,9 @@ def __copy__(self) -> S2KSpecifier: s2k._count = self._count s2k._gnupg_extension = self._gnupg_extension s2k._smartcard_serial = copy.copy(self._smartcard_serial) + s2k._a2_t = self._a2_t + s2k._a2_p = self._a2_p + s2k._a2_m = self._a2_m return s2k @sdproperty @@ -1031,6 +1053,10 @@ def __bytearray__(self) -> bytearray: _bytes += self.salt if self._type is String2KeyType.Iterated: _bytes += self.iteration_octet + if self._type is String2KeyType.Argon2: + _bytes.append(self._a2_t) + _bytes.append(self._a2_p) + _bytes.append(self._a2_m) return _bytes def __len__(self) -> int: @@ -1053,10 +1079,14 @@ def parse(self, packet: bytearray) -> None: self._salt = bytes(packet[:self._type.salt_length]) del packet[:self._type.salt_length] - if self._type == String2KeyType.Iterated: + if self._type is String2KeyType.Iterated: self.iteration_count = packet[:1] del packet[:1] + if self._type is String2KeyType.Argon2: + (self._a2_t, self._a2_p, self._a2_m) = packet[:3] + del packet[:3] + def _gnu_bytearray(self, _bytes): if self._type is not String2KeyType.GNUExtension: raise ValueError(f"This is not a GnuPG-extended S2K specifier ({self._type})") @@ -1109,12 +1139,15 @@ def _parse_gnu_extension(self, packet) -> None: del packet[:slen] def derive_key(self, passphrase: Union[str, bytes], keylen_bits: int) -> bytes: - if self._type not in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated}: + if self._type not in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated, String2KeyType.Argon2}: raise NotImplementedError(f"Cannot derive key from S2KSpecifier {self._type!r}") if not isinstance(passphrase, bytes): passphrase = passphrase.encode('utf-8') + if self._type is String2KeyType.Argon2: + return hash_secret_raw(passphrase, self.salt, self._a2_t, 1 << self._a2_m, self._a2_p, keylen_bits // 8, ArgonType.ID, 0x13) + hashlen = self._halg.digest_size * 8 ctx = int(math.ceil((keylen_bits / hashlen))) diff --git a/requirements.txt b/requirements.txt index 4a173cdc..1218b0c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +argon2_cffi cryptography>=3.3.2 sop>=0.5.1[sopgpy] diff --git a/setup.cfg b/setup.cfg index dbd37c8c..4b7f444a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ packages = pgpy.packet.subpackets # TODO: fix support for cryptography >= 38.0.0 (https://github.com/SecurityInnovation/PGPy/issues/402) install_requires = + argon2_cffi cryptography>=3.3.2 python_requires = >=3.6 diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index cbdbf0a4..d06b7ac0 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -22,7 +22,6 @@ def test_v4_sigs(self) -> None: def test_v4_skesk_argon2(self, cipher: str) -> None: msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v4skesk-argon2-{cipher}.pgp') assert msg.is_encrypted - pytest.xfail('Argon2 is not supported') unlocked = msg.decrypt('password') assert not unlocked.is_encrypted assert unlocked.message == b'Hello, world!' From d0a2a04a611139dd16d6f82d23875d4aee519711 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 1 Feb 2023 13:35:37 -0500 Subject: [PATCH 257/287] Include SHA3 functions from crypto-refresh --- pgpy/constants.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 67782919..78d824a8 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -335,9 +335,9 @@ class HashAlgorithm(IntEnum): SHA384 = 0x09 SHA512 = 0x0A SHA224 = 0x0B - #SHA3_256 = 13 - #SHA3_384 = 14 - #SHA3_512 = 15 + SHA3_256 = 12 + _reserved_5 = 13 + SHA3_512 = 14 @classmethod def _missing_(cls, val: object) -> HashAlgorithm: @@ -363,7 +363,8 @@ def is_second_preimage_resistant(self) -> bool: @property def is_collision_resistant(self) -> bool: - return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512} + return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512, + HashAlgorithm.SHA3_256, HashAlgorithm.SHA512} @property def is_considered_secure(self) -> SecurityIssues: From 726b4ec1e2e5e6c99bc035d27e7745d74f826a58 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 18:32:50 -0500 Subject: [PATCH 258/287] Add SEIPDv1 alias for the first bit of the first octet of Features --- pgpy/constants.py | 4 +++- pgpy/sopgpy.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 78d824a8..2ca15842 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -732,6 +732,8 @@ class KeyFlags(IntFlag): class Features(IntFlag): + SEIPDv1 = 0x01 + # alias (the old name, in RFC 4880): ModificationDetection = 0x01 UnknownFeature02 = 0x02 UnknownFeature04 = 0x04 @@ -743,7 +745,7 @@ class Features(IntFlag): @classproperty def pgpy_features(cls) -> Features: - return Features.ModificationDetection + return Features.SEIPDv1 class RevocationKeyClass(IntFlag): diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index d7e5a50f..7728bd4d 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -156,7 +156,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], primaryflags = pgpy.constants.KeyFlags.Certify | pgpy.constants.KeyFlags.Sign first: bool = True - features = pgpy.constants.Features.ModificationDetection + features = pgpy.constants.Features.SEIPDv1 hashes: List[pgpy.constants.HashAlgorithm] = [] hashes += [pgpy.constants.HashAlgorithm.SHA512, From e27940dd16247674c4ef6a236c5440a7fa99c23d Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 04:27:46 -0400 Subject: [PATCH 259/287] identify SEIPDv2 bit in Features flags --- pgpy/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 2ca15842..03d5c0ad 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -737,7 +737,7 @@ class Features(IntFlag): ModificationDetection = 0x01 UnknownFeature02 = 0x02 UnknownFeature04 = 0x04 - UnknownFeature08 = 0x08 + SEIPDv2 = 0x08 UnknownFeature10 = 0x10 UnknownFeature20 = 0x20 UnknownFeature40 = 0x40 From c372523731888f28199d481509c8cb16cea2e910 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 18:34:09 -0500 Subject: [PATCH 260/287] Add AEADMode table --- pgpy/constants.py | 33 +++++++++++++++++++++++++++++++++ pgpy/pgp.py | 1 + 2 files changed, 34 insertions(+) diff --git a/pgpy/constants.py b/pgpy/constants.py index 03d5c0ad..63ee0b1f 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -48,6 +48,7 @@ 'RevocationKeyClass', 'NotationDataFlags', 'TrustFlags', + 'AEADMode', ] @@ -186,6 +187,38 @@ def gen_key(self) -> bytes: return os.urandom(self.key_size // 8) +class AEADMode(IntEnum): + '''Supported AEAD Modes''' + Invalid = 0x00 + EAX = 1 + OCB = 2 + GCM = 3 + + @property + def iv_len(self) -> int: + 'IV length in octets' + ivl = { + AEADMode.EAX: 16, + AEADMode.OCB: 15, + AEADMode.GCM: 12, + } + if self in ivl: + return ivl[self] + raise NotImplementedError + + @property + def tag_len(self) -> int: + 'authentication tag length in octets' + tagl = { + AEADMode.EAX: 16, + AEADMode.OCB: 16, + AEADMode.GCM: 16, + } + if self in tagl: + return tagl[self] + raise NotImplementedError + + class PubKeyAlgorithm(IntEnum): """Supported public key algorithms.""" Unknown = -1 diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 8bf49cf3..46ae63cb 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -38,6 +38,7 @@ from .constants import SignatureType from .constants import SymmetricKeyAlgorithm from .constants import SecurityIssues +from .constants import AEADMode from .decorators import KeyAction From e781a6d5797fa1adb35fef3e1e9746278e1b5b70 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 18:37:43 -0500 Subject: [PATCH 261/287] define AEADCiphersuiteList in a way that it is both inspectable and serializable --- pgpy/packet/types.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pgpy/packet/types.py b/pgpy/packet/types.py index 16fbcc89..fb2a37ca 100644 --- a/pgpy/packet/types.py +++ b/pgpy/packet/types.py @@ -5,7 +5,7 @@ import abc import copy -from typing import Iterator, Optional, Tuple, Type, Union +from typing import Iterator, List, Optional, Tuple, Type, Union from ..constants import PacketType @@ -17,6 +17,8 @@ from ..types import Header as _Header from ..constants import PubKeyAlgorithm +from ..constants import SymmetricKeyAlgorithm +from ..constants import AEADMode __all__ = ['Header', 'VersionedHeader', @@ -29,7 +31,9 @@ 'Primary', 'Sub', 'MPI', - 'MPIs', ] + 'MPIs', + 'AEADCiphersuiteList', + ] class Header(_Header): @@ -299,3 +303,17 @@ def __copy__(self) -> MPIs: setattr(pk, m, copy.copy(getattr(self, m))) return pk + + +class AEADCiphersuiteList(List[Tuple[SymmetricKeyAlgorithm, AEADMode]]): + '''a list of AEAD Ciphersuites''' + + def __init__(self, val: List[Tuple[SymmetricKeyAlgorithm, AEADMode]] = []) -> None: + for pair in val: + self.append(pair) + + def __bytearray__(self) -> bytes: + _bytes = bytearray() + for pair in self: + _bytes += bytes([pair[0], pair[1]]) + return _bytes From 231b2eab2f09e4099b4f2fb0e6e14388f73bffd3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 18:38:08 -0500 Subject: [PATCH 262/287] Define PreferredAEADCiphersuites subpacket --- pgpy/constants.py | 1 + pgpy/packet/subpackets/signature.py | 67 ++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 63ee0b1f..29d91f73 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -589,6 +589,7 @@ class SigSubpacketType(IntEnum): IssuerFingerprint = 33 IntendedRecipientFingerprint = 35 AttestedCertifications = 37 + PreferredAEADCiphersuites = 39 class AttributeType(IntEnum): diff --git a/pgpy/packet/subpackets/signature.py b/pgpy/packet/subpackets/signature.py index 82807f93..ee11562a 100644 --- a/pgpy/packet/subpackets/signature.py +++ b/pgpy/packet/subpackets/signature.py @@ -20,6 +20,7 @@ from .types import Signature from ..types import VersionedHeader +from ..types import AEADCiphersuiteList from ...constants import CompressionAlgorithm from ...constants import Features as _Features @@ -34,6 +35,7 @@ from ...constants import SigSubpacketType from ...constants import SignatureType from ...constants import SymmetricKeyAlgorithm +from ...constants import AEADMode from ...decorators import sdproperty @@ -75,7 +77,9 @@ 'EmbeddedSignature', 'IssuerFingerprint', 'IntendedRecipient', - 'AttestedCertifications'] + 'AttestedCertifications', + 'PreferredAEADCiphersuites', + ] class URI(Signature): @@ -1111,3 +1115,64 @@ def parse(self, packet): super().parse(packet) self.attested_certifications = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] + + +class PreferredAEADCiphersuites(Signature): + ''' + (array of pairs of octets indicating Symmetric Cipher and AEAD algorithms) + + A series of paired algorithm identifiers indicating how the keyholder prefers to receive version 2 Symmetrically Encrypted Integrity Protected Data ({{version-two-seipd}}). + Each pair of octets indicates a combination of a symmetric cipher and an AEAD mode that the key holder prefers to use. + The symmetric cipher identifier precedes the AEAD identifier in each pair. + The subpacket body is an ordered list of pairs of octets with the most preferred algorithm combination listed first. + + It is assumed that only the combinations of algorithms listed are supported by the recipient's software, with the exception of the mandatory-to-implement combination of AES-128 and OCB. + If AES-128 and OCB are not found in the subpacket, it is implicitly listed at the end. + + AEAD algorithm numbers are listed in {{aead-algorithms}}. + Symmetric cipher algorithm numbers are listed in {{symmetric-algos}}. + + For example, a subpacket with content of these six octets: + + 09 02 09 03 13 02 + + Indicates that the keyholder prefers to receive v2 SEIPD using AES-256 with OCB, then AES-256 with GCM, then Camellia-256 with OCB, and finally the implicit AES-128 with OCB. + + Note that support for version 2 of the Symmetrically Encrypted Integrity Protected Data packet ({{version-two-seipd}}) in general is indicated by a Feature Flag ({{features-subpacket}}). + + This subpacket is only found on a self-signature. + + When generating a v1 SEIPD packet, this preference list is not relevant. + See {{preferred-v1-seipd}} instead. + ''' + __typeid__ = SigSubpacketType.PreferredAEADCiphersuites + + def __init__(self) -> None: + super().__init__() + self._preferred_ciphersuites: AEADCiphersuiteList = AEADCiphersuiteList() + + @sdproperty + def preferred_ciphersuites(self) -> AEADCiphersuiteList: + return self._preferred_ciphersuites + + @preferred_ciphersuites.register + def preferred_ciphersuites_native(self, val: AEADCiphersuiteList) -> None: + self._preferred_ciphersuites = val + + @preferred_ciphersuites.register + def preferred_ciphersuites_bytearray(self, val: Union[bytes, bytearray]) -> None: + if len(val) % 2: + raise ValueError(f"PreferredAEADCiphersuites should have an even length, not {len(val)}") + self._preferred_ciphersuites = AEADCiphersuiteList() + for i in range(0, len(val), 2): + self._preferred_ciphersuites.append((SymmetricKeyAlgorithm(val[i]), AEADMode(val[i + 1]))) + + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() + _bytes += self._preferred_ciphersuites.__bytearray__() + return _bytes + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.preferred_ciphersuites = packet[:(self.header.length - 1)] + del packet[:(self.header.length - 1)] From 05fd07f411294f251ad1295871e640bf40bafda3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 3 Feb 2023 23:00:33 -0500 Subject: [PATCH 263/287] allow aead_ciphersuites preferences when certifying --- pgpy/pgp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 46ae63cb..b91532e3 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2392,6 +2392,7 @@ def certify(self, subject: PGPSubject, level: SignatureType = SignatureType.Gene primary_uid = prefs.pop('primary', None) attested_certifications = prefs.pop('attested_certifications', []) features = prefs.pop('features', Features.pgpy_features) + aead_ciphersuites = prefs.pop('aead_ciphersuites', []) if key_expires is not None: # key expires should be a timedelta, so if it's a datetime, turn it into a timedelta @@ -2451,6 +2452,9 @@ def certify(self, subject: PGPSubject, level: SignatureType = SignatureType.Gene ) sig._signature.subpackets.addnew('AttestedCertifications', hashed=True, attested_certifications=b''.join(sorted(attestations))) + if aead_ciphersuites: + sig._signature.subpackets.addnew('PreferredAEADCiphersuites', hashed=True, preferred_ciphersuites=aead_ciphersuites) + else: # signature options that only make sense in non-self-certifications trust = prefs.pop('trust', None) From c2aca3139a89727d03dcf7bb3b4b68b373f8e6c2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 05:11:35 -0400 Subject: [PATCH 264/287] Make an AEAD object that can encrypt and decrypt --- pgpy/symenc.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pgpy/symenc.py b/pgpy/symenc.py index 51173ae5..281596ed 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -1,21 +1,23 @@ """ symenc.py """ -from typing import Optional +from typing import Optional, Union from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import modes +from cryptography.hazmat.primitives.ciphers.aead import AESOCB3, AESGCM -from .constants import SymmetricKeyAlgorithm +from .constants import AEADMode, SymmetricKeyAlgorithm from .errors import PGPDecryptionError from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError __all__ = ['_cfb_encrypt', - '_cfb_decrypt'] + '_cfb_decrypt', + 'AEAD'] def _cfb_encrypt(pt: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional[bytes] = None) -> bytearray: @@ -57,3 +59,22 @@ def _cfb_decrypt(ct: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional else: return bytearray(decryptor.update(ct) + decryptor.finalize()) + + +class AEAD: + def __init__(self, cipher: SymmetricKeyAlgorithm, mode: AEADMode, key: bytes) -> None: + self._aead: Union[AESOCB3, AESGCM] + if cipher not in [SymmetricKeyAlgorithm.AES128, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES256]: + raise NotImplementedError(f"Cannot do AEAD with non-AES cipher (requested cipher: {cipher!r})") + if mode == AEADMode.OCB: + self._aead = AESOCB3(key) + elif mode == AEADMode.GCM: + self._aead = AESGCM(key) + else: + raise NotImplementedError(f"Cannot do AEAD mode other than OCB, and GCM (requested mode: {mode!r})") + + def encrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + return self._aead.encrypt(nonce, data, associated_data) + + def decrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + return self._aead.decrypt(nonce, data, associated_data) From 19901a517d5a978370b94f8d918d8dd92aa1ffa7 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 15 Feb 2023 12:52:24 -0500 Subject: [PATCH 265/287] implement SEIPDv2 --- pgpy/constants.py | 2 +- pgpy/packet/packets.py | 143 ++++++++++++++++++++++++++++++++ pgpy/pgp.py | 4 +- tests/test_05_actions.py | 2 +- tests/test_12_crypto_refresh.py | 2 +- 5 files changed, 149 insertions(+), 4 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 29d91f73..b1184d6a 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -779,7 +779,7 @@ class Features(IntFlag): @classproperty def pgpy_features(cls) -> Features: - return Features.SEIPDv1 + return Features.SEIPDv1 | Features.SEIPDv2 class RevocationKeyClass(IntFlag): diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 2d37022f..48e1d10f 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -11,9 +11,13 @@ from datetime import datetime, timezone +from math import log2 + from typing import ByteString, Optional, Tuple, Union from cryptography.hazmat.primitives import constant_time +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.hashes import SHA256 from .fields import DSAPriv, DSAPub, DSASignature from .fields import ECDSAPub, ECDSAPriv, ECDSASignature @@ -49,6 +53,7 @@ from ..constants import SymmetricKeyAlgorithm from ..constants import TrustFlags from ..constants import TrustLevel +from ..constants import AEADMode from ..decorators import sdproperty @@ -56,6 +61,7 @@ from ..symenc import _cfb_decrypt from ..symenc import _cfb_encrypt +from ..symenc import AEAD from ..types import Fingerprint from ..types import KeyID @@ -85,6 +91,7 @@ 'UserAttribute', 'IntegrityProtectedSKEData', 'IntegrityProtectedSKEDataV1', + 'IntegrityProtectedSKEDataV2', 'MDC'] @@ -1691,6 +1698,142 @@ def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray return pt +class IntegrityProtectedSKEDataV2(IntegrityProtectedSKEData): + __ver__ = 2 + + def __init__(self) -> None: + super().__init__() + self.cipher: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES128 + self.aead: AEADMode = AEADMode.OCB + self._chunksize: int = 6 + self._salt: Optional[bytearray] = None + self.ct: bytearray = bytearray() + self.final_tag: bytearray = bytearray() + + @sdproperty + def chunksize(self) -> int: + 'The numeric value of chunksize, as opposed to the octet stored on the wire' + return 1 << (self._chunksize + 6) + + @chunksize.register + def chunksize_int(self, val: int) -> None: + n = int(log2(val)) - 6 + if n < 0: + raise ValueError(f"AEAD chunksize cannot be less than {1 << 6}") + if n > 16: + raise ValueError(f"AEAD chunksize cannot be more than {1 << 22}") + if 1 << (n + 6) != val: + raise ValueError(f"AEAD chunksize must be a power of 2") + self._chunksize = n + + @sdproperty + def salt(self) -> bytearray: + if self._salt is None: + self._salt = bytearray(os.urandom(32)) + return self._salt + + @salt.register + def salt_set(self, val: Union[bytes, bytearray]) -> None: + if len(val) != 32: + raise ValueError(f"SEIPDv2 expected 32-octet salt, got {len(val)} octets") + self._salt = bytearray(val) + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes.append(int(self.cipher)) + _bytes.append(int(self.aead)) + _bytes.append(int(self._chunksize)) + _bytes += self.salt + _bytes += self.ct + _bytes += self.final_tag + return _bytes + + def __copy__(self) -> IntegrityProtectedSKEDataV2: + skd = IntegrityProtectedSKEDataV2() + skd.cipher = self.cipher + skd.aead = self.aead + skd._chunksize = self._chunksize + skd.salt = self.salt[:] + skd.ct = self.ct[:] + skd.final_tag = self.final_tag[:] + return skd + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.cipher = SymmetricKeyAlgorithm(packet[0]) + del packet[0] + self.aead = AEADMode(packet[0]) + del packet[0] + self._chunksize = packet[0] + del packet[0] + self.salt = packet[:32] + del packet[:32] + + remainder = self.header.length - 36 + # we need both the final tag, and we need at least one full + # tag length in the main ciphertext itself: + minlen = 2 * self.aead.tag_len + if remainder < minlen: + raise ValueError(f"Not enough material for an SEIPD v2 packet using {self.aead!r}: expected at least {minlen} octets, got {remainder}") + self.ct = packet[:remainder - self.aead.tag_len] + del packet[:remainder - self.aead.tag_len] + self.final_tag = packet[:self.aead.tag_len] + del packet[:self.aead.tag_len] + + def _get_info(self) -> bytes: + 'the info parameter used for HKDF and AEAD' + return bytes([0b11000000 + self.__typeid__, self.__ver__, int(self.cipher), int(self.aead), self._chunksize]) + + def _get_aead_and_iv(self, session_key: bytes) -> Tuple[AEAD, bytes]: + ivlen = self.aead.iv_len - 8 + keylen = self.cipher.key_size // 8 + hkdf = HKDF(algorithm=SHA256(), length=keylen + ivlen, salt=bytes(self.salt), info=self._get_info()) + hkdf_output = hkdf.derive(session_key) + key = bytes(hkdf_output[:keylen]) + iv = bytes(hkdf_output[keylen:]) + aead = AEAD(self.cipher, self.aead, key) + return (aead, iv) + + def encrypt(self, key: bytes, data: bytes) -> None: + aead, iv = self._get_aead_and_iv(key) + ad = self._get_info() + new_ct: bytearray = bytearray() + chunk_index: int = 0 + # handle the chunks: + for offset in range(0, len(data), self.chunksize): + chunk: bytes = data[offset:offset + self.chunksize] + nonce: bytes = iv + self.int_to_bytes(chunk_index, 8) + new_ct += aead.encrypt(nonce, chunk, associated_data=ad) + chunk_index += 1 + + self.ct = new_ct + nonce = iv + self.int_to_bytes(chunk_index, 8) + self.final_tag += aead.encrypt(nonce, b'', associated_data=ad + self.int_to_bytes(len(data), 8)) + self.update_hlen() + + def decrypt(self, key: bytes, algo: Optional[SymmetricKeyAlgorithm] = None) -> bytearray: + if algo is not None: + raise PGPDecryptionError( + f"v2 SEIPD knows its own algorithm ({self.cipher!r}), should not be explicitly passed one, but it got {algo!r} (maybe v3 PKESK or v4 SKESK precedes it instead of v6?)") + aead, iv = self._get_aead_and_iv(key) + ad = self._get_info() + cleartext: bytearray = bytearray() + chunk_index: int = 0 + # handle the chunks: + for offset in range(0, len(self.ct), self.chunksize + self.aead.tag_len): + chunk: bytes = bytes(self.ct[offset:offset + self.chunksize + self.aead.tag_len]) + nonce: bytes = iv + self.int_to_bytes(chunk_index, 8) + cleartext += aead.decrypt(nonce, chunk, associated_data=ad) + chunk_index += 1 + + nonce = iv + self.int_to_bytes(chunk_index, 8) + final_check: bytes = aead.decrypt(nonce, bytes(self.final_tag), associated_data=ad + self.int_to_bytes(len(cleartext), 8)) + if final_check != b'': + raise PGPDecryptionError("AEAD final tag was not made over the empty string") + return cleartext + + class MDC(Packet): """ 5.14. Modification Detection Code Packet (Tag 19) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b91532e3..6b6229a8 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -22,6 +22,7 @@ from typing import Any, ByteString, Generator, Literal, List, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union from cryptography.hazmat.primitives import hashes +from cryptography.exceptions import InvalidTag from .constants import CompressionAlgorithm from .constants import EllipticCurveOID @@ -64,6 +65,7 @@ from .packet.packets import CompressedData from .packet.packets import IntegrityProtectedSKEData from .packet.packets import IntegrityProtectedSKEDataV1 +from .packet.packets import IntegrityProtectedSKEDataV2 from .packet.packets import LiteralData from .packet.packets import OnePassSignature from .packet.packets import OnePassSignatureV3 @@ -1409,7 +1411,7 @@ def decrypt(self, passphrase: Union[str, bytes]) -> PGPMessage: decmsg = PGPMessage() decmsg.parse(self._message.decrypt(key, symalg)) - except (TypeError, ValueError, NotImplementedError, PGPDecryptionError): + except (TypeError, ValueError, NotImplementedError, PGPDecryptionError, InvalidTag) as e: continue else: diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 36fad262..4a713f13 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -347,7 +347,7 @@ def test_add_altuid(self, pkspec): assert sig.cipherprefs == [SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.Camellia256] assert sig.hashprefs == [HashAlgorithm.SHA384] assert sig.compprefs == [CompressionAlgorithm.ZLIB] - assert sig.features == Features.ModificationDetection + assert sig.features == Features.pgpy_features assert sig.key_expiration == expiration - key.created assert sig.keyserver == 'about:none' assert sig.keyserverprefs == KeyServerPreferences.NoModify diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index d06b7ac0..240e1160 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -29,8 +29,8 @@ def test_v4_skesk_argon2(self, cipher: str) -> None: @pytest.mark.parametrize('aead', {'ocb', 'eax', 'gcm'}) def test_v6_skesk(self, aead: str) -> None: msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v6skesk-aes128-{aead}.pgp') - pytest.xfail('v2 SEIPD is not supported') assert msg.is_encrypted + pytest.xfail('v6 SKESK is not supported') unlocked = msg.decrypt('password') assert not unlocked.is_encrypted assert unlocked.message == b'Hello, world!' From 747e0cb1adc3b4c7ff7ffd9e2efe5dd97c10beb0 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 16:09:46 -0400 Subject: [PATCH 266/287] String2Key: add AEAD mode (we will re-use the "iv" field for nonce) --- pgpy/constants.py | 2 ++ pgpy/packet/fields.py | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index b1184d6a..9692a118 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -305,6 +305,8 @@ class S2KUsage(IntEnum): Camellia192 = 12 Camellia256 = 13 + # modern AEAD protection: + AEAD = 253 # sensible use of tamper-resistant CFB: CFB = 254 # legacy use of CFB: diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index e3dc9657..c34e33f2 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -57,6 +57,7 @@ from ..constants import S2KGNUExtension from ..constants import SymmetricKeyAlgorithm from ..constants import S2KUsage +from ..constants import AEADMode from ..decorators import sdproperty @@ -1173,7 +1174,7 @@ def derive_key(self, passphrase: Union[str, bytes], keylen_bits: int) -> bytes: class String2Key(Field): """ Used for secret key protection. - This contains an S2KUsage flag. Depending on the S2KUsage flag, it can also contain an S2KSpecifier, an encryption algorithm, and an IV. + This contains an S2KUsage flag. Depending on the S2KUsage flag, it can also contain an S2KSpecifier, an encryption algorithm, an AEAD mode, and an IV. """ @sdproperty @@ -1191,11 +1192,15 @@ def encalg_int(self, val: int) -> None: def _iv_length(self) -> int: if self.usage is S2KUsage.Unprotected: return 0 - elif self.usage in [S2KUsage.MalleableCFB, S2KUsage.CFB]: + elif self.usage in {S2KUsage.MalleableCFB, S2KUsage.CFB}: if not self._specifier._type.has_iv: # this is likely some sort of weird extension case return 0 return self.encalg.block_size // 8 + elif self.usage is S2KUsage.AEAD: + if self._aead_mode is None: + raise TypeError("missing AEAD mode for String2Key with AEAD usage") + return self._aead_mode.iv_len else: return SymmetricKeyAlgorithm(self.usage).block_size // 8 @@ -1230,6 +1235,7 @@ def __init__(self, key_version: int) -> None: self.key_version = key_version self.usage = S2KUsage.Unprotected self._encalg = SymmetricKeyAlgorithm.AES256 + self._aead_mode: Optional[AEADMode] = None self._specifier = S2KSpecifier() self._iv = None @@ -1238,6 +1244,8 @@ def __bytearray__(self) -> bytearray: _bytes.append(self.usage) if bool(self): _bytes.append(self.encalg) + if self.usage == S2KUsage.AEAD: + _bytes.append(self._specifier.aead_mode) _bytes += self._specifier.__bytearray__() if self.iv is not None: _bytes += self.iv @@ -1247,7 +1255,10 @@ def __len__(self) -> int: return len(self.__bytearray__()) def __bool__(self) -> bool: - return self.usage in [S2KUsage.CFB, S2KUsage.MalleableCFB] + # FIXME: what if usage octet is a cipher algorithm? This is + # deprecated enough that it must not be generated, but we + # might want to handle it properly on decryption + return self.usage in {S2KUsage.AEAD, S2KUsage.CFB, S2KUsage.MalleableCFB} def __copy__(self) -> String2Key: s2k = String2Key(self.key_version) @@ -1266,6 +1277,10 @@ def parse(self, packet: bytearray) -> None: self.encalg = SymmetricKeyAlgorithm(packet[0]) del packet[0] + if self.usage is S2KUsage.AEAD: + self._aead_mode = AEADMode(packet[0]) + del packet[0] + self._specifier.parse(packet) if self.encalg is not SymmetricKeyAlgorithm.Plaintext: ivlen = self._iv_length @@ -1274,9 +1289,11 @@ def parse(self, packet: bytearray) -> None: del packet[:(ivlen)] def derive_key(self, passphrase) -> bytes: - derivable = {S2KUsage.MalleableCFB, S2KUsage.CFB} + derivable = {S2KUsage.MalleableCFB, S2KUsage.CFB, S2KUsage.AEAD} if self.usage not in derivable: raise ValueError(f"can only derive key from String2Key object when usage octet is {derivable}, not {self.usage}") + if self.encalg is None: + raise ValueError("cannot derive key from String2Key object when encalg is unset") return self._specifier.derive_key(passphrase, self.encalg.key_size) From a4dcb9fe06b8e18a64f6621e54761a8e761bfa7a Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Mon, 27 Mar 2023 04:39:06 +0900 Subject: [PATCH 267/287] enable AEAD protection of secret key material (reading not yet supported) --- pgpy/packet/fields.py | 80 +++++++++++++++++++++++++++++++++++------- pgpy/packet/packets.py | 7 ++-- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c34e33f2..1c3b51d8 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -11,6 +11,7 @@ import os import collections.abc +from datetime import datetime from typing import Optional, Tuple, Union @@ -31,6 +32,8 @@ from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.asymmetric import utils +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash @@ -51,6 +54,7 @@ from ..constants import EllipticCurveOID from ..constants import ECPointFormat +from ..constants import PacketType from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm from ..constants import String2KeyType @@ -67,6 +71,7 @@ from ..symenc import _cfb_decrypt from ..symenc import _cfb_encrypt +from ..symenc import AEAD from ..types import Field from ..types import Fingerprint @@ -1450,13 +1455,44 @@ def _compute_chksum(self): def publen(self) -> int: return super().__len__() + def _aead_object_and_ad(self, passphrase: Union[str, bytes], + packet_type: PacketType, + creation_time: datetime) -> Tuple[AEAD, bytes]: + if self.__pubkey_algo__ is None: + raise ValueError(f"S2K Usage Octet indicates AEAD, but the public key algorithm of this secret key is unknown ({type(self)})") + if self.s2k._aead_mode is None: + raise ValueError(f"S2K Usage Octet indicates AEAD, but no AEAD mode set") + # The info parameter is comprised of the Packet Tag in OpenPGP format encoding (bits 7 and 6 set, bits 5-0 carry the packet tag), the packet version, and the cipher-algo and AEAD-mode used to encrypt the key material. + hkdf_info = bytes([0xc0 | int(packet_type), self.key_version, int(self.s2k.encalg), int(self.s2k._aead_mode)]) + hkdf = HKDF(algorithm=SHA256(), length=self.s2k.encalg.key_size // 8, salt=None, info=hkdf_info) + aeadkey: bytes = hkdf.derive(self.s2k.derive_key(passphrase)) + aead = AEAD(self.s2k.encalg, self.s2k._aead_mode, aeadkey) + + # As additional data, the Packet Tag in OpenPGP format encoding (bits 7 and 6 set, bits 5-0 carry the packet tag), followed by the public key packet fields, starting with the packet version number, are passed to the AEAD algorithm. + # For example, the additional data used with a Secret-Key Packet of version 4 consists of the octets 0xC5, 0x04, followed by four octets of creation time, one octet denoting the public-key algorithm, and the algorithm-specific public-key parameters. + # For a Secret-Subkey Packet, the first octet would be 0xC7. + # For a version 6 key packet, the second octet would be 0x06, and the four-octet octet count of the public key material would be included as well (see {{public-key-packet-formats}}). + associated_data = bytes([0xc0 | int(packet_type), self.key_version]) + associated_data += self.int_to_bytes(int(creation_time.timestamp()), 4) + associated_data += bytes([int(self.__pubkey_algo__)]) + pubkey_data = bytes(super().__bytearray__()) + associated_data += self.int_to_bytes(len(pubkey_data), 4) + associated_data += pubkey_data + return (aead, associated_data) + def encrypt_keyblob(self, passphrase: str, enc_alg: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, hash_alg: Optional[HashAlgorithm] = None, s2kspec: Optional[S2KSpecifier] = None, - iv: Optional[bytes] = None) -> None: - # PGPy will only ever use iterated and salted S2k mode - self.s2k.usage = S2KUsage.CFB + iv: Optional[bytes] = None, + aead_mode: Optional[AEADMode] = None, + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + if aead_mode is not None: + self.s2k.usage = S2KUsage.AEAD + self.s2k._aead_mode = aead_mode + else: + self.s2k.usage = S2KUsage.CFB self.s2k.encalg = enc_alg passed_s2kspec: bool if s2kspec is not None: @@ -1474,19 +1510,37 @@ def encrypt_keyblob(self, passphrase: str, self.s2k._specifier = copy.copy(s2kspec) self.s2k.gen_iv() - # now that String-to-Key is ready to go, derive sessionkey from passphrase - # and then unreference passphrase - sessionkey = self.s2k.derive_key(passphrase) - del passphrase - pt = bytearray() self._append_private_fields(pt) - # append a SHA-1 hash of the plaintext so far to the plaintext - pt += HashAlgorithm.SHA1.digest(pt) - - # encrypt - self.encbytes = bytearray(_cfb_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) + if self.s2k.usage is S2KUsage.CFB: + # append a SHA-1 hash of the plaintext so far to the plaintext + pt += HashAlgorithm.SHA1.digest(pt) + + sessionkey = self.s2k.derive_key(passphrase) + del passphrase + + # encrypt + self.encbytes = bytearray(_cfb_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) + elif self.s2k.usage is S2KUsage.AEAD: + if creation_time is None: + raise ValueError("S2K Usage Octet indicates AEAD, but no creation time provided") + if aead_mode is None: + if self.s2k._aead_mode is None: + raise ValueError("S2K Usage Octet indicates AEAD, but no AEAD mode provided") + else: + aead_mode = self.s2k._aead_mode + else: + if self.s2k._aead_mode is None: + self.s2k._aead_mode = aead_mode + else: + if aead_mode is not self.s2k._aead_mode: + raise ValueError(f"Conflicting String2Key AEAD Modes: {aead_mode}, {self.s2k._aead_mode}") + + (aead, associated_data) = self._aead_object_and_ad(passphrase, packet_type, creation_time) + self.encbytes = bytearray(aead.encrypt(bytes(self.s2k.iv), bytes(pt), associated_data)) + else: + raise PGPError(f"Unknown S2K usage octet {self.s2k.usage!r}, expected {S2KUsage.AEAD!r} or {S2KUsage.CFB!r}") # delete pt and clear self del pt diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 48e1d10f..d8d4dcaf 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -976,12 +976,15 @@ def protect(self, passphrase: str, enc_alg: Optional[SymmetricKeyAlgorithm] = None, hash_alg: Optional[HashAlgorithm] = None, s2kspec: Optional[S2KSpecifier] = None, - iv: Optional[bytes] = None) -> None: + iv: Optional[bytes] = None, + aead_mode: Optional[AEADMode] = None) -> None: if enc_alg is None: enc_alg = SymmetricKeyAlgorithm.AES256 if not isinstance(self.keymaterial, PrivKeyField): raise TypeError("Key material is not a private key, cannot protect") - self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec, iv=iv) + self.keymaterial.encrypt_keyblob(passphrase, enc_alg=enc_alg, hash_alg=hash_alg, s2kspec=s2kspec, iv=iv, aead_mode=aead_mode, + packet_type=self.__typeid__, + creation_time=self._created) del passphrase self.update_hlen() From 1a49ef6aef2be287f9d9605687ba5a1230b8d330 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sat, 11 Feb 2023 10:28:02 -0500 Subject: [PATCH 268/287] implement version 6 of SKESK --- pgpy/packet/packets.py | 134 ++++++++++++++++++++++++++++++++ pgpy/pgp.py | 1 + tests/test_12_crypto_refresh.py | 3 +- 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index d8d4dcaf..e32980b5 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -19,6 +19,8 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.hashes import SHA256 +from ..symenc import AEAD + from .fields import DSAPriv, DSAPub, DSASignature from .fields import ECDSAPub, ECDSAPriv, ECDSASignature from .fields import ECDHPub, ECDHPriv, ECDHCipherText @@ -72,6 +74,7 @@ 'SignatureV4', 'SKESessionKey', 'SKESessionKeyV4', + 'SKESessionKeyV6', 'OnePassSignature', 'OnePassSignatureV3', 'PrivKey', @@ -668,6 +671,137 @@ def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: self.update_hlen() +class SKESessionKeyV6(SKESessionKey): + ''' + From crypto-refresh-08: + + A version 6 Symmetric-Key Encrypted Session Key (SKESK) packet + precedes a version 2 Symmetrically Encrypted Integrity Protected Data + (v2 SEIPD, see Section 5.13.2) packet. A v6 SKESK packet MUST NOT + precede a v1 SEIPD packet or a deprecated Symmetrically Encrypted + Data packet (see Section 11.3.2.1). + + A version 6 Symmetric-Key Encrypted Session Key packet consists of: + + * A one-octet version number with value 6. + + * A one-octet scalar octet count of the following 5 fields. + + * A one-octet symmetric cipher algorithm identifier. + + * A one-octet AEAD algorithm identifier. + + * A one-octet scalar octet count of the following field. + + * A string-to-key (S2K) specifier. The length of the string-to-key + specifier depends on its type (see Section 3.7.1). + + * A starting initialization vector of size specified by the AEAD + algorithm. + + * The encrypted session key itself. + + * An authentication tag for the AEAD mode. + + HKDF is used with SHA256 as hash algorithm, the key derived from S2K + as Initial Keying Material (IKM), no salt, and the Packet Tag in the + OpenPGP format encoding (bits 7 and 6 set, bits 5-0 carry the packet + tag), the packet version, and the cipher-algo and AEAD-mode used to + encrypt the key material, are used as info parameter. Then, the + session key is encrypted using the resulting key, with the AEAD + algorithm specified for version 2 of the Symmetrically Encrypted + Integrity Protected Data packet. Note that no chunks are used and + that there is only one authentication tag. The Packet Tag in OpenPGP + format encoding (bits 7 and 6 set, bits 5-0 carry the packet tag), + the packet version number, the cipher algorithm octet, and the AEAD + algorithm octet are given as additional data. For example, the + additional data used with AES-128 with OCB consists of the octets + 0xC3, 0x06, 0x07, and 0x02. + ''' + __ver__ = 6 + + def __init__(self) -> None: + super().__init__() + self._aead_algo: AEADMode = AEADMode.OCB + self._iv: Optional[bytes] = None + self._ct_and_tag: Optional[bytes] = None + + @property + def iv(self) -> bytes: + if self._iv is None: + self._iv = os.urandom(self._aead_algo.iv_len) + return self._iv + + def __bytearray__(self) -> bytearray: + if self._ct_and_tag is None: + raise ValueError("SKESK has not been fully initialized, cannot write") + _bytes = bytearray() + _bytes += super().__bytearray__() + + s2k_field: bytearray = self.s2kspec.__bytearray__() + + _bytes.append(3 + len(s2k_field) + self._aead_algo.iv_len) + _bytes.append(self.symalg) + _bytes.append(self._aead_algo) + _bytes.append(len(s2k_field)) + _bytes += s2k_field + _bytes += self.iv + _bytes += self._ct_and_tag + return _bytes + + def _get_info(self) -> bytes: + 'used for HKDF info and AEAD additional data' + return bytes([0b11000000 + self.__typeid__, self.__ver__, int(self.symalg), int(self._aead_algo)]) + + def _get_derived_key(self, passphrase: Union[str, bytes]) -> bytes: + s2k_derived_key = self.s2kspec.derive_key(passphrase, self.symalg.key_size) + hkdf = HKDF(algorithm=SHA256(), length=self.symalg.key_size // 8, salt=None, info=self._get_info()) + return hkdf.derive(s2k_derived_key) + + def _get_aead(self, passphrase: Union[str, bytes]) -> AEAD: + derived_key = self._get_derived_key(passphrase) + return AEAD(self.symalg, self._aead_algo, derived_key) + + def decrypt_sk(self, passphrase: Union[str, bytes]) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + if self._iv is None or self._ct_and_tag is None: + raise ValueError("SKESK is not fully initialized, cannot decrypt") + aead = self._get_aead(passphrase) + return None, aead.decrypt(nonce=self._iv, data=self._ct_and_tag, associated_data=self._get_info()) + + def encrypt_sk(self, passphrase: Union[str, bytes], sk: ByteString) -> None: + aead = self._get_aead(passphrase) + self._ct_and_tag = aead.encrypt(nonce=self.iv, data=bytes(sk), associated_data=self._get_info()) + # update header length and return sk + self.update_hlen() + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + param_len: int = packet[0] + del packet[0] + # FIXME: we should assert that the length of the packets up to but not including the ciphertext match this param_len count. + + self.symalg = SymmetricKeyAlgorithm(packet[0]) + del packet[0] + + self._aead_algo = AEADMode(packet[0]) + del packet[0] + + s2k_len = packet[0] + del packet[0] + + self.s2kspec.parse(packet) + + self._iv = bytes(packet[:self._aead_algo.iv_len]) + del packet[:self._aead_algo.iv_len] + + # how do we know the size of the encrypted session key? + # we cannot know this during this packet parsing alone, we assume it runs up through the tag length. + # due to the cryptography module's AEAD interface, we do not separate out the tag from the ciphertext. + ctlen = self.header.length - (param_len + 2) + self._ct_and_tag = bytes(packet[:ctlen]) + del packet[:ctlen] + + class OnePassSignature(VersionedPacket): '''Holds common members of various OPS packet versions''' __typeid__ = PacketType.OnePassSignature diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 6b6229a8..1bbd9777 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -77,6 +77,7 @@ from .packet.packets import Marker from .packet.packets import SKESessionKey from .packet.packets import SKESessionKeyV4 +from .packet.packets import SKESessionKeyV6 from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub from .packet.fields import PrivKey as field_PrivKey diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index 240e1160..75c4bee0 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -30,7 +30,8 @@ def test_v4_skesk_argon2(self, cipher: str) -> None: def test_v6_skesk(self, aead: str) -> None: msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v6skesk-aes128-{aead}.pgp') assert msg.is_encrypted - pytest.xfail('v6 SKESK is not supported') + if aead == 'eax': + pytest.xfail('AEAD Mode EAX not supported') unlocked = msg.decrypt('password') assert not unlocked.is_encrypted assert unlocked.message == b'Hello, world!' From e96068d218c5c1c0cac1882a2da4dc95bda5890c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 15 Feb 2023 14:16:53 -0500 Subject: [PATCH 269/287] implement padding packet --- pgpy/constants.py | 1 + pgpy/packet/packets.py | 32 ++++++++++++++++++++++++++++++++ pgpy/pgp.py | 5 +++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 9692a118..ce454f39 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -84,6 +84,7 @@ class PacketType(IntEnum): UserAttribute = 17 SymmetricallyEncryptedIntegrityProtectedData = 18 ModificationDetectionCode = 19 + Padding = 21 @classmethod def _missing_(cls, val: object) -> PacketType: diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index e32980b5..309b6a60 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -86,6 +86,7 @@ 'CompressedData', 'SKEData', 'Marker', + 'Padding', 'LiteralData', 'Trust', 'UserID', @@ -1369,6 +1370,37 @@ def parse(self, packet): del packet[:self.header.length] +class Padding(Packet): + __typeid__ = PacketType.Padding + + def __init__(self) -> None: + super().__init__() + self.data: bytes = b'' + + @sdproperty + def size(self) -> int: + 'The full size of the packet in its standard form' + return self.header.length + 2 + + @size.register + def size_int(self, val: int) -> None: + if val < 2: + raise ValueError(f"padding needs to be at least 2 octets, not {val}") + self.data = os.urandom(val - 2) + self.update_hlen() + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes += self.data + return _bytes + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.data = packet[:self.header.length] + del packet[:self.header.length] + + class LiteralData(Packet): """ 5.9. Literal Data Packet (Tag 11) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 1bbd9777..197eb426 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -75,6 +75,7 @@ from .packet.packets import SignatureV4 from .packet.packets import SKEData from .packet.packets import Marker +from .packet.packets import Padding from .packet.packets import SKESessionKey from .packet.packets import SKESessionKeyV4 from .packet.packets import SKESessionKeyV6 @@ -1183,7 +1184,7 @@ def __iter__(self): yield sig def __or__(self, other) -> PGPMessage: - if isinstance(other, Marker): + if isinstance(other, (Marker, Padding)): return self if isinstance(other, CompressedData): @@ -2969,7 +2970,7 @@ def parse(self, data): def _getpkt(d): return Packet(d) if d else None # some packets are filtered out - getpkt = filter(lambda p: p.header.typeid is not PacketType.Trust, iter(functools.partial(_getpkt, data), None)) + getpkt = filter(lambda p: p.header.typeid not in {PacketType.Trust, PacketType.Padding, PacketType.Marker}, iter(functools.partial(_getpkt, data), None)) def pktgrouper(): class PktGrouper: From a11a537833e4a9fa29b18054924a1b52edcb0fba Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 15:31:55 -0400 Subject: [PATCH 270/287] Handle v6 fingerprints --- pgpy/packet/packets.py | 2 +- pgpy/types.py | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 309b6a60..ec87d257 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1054,7 +1054,7 @@ def fingerprint(self) -> Fingerprint: fp.update(self.keymaterial.__bytearray__()[:plen]) # and return the digest - return Fingerprint(fp.finalize()) + return Fingerprint(fp.finalize(), version=4) def __init__(self) -> None: super().__init__() diff --git a/pgpy/types.py b/pgpy/types.py index 7254c592..0e9536f5 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -744,7 +744,12 @@ class Fingerprint(str): """ @property def keyid(self) -> KeyID: - return KeyID(self[-16:]) + if self._version == 4: + return KeyID(self[-16:]) + elif self._version == 6: + return KeyID(self[:16]) + else: + raise ValueError(f"Do not know how to calculate a keyID for fingerprint version {self._version}.") @property def shortid(self) -> str: @@ -754,27 +759,43 @@ def shortid(self) -> str: @sdproperty def version(self) -> int: - 'Returns None if the version is unknown' + 'The version of the key this fingerprint belongs to' return self._version @version.register def version_int(self, version: int) -> None: self._version: int = version + @staticmethod + def confirm_expected_length(version: int, length: int) -> None: + 'Raises PGPError if fingerprint version `version` should not be length `length` (in octets)' + expected_lengths = { 4: 20, 6: 32 } + if version in expected_lengths and length != expected_lengths[version]: + raise PGPError(f"Version {version} fingerprints should be {expected_lengths[version]} octets, got {length} octets ") + def __new__(cls, content: Union[str, bytes, bytearray], version=None) -> Fingerprint: if isinstance(content, Fingerprint): + if version is not None and version != content.version: + raise ValueError(f"requested version {version} but existing Fingerprint is version {content.version}") + return content if isinstance(content, (bytes, bytearray)): - if len(content) != 20: - raise ValueError(f'binary Fingerprint must be 20 bytes, not {len(content)}') - return Fingerprint(binascii.b2a_hex(content).decode('latin-1').upper()) - # validate input before continuing: this should be a string of 40 hex digits + if len(content) not in {20, 32}: + raise ValueError(f'binary Fingerprint must be either 20 or 32 bytes, not {len(content)}') + return Fingerprint(binascii.b2a_hex(content).decode('latin-1').upper(), version=version) content = content.upper().replace(' ', '') - if not re.match(r'^[0-9A-F]{40}$', content): - raise ValueError('Fingerprint must be a string of 40 hex digits') + if not re.match(r'^[0-9A-F]{40,64}$', content): + raise ValueError('Fingerprint must be a string of 40 or 64 hex digits') ret = str.__new__(cls, content) - ret._version = 4 if version is None else version + if version is None: + if len(content) == 64: + ret._version = 6 + else: + ret._version = 4 + else: + Fingerprint.confirm_expected_length(version, len(content) // 2) + ret._version = version return ret def __eq__(self, other: object) -> bool: From 819157160fc46ee0f7199415924cec560c33e736 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 28 Jun 2023 00:03:35 -0400 Subject: [PATCH 271/287] add v6 PKESK --- pgpy/packet/packets.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index ec87d257..c30ffb0d 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -60,6 +60,7 @@ from ..decorators import sdproperty from ..errors import PGPDecryptionError +from ..errors import PGPEncryptionError from ..symenc import _cfb_decrypt from ..symenc import _cfb_encrypt @@ -70,6 +71,7 @@ __all__ = ['PKESessionKey', 'PKESessionKeyV3', + 'PKESessionKeyV6', 'Signature', 'SignatureV4', 'SKESessionKey', @@ -284,6 +286,80 @@ def parse(self, packet): del packet[:(self.header.length - 10)] +class PKESessionKeyV6(PKESessionKey): + __ver__ = 6 + + def __init__(self) -> None: + super().__init__() + self._encrypter: Optional[Fingerprint] = None + + @sdproperty + def encrypter(self) -> Optional[Fingerprint]: + return self._encrypter + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes += super().__bytearray__() + if self._encrypter is None: + _bytes.append(0) + else: + _bytes.append(len(bytes(self._encrypter)) + 1) + _bytes.append(self._encrypter.version) + _bytes += bytes(self._encrypter) + _bytes.append(self.pkalg) + _bytes += self.ct.__bytearray__() if self.ct is not None else b'\x00' * (self.header.length - 10) + return _bytes + + def __copy__(self) -> PKESessionKeyV6: + sk = self.__class__() + sk.header = copy.copy(self.header) + sk._encrypter = self._encrypter + sk.pkalg = self.pkalg + if self.ct is not None: + sk.ct = copy.copy(self.ct) + return sk + + def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + algo: Optional[SymmetricKeyAlgorithm] + symkey: bytes + if self.ct is None: + raise PGPDecryptionError("PKESKv6: Tried to decrypt session key when ciphertext was not initialized") + return pk.keymaterial.decrypt(self.ct, pk.fingerprint, False) + + def encrypt_sk(self, pk: PubKey, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes, **kwargs) -> None: + if symalg is not None: + raise ValueError(f"PKESKv6 does not encrypt the symmetric key algorithm, but {symalg} was supplied (should be None)") + self._encrypter = pk.fingerprint + self.pkalg = pk.pkalg + if self.ct is None: + raise PGPEncryptionError(f"Don't know how to encrypt to {pk.pkalg!r}") + self.ct = pk.keymaterial.encrypt(None, symkey, pk.fingerprint) + self.update_hlen() + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + fplen = packet[0] + del packet[0] + + if fplen: + # the key version + fpversion = packet[0] + del packet[0] + + Fingerprint.confirm_expected_length(fpversion, fplen - 1) + # extract the fingerprint + self._encrypter = Fingerprint(bytes(packet[:fplen - 1]), version=fpversion) + del packet[:fplen - 1] + + self.pkalg = packet[0] + del packet[0] + + if self.ct is not None: + self.ct.parse(packet) + else: # pragma: no cover + del packet[:(self.header.length - (2 + fplen + 1))] + + class Signature(VersionedPacket): __typeid__ = PacketType.Signature __ver__ = 0 From 400ede11102c7f3fa37c131e8e19cb2113154283 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 17 Nov 2022 15:25:47 -0500 Subject: [PATCH 272/287] add v6 keys, v6 signatures - serializing v6 keys is subtly different than v4 keys - v6 certs (or later) don't need a UID any longer - v6 signature salt length varies by hash algorithm --- pgpy/constants.py | 13 ++ pgpy/decorators.py | 4 +- pgpy/packet/fields.py | 31 +++- pgpy/packet/packets.py | 341 +++++++++++++++++++++++++++++++++++++++++ pgpy/pgp.py | 65 ++++++-- pgpy/types.py | 6 +- 6 files changed, 437 insertions(+), 23 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index ce454f39..2aec611b 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -421,6 +421,19 @@ def digest(self, data: bytes) -> bytes: ctx.update(data) return ctx.finalize() + @property + def sig_salt_size(self) -> Optional[int]: + ss = { + HashAlgorithm.SHA256: 16, + HashAlgorithm.SHA384: 24, + HashAlgorithm.SHA512: 32, + HashAlgorithm.SHA224: 16, + HashAlgorithm.SHA3_256: 16, + HashAlgorithm.SHA3_512: 32, + } + + return ss.get(self, None) + class ECFields(NamedTuple): name: str diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 303f24d8..4d16ba93 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -115,8 +115,8 @@ def _action(key, *args, **kwargs): if key._key is None: raise PGPError("No key!") - # keys must have a user id: - if len(key._uids) == 0 and key.is_primary: + # v4 and earlier keys must have a user id: + if len(key._uids) == 0 and key.is_primary and key._key.__ver__ < 6: # if a key is in the process of being created, it needs to be allowed to certify its own user id if action is not key.certify.__wrapped__: raise PGPError("Key is not complete - please add a User ID!") diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 1c3b51d8..b2829095 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1248,12 +1248,21 @@ def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes.append(self.usage) if bool(self): - _bytes.append(self.encalg) - if self.usage == S2KUsage.AEAD: - _bytes.append(self._specifier.aead_mode) - _bytes += self._specifier.__bytearray__() + conditionals = bytearray() + conditionals.append(self.encalg) + if self.usage is S2KUsage.AEAD: + if self._aead_mode is None: + raise TypeError("AEAD Mode was not set") + conditionals.append(self._aead_mode) + s2kbytes = self._specifier.__bytearray__() + if self.key_version == 6 and self.usage in {S2KUsage.MalleableCFB, S2KUsage.CFB, S2KUsage.AEAD}: + conditionals.append(len(s2kbytes)) + conditionals += s2kbytes if self.iv is not None: - _bytes += self.iv + conditionals += self.iv + if self.key_version == 6: + _bytes.append(len(conditionals)) + _bytes += conditionals return _bytes def __len__(self) -> int: @@ -1279,6 +1288,10 @@ def parse(self, packet: bytearray) -> None: del packet[0] if bool(self): + if self.key_version == 6: + paramlen = packet[0] + del packet[0] + self.encalg = SymmetricKeyAlgorithm(packet[0]) del packet[0] @@ -1286,6 +1299,10 @@ def parse(self, packet: bytearray) -> None: self._aead_mode = AEADMode(packet[0]) del packet[0] + if self.key_version == 6: + speclen = packet[0] + del packet[0] + self._specifier.parse(packet) if self.encalg is not SymmetricKeyAlgorithm.Plaintext: ivlen = self._iv_length @@ -1407,7 +1424,7 @@ def _append_private_fields(self, _bytes: bytearray) -> None: for field in self.__privfields__: _bytes += getattr(self, field).to_mpibytes() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes += super().__bytearray__() @@ -1418,7 +1435,7 @@ def __bytearray__(self): else: self._append_private_fields(_bytes) - if self.s2k.usage is S2KUsage.Unprotected: + if self.s2k.usage is S2KUsage.Unprotected and self.key_version == 4: # checksum is only appropriate for v4 keys: _bytes += self.chksum return _bytes diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index c30ffb0d..e27a9f9b 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -48,6 +48,7 @@ from .types import VersionedHeader from ..constants import PacketType +from ..constants import EllipticCurveOID from ..constants import CompressionAlgorithm from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm @@ -61,6 +62,7 @@ from ..errors import PGPDecryptionError from ..errors import PGPEncryptionError +from ..errors import PGPError from ..symenc import _cfb_decrypt from ..symenc import _cfb_encrypt @@ -74,17 +76,22 @@ 'PKESessionKeyV6', 'Signature', 'SignatureV4', + 'SignatureV6', 'SKESessionKey', 'SKESessionKeyV4', 'SKESessionKeyV6', 'OnePassSignature', 'OnePassSignatureV3', + 'OnePassSignatureV6', 'PrivKey', 'PubKey', 'PubKeyV4', + 'PubKeyV6', 'PrivKeyV4', + 'PrivKeyV6', 'PrivSubKey', 'PrivSubKeyV4', + 'PrivSubKeyV6', 'CompressedData', 'SKEData', 'Marker', @@ -94,6 +101,7 @@ 'UserID', 'PubSubKey', 'PubSubKeyV4', + 'PubSubKeyV6', 'UserAttribute', 'IntegrityProtectedSKEData', 'IntegrityProtectedSKEDataV1', @@ -610,6 +618,154 @@ def make_onepass(self) -> OnePassSignatureV3: return onepass +class SignatureV6(Signature): + """from crypto-refresh-07: + + A v6 Signature Packet is identical to a v4 Signature Packet, with + two exceptions: + + - the size of the subpacket fields are four-octets instead of two + + - they contain a salt, which is hashed first. + + """ + __ver__ = 6 + __subpacket_width__ = 4 + + def __init__(self) -> None: + super().__init__() + self._salt: Optional[bytes] = None + + @property + def signer(self) -> Optional[Fingerprint]: + if 'IssuerFingerprint' in self.subpackets: + return self.subpackets['IssuerFingerprint'][-1].issuer_fingerprint + return None + + @sdproperty + def salt(self) -> bytes: + if self._salt is None: + saltsize = self.halg.sig_salt_size + if saltsize is None: + raise PGPError(f"Cannot make a v6 signature with hash algorithm {self.halg!r} because there is no applicable salt size") + self._salt = bytes(os.urandom(saltsize)) + return self._salt + + @salt.register + def salt_bytes(self, val: Union[bytes, bytearray]): + if self._halg is None: + raise PGPError(f"Must set the hash algorithm of a v6 signature before setting a salt, as the salt length varies by hash algorithm") + saltsize = self._halg.sig_salt_size + if saltsize is None: + raise PGPError(f"Cannot make a v6 signature with hash algorithm {self.halg!r} because there is no applicable salt size") + if len(val) != saltsize: + raise ValueError(f"SignatureV6 salt must be {saltsize} octets for a v6 signature with hash {self.halg!r}, not {len(val)}") + if self._salt is not None: + raise ValueError(f"salt is already set, it cannot be set multiple times!") + self._salt = bytes(val) + + def __bytearray__(self): + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes += self.int_to_bytes(self.sigtype) + _bytes += self.int_to_bytes(self.pubalg) + _bytes += self.int_to_bytes(self.halg) + _bytes += self.subpackets.__bytearray__() + _bytes += self.hash2 + _bytes.append(self.halg.sig_salt_size) + _bytes += self.salt + _bytes += self.signature.__bytearray__() + + return _bytes + + def canonical_bytes(self): + '''Returns a bytearray that is the way the signature packet + should be represented if it is itself being signed. + + from RFC 4880 section 5.2.4: + + When a signature is made over a Signature packet (type 0x50), the + hash data starts with the octet 0x88, followed by the four-octet + length of the signature, and then the body of the Signature packet. + (Note that this is an old-style packet header for a Signature packet + with the length-of-length set to zero.) The unhashed subpacket data + of the Signature packet being hashed is not included in the hash, and + the unhashed subpacket data length value is set to zero. + + ''' + _body = bytearray() + _body += self.int_to_bytes(self.header.version) + _body += self.int_to_bytes(self.sigtype) + _body += self.int_to_bytes(self.pubalg) + _body += self.int_to_bytes(self.halg) + _body += self.subpackets.__hashbytearray__() + _body += self.int_to_bytes(0, minlen=4) # empty unhashed subpackets + _body += self.hash2 + _body.append(self.halg.sig_salt_size) + _body += self.salt + _body += self.signature.__bytearray__() + + _hdr = bytearray() + _hdr += b'\x88' + _hdr += self.int_to_bytes(len(_body), minlen=4) + return _hdr + _body + + def __copy__(self): + spkt = SignatureV6() + spkt.header = copy.copy(self.header) + spkt._sigtype = self._sigtype + spkt._pubalg = self._pubalg + spkt._halg = self._halg + + spkt.subpackets = copy.copy(self.subpackets) + spkt.hash2 = copy.copy(self.hash2) + spkt.salt = copy.copy(self.salt) + spkt.signature = copy.copy(self.signature) + + return spkt + + def parse(self, packet): + super().parse(packet) + self.sigtype = packet[0] + del packet[0] + + self.pubalg = packet[0] + del packet[0] + + self.halg = packet[0] + del packet[0] + + self.subpackets.parse(packet) + + self.hash2 = packet[:2] + del packet[:2] + + saltsize = packet[0] + del packet[0] + + self.salt = packet[:saltsize] + del packet[:saltsize] + + self.signature.parse(packet) + + def make_onepass(self) -> OnePassSignatureV6: + signer = self.signer + if signer is None: + raise ValueError("Cannot make a one-pass signature without knowledge of who the signer is") + if isinstance(signer, KeyID): + raise ValueError("Cannot make a v6 one-pass signature without the full fingerprint of the signer") + + onepass = OnePassSignatureV6() + onepass.sigtype = self.sigtype + onepass.halg = self.halg + onepass.pubalg = self.pubalg + onepass.salt = self.salt + + onepass._signer = signer + onepass.update_hlen() + return onepass + + class SKESessionKey(VersionedPacket): __typeid__ = PacketType.SymmetricKeyEncryptedSessionKey __ver__ = 0 @@ -1017,6 +1173,81 @@ def parse(self, packet: bytearray) -> None: del packet[0] +class OnePassSignatureV6(OnePassSignature): + __ver__ = 6 + + def __init__(self) -> None: + super().__init__() + self._signer = Fingerprint(b'\x00' * 32, version=6) + self._salt: Optional[bytes] = None + + @property + def signer(self) -> Fingerprint: + return self._signer + + def signer_set(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: + if isinstance(val, KeyID): + raise ValueError("Cannot set the signer of a v6 OPS packet with only a KeyID. Use a Fingerprint instead.") + self._signer = Fingerprint(val) + + @sdproperty + def salt(self) -> Optional[bytes]: + return self._salt + + @salt.register + def salt_set(self, val: bytes) -> None: + if self.halg is not None and len(val) != self.halg.sig_salt_size: + raise ValueError(f"v6 OPS: signatures over {self.halg!r} must use a salt of {self.halg.sig_salt_size} octets, got {len(val)} octets of salt.") + self._salt = val + + def halg_set(self, val: int) -> None: + if not isinstance(val, HashAlgorithm): + val = HashAlgorithm(val) + if self._salt is not None and len(self._salt) != val.sig_salt_size: + raise ValueError( + f"v6 OPS: salt is already set to {len(self._salt)} octets, cannot set hash algorithm to {val!r} since it would require {val.sig_salt_size} octets.") + self._halg = val + + def __bytearray__(self) -> bytearray: + if (self.sigtype is None + or self.halg is None + or self.salt is None + or self.pubalg is None): + raise ValueError("v6 OPS has not been fully initialized, it cannot be converted to wire format") + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes.append(self.sigtype) + _bytes.append(self.halg) + _bytes.append(self.pubalg) + _bytes.append(self.halg.sig_salt_size) + _bytes += self.salt + _bytes += bytes(self.signer) + _bytes.append(not self.nested) + return _bytes + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.sigtype = SignatureType(packet[0]) + del packet[0] + + self.halg = HashAlgorithm(packet[0]) + del packet[0] + + self.pubalg = PubKeyAlgorithm(packet[0]) + del packet[0] + + saltsize: int = packet[0] + del packet[0] + self.salt = bytes(packet[:saltsize]) + del packet[:saltsize] + + self._signer = Fingerprint(bytes(packet[:32])) + del packet[:32] + + self.nested = (packet[0] == 0) + del packet[0] + + class PubKey(VersionedPacket, Primary, Public): __typeid__ = PacketType.PublicKey __ver__ = 0 @@ -1165,6 +1396,83 @@ def parse(self, packet: bytearray) -> None: del packet[:pend] +class PubKeyV6(PubKey): + '''From crypto-refresh-07 a v6 key is the same as a v4 key but with a + four-octet count of the size of the public key material, and a + different fingerprint format + + ''' + __ver__ = 6 + + def __init__(self): + super().__init__() + + def __bytearray__(self): + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes += self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4) + _bytes += self.int_to_bytes(self.pkalg) + _bytes += self.int_to_bytes(self.keymaterial.publen(), 4) + _bytes += self.keymaterial.__bytearray__() + return _bytes + + @property + def fingerprint(self): + # A V6 fingerprint is the SHA-256 hash of the octet 0x9b, followed by the four-octet packet length, + # followed by the entire Public-Key packet starting with the version field. + fp = HashAlgorithm.SHA256.hasher + + plen = self.keymaterial.publen() + bcde_len = self.int_to_bytes(10 + plen, 4) + + # a.1) 0x9b (1 octet) + # a.2) four-octet length + fp.update(b'\x9B' + bcde_len) + # b) version number = 6 (1 octet); + fp.update(b'\x06') + # c) timestamp of key creation (4 octets); + fp.update(self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4)) + # d) algorithm (1 octet): 17 = DSA (example); + fp.update(self.int_to_bytes(self.pkalg)) + # e) four-octet length + fp.update(self.int_to_bytes(plen, 4)) + # f) Algorithm-specific fields. + fp.update(self.keymaterial.__bytearray__()[:plen]) + + # and return the digest + return Fingerprint(fp.finalize(), version=6) + + def parse(self, packet): + super().parse(packet) + + self.created = packet[:4] + del packet[:4] + + self.pkalg = packet[0] + del packet[0] + + # the entire point of this field is to be able to safely + # convert a v6 secret key packet to a v6 public key packet + # without knowing the details of the asymmetric algorithm. We + # aren't trying to do that, so maybe we can just ignore it? + + # or, maybe it is better to move it into the self.keymaterial + # object directly, although those objects today don't seem to + # know whether they're v4 or v6, so they can't parse + # differently. + pubsize = self.bytes_to_int(packet[:4]) + del packet[:4] + + # bound keymaterial to the remaining length of the packet or the maximum + pend = self.header.length - 10 + if pubsize > pend: + raise ValueError(f"v6 public key material is larger ({pubsize} octets) than the remaining key packet ({pend} octets)") + if not isinstance(self, PrivKey) and pubsize < pend: + raise ValueError(f"v6 public key packet has a key material size that does not exhaust the packet ({pend - pubsize} octets left over)") + self.keymaterial.parse(packet[:pend]) + del packet[:pend] + + class PrivKey(PubKey, Private): __typeid__ = PacketType.SecretKey __ver__ = 0 @@ -1259,6 +1567,31 @@ def pubkey(self) -> Public: return pk +class PrivKeyV6(PrivKey, PubKeyV6): + __ver__ = 6 + + @classmethod + def new(cls, key_algorithm, key_size: Optional[Union[int, EllipticCurveOID]] = None, created=None) -> PrivKeyV6: + # build a key packet + pk = PrivKeyV6() + pk.pkalg = key_algorithm + if pk.keymaterial is None: + raise NotImplementedError(key_algorithm) + if not isinstance(pk.keymaterial, PrivKeyField): + raise TypeError(f"Secret key material is missing, got {type(pk.keymaterial)} instead!") + pk.keymaterial._generate(key_size) + if created is not None: + pk.created = created + pk.update_hlen() + return pk + + def pubkey(self) -> Public: + # return a copy of ourselves, but just the public half + pk = PubKeyV6() if not isinstance(self, PrivSubKeyV6) else PubSubKeyV6() + self._extract_pubkey(pk) + return pk + + class PrivSubKey(PrivKey, Sub): __typeid__ = PacketType.SecretSubKey __ver__ = 0 @@ -1268,6 +1601,10 @@ class PrivSubKeyV4(PrivSubKey, PrivKeyV4): __ver__ = 4 +class PrivSubKeyV6(PrivSubKey, PrivKeyV6): + __ver__ = 6 + + class CompressedData(Packet): """ 5.6. Compressed Data Packet (Tag 8) @@ -1713,6 +2050,10 @@ class PubSubKeyV4(PubSubKey, PubKeyV4): __ver__ = 4 +class PubSubKeyV6(PubSubKey, PubKeyV6): + __ver__ = 6 + + class UserAttribute(Packet): """ 5.12. User Attribute Packet (Tag 17) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 197eb426..f965ff14 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -57,6 +57,9 @@ from .packet import PrivSubKey from .packet import PrivKeyV4 from .packet import PrivSubKeyV4 +from .packet import PubKeyV6 +from .packet import PrivKeyV6 +from .packet import PrivSubKeyV6 from .packet import Public from .packet import Sub from .packet import UserID @@ -73,6 +76,7 @@ from .packet.packets import PKESessionKeyV3 from .packet.packets import Signature from .packet.packets import SignatureV4 +from .packet.packets import SignatureV6 from .packet.packets import SKEData from .packet.packets import Marker from .packet.packets import Padding @@ -385,11 +389,14 @@ def new(cls, sigtype, pkalg: PubKeyAlgorithm, halg: HashAlgorithm, signer: Finge sigpkt: Signature if created is None: created = datetime.now(timezone.utc) - sigpkt = SignatureV4() + if signer.version == 6: + sigpkt = SignatureV6() + else: + sigpkt = SignatureV4() sigpkt.header.typeid = PacketType.Signature if not isinstance(sigpkt.header, VersionedHeader): raise TypeError(f"Signature packet should have VersionedHeader, had {type(sigpkt.header)}") - sigpkt.header.version = 4 + sigpkt.header.version = signer.version sigpkt.subpackets.addnew('CreationTime', critical=True, hashed=True, created=created) if signer.version <= 4: sigpkt.subpackets.addnew('Issuer', _issuer=signer.keyid) @@ -457,6 +464,17 @@ def attests_to(self, othersig: PGPSignature) -> bool: h.update(othersig._signature.canonical_bytes()) return h.finalize() in self.attested_certifications + def _get_key_length_prefix(self, keylen: int) -> bytes: + if self._signature is None: + raise ValueError("uninitialized PGPSignature") + if not isinstance(self._signature.header, VersionedHeader): + raise TypeError(f"PGPSignature expects a Versioned Header, got {type(self._signature.header)}") + if self._signature.header.version == 4: + return b'\x99' + self.int_to_bytes(keylen, 2) + elif self._signature.header.version == 6: + return b'\x9b' + self.int_to_bytes(keylen, 4) + raise ValueError(f"cannot assemble key length prefix from version {self._signature.header.version}") + def hashdata(self, subject: PGPSubject) -> bytes: if self._signature is None: raise TypeError("called hashdata on uninitializaed PGPSignature") @@ -469,6 +487,12 @@ def hashdata(self, subject: PGPSubject) -> bytes: subject_bytes = subject.encode('charmap') subject = subject_bytes + if not isinstance(self._signature.header, VersionedHeader): + raise TypeError(f"PGPSignature expects a Versioned Header, got {type(self._signature.header)}") + + if isinstance(self._signature, SignatureV6): + _data += self._signature.salt + """ All signatures are formed by producing a hash over the signature data, and then using the resulting hash in the signature algorithm. @@ -518,7 +542,7 @@ def hashdata(self, subject: PGPSubject) -> bytes: _s = subject.hashdata if len(_s) > 0: - _data += b'\x99' + self.int_to_bytes(len(_s), 2) + _s + _data += self._get_key_length_prefix(len(_s)) + _s if self.type in {SignatureType.Subkey_Binding, SignatureType.PrimaryKey_Binding}: """ @@ -538,7 +562,7 @@ def hashdata(self, subject: PGPSubject) -> bytes: else: _s = subject.hashdata - _data += b'\x99' + self.int_to_bytes(len(_s), 2) + _s + _data += self._get_key_length_prefix(len(_s)) + _s if self.type in {SignatureType.KeyRevocation, SignatureType.SubkeyRevocation, SignatureType.DirectlyOnKey}: """ @@ -574,10 +598,10 @@ def hashdata(self, subject: PGPSubject) -> bytes: if self.type == SignatureType.SubkeyRevocation: # hash the primary key first if this is a Subkey Revocation signature _s = subject.parent.hashdata - _data += b'\x99' + self.int_to_bytes(len(_s), 2) + _s + _data += self._get_key_length_prefix(len(_s)) + _s _s = subject.hashdata - _data += b'\x99' + self.int_to_bytes(len(_s), 2) + _s + _data += self._get_key_length_prefix(len(_s)) + _s if self.type in {SignatureType.Generic_Cert, SignatureType.Persona_Cert, SignatureType.Casual_Cert, SignatureType.Positive_Cert, SignatureType.CertRevocation}: @@ -639,8 +663,10 @@ def hashdata(self, subject: PGPSubject) -> bytes: hcontext += self._signature.subpackets.__hashbytearray__() hlen = len(hcontext) _data += hcontext - _data += b'\x04\xff' - _data += self.int_to_bytes(hlen, 4) + _data.append(self._signature.header.version) + _data.append(0xff) + if self._signature.header.version in [4, 6]: + _data += self.int_to_bytes(hlen % (2**32), 4) return bytes(_data) def make_onepass(self) -> OnePassSignature: @@ -1782,8 +1808,6 @@ def new(cls, key_algorithm: PubKeyAlgorithm, :type created: :py:obj:`~datetime.datetime` or ``None`` :return: A newly generated :py:obj:`PGPKey` """ - if version != 4: - raise ValueError(f"PGPKey.new requested key version {version}, but can only generate v4 keys.") # new private key shell first key = PGPKey() @@ -1792,7 +1816,12 @@ def new(cls, key_algorithm: PubKeyAlgorithm, key_algorithm = PubKeyAlgorithm.RSAEncryptOrSign # generate some key data to match key_algorithm and key_size - key._key = PrivKeyV4.new(key_algorithm, key_size, created=created) + if version == 4: + key._key = PrivKeyV4.new(key_algorithm, key_size, created=created) + elif version == 6: + key._key = PrivKeyV6.new(key_algorithm, key_size, created=created) + else: + raise ValueError(f"Requested key version {version}, only know how to make v4 or v6 keys") return key @@ -2107,7 +2136,10 @@ def add_subkey(self, key: PGPKey, **prefs) -> None: raise PGPError("Cannot add a PGPKey as a subkey when it has no proper key object") # convert key into a subkey - npk: PrivSubKey = PrivSubKeyV4() + if key._key.__ver__ == 6: + npk: PrivSubKey = PrivSubKeyV6() + else: + npk = PrivSubKeyV4() if key._key is None: raise PGPError("Cannot add a subkey with an unknown algorithm") npk.pkalg = key._key.pkalg @@ -2221,9 +2253,16 @@ def _sign(self, subject: PGPSubject, sig: PGPSignature, **prefs) -> PGPSignature sig._signature.sigtype = SignatureType.Standalone if prefs.pop('include_issuer_fingerprint', True): - if isinstance(self._key, PrivKeyV4): + if isinstance(self._key, (PrivKeyV4, PrivKeyV6)): sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _fpr=self.fingerprint) + salt = prefs.pop('salt', None) + if salt: + if isinstance(sig._signature, SignatureV6): + sig._signature.salt = salt + else: + warnings.warn(f"offered a salt for a signature that is not v6 ({type(sig._signature)})") + # place the subpackets in order by the subpacket type identifier octet sig._signature.subpackets._normalize() diff --git a/pgpy/types.py b/pgpy/types.py index 0e9536f5..55bf4287 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -830,7 +830,11 @@ def __wireformat__(self) -> bytes: def __pretty__(self) -> str: content = self if not bool(re.match(r'^[A-F0-9]{40}$', content)): - raise ValueError("Expected: String of 40 hex digits") + if bool(re.match(r'^[A-F0-9]{64}$', content)): + # Based on size, this is a v6 fingerprint, we don't have a preferred "pretty" format other than the raw hex string: + return str(content) + else: + raise ValueError("Expected: String of 40 or 64 hex digits") halves = [ [content[i:i + 4] for i in range(0, 20, 4)], From 9462547e07b308ef66882eee3bf4d33a03200a34 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Feb 2023 16:43:06 -0500 Subject: [PATCH 273/287] Implement Ed25519 and Ed448 as top-level pubkey algorithm ids --- pgpy/constants.py | 23 +++- pgpy/packet/fields.py | 198 +++++++++++++++++++++++++++++++- pgpy/packet/packets.py | 21 +++- pgpy/pgp.py | 3 + tests/test_12_crypto_refresh.py | 1 - 5 files changed, 241 insertions(+), 5 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 2aec611b..68e161dd 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -239,6 +239,8 @@ class PubKeyAlgorithm(IntEnum): FormerlyElGamalEncryptOrSign = 0x14 # deprecated - do not generate DiffieHellman = 0x15 # X9.42 EdDSA = 0x16 # https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04 + Ed25519 = 27 + Ed448 = 28 @classmethod def _missing_(cls, val: object) -> PubKeyAlgorithm: @@ -252,7 +254,10 @@ def can_gen(self) -> bool: PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, - PubKeyAlgorithm.EdDSA} + PubKeyAlgorithm.EdDSA, + PubKeyAlgorithm.Ed25519, + PubKeyAlgorithm.Ed448, + } @property def can_encrypt(self) -> bool: # pragma: no cover @@ -260,7 +265,13 @@ def can_encrypt(self) -> bool: # pragma: no cover @property def can_sign(self) -> bool: - return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA} + return self in {PubKeyAlgorithm.RSAEncryptOrSign, + PubKeyAlgorithm.DSA, + PubKeyAlgorithm.ECDSA, + PubKeyAlgorithm.EdDSA, + PubKeyAlgorithm.Ed25519, + PubKeyAlgorithm.Ed448, + } @property def deprecated(self) -> bool: @@ -279,6 +290,11 @@ def validate_params(self, size) -> SecurityIssues: return SecurityIssues.OK else: return SecurityIssues.InsecureCurve + elif isinstance(min_size, bool) and min_size: + if min_size: + return SecurityIssues.OK + else: + return SecurityIssues.BrokenAsymmetricFunc else: # not ECC if size >= min_size: @@ -856,4 +872,7 @@ def causes_signature_verify_to_fail(self) -> bool: PubKeyAlgorithm.ECDSA: SAFE_CURVES, PubKeyAlgorithm.EdDSA: SAFE_CURVES, PubKeyAlgorithm.ECDH: SAFE_CURVES, + # the following algorithms are all known to be acceptable, with no keylength variations, so just return True + PubKeyAlgorithm.Ed448: True, + PubKeyAlgorithm.Ed25519: True, } diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index b2829095..70bbf3a7 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -13,7 +13,7 @@ import collections.abc from datetime import datetime -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Type, Union from warnings import warn @@ -29,6 +29,7 @@ from cryptography.hazmat.primitives.asymmetric import dsa from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.asymmetric import ed448 from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.asymmetric import utils @@ -84,6 +85,8 @@ 'DSASignature', 'ECDSASignature', 'EdDSASignature', + 'Ed25519Signature', + 'Ed448Signature', 'PubKey', 'OpaquePubKey', 'RSAPub', @@ -92,10 +95,15 @@ 'ECPoint', 'ECDSAPub', 'EdDSAPub', + 'Ed25519Pub', + 'Ed448Pub', 'ECDHPub', 'S2KSpecifier', 'String2Key', 'ECKDF', + 'NativeEdDSAPub', + 'NativeEdDSAPriv', + 'NativeEdDSASignature', 'PrivKey', 'OpaquePrivKey', 'RSAPriv', @@ -103,6 +111,8 @@ 'ElGPriv', 'ECDSAPriv', 'EdDSAPriv', + 'Ed25519Priv', + 'Ed448Priv', 'ECDHPriv', 'CipherText', 'RSACipherText', @@ -360,6 +370,44 @@ def __sig__(self): return self.int_to_bytes(self.r, siglen) + self.int_to_bytes(self.s, siglen) +class NativeEdDSASignature(Signature): + @abc.abstractproperty + def __siglen__(self) -> int: + 'the size of this native EdDSA signature object' + + def __bytearray__(self) -> bytearray: + return bytearray(self._rawsig) + + def from_signer(self, sig: bytes) -> None: + if len(sig) != self.__siglen__: + raise ValueError(f'{self!r} must be {self.__siglen__} bytes long, not {len(sig)}') + self._rawsig = sig + + def __sig__(self) -> bytes: + return self._rawsig + + def __copy__(self) -> NativeEdDSASignature: + sig = self.__class__() + sig._rawsig = self._rawsig + return sig + + def parse(self, packet: bytearray) -> None: + self._rawsig = bytes(packet[:self.__siglen__]) + del packet[:self.__siglen__] + + +class Ed25519Signature(NativeEdDSASignature): + @property + def __siglen__(self) -> int: + return 64 + + +class Ed448Signature(NativeEdDSASignature): + @property + def __siglen__(self) -> int: + return 114 + + class PubKey(MPIs): __pubfields__: Tuple = () __pubkey_algo__: Optional[PubKeyAlgorithm] = None @@ -647,6 +695,64 @@ def parse(self, packet: bytearray) -> None: self.p = MPI(packet) +NativeEdDSAPrivType = Union[ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey] +NativeEdDSAPubType = Union[ed25519.Ed25519PublicKey, ed448.Ed448PublicKey] + + +class NativeEdDSAPub(PubKey): + @abc.abstractproperty + def _public_length(self) -> int: + 'the size of this native EdDSA public key object' + @abc.abstractmethod + def pub_from_bytes(self, b: bytes) -> NativeEdDSAPubType: + ''''derive a public key from bytes''' + + def __pubkey__(self) -> NativeEdDSAPubType: + return self._raw_pubkey + + def __bytearray__(self) -> bytearray: + return bytearray(self._raw_pubkey.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)) + + def parse(self, packet: bytearray) -> None: + self._raw_pubkey = self.pub_from_bytes(bytes(packet[:self._public_length])) + del packet[:self._public_length] + + def verify(self, subj: bytes, sigbytes: bytes, hash_alg: cryptography_HashAlgorithm) -> bool: + hasher = hashes.Hash(hash_alg) + hasher.update(subj) + subj = hasher.finalize() + try: + self._raw_pubkey.verify(sigbytes, subj) + except InvalidSignature: + return False + return True + + def __len__(self) -> int: + return self._public_length + + +class Ed25519Pub(NativeEdDSAPub): + __pubkey_algo__ = PubKeyAlgorithm.Ed25519 + + @property + def _public_length(self) -> int: + return 32 + + def pub_from_bytes(self, b: bytes) -> ed25519.Ed25519PublicKey: + return ed25519.Ed25519PublicKey.from_public_bytes(b) + + +class Ed448Pub(NativeEdDSAPub): + __pubkey_algo__ = PubKeyAlgorithm.Ed448 + + @property + def _public_length(self) -> int: + return 56 + + def pub_from_bytes(self, b: bytes) -> ed448.Ed448PublicKey: + return ed448.Ed448PublicKey.from_public_bytes(b) + + class ECDHPub(PubKey): __pubfields__ = ('p',) __pubkey_algo__ = PubKeyAlgorithm.ECDH @@ -1983,6 +2089,96 @@ def sign(self, sigdata, hash_alg): return self.__privkey__().sign(sigdata) +class NativeEdDSAPriv(PrivKey, NativeEdDSAPub): + @abc.abstractproperty + def _private_length(self) -> int: + 'the length in bytes of the native private key object' + @abc.abstractmethod + def gen_priv(self) -> NativeEdDSAPrivType: + 'generate a new secret key' + @abc.abstractmethod + def priv_from_bytes(self, b: bytes) -> NativeEdDSAPrivType: + 'load a private key from native bytes representation' + + def sign(self, sigdata: bytes, hash_alg: cryptography_HashAlgorithm) -> bytes: + hasher = hashes.Hash(hash_alg) + hasher.update(sigdata) + sigdata = hasher.finalize() + return self._raw_privkey.sign(sigdata) + + def _compute_chksum(self): + b = bytearray() + self._append_private_fields(b) + chs = sum(b) % 65536 + self.chksum = bytearray(self.int_to_bytes(chs, 2)) + + def clear(self) -> None: + if hasattr(self, '_raw_privkey'): + delattr(self, '_raw_privkey') + + def _generate(self, keysize: Optional[Union[int, EllipticCurveOID]] = None) -> None: + if keysize is not None: + raise ValueError("Native EdDSA keys should always receive a None parameter for the keysize, as they are fixed size") + self._raw_privkey = self.gen_priv() + self._raw_pubkey = self._raw_privkey.public_key() + self._compute_chksum() + + def __privkey__(self): + return self._raw_privkey + + def _append_private_fields(self, _bytes: bytearray) -> None: + _bytes += self._raw_privkey.private_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption()) + + def parse(self, packet: bytearray) -> None: + NativeEdDSAPub.parse(self, packet) + # parse s2k business + self.s2k.parse(packet) + + if not self.s2k: + self._raw_privkey = self.priv_from_bytes(packet[:self._private_length]) + del packet[:self._private_length] + else: + ##TODO: this needs to be bounded to the length of the encrypted key material + self.encbytes = packet + + def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + kb = super().decrypt_keyblob(passphrase) + del passphrase + + self._raw_privkey = self.priv_from_bytes(kb[:self._private_length]) + del kb[:self._private_length] + + if self.s2k.usage in {S2KUsage.MalleableCFB, S2KUsage.CFB}: + self.chksum = kb + del kb + + +class Ed25519Priv(NativeEdDSAPriv, Ed25519Pub): + @property + def _private_length(self) -> int: + return 32 + + def gen_priv(self) -> ed25519.Ed25519PrivateKey: + return ed25519.Ed25519PrivateKey.generate() + + def priv_from_bytes(self, b: bytes) -> ed25519.Ed25519PrivateKey: + return ed25519.Ed25519PrivateKey.from_private_bytes(b) + + +class Ed448Priv(NativeEdDSAPriv, Ed448Pub): + @property + def _private_length(self) -> int: + return 57 + + def gen_priv(self) -> ed448.Ed448PrivateKey: + return ed448.Ed448PrivateKey.generate() + + def priv_from_bytes(self, b: bytes) -> ed448.Ed448PrivateKey: + return ed448.Ed448PrivateKey.from_private_bytes(b) + + class ECDHPriv(ECDSAPriv, ECDHPub): # type: ignore[misc] # (definition of __copy__ in base classes ECDHPub and ECDSAPub differs) def __bytearray__(self) -> bytearray: _b = ECDHPub.__bytearray__(self) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index e27a9f9b..69fc9375 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -27,6 +27,9 @@ from .fields import EdDSAPub, EdDSAPriv, EdDSASignature from .fields import ElGCipherText, ElGPriv, ElGPub from .fields import CipherText +from .fields import NativeEdDSAPub +from .fields import Ed25519Pub, Ed25519Priv, Ed25519Signature +from .fields import Ed448Pub, Ed448Priv, Ed448Signature from .fields import Signature as SignatureField from .fields import PubKey as PubKeyField from .fields import PrivKey as PrivKeyField @@ -411,6 +414,10 @@ def pubalg_int(self, val: int) -> None: self.signature = ECDSASignature() elif self.pubalg is PubKeyAlgorithm.EdDSA: self.signature = EdDSASignature() + elif self.pubalg is PubKeyAlgorithm.Ed25519: + self.signature = Ed25519Signature() + elif self.pubalg is PubKeyAlgorithm.Ed448: + self.signature = Ed448Signature() else: self.signature = OpaqueSignature() @@ -1305,6 +1312,10 @@ def pkalg_int(self, val: int) -> None: self.keymaterial = ECDHPub() if self.public else ECDHPriv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.EdDSA: self.keymaterial = EdDSAPub() if self.public else EdDSAPriv(self.__ver__) + elif self.pkalg is PubKeyAlgorithm.Ed25519: + self.keymaterial = Ed25519Pub() if self.public else Ed25519Priv(self.__ver__) + elif self.pkalg is PubKeyAlgorithm.Ed448: + self.keymaterial = Ed448Pub() if self.public else Ed448Priv(self.__ver__) else: self.keymaterial = OpaquePubKey() if self.public else OpaquePrivKey(self.__ver__) @@ -1488,7 +1499,10 @@ def unlocked(self) -> bool: if self.keymaterial is None: return True if self.protected: - return 0 not in list(self.keymaterial) + if len(list(self.keymaterial)): + return 0 not in list(self.keymaterial) + else: + return hasattr(self.keymaterial, '_raw_privkey') return True # pragma: no cover def protect(self, passphrase: str, @@ -1539,6 +1553,11 @@ def _extract_pubkey(self, pk: PubKey) -> None: raise TypeError(f"Expected ECDH, got {type(pk.keymaterial)} instead") pk.keymaterial.kdf = copy.copy(self.keymaterial.kdf) + elif isinstance(self.keymaterial, NativeEdDSAPub): + if not isinstance(pk.keymaterial, NativeEdDSAPub): + raise TypeError(f"Expected CFRG public key, got {type(pk.keymaterial)} instead") + pk.keymaterial._raw_pubkey = copy.copy(self.keymaterial._raw_pubkey) + pk.update_hlen() diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f965ff14..03e97e0e 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -86,6 +86,7 @@ from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub from .packet.fields import PrivKey as field_PrivKey +from .packet.fields import NativeEdDSAPub from .packet.types import Opaque from .packet.types import VersionedHeader @@ -1666,6 +1667,8 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: else: # this is an unknown elliptic curve return 0 + if isinstance(self._key.keymaterial, NativeEdDSAPub): + return self._key.keymaterial._public_length * 8 if self._key.keymaterial is None: return None # check if keymaterial is not an Opaque class containing a bytearray diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index 75c4bee0..74560672 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -39,7 +39,6 @@ def test_v6_skesk(self, aead: str) -> None: @pytest.mark.parametrize('msg', {'inline-signed-message.pgp', 'cleartext-signed-message.txt'}) def test_v6_signed_messages(self, msg: str) -> None: (cert, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-cert.key') - pytest.xfail('v6 keys are not supported') assert cert.is_public pgpmsg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/{msg}') assert not pgpmsg.is_encrypted From b8e72f89e7109a5e11c32a8ed31822f80ddf964c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Sun, 26 Feb 2023 17:46:20 -0500 Subject: [PATCH 274/287] Add X25519 and X448 Note that for PKESKv3, they store their symmetric key in the clear, outside of the ciphertext. this allows us to avoid padding shenanigans on the cleartext. --- pgpy/constants.py | 13 +- pgpy/packet/fields.py | 304 +++++++++++++++++++++++++++++++- pgpy/packet/packets.py | 24 ++- pgpy/pgp.py | 9 +- tests/test_12_crypto_refresh.py | 2 +- 5 files changed, 342 insertions(+), 10 deletions(-) diff --git a/pgpy/constants.py b/pgpy/constants.py index 68e161dd..487eaa6b 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -239,6 +239,8 @@ class PubKeyAlgorithm(IntEnum): FormerlyElGamalEncryptOrSign = 0x14 # deprecated - do not generate DiffieHellman = 0x15 # X9.42 EdDSA = 0x16 # https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04 + X25519 = 25 + X448 = 26 Ed25519 = 27 Ed448 = 28 @@ -255,13 +257,20 @@ def can_gen(self) -> bool: PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA, + PubKeyAlgorithm.X25519, + PubKeyAlgorithm.X448, PubKeyAlgorithm.Ed25519, PubKeyAlgorithm.Ed448, } @property def can_encrypt(self) -> bool: # pragma: no cover - return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.ECDH} + return self in {PubKeyAlgorithm.RSAEncryptOrSign, + PubKeyAlgorithm.ElGamal, + PubKeyAlgorithm.ECDH, + PubKeyAlgorithm.X25519, + PubKeyAlgorithm.X448, + } @property def can_sign(self) -> bool: @@ -875,4 +884,6 @@ def causes_signature_verify_to_fail(self) -> bool: # the following algorithms are all known to be acceptable, with no keylength variations, so just return True PubKeyAlgorithm.Ed448: True, PubKeyAlgorithm.Ed25519: True, + PubKeyAlgorithm.X448: True, + PubKeyAlgorithm.X25519: True, } diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 70bbf3a7..b81cae70 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -32,9 +32,10 @@ from cryptography.hazmat.primitives.asymmetric import ed448 from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives.asymmetric import x448 from cryptography.hazmat.primitives.asymmetric import utils from cryptography.hazmat.primitives.kdf.hkdf import HKDF -from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.hashes import SHA256, SHA512, HashAlgorithm as cryptography_HashAlgorithm from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash @@ -98,12 +99,17 @@ 'Ed25519Pub', 'Ed448Pub', 'ECDHPub', + 'X25519Pub', + 'X448Pub', 'S2KSpecifier', 'String2Key', 'ECKDF', 'NativeEdDSAPub', 'NativeEdDSAPriv', 'NativeEdDSASignature', + 'NativeCFRGXPriv', + 'NativeCFRGXPub', + 'NativeCFRGXCipherText', 'PrivKey', 'OpaquePrivKey', 'RSAPriv', @@ -114,10 +120,15 @@ 'Ed25519Priv', 'Ed448Priv', 'ECDHPriv', + 'X25519Priv', + 'X448Priv', 'CipherText', 'RSACipherText', 'ElGCipherText', - 'ECDHCipherText', ] + 'ECDHCipherText', + 'X25519CipherText', + 'X448CipherText', + ] class SubPackets(collections.abc.MutableMapping[str, SubPacket], Field): @@ -884,6 +895,87 @@ def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fin return ct +class NativeCFRGXPub(PubKey): + @abc.abstractproperty + def _public_length(self) -> int: + 'the size of this native CFRG X* public key object' + @abc.abstractproperty + def _native_type(self) -> Union[Type[x25519.X25519PublicKey], Type[x448.X448PublicKey]]: + 'what is the native type to use?' + + def exchange(self, + priv: Union[x25519.X25519PrivateKey, x448.X448PrivateKey], + pub: Union[x25519.X25519PublicKey, x448.X448PublicKey]) -> bytes: + if isinstance(pub, x25519.X25519PublicKey) and isinstance(priv, x25519.X25519PrivateKey): + return priv.exchange(pub) + if isinstance(pub, x448.X448PublicKey) and isinstance(priv, x448.X448PrivateKey): + return priv.exchange(pub) + raise TypeError(f"{type(self)}: mismatched public key {type(pub)} and private key {type(priv)}") + + def __pubkey__(self) -> Union[x25519.X25519PublicKey, x448.X448PublicKey]: + return self._raw_pubkey + + @abc.abstractmethod + def new_ciphertext(self) -> NativeCFRGXCipherText: + 'generate a new ciphertext' + + def __bytearray__(self) -> bytearray: + return bytearray(self._raw_pubkey.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)) + + def parse(self, packet: bytearray) -> None: + self._raw_pubkey = self._native_type.from_public_bytes(bytes(packet[:self._public_length])) + del packet[:self._public_length] + + def __len__(self) -> int: + return self._public_length + + def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> NativeCFRGXCipherText: + ct = self.new_ciphertext() + ephemeral_key = ct.gen_priv() + ct._sym_algo = symalg + + shared_secret: bytes = self.exchange(ephemeral_key, self._raw_pubkey) + hkdf = HKDF(algorithm=ct.kdf_hash_algo(), length=ct.aes_keywrap_keylen, salt=None, info=ct.hkdf_info) + mykey_bytes = self._raw_pubkey.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + if ct._ephemeral is None: + raise TypeError("CipherText ephemeral value is missing") + ephemeral_bytes = ct._ephemeral.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + key_wrap_key: bytes = hkdf.derive(ephemeral_bytes + mykey_bytes + shared_secret) + + ct._text = aes_key_wrap(key_wrap_key, data) + return ct + + +class X25519Pub(NativeCFRGXPub): + __pubkey_algo__ = PubKeyAlgorithm.X25519 + + @property + def _public_length(self) -> int: + return 32 + + @property + def _native_type(self) -> Union[Type[x25519.X25519PublicKey], Type[x448.X448PublicKey]]: + return x25519.X25519PublicKey + + def new_ciphertext(self) -> X25519CipherText: + return X25519CipherText() + + +class X448Pub(NativeCFRGXPub): + __pubkey_algo__ = PubKeyAlgorithm.X448 + + @property + def _public_length(self) -> int: + return 56 + + @property + def _native_type(self) -> Union[Type[x25519.X25519PublicKey], Type[x448.X448PublicKey]]: + return x448.X448PublicKey + + def new_ciphertext(self) -> X448CipherText: + return X448CipherText() + + class S2KSpecifier(Field): """ This is just the S2K specifier and its various options @@ -2282,6 +2374,104 @@ def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[O return self._decrypt_helper(padder.update(_m) + padder.finalize(), get_symalg) +class NativeCFRGXPriv(PrivKey, NativeCFRGXPub): + def __privkey__(self) -> Union[x25519.X25519PrivateKey, x448.X448PrivateKey]: + return self._raw_privkey + + def clear(self) -> None: + if hasattr(self, '_raw_privkey'): + delattr(self, '_raw_privkey') + + @abc.abstractproperty + def _private_length(self) -> int: + 'the length in byes of the native private key object' + @abc.abstractproperty + def _native_private_type(self) -> Union[Type[x25519.X25519PrivateKey], Type[x448.X448PrivateKey]]: + 'the native object type from the cryptography library' + + def _compute_chksum(self): + b = bytearray() + self._append_private_fields(b) + chs = sum(b) % 65536 + self.chksum = bytearray(self.int_to_bytes(chs, 2)) + + def _generate(self, keysize: Optional[Union[int, EllipticCurveOID]] = None) -> None: + if keysize is not None: + raise ValueError("Native CFRG key exchange ('X*') keys should always receive a None parameter for keysize, as they are fixed length") + self._raw_privkey = self._native_private_type.generate() + self._raw_pubkey = self._raw_privkey.public_key() + self._compute_chksum() + + def parse(self, packet: bytearray) -> None: + NativeCFRGXPub.parse(self, packet) + # parse s2k business + self.s2k.parse(packet) + + if not self.s2k: + self._raw_privkey = self._native_private_type.from_private_bytes(packet[:self._private_length]) + del packet[:self._private_length] + else: + ##TODO: this needs to be bounded to the length of the encrypted key material + self.encbytes = packet + + def _append_private_fields(self, _bytes: bytearray) -> None: + _bytes += self._raw_privkey.private_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption()) + + def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: + raise PGPError("Cannot sign with a CFRG X* key") + + def decrypt_keyblob(self, passphrase): + kb = super().decrypt_keyblob(passphrase) + del passphrase + + self._raw_privkey = self._native_private_type.from_private_bytes(kb[:self._private_length]) + del kb[:self._private_length] + + if self.s2k.usage in [254, 255]: + self.chksum = kb + del kb + + def decrypt(self, ct: CipherText, fpr: Fingerprint, get_symalg: bool) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + if not isinstance(ct, NativeCFRGXCipherText): + raise TypeError(f"Cannot decrypt {type(ct)}, expected NativeCFRGXCipherText") + if ct._ephemeral is None or ct._text is None: + raise PGPDecryptionError(f"Cannot decrypt uninitialized {type(ct)}") + + if ct._sym_algo is None and get_symalg: + raise TypeError("Asked for symmetric algorithm but none was present") + + shared_secret: bytes = self.exchange(self.__privkey__(), ct._ephemeral) + hkdf = HKDF(algorithm=ct.kdf_hash_algo(), length=ct.aes_keywrap_keylen, salt=None, info=ct.hkdf_info) + mykey_bytes = self.__privkey__().public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + ephemeral_bytes = ct._ephemeral.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + key_wrap_key: bytes = hkdf.derive(ephemeral_bytes + mykey_bytes + shared_secret) + cleartext = aes_key_unwrap(key_wrap_key, ct._text) + + return (ct._sym_algo, cleartext) + + +class X25519Priv(NativeCFRGXPriv, X25519Pub): + @property + def _private_length(self) -> int: + return 32 + + @property + def _native_private_type(self) -> Union[Type[x25519.X25519PrivateKey], Type[x448.X448PrivateKey]]: + return x25519.X25519PrivateKey + + +class X448Priv(NativeCFRGXPriv, X448Pub): + @property + def _private_length(self) -> int: + return 56 + + @property + def _native_private_type(self) -> Union[Type[x25519.X25519PrivateKey], Type[x448.X448PrivateKey]]: + return x448.X448PrivateKey + + class CipherText(MPIs): def __init__(self) -> None: super().__init__() @@ -2335,3 +2525,113 @@ def parse(self, packet: bytearray) -> None: del packet[0] self.c += packet[:clen] del packet[:clen] + + +NativeCFRGXPrivType = Union[x25519.X25519PrivateKey, x448.X448PrivateKey] +NativeCFRGXPubType = Union[x25519.X25519PublicKey, x448.X448PublicKey] + + +class NativeCFRGXCipherText(CipherText): + @abc.abstractproperty + def public_bytes(self) -> int: + '''size of public key (in bytes)''' + @abc.abstractproperty + def aes_keywrap_keylen(self) -> int: + '''size of AES key (bytes)''' + @abc.abstractproperty + def hkdf_info(self) -> bytes: + '''the prefix string for key derivation''' + @abc.abstractmethod + def gen_priv(self) -> NativeCFRGXPrivType: + '''generate a private key, setting the internal ephemeral''' + @abc.abstractmethod + def pub_from_bytes(self, b: bytes) -> NativeCFRGXPubType: + '''derive a public key from bytes''' + @abc.abstractmethod + def kdf_hash_algo(self) -> cryptography_HashAlgorithm: + '''generate a new hash algorithm for use with HKDF''' + + def __init__(self) -> None: + self._text: Optional[bytes] = None + self._sym_algo: Optional[SymmetricKeyAlgorithm] = None + self._ephemeral: Optional[NativeCFRGXPubType] = None + + def __bytearray__(self) -> bytearray: + if self._ephemeral is None: + raise ValueError(f"ephemeral value for {type(self)} is not initialized, cannot produce wire format") + if self._text is None: + raise ValueError(f"ciphertext for {type(self)} is not initialized, cannot produce wire format") + _bytes = bytearray() + _bytes += self._ephemeral.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + trailerlen = len(self._text) + if self._sym_algo is not None: + trailerlen += 1 + _bytes.append(trailerlen) + if self._sym_algo is not None: + _bytes.append(int(self._sym_algo)) + _bytes += self._text + return _bytes + + def parse(self, packet: bytearray) -> None: + self._ephemeral = self.pub_from_bytes(bytes(packet[:self.public_bytes])) + del packet[:self.public_bytes] + sz = packet[0] + del packet[0] + # for PKESKv3 ciphertexts, the symmetric key algorithm is + # stuck in the clear outside of the ciphertext. + if sz % 8 == 1: + self._sym_algo = SymmetricKeyAlgorithm(packet[0]) + del packet[0] + sz -= 1 + self._text = bytes(packet[:sz]) + del packet[:sz] + + +class X25519CipherText(NativeCFRGXCipherText): + @property + def public_bytes(self) -> int: + return 32 + + @property + def aes_keywrap_keylen(self) -> int: + return 16 + + @property + def hkdf_info(self) -> bytes: + return b'OpenPGP X25519' + + def gen_priv(self) -> x25519.X25519PrivateKey: + privkey = x25519.X25519PrivateKey.generate() + self._ephemeral = privkey.public_key() + return privkey + + def pub_from_bytes(self, b: bytes) -> x25519.X25519PublicKey: + return x25519.X25519PublicKey.from_public_bytes(b) + + def kdf_hash_algo(self) -> cryptography_HashAlgorithm: + return SHA256() + + +class X448CipherText(NativeCFRGXCipherText): + @property + def public_bytes(self) -> int: + return 56 + + @property + def aes_keywrap_keylen(self) -> int: + return 32 + + @property + def hkdf_info(self) -> bytes: + return b'OpenPGP X448' + + def gen_priv(self) -> x448.X448PrivateKey: + privkey = x448.X448PrivateKey.generate() + self._ephemeral = privkey.public_key() + return privkey + + def pub_from_bytes(self, b: bytes) -> x448.X448PublicKey: + return x448.X448PublicKey.from_public_bytes(b) + + def kdf_hash_algo(self) -> cryptography_HashAlgorithm: + return SHA512() diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 69fc9375..85aadbeb 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -30,6 +30,10 @@ from .fields import NativeEdDSAPub from .fields import Ed25519Pub, Ed25519Priv, Ed25519Signature from .fields import Ed448Pub, Ed448Priv, Ed448Signature +from .fields import NativeCFRGXPub +from .fields import NativeCFRGXCipherText +from .fields import X25519Pub, X25519Priv, X25519CipherText +from .fields import X448Pub, X448Priv, X448CipherText from .fields import Signature as SignatureField from .fields import PubKey as PubKeyField from .fields import PrivKey as PrivKeyField @@ -154,6 +158,10 @@ def pkalg_int(self, val: int) -> None: self.ct = ElGCipherText() elif self._pkalg is PubKeyAlgorithm.ECDH: self.ct = ECDHCipherText() + elif self._pkalg is PubKeyAlgorithm.X25519: + self.ct = X25519CipherText() + elif self._pkalg is PubKeyAlgorithm.X448: + self.ct = X448CipherText() class PKESessionKeyV3(PKESessionKey): @@ -237,6 +245,7 @@ def encrypter_bin(self, val: Union[bytearray, KeyID]) -> None: def __init__(self) -> None: super().__init__() self._encrypter = None + self.symalg: Optional[SymmetricKeyAlgorithm] = None def __bytearray__(self): _bytes = bytearray() @@ -249,6 +258,7 @@ def __bytearray__(self): _bytes.append(self._opaque_pkalg) else: _bytes.append(self.pkalg) + _bytes += self.ct.__bytearray__() if self.ct is not None else b'\x00' * (self.header.length - 10) return _bytes @@ -257,6 +267,7 @@ def __copy__(self): sk.header = copy.copy(self.header) sk._encrypter = self._encrypter sk.pkalg = self.pkalg + sk.symalg = self.symalg if self.pkalg == PubKeyAlgorithm.Invalid: sk._opaque_pkalg = self._opaque_pkalg if self.ct is not None: @@ -333,6 +344,9 @@ def __copy__(self) -> PKESessionKeyV6: def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: algo: Optional[SymmetricKeyAlgorithm] symkey: bytes + if not isinstance(pk.keymaterial, PrivKeyField): + raise TypeError(f"PKESKv6.decrypt_sk() expected private key material, got {type(pk.keymaterial)}") + if self.ct is None: raise PGPDecryptionError("PKESKv6: Tried to decrypt session key when ciphertext was not initialized") return pk.keymaterial.decrypt(self.ct, pk.fingerprint, False) @@ -340,6 +354,8 @@ def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], byte def encrypt_sk(self, pk: PubKey, symalg: Optional[SymmetricKeyAlgorithm], symkey: bytes, **kwargs) -> None: if symalg is not None: raise ValueError(f"PKESKv6 does not encrypt the symmetric key algorithm, but {symalg} was supplied (should be None)") + if pk.keymaterial is None: + raise ValueError('PKESKv6: public key material must be instantiated') self._encrypter = pk.fingerprint self.pkalg = pk.pkalg if self.ct is None: @@ -1316,6 +1332,10 @@ def pkalg_int(self, val: int) -> None: self.keymaterial = Ed25519Pub() if self.public else Ed25519Priv(self.__ver__) elif self.pkalg is PubKeyAlgorithm.Ed448: self.keymaterial = Ed448Pub() if self.public else Ed448Priv(self.__ver__) + elif self.pkalg is PubKeyAlgorithm.X25519: + self.keymaterial = X25519Pub() if self.public else X25519Priv(self.__ver__) + elif self.pkalg is PubKeyAlgorithm.X448: + self.keymaterial = X448Pub() if self.public else X448Priv(self.__ver__) else: self.keymaterial = OpaquePubKey() if self.public else OpaquePrivKey(self.__ver__) @@ -1553,8 +1573,8 @@ def _extract_pubkey(self, pk: PubKey) -> None: raise TypeError(f"Expected ECDH, got {type(pk.keymaterial)} instead") pk.keymaterial.kdf = copy.copy(self.keymaterial.kdf) - elif isinstance(self.keymaterial, NativeEdDSAPub): - if not isinstance(pk.keymaterial, NativeEdDSAPub): + elif isinstance(self.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): + if not isinstance(pk.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): raise TypeError(f"Expected CFRG public key, got {type(pk.keymaterial)} instead") pk.keymaterial._raw_pubkey = copy.copy(self.keymaterial._raw_pubkey) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 03e97e0e..9a092926 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -84,9 +84,9 @@ from .packet.packets import SKESessionKeyV4 from .packet.packets import SKESessionKeyV6 -from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub +from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub, Ed25519Pub, Ed448Pub, X25519Pub, X448Pub from .packet.fields import PrivKey as field_PrivKey -from .packet.fields import NativeEdDSAPub +from .packet.fields import NativeEdDSAPub, NativeCFRGXPub from .packet.types import Opaque from .packet.types import VersionedHeader @@ -1667,7 +1667,7 @@ def key_size(self) -> Optional[Union[int, EllipticCurveOID]]: else: # this is an unknown elliptic curve return 0 - if isinstance(self._key.keymaterial, NativeEdDSAPub): + if isinstance(self._key.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): return self._key.keymaterial._public_length * 8 if self._key.keymaterial is None: return None @@ -3012,7 +3012,8 @@ def parse(self, data): def _getpkt(d): return Packet(d) if d else None # some packets are filtered out - getpkt = filter(lambda p: p.header.typeid not in {PacketType.Trust, PacketType.Padding, PacketType.Marker}, iter(functools.partial(_getpkt, data), None)) + getpkt = filter(lambda p: p.header.typeid not in {PacketType.Trust, PacketType.Padding, + PacketType.Marker}, iter(functools.partial(_getpkt, data), None)) def pktgrouper(): class PktGrouper: diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index 74560672..a546f3c2 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -47,7 +47,6 @@ def test_v6_signed_messages(self, msg: str) -> None: def test_v6_key(self): (cert, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-cert.key') - pytest.xfail('v6 keys are not supported') assert cert.is_public (key, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v6-minimal-secret.key') assert not key.is_public @@ -64,6 +63,7 @@ def test_v6_key(self): assert not locked.is_public assert locked.is_protected assert not locked.is_unlocked + pytest.xfail('AEAD-protected secret keys cannot yet be unlocked') with locked.unlock("correct horse battery staple"): assert locked.is_unlocked clearmsg2 = locked.decrypt(msg) From 5d35c41649227790d7736746178256c8752f62a6 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 30 Jun 2023 18:58:17 -0400 Subject: [PATCH 275/287] enable decryption of secret keys with AEAD --- pgpy/packet/fields.py | 64 ++++++++++++++++++++++++--------- pgpy/packet/packets.py | 3 +- tests/test_12_crypto_refresh.py | 1 - 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index b81cae70..6d96d9e1 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1762,14 +1762,24 @@ def encrypt_keyblob(self, passphrase: str, self.clear() @abc.abstractmethod - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: raise NotImplementedError() - def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes]) -> Optional[bytearray]: + def _decrypt_keyblob_helper(self, passphrase: Union[str, bytes], + packet_type: PacketType, + creation_time: Optional[datetime]) -> Optional[bytearray]: if not self.s2k: # pragma: no cover # not encrypted return None + if self.s2k.usage is S2KUsage.AEAD: + if creation_time is None: + raise ValueError("S2K Usage Octet indicates AEAD, but no creation time provided") + (aead, associated_data) = self._aead_object_and_ad(passphrase, packet_type, creation_time) + return bytearray(aead.decrypt(self.s2k.iv, bytes(self.encbytes), associated_data)) + # Encryption/decryption of the secret data is done in CFB mode using # the key created from the passphrase and the Initial Vector from the # packet. A different mode is used with V3 keys (which are only RSA) @@ -1854,7 +1864,9 @@ def __privkey__(self): def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: raise NotImplementedError() - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: raise NotImplementedError() @@ -1921,8 +1933,10 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = self._decrypt_keyblob_helper(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase if kb is None: return @@ -2001,8 +2015,10 @@ def parse(self, packet: bytearray) -> None: self.chksum = packet[:2] del packet[:2] - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = self._decrypt_keyblob_helper(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase if kb is None: return @@ -2044,8 +2060,10 @@ def parse(self, packet: bytearray) -> None: self.chksum = packet[:2] del packet[:2] - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = self._decrypt_keyblob_helper(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase if kb is None: return @@ -2103,8 +2121,10 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = self._decrypt_keyblob_helper(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase if kb is None: return @@ -2165,8 +2185,10 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = self._decrypt_keyblob_helper(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase if kb is None: return @@ -2235,9 +2257,13 @@ def parse(self, packet: bytearray) -> None: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase: Union[str, bytes]) -> None: - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase + if kb is None: + return None self._raw_privkey = self.priv_from_bytes(kb[:self._private_length]) del kb[:self._private_length] @@ -2422,9 +2448,13 @@ def _append_private_fields(self, _bytes: bytearray) -> None: def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: raise PGPError("Cannot sign with a CFRG X* key") - def decrypt_keyblob(self, passphrase): - kb = super().decrypt_keyblob(passphrase) + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + kb = self._decrypt_keyblob_helper(passphrase, packet_type, creation_time) del passphrase + if kb is None: + return None self._raw_privkey = self._native_private_type.from_private_bytes(kb[:self._private_length]) del kb[:self._private_length] diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 85aadbeb..49aa04c0 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1544,7 +1544,8 @@ def protect(self, passphrase: str, def unprotect(self, passphrase: Union[str, bytes]) -> None: if not isinstance(self.keymaterial, PrivKeyField): raise TypeError("Key material is not a private key, cannot unprotect") - self.keymaterial.decrypt_keyblob(passphrase) + self.keymaterial.decrypt_keyblob(passphrase, packet_type=self.__typeid__, + creation_time=self._created) del passphrase def sign(self, sigdata: bytes, hash_alg: HashAlgorithm) -> bytes: diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index a546f3c2..a4249f42 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -63,7 +63,6 @@ def test_v6_key(self): assert not locked.is_public assert locked.is_protected assert not locked.is_unlocked - pytest.xfail('AEAD-protected secret keys cannot yet be unlocked') with locked.unlock("correct horse battery staple"): assert locked.is_unlocked clearmsg2 = locked.decrypt(msg) From 0316e1550cf4d86b6582f3f9a3a6e48c1e95da44 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Tue, 11 Jul 2023 19:06:21 -0400 Subject: [PATCH 276/287] sopgpy: generate-key profile for crypto-refresh --- pgpy/sopgpy.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 7728bd4d..5f74d372 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -60,13 +60,17 @@ def __init__(self) -> None: @property def generate_key_profiles(self) -> List[sop.SOPProfile]: - return [sop.SOPProfile('draft-koch-eddsa-for-openpgp-00', 'EdDSA/ECDH with Curve25519 '), - sop.SOPProfile('rfc4880', '3072-bit RSA'),] + return [ + sop.SOPProfile('draft-koch-eddsa-for-openpgp-00', 'EdDSA/ECDH with Curve25519'), + sop.SOPProfile('draft-ietf-openpgp-crypto-refresh-10', 'Ed25519 with X25519'), + sop.SOPProfile('rfc4880', '3072-bit RSA'), + ] @property def encrypt_profiles(self) -> List[sop.SOPProfile]: - '''Override this to offer multiple encryption profiles''' - return [sop.SOPProfile('rfc4880', 'Algorithms from RFC 4880')] + return [ + sop.SOPProfile('rfc4880', 'Algorithms from RFC 4880'), + ] # implemented ciphers that we are willing to use to encrypt, in # the order we prefer them: @@ -149,8 +153,11 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], profile: Optional[sop.SOPProfile] = None, **kwargs: Namespace) -> bytes: self.raise_on_unknown_options(**kwargs) + if profile is not None and profile.name == 'rfc4880': primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) + elif profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.Ed25519, version=6) else: primary = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.EdDSA) primaryflags = pgpy.constants.KeyFlags.Certify | pgpy.constants.KeyFlags.Sign @@ -159,6 +166,11 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], features = pgpy.constants.Features.SEIPDv1 hashes: List[pgpy.constants.HashAlgorithm] = [] + if profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + hashes += [pgpy.constants.HashAlgorithm.SHA3_512, + pgpy.constants.HashAlgorithm.SHA3_256] + features |= pgpy.constants.Features.SEIPDv2 + hashes += [pgpy.constants.HashAlgorithm.SHA512, pgpy.constants.HashAlgorithm.SHA384, pgpy.constants.HashAlgorithm.SHA256, @@ -171,6 +183,7 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], pgpy.constants.KeyFlags, pgpy.constants.Features, pgpy.constants.KeyServerPreferences, + pgpy.packet.types.AEADCiphersuiteList, ]] = { 'usage': primaryflags, 'hashes': hashes, @@ -182,6 +195,12 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], 'features': features, } + if profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + prefs['aead_ciphersuites'] = pgpy.packet.types.AEADCiphersuiteList([ + (pgpy.constants.SymmetricKeyAlgorithm.AES256, pgpy.constants.AEADMode.OCB), + (pgpy.constants.SymmetricKeyAlgorithm.AES128, pgpy.constants.AEADMode.OCB), + ]) + # make a direct key signature with prefs: direct_key_sig = primary.certify(primary, **prefs) primary |= direct_key_sig @@ -194,6 +213,8 @@ def generate_key(self, armor: bool = True, uids: List[str] = [], if profile is not None and profile.name == 'rfc4880': subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign) + elif profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.X25519, version=6) else: subkey = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.ECDH) subflags: Set[int] = set() From bb71ac18d31385c7ea0828c5ebe85938e094efe2 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 01:01:07 -0400 Subject: [PATCH 277/287] Avoid emitting CRC during ASCII armor of new elements See also the "Optional checksum" part of draft-ietf-openpgp-crypto-refresh-10) --- pgpy/pgp.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 9a092926..520f1406 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -151,6 +151,10 @@ def created(self): def embedded(self): return self.parent is not None + @property + def emit_crc(self) -> bool: + return self._signature is not None and self._signature.__ver__ < 6 + @property def expires_at(self) -> Optional[datetime]: """ @@ -728,6 +732,13 @@ def __bytearray__(self) -> bytearray: b += sig.__bytearray__() return b + @property + def emit_crc(self) -> bool: + for sig in self._sigs: + if sig._signature is not None and sig._signature.__ver__ < 6: + return True + return False + def parse(self, packet: bytes) -> None: unarmored = self.ascii_unarmor(packet) data = unarmored['body'] @@ -1055,6 +1066,22 @@ def encrypters(self) -> Set[Union[KeyID, Fingerprint]]: """A ``set`` containing all key ids (if any) to which this message was encrypted.""" return {m.encrypter for m in self._sessionkeys if isinstance(m, PKESessionKey) and m.encrypter is not None} + @property + def emit_crc(self) -> bool: + if self.is_encrypted: + if isinstance(self._message, IntegrityProtectedSKEDataV2): + return False + return True + # unencrypted messages with no signatures at all are pointless, but + # let's go ahead and leave the CRC on them anyway, so that legacy tools will + # base64-decode them without complaint: + if len(self._signatures) == 0: + return True + for sig in self._signatures: + if sig._signature is not None and sig._signature.__ver__ < 6: + return True + return False + @property def filename(self) -> Optional[str]: """If applicable, returns the original filename of the message. Otherwise, returns None.""" @@ -1600,6 +1627,12 @@ def fingerprint(self): if self._key: return self._key.fingerprint + @property + def emit_crc(self) -> bool: + if self._key is not None and self._key.__ver__ < 6: + return True + return False + @property def hashdata(self): # when signing a key, only the public portion of the keys is hashed From 7deb51ce2af23236b223bdb47e9abee610c3eee1 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 01:18:07 -0400 Subject: [PATCH 278/287] When protecting a v6 key, protect it using AEAD with OCB by default. --- pgpy/pgp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index 520f1406..b6041c9e 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2020,6 +2020,9 @@ def protect(self, passphrase: str, prefs['enc_alg'] = posargs[0] prefs['hash_alg'] = posargs[1] + if self._key is not None and self._key.__ver__ >= 6 and 'aead_mode' not in prefs: + prefs['aead_mode'] = AEADMode.OCB + # allow the user to pass in a list of initialization vectors # (this is suitable for trying to create reproducible objects, but should not normally be used) ivs: List[bytes] = prefs.pop('ivs', []) From 44931715ae1d739c0772ba9d3d3bb9995fba9e3c Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 03:03:16 -0400 Subject: [PATCH 279/287] PGPKey.encrypt: select SEIPDv1 or SEIPDv2 according to Features flag --- pgpy/pgp.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index b6041c9e..f5fa6d30 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -74,6 +74,7 @@ from .packet.packets import OnePassSignatureV3 from .packet.packets import PKESessionKey from .packet.packets import PKESessionKeyV3 +from .packet.packets import PKESessionKeyV6 from .packet.packets import Signature from .packet.packets import SignatureV4 from .packet.packets import SignatureV6 @@ -1274,6 +1275,7 @@ def __or__(self, other) -> PGPMessage: return self if isinstance(other, (PKESessionKey, SKESessionKey)): + # FIXME: throw an error here if the version of the PKESK or SKESK don't match the SEIPD. self._sessionkeys.append(other) return self @@ -2940,6 +2942,8 @@ def encrypt(self, raise PGPError("PGPKey: cannot encrypt with incomplete key material") cipherprefs: Optional[List[SymmetricKeyAlgorithm]] = None compprefs: Optional[List[CompressionAlgorithm]] = None + features: Optional[Features] = None + # FIXME: check AEAD Ciphersuites preferences sig: PGPSignature # line up a list of iterators of self-signatures: walk @@ -2951,13 +2955,17 @@ def encrypt(self, cipherprefs = sig.cipherprefs if sig.compprefs is not None: compprefs = sig.compprefs - if cipherprefs is not None and compprefs is not None: + if sig.features is not None: + features = sig.features + if cipherprefs is not None and compprefs is not None and features is not None: break if cipherprefs is None: cipherprefs = [] if compprefs is None: compprefs = [] + if features is None: + features = Features.SEIPDv1 if max_featureset is not None: features &= max_featureset @@ -2974,19 +2982,30 @@ def encrypt(self, if sessionkey is None: sessionkey = cipher_algo.gen_key() - # set up a new PKESessionKeyV3 - pkesk = PKESessionKeyV3() - pkesk.encrypter = bytearray(binascii.unhexlify(self.fingerprint.keyid.encode('latin-1'))) - pkesk.pkalg = self.key_algorithm - pkesk.encrypt_sk(self._key, cipher_algo, sessionkey) + if features & Features.SEIPDv2: + pkesk: PKESessionKey = PKESessionKeyV6() + pkesk.encrypt_sk(self._key, None, sessionkey) + else: + # set up a new PKESessionKeyV3 + pkesk = PKESessionKeyV3() + pkesk.encrypter = bytearray(binascii.unhexlify(self.fingerprint.keyid.encode('latin-1'))) + pkesk.pkalg = self.key_algorithm + pkesk.encrypt_sk(self._key, cipher_algo, sessionkey) if message.is_encrypted: # pragma: no cover _m = message else: _m = PGPMessage() - skedata = IntegrityProtectedSKEDataV1() - skedata.encrypt(sessionkey, cipher_algo, message.__bytes__()) + if features & Features.SEIPDv2: + seipd2 = IntegrityProtectedSKEDataV2() + seipd2.cipher = cipher_algo + seipd2.encrypt(sessionkey, message.__bytes__()) + skedata: IntegrityProtectedSKEData = seipd2 + else: + seipd1 = IntegrityProtectedSKEDataV1() + seipd1.encrypt(sessionkey, cipher_algo, message.__bytes__()) + skedata = seipd1 _m |= skedata _m |= pkesk From 03f315a0c08797554e153a158aa442a3fb604405 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 21:41:52 -0400 Subject: [PATCH 280/287] PGPMessage.encrypt: passing aead_mode parameter will use SEIPDv2 with Argon2 (we also allow passing an explicit salt, which normally should not be used but is useful for generating deterministic test vectors) --- pgpy/pgp.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f5fa6d30..9bf60a33 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -40,6 +40,7 @@ from .constants import SymmetricKeyAlgorithm from .constants import SecurityIssues from .constants import AEADMode +from .constants import String2KeyType from .decorators import KeyAction @@ -1404,7 +1405,9 @@ def encrypt(self, passphrase: Union[str, bytes], sessionkey: Optional[Union[int, ByteString]] = None, cipher: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES256, hash: HashAlgorithm = HashAlgorithm.SHA256, - iv: Optional[bytes] = None) -> PGPMessage: + iv: Optional[bytes] = None, + aead_mode: Optional[AEADMode] = None, + salt: Optional[bytes] = None) -> PGPMessage: """ encrypt(passphrase, [sessionkey=None,] **prefs) @@ -1426,11 +1429,19 @@ def encrypt(self, passphrase: Union[str, bytes], :raises: :py:exc:`~errors.PGPEncryptionError` :returns: A new :py:obj:`PGPMessage` containing the encrypted contents of this message. """ - # set up a new SKESessionKeyV4 - skesk = SKESessionKeyV4() + if aead_mode is not None: + if iv is not None: + raise ValueError(f'PGPMessage.encrypt using AEAD mode {aead_mode!r}, cannot use explicit IV') + # if we use AEAD, we're going to use SKESKv6 with Argon2 as S2K + skesk: SKESessionKey = SKESessionKeyV6() + skesk.s2kspec._type = String2KeyType.Argon2 + else: + if salt is not None: + raise ValueError('PGPMessage.encrypt using CFB mode, cannot use salt') + # otherwise, we're going to use SKESKv4 with Iterated+Salted S2K + skesk = SKESessionKeyV4() + skesk.s2kspec.halg = hash skesk.symalg = cipher - skesk.s2kspec.halg = hash - skesk.s2kspec.iteration_count = b'\xff' if sessionkey is None: sessionkey = cipher.gen_key() @@ -1442,8 +1453,16 @@ def encrypt(self, passphrase: Union[str, bytes], msg = PGPMessage() | skesk if not self.is_encrypted: - skedata = IntegrityProtectedSKEDataV1() - skedata.encrypt(sessionkey, cipher, self.__bytes__(), iv = iv) + if skesk.__ver__ == 6: + seipd2 = IntegrityProtectedSKEDataV2() + if salt is not None: + seipd2.salt = salt + seipd2.encrypt(bytes(sessionkey), self.__bytes__()) + skedata: IntegrityProtectedSKEData = seipd2 + else: + seipd1 = IntegrityProtectedSKEDataV1() + seipd1.encrypt(sessionkey, cipher, self.__bytes__(), iv = iv) + skedata = seipd1 msg |= skedata else: From 93fcc25956cd55ce75a1b9e05f8225ae8315b564 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 12 Jul 2023 21:48:50 -0400 Subject: [PATCH 281/287] sopgpy: encrypt: add a profile that distinguishes password-based encryption --- pgpy/sopgpy.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 5f74d372..505fb3a7 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -69,7 +69,8 @@ def generate_key_profiles(self) -> List[sop.SOPProfile]: @property def encrypt_profiles(self) -> List[sop.SOPProfile]: return [ - sop.SOPProfile('rfc4880', 'Algorithms from RFC 4880'), + sop.SOPProfile('default', 'passwords: Use CFB (SEIPDv1); pubkeys: determine from key features'), + sop.SOPProfile('draft-ietf-openpgp-crypto-refresh-10', 'passwords: Use AEAD (SEIPDv2); pubkeys: determine from key features'), ] # implemented ciphers that we are willing to use to encrypt, in @@ -382,7 +383,10 @@ def encrypt(self, for handle, cert in certs.items(): msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) for p, pw in pws.items(): - msg = msg.encrypt(passphrase=pw, sessionkey=sessionkey) + aead_mode: Optional[pgpy.constants.AEADMode] = None + if profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + aead_mode = pgpy.constants.AEADMode.OCB + msg = msg.encrypt(passphrase=pw, sessionkey=sessionkey, aead_mode=aead_mode) del sessionkey return self._maybe_armor(armor, msg) From 070de1fefcc33d1d5d2883534d193975a716ee78 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 21 Jul 2023 14:50:09 -0700 Subject: [PATCH 282/287] Add encrypt/decrypt and sign/verify roundtrips This uses cached key generation, and ensures that we cover versions 4 and 6 of keys, as well as all the pubkey algorithms, and different possible feature sets (SEIPDv1 and SEIPDv2). --- tests/test_13_version_and_pubkey.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_13_version_and_pubkey.py diff --git a/tests/test_13_version_and_pubkey.py b/tests/test_13_version_and_pubkey.py new file mode 100644 index 00000000..2ef9c454 --- /dev/null +++ b/tests/test_13_version_and_pubkey.py @@ -0,0 +1,89 @@ +# coding=utf-8 +""" Testing different versions of public keys across different versions of packets +""" + +from typing import Dict, NamedTuple, Optional, Tuple + +import pytest + +from warnings import warn + +from itertools import product +from datetime import datetime, timezone + +from pgpy import PGPKey, PGPSignature, PGPMessage, PGPUID +from pgpy.constants import PubKeyAlgorithm, KeyFlags, HashAlgorithm, SymmetricKeyAlgorithm, CompressionAlgorithm, Features +from pgpy.errors import PGPDecryptionError + + +class KeyDescriptor(NamedTuple): + primary_alg: PubKeyAlgorithm + subkey_alg: PubKeyAlgorithm + key_version: int + features: Features + + @property + def key_and_cert(self) -> Tuple[PGPKey, PGPKey]: + 'cache the created keys to be able to reuse them cleanly' + if self not in kd_instances: + if (self.primary_alg, self.key_version) not in kd_primary_keys: + kd_primary_keys[(self.primary_alg, self.key_version)] = PGPKey.new(self.primary_alg, version=self.key_version, created=creation_time) + key = kd_primary_keys[(self.primary_alg, self.key_version)] + prefs = { + 'usage': KeyFlags.Certify | KeyFlags.Sign, + 'hashes': [ + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA256 + ], + 'ciphers': [SymmetricKeyAlgorithm.AES256, + SymmetricKeyAlgorithm.AES128], + 'compression': [CompressionAlgorithm.Uncompressed], + 'features': self.features, + } + key |= key.certify(key, **prefs) + if self.key_version == 4: + key.add_uid(PGPUID.new('Example User '), selfsign=True, **prefs) + if (self.subkey_alg, self.key_version) not in kd_subkeys: + kd_subkeys[(self.subkey_alg, self.key_version)] = PGPKey.new(self.subkey_alg, version=self.key_version, created=creation_time) + key.add_subkey(kd_subkeys[(self.subkey_alg, self.key_version)], + usage=KeyFlags.EncryptCommunications | KeyFlags.EncryptStorage) + cert = key.pubkey + kd_instances[self] = (key, cert) + return kd_instances[self] + + def __repr__(self) -> str: + return f"v{self.key_version} {self.primary_alg.name},{self.subkey_alg.name} {self.features!r}" + +kdescs = list(KeyDescriptor(*x) for x in product(list(filter(lambda x: x.can_sign and x.can_gen, PubKeyAlgorithm)), + list(filter(lambda x: x.can_encrypt and x.can_gen, PubKeyAlgorithm)), + [4,6], + [Features.SEIPDv1, Features.SEIPDv1 | Features.SEIPDv2] + )) + +creation_time = datetime.now(tz=timezone.utc) + +kd_primary_keys: Dict[Tuple[PubKeyAlgorithm, int], PGPKey] = {} +kd_subkeys: Dict[Tuple[PubKeyAlgorithm, int], PGPKey] = {} +kd_instances: Dict[KeyDescriptor, Tuple[PGPKey, PGPKey]] = {} + +class TestPGP_Version_Pubkey(object): + @pytest.mark.parametrize('kdesc', kdescs, ids=list(map(repr, kdescs))) + def test_encrypt_decrypt_roundtrip(self, kdesc: KeyDescriptor) -> None: + key, cert = kdesc.key_and_cert + msg = PGPMessage.new('this is a test', compression=CompressionAlgorithm.Uncompressed) + + encmsg = cert.encrypt(msg) + encmsg2 = PGPMessage.from_blob(bytes(encmsg)) + newmsg = key.decrypt(encmsg2) + assert newmsg.message == msg.message + + @pytest.mark.parametrize('kdesc', kdescs, ids=list(map(repr, kdescs))) + def test_sign_verify_roundtrip(self, kdesc: KeyDescriptor) -> None: + key, cert = kdesc.key_and_cert + + msg = 'this is a test' + sig = key.sign(msg) + sig2 = PGPSignature.from_blob(bytes(sig)) + assert cert.verify(msg, sig2) From b55fdd2f2c5a51156eee420626c5749f9917e381 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:47:02 -0400 Subject: [PATCH 283/287] sopgpy: ensure alignment between {P,S}KESK and SEIPD packets make this work even in the face of multiple passwords and keys --- pgpy/sopgpy.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 505fb3a7..84bc7ba7 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -351,7 +351,15 @@ def encrypt(self, cipher: Optional[pgpy.constants.SymmetricKeyAlgorithm] = None ciphers = set(self._cipherprefs) + max_featureset: pgpy.constants.Features = pgpy.constants.Features.pgpy_features + if (len(pws) > 0 and not (profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10')): + max_featureset &= pgpy.constants.Features.SEIPDv1 + for handle, cert in certs.items(): + f: Optional[pgpy.constants.Features] = cert.features + if f is not None: + max_featureset &= f + keyciphers: Set[pgpy.constants.SymmetricKeyAlgorithm] = set() for sig in cert.search_pref_sigs(): if sig.cipherprefs is not None: @@ -381,10 +389,12 @@ def encrypt(self, msg |= sig for handle, cert in certs.items(): - msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey) + msg = cert.encrypt(msg, cipher=cipher, sessionkey=sessionkey, max_featureset=max_featureset) for p, pw in pws.items(): aead_mode: Optional[pgpy.constants.AEADMode] = None - if profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10': + if max_featureset & pgpy.constants.Features.SEIPDv2 and \ + profile is not None and \ + profile.name == 'draft-ietf-openpgp-crypto-refresh-10': aead_mode = pgpy.constants.AEADMode.OCB msg = msg.encrypt(passphrase=pw, sessionkey=sessionkey, aead_mode=aead_mode) del sessionkey From d2813ae7d506f66f8afead50c6da83eca5b89697 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Thu, 10 Aug 2023 16:47:39 -0400 Subject: [PATCH 284/287] sopgpy encrypt: add profile "rfc4880" which only uses SEIPDv1 --- pgpy/sopgpy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 84bc7ba7..7407af0e 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -71,6 +71,7 @@ def encrypt_profiles(self) -> List[sop.SOPProfile]: return [ sop.SOPProfile('default', 'passwords: Use CFB (SEIPDv1); pubkeys: determine from key features'), sop.SOPProfile('draft-ietf-openpgp-crypto-refresh-10', 'passwords: Use AEAD (SEIPDv2); pubkeys: determine from key features'), + sop.SOPProfile('rfc4880', 'Always use CFB (SEIPDv1)'), ] # implemented ciphers that we are willing to use to encrypt, in @@ -352,7 +353,8 @@ def encrypt(self, ciphers = set(self._cipherprefs) max_featureset: pgpy.constants.Features = pgpy.constants.Features.pgpy_features - if (len(pws) > 0 and not (profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10')): + if (len(pws) > 0 and not (profile is not None and profile.name == 'draft-ietf-openpgp-crypto-refresh-10')) or \ + (profile is not None and profile.name == 'rfc4880'): max_featureset &= pgpy.constants.Features.SEIPDv1 for handle, cert in certs.items(): From 50efe13152b926fb8c46ce274c0ea30b815045fc Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 11 Aug 2023 10:05:07 -0400 Subject: [PATCH 285/287] Avoid copy on raw pubkey objects These pubkey objects are immutable, as noted in https://github.com/pyca/cryptography/issues/9403, so it should be safe to just assign. --- pgpy/packet/packets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 49aa04c0..695392ee 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1577,7 +1577,7 @@ def _extract_pubkey(self, pk: PubKey) -> None: elif isinstance(self.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): if not isinstance(pk.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): raise TypeError(f"Expected CFRG public key, got {type(pk.keymaterial)} instead") - pk.keymaterial._raw_pubkey = copy.copy(self.keymaterial._raw_pubkey) + pk.keymaterial._raw_pubkey = self.keymaterial._raw_pubkey pk.update_hlen() From c030e7ab5ed1e35bf4730144d9bf2c14a98947c3 Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Wed, 10 May 2023 05:12:21 -0400 Subject: [PATCH 286/287] AEAD: handle EAX, using Cryptodome if it is available --- README.rst | 4 +++ pgpy/sopgpy.py | 13 +++++++++- pgpy/symenc.py | 44 +++++++++++++++++++++++++++++++-- requirements.txt | 1 + setup.cfg | 1 + tests/test_12_crypto_refresh.py | 11 +++++++-- tox.ini | 2 ++ 7 files changed, 71 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b1eda839..6d2c4a8c 100644 --- a/README.rst +++ b/README.rst @@ -67,6 +67,10 @@ To use `sopgpy` you'll also need: - `sop `_ >= 0.5.1 +To use EAX as an AEAD mode, you'll also need: + +- `Cryptodome `_ + License ------- diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py index 7407af0e..73638a7e 100755 --- a/pgpy/sopgpy.py +++ b/pgpy/sopgpy.py @@ -38,6 +38,7 @@ import logging import packaging.version from importlib import metadata +from types import ModuleType from datetime import datetime, timezone from typing import List, Literal, Union, Optional, Set, Tuple, MutableMapping, Dict, Callable @@ -48,14 +49,24 @@ import sop import pgpy +Maybe_Cryptodome: Optional[ModuleType] +try: + import Cryptodome as Maybe_Cryptodome +except ModuleNotFoundError: + Maybe_Cryptodome = None + class SOPGPy(sop.StatelessOpenPGP): def __init__(self) -> None: self.pgpy_version = packaging.version.Version(metadata.version('pgpy')) self.cryptography_version = packaging.version.Version(metadata.version('cryptography')) + self.cryptodome_version = '' + if Maybe_Cryptodome is not None: + cdv: Tuple[int, int, str] = Maybe_Cryptodome.version_info + self.cryptodome_version = f'\nCryptodome (for EAX): {cdv[0]}.{cdv[1]}.{cdv[2]}' super().__init__(name='sopgpy', version=f'{self.pgpy_version}', backend=f'PGPy {self.pgpy_version}', - extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}', + extended=f'python-cryptography {self.cryptography_version}\n{openssl.backend.openssl_version_text()}{self.cryptodome_version}', description=f'Stateless OpenPGP using PGPy {self.pgpy_version}') @property diff --git a/pgpy/symenc.py b/pgpy/symenc.py index 281596ed..e8d011fc 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -2,6 +2,7 @@ """ from typing import Optional, Union +from types import ModuleType from cryptography.exceptions import UnsupportedAlgorithm @@ -15,6 +16,12 @@ from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError +AES_Cryptodome: Optional[ModuleType] +try: + from Cryptodome.Cipher import AES as AES_Cryptodome +except ModuleNotFoundError: + AES_Cryptodome = None + __all__ = ['_cfb_encrypt', '_cfb_decrypt', 'AEAD'] @@ -62,16 +69,49 @@ def _cfb_decrypt(ct: bytes, key: bytes, alg: SymmetricKeyAlgorithm, iv: Optional class AEAD: + class AESEAX: + '''This class supports the same interface as AESOCB3 and AESGCM from python's cryptography module + + We don't use that module because it doesn't support EAX + (see https://github.com/pyca/cryptography/issues/6903) + ''' + + def __init__(self, key: bytes) -> None: + self._key: bytes = key + + def decrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + if AES_Cryptodome is None: + raise NotImplementedError("AEAD Mode EAX needs the python Cryptodome module installed") + if len(nonce) != AEADMode.EAX.iv_len: + raise ValueError(f"EAX nonce should be {AEADMode.EAX.iv_len} octets, got {len(nonce)}") + a = AES_Cryptodome.new(self._key, AES_Cryptodome.MODE_EAX, nonce, mac_len=AEADMode.EAX.tag_len) + if associated_data is not None: + a.update(associated_data) + return a.decrypt_and_verify(data[:-AEADMode.EAX.tag_len], data[-AEADMode.EAX.tag_len:]) + + def encrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + if AES_Cryptodome is None: + raise NotImplementedError("AEAD Mode EAX needs the python Cryptodome module installed") + if len(nonce) != AEADMode.EAX.iv_len: + raise ValueError(f"EAX nonce should be {AEADMode.EAX.iv_len} octets, got {len(nonce)}") + a = AES_Cryptodome.new(self._key, AES_Cryptodome.MODE_EAX, nonce, mac_len=AEADMode.EAX.tag_len) + if associated_data is not None: + a.update(associated_data) + ciphertext, tag = a.encrypt_and_digest(data) + return ciphertext + tag + def __init__(self, cipher: SymmetricKeyAlgorithm, mode: AEADMode, key: bytes) -> None: - self._aead: Union[AESOCB3, AESGCM] + self._aead: Union[AESOCB3, AESGCM, AEAD.AESEAX] if cipher not in [SymmetricKeyAlgorithm.AES128, SymmetricKeyAlgorithm.AES192, SymmetricKeyAlgorithm.AES256]: raise NotImplementedError(f"Cannot do AEAD with non-AES cipher (requested cipher: {cipher!r})") if mode == AEADMode.OCB: self._aead = AESOCB3(key) elif mode == AEADMode.GCM: self._aead = AESGCM(key) + elif mode == AEADMode.EAX: + self._aead = AEAD.AESEAX(key) else: - raise NotImplementedError(f"Cannot do AEAD mode other than OCB, and GCM (requested mode: {mode!r})") + raise NotImplementedError(f"Cannot do AEAD mode other than OCB, GCM, and EAX (requested mode: {mode!r})") def encrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: return self._aead.encrypt(nonce, data, associated_data) diff --git a/requirements.txt b/requirements.txt index 1218b0c1..f135d792 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ argon2_cffi cryptography>=3.3.2 sop>=0.5.1[sopgpy] +pycryptodomex[eax] diff --git a/setup.cfg b/setup.cfg index 4b7f444a..a38c02cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ python_requires = >=3.6 [options.extras_require] sopgpy = sop>=0.5.1 +eax = pycryptodomex [build_sphinx] source-dir = docs/source diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py index a4249f42..2866602a 100644 --- a/tests/test_12_crypto_refresh.py +++ b/tests/test_12_crypto_refresh.py @@ -3,6 +3,7 @@ """ from typing import Dict, Optional, Tuple +from types import ModuleType import pytest @@ -11,6 +12,12 @@ from pgpy import PGPKey, PGPSignature, PGPMessage from pgpy.errors import PGPDecryptionError +Cryptodome:Optional[ModuleType] +try: + import Cryptodome +except ModuleNotFoundError: + Cryptodome = None + class TestPGP_CryptoRefresh(object): def test_v4_sigs(self) -> None: (k, _) = PGPKey.from_file('tests/testdata/crypto-refresh/v4-ed25519-pubkey-packet.key') @@ -30,8 +37,8 @@ def test_v4_skesk_argon2(self, cipher: str) -> None: def test_v6_skesk(self, aead: str) -> None: msg = PGPMessage.from_file(f'tests/testdata/crypto-refresh/v6skesk-aes128-{aead}.pgp') assert msg.is_encrypted - if aead == 'eax': - pytest.xfail('AEAD Mode EAX not supported') + if aead == 'eax' and Cryptodome is None: + pytest.xfail('AEAD Mode EAX is not supported unless the Cryptodome module is available') unlocked = msg.decrypt('password') assert not unlocked.is_encrypted assert unlocked.message == b'Hello, world!' diff --git a/tox.ini b/tox.ini index e6387bb9..8da29019 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,8 @@ deps = pytest pytest-cov pytest-order +extras = + eax install_command = pip install {opts} --no-cache-dir {packages} commands = From eb631c1d27178527dfce58df5d6d42dc997dfb5e Mon Sep 17 00:00:00 2001 From: Daniel Kahn Gillmor Date: Fri, 18 Aug 2023 12:17:26 -0400 Subject: [PATCH 287/287] Allow creation and use of v4 keys without a User ID (with warning) draft-ietf-openpgp-crypto-refresh-10 makes it clear that even v4 OpenPGP certificates MAY not have a user ID. Keep a warning in place though, to encourage interoperability with legacy v4 implementations. --- pgpy/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 4d16ba93..1c454771 100644 --- a/pgpy/decorators.py +++ b/pgpy/decorators.py @@ -119,7 +119,7 @@ def _action(key, *args, **kwargs): if len(key._uids) == 0 and key.is_primary and key._key.__ver__ < 6: # if a key is in the process of being created, it needs to be allowed to certify its own user id if action is not key.certify.__wrapped__: - raise PGPError("Key is not complete - please add a User ID!") + logging.warning("Version 4 Key has no User ID -- may be incompatible with some legacy OpenPGP implementations.") with self.usage(key, kwargs.get('user', None)) as _key: self.check_attributes(key)