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..6d2c4a8c 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 ------------- @@ -52,14 +57,20 @@ 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 `_ -- `pyasn1 `_ +- `argon2_cffi `_ + +To use `sopgpy` you'll also need: + +- `sop `_ >= 0.5.1 -- `six `_ +To use EAX as an AEAD mode, you'll also need: +- `Cryptodome `_ + License ------- diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6454614e..7074d40b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,105 @@ Changelog ********* +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 +------------- + +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 +----------- + +Armorable.is_ascii() is deprecated. You probably want +Armorable.is_utf8() instead, since OpenPGP assumes that all text is +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(). + +The following properties of PGPSignature now return None if the +corresponding subpacket is not present (they used to return an empty +list or string in that case): + +* cipherprefs +* compprefs +* hashprefs +* keyserver +* policy_uri +* 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 +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. + +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. + +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). + +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. + +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. + +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/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/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/_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 28a4561a..487eaa6b 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -1,8 +1,8 @@ """ constants.py """ +from __future__ import annotations + import bz2 -import hashlib -import imghdr import os import zlib import warnings @@ -12,30 +12,34 @@ 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 cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._cipheralgorithm import CipherAlgorithm -from .types import FlagEnum from .decorators import classproperty -from ._curves import BrainpoolP256R1, BrainpoolP384R1, BrainpoolP512R1, X25519, Ed25519 __all__ = [ - 'Backend', + 'ECFields', 'EllipticCurveOID', 'ECPointFormat', - 'PacketTag', + 'PacketType', 'SymmetricKeyAlgorithm', 'PubKeyAlgorithm', 'CompressionAlgorithm', 'HashAlgorithm', 'RevocationReason', + 'SigSubpacketType', + 'AttributeType', 'ImageEncoding', 'SignatureType', 'KeyServerPreferences', 'S2KGNUExtension', + 'S2KUsage', 'SecurityIssues', 'String2KeyType', 'TrustLevel', @@ -44,6 +48,7 @@ 'RevocationKeyClass', 'NotationDataFlags', 'TrustFlags', + 'AEADMode', ] @@ -51,87 +56,6 @@ _hashtunedata = bytearray([10, 11, 12, 13, 14, 15, 16, 17] * 128 * 50) -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 @@ -140,7 +64,8 @@ class ECPointFormat(IntEnum): OnlyY = 0x42 -class PacketTag(IntEnum): +class PacketType(IntEnum): + Unknown = -1 Invalid = 0 PublicKeyEncryptedSessionKey = 1 Signature = 2 @@ -159,6 +84,13 @@ class PacketTag(IntEnum): UserAttribute = 17 SymmetricallyEncryptedIntegrityProtectedData = 18 ModificationDetectionCode = 19 + Padding = 21 + + @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): @@ -187,40 +119,51 @@ 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): + def is_insecure(self) -> bool: insecure_ciphers = {SymmetricKeyAlgorithm.IDEA} 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): + def key_size(self) -> int: ks = {SymmetricKeyAlgorithm.IDEA: 128, SymmetricKeyAlgorithm.TripleDES: 192, SymmetricKeyAlgorithm.CAST5: 128, @@ -238,15 +181,48 @@ 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) +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 Invalid = 0x00 #: Signifies that a key is an RSA key. RSAEncryptOrSign = 0x01 @@ -263,30 +239,56 @@ 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 + + @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): + def can_gen(self) -> bool: return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, - PubKeyAlgorithm.EdDSA} + PubKeyAlgorithm.EdDSA, + PubKeyAlgorithm.X25519, + PubKeyAlgorithm.X448, + PubKeyAlgorithm.Ed25519, + PubKeyAlgorithm.Ed448, + } @property - def can_encrypt(self): # pragma: no cover - return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.ECDH} + def can_encrypt(self) -> bool: # pragma: no cover + return self in {PubKeyAlgorithm.RSAEncryptOrSign, + PubKeyAlgorithm.ElGamal, + PubKeyAlgorithm.ECDH, + PubKeyAlgorithm.X25519, + PubKeyAlgorithm.X448, + } @property - def can_sign(self): - return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA} + def can_sign(self) -> bool: + return self in {PubKeyAlgorithm.RSAEncryptOrSign, + PubKeyAlgorithm.DSA, + PubKeyAlgorithm.ECDSA, + PubKeyAlgorithm.EdDSA, + PubKeyAlgorithm.Ed25519, + PubKeyAlgorithm.Ed448, + } @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): @@ -297,6 +299,11 @@ def validate_params(self, size): 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: @@ -307,6 +314,31 @@ 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 + + # modern AEAD protection: + AEAD = 253 + # sensible use of tamper-resistant CFB: + CFB = 254 + # legacy use of CFB: + MalleableCFB = 255 + + class CompressionAlgorithm(IntEnum): """Supported compression algorithms.""" #: No compression @@ -318,7 +350,7 @@ class CompressionAlgorithm(IntEnum): #: Bzip2 BZ2 = 0x03 - def compress(self, data): + def compress(self, data: bytes) -> bytes: if self is CompressionAlgorithm.Uncompressed: return data @@ -333,7 +365,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 @@ -351,6 +383,7 @@ def decompress(self, data): class HashAlgorithm(IntEnum): """Supported hash algorithms.""" + Unknown = -1 Invalid = 0x00 MD5 = 0x01 SHA1 = 0x02 @@ -363,40 +396,39 @@ class HashAlgorithm(IntEnum): SHA384 = 0x09 SHA512 = 0x0A SHA224 = 0x0B - #SHA3_256 = 13 - #SHA3_384 = 14 - #SHA3_512 = 15 - - def __init__(self, *args): - super(self.__class__, self).__init__() - self._tuned_count = 255 + SHA3_256 = 12 + _reserved_5 = 13 + SHA3_512 = 14 - @property - def hasher(self): - return hashlib.new(self.name) + @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 @property - def digest_size(self): - return self.hasher.digest_size + def hasher(self) -> hashes.Hash: + return hashes.Hash(getattr(hashes, self.name)()) @property - def tuned_count(self): - return self._tuned_count + 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): - return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512} + def is_collision_resistant(self) -> bool: + return self in {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512, + HashAlgorithm.SHA3_256, HashAlgorithm.SHA512} @property - def is_considered_secure(self): + def is_considered_secure(self) -> SecurityIssues: if self.is_collision_resistant: return SecurityIssues.OK @@ -408,6 +440,154 @@ 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() + + @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 + 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.""" @@ -423,17 +603,57 @@ class RevocationReason(IntEnum): 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 + PreferredAEADCiphersuites = 39 + + +class AttributeType(IntEnum): + Image = 1 + + class ImageEncoding(IntEnum): - Unknown = 0x00 + Unknown = -1 + Invalid = 0x00 JPEG = 0x01 @classmethod - def encodingof(cls, imagebytes): - type = imghdr.what(None, h=imagebytes) - if type == 'jpeg': + 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 + @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.""" @@ -516,17 +736,41 @@ class SignatureType(IntEnum): ThirdParty_Confirmation = 0x50 -class KeyServerPreferences(FlagEnum): +class KeyServerPreferences(IntFlag): NoModify = 0x80 class String2KeyType(IntEnum): + Unknown = -1 Simple = 0 Salted = 1 Reserved = 2 Iterated = 3 + Argon2 = 4 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 + + @property + def salt_length(self) -> int: + ks = {String2KeyType.Salted: 8, + String2KeyType.Iterated: 8, + String2KeyType.Argon2: 16, + } + 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): NoSecret = 1 @@ -543,7 +787,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 @@ -562,24 +806,33 @@ class KeyFlags(FlagEnum): MultiPerson = 0x80 -class Features(FlagEnum): +class Features(IntFlag): + SEIPDv1 = 0x01 + # alias (the old name, in RFC 4880): ModificationDetection = 0x01 + UnknownFeature02 = 0x02 + UnknownFeature04 = 0x04 + SEIPDv2 = 0x08 + UnknownFeature10 = 0x10 + UnknownFeature20 = 0x20 + UnknownFeature40 = 0x40 + UnknownFeature80 = 0x80 @classproperty - def pgpy_features(cls): - return Features.ModificationDetection + def pgpy_features(cls) -> Features: + return Features.SEIPDv1 | Features.SEIPDv2 -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 @@ -599,15 +852,17 @@ class SecurityIssues(IntFlag): AsymmetricKeyLengthIsTooShort = (1 << 8) InsecureCurve = (1 << 9) NoSelfSignature = (1 << 10) + AlgorithmUnknown = (1 << 11) @property - def causes_signature_verify_to_fail(self): + def causes_signature_verify_to_fail(self) -> bool: return self in { SecurityIssues.WrongSig, SecurityIssues.Expired, SecurityIssues.Disabled, SecurityIssues.Invalid, SecurityIssues.NoSelfSignature, + SecurityIssues.AlgorithmUnknown, } @@ -626,4 +881,9 @@ def causes_signature_verify_to_fail(self): 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, + PubKeyAlgorithm.X448: True, + PubKeyAlgorithm.X25519: True, } diff --git a/pgpy/decorators.py b/pgpy/decorators.py index 8a050c15..1c454771 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 @@ -20,7 +16,7 @@ def classproperty(fget): - class ClassProperty(object): + class ClassProperty: def __init__(self, fget): self.fget = fget self.__doc__ = fget.__doc__ @@ -69,9 +65,9 @@ def setter(self, fset): return SDProperty(fget, sdmethod(defset)) -class KeyAction(object): +class KeyAction: def __init__(self, *usage, **conditions): - super(KeyAction, self).__init__() + super().__init__() self.flags = set(usage) self.conditions = conditions @@ -79,8 +75,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 @@ -120,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!") + # 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__: + 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) diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index c94021c6..6d96d9e1 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -1,30 +1,26 @@ """ fields.py """ -from __future__ import absolute_import, division +from __future__ import annotations import abc import binascii import collections import copy -import hashlib import itertools import math import os -try: - import collections.abc as collections_abc -except ImportError: - collections_abc = collections +import collections.abc +from datetime import datetime -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 typing import Optional, Tuple, Type, Union -from cryptography.exceptions import InvalidSignature +from warnings import warn + +from argon2.low_level import hash_secret_raw # type: ignore +from argon2 import Type as ArgonType # type: ignore -from cryptography.hazmat.backends import default_backend +from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization @@ -33,8 +29,13 @@ 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 x448 +from cryptography.hazmat.primitives.asymmetric import utils +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.hashes import SHA256, SHA512, HashAlgorithm as cryptography_HashAlgorithm from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash @@ -48,16 +49,21 @@ from .subpackets import signature from .subpackets import userattribute +from .subpackets.types import SubPacket + from .types import MPI from .types import MPIs from ..constants import EllipticCurveOID from ..constants import ECPointFormat +from ..constants import PacketType from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm from ..constants import String2KeyType from ..constants import S2KGNUExtension from ..constants import SymmetricKeyAlgorithm +from ..constants import S2KUsage +from ..constants import AEADMode from ..decorators import sdproperty @@ -65,10 +71,12 @@ 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 ..symenc import AEAD from ..types import Field +from ..types import Fingerprint __all__ = ['SubPackets', 'UserAttributeSubPackets', @@ -78,6 +86,8 @@ 'DSASignature', 'ECDSASignature', 'EdDSASignature', + 'Ed25519Signature', + 'Ed448Signature', 'PubKey', 'OpaquePubKey', 'RSAPub', @@ -86,9 +96,20 @@ 'ECPoint', 'ECDSAPub', 'EdDSAPub', + 'Ed25519Pub', + 'Ed448Pub', 'ECDHPub', + 'X25519Pub', + 'X448Pub', + 'S2KSpecifier', 'String2Key', 'ECKDF', + 'NativeEdDSAPub', + 'NativeEdDSAPriv', + 'NativeEdDSASignature', + 'NativeCFRGXPriv', + 'NativeCFRGXPub', + 'NativeCFRGXCipherText', 'PrivKey', 'OpaquePrivKey', 'RSAPriv', @@ -96,47 +117,57 @@ 'ElGPriv', 'ECDSAPriv', 'EdDSAPriv', + 'Ed25519Priv', + 'Ed448Priv', 'ECDHPriv', + 'X25519Priv', + 'X448Priv', 'CipherText', 'RSACipherText', 'ElGCipherText', - 'ECDHCipherText', ] + 'ECDHCipherText', + 'X25519CipherText', + 'X448CipherText', + ] -class SubPackets(collections_abc.MutableMapping, Field): +class SubPackets(collections.abc.MutableMapping[str, SubPacket], Field): _spmodule = signature - def __init__(self): - super(SubPackets, self).__init__() - self._hashed_sp = collections.OrderedDict() - self._unhashed_sp = collections.OrderedDict() + def __init__(self, width: int = 2) -> None: + super().__init__() + 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): - 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 @@ -171,27 +202,34 @@ def __getitem__(self, key): def __delitem__(self, key): ##TODO: this - raise NotImplementedError + 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() + sp = SubPackets(self._width) sp._hashed_sp = self._hashed_sp.copy() sp._unhashed_sp = self._unhashed_sp.copy() 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) + else: + warn(f"subpacket {spname} does not have attr {p}") 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 @@ -199,24 +237,34 @@ def update_hlen(self): for sp in self: sp.update_hlen() - def parse(self, packet): - hl = self.bytes_to_int(packet[:2]) - del packet[:2] + def _normalize(self) -> None: + '''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 + + ''' + 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: bytearray) -> None: + 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 # 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[:2]) - del packet[:2] + uhl = self.bytes_to_int(packet[:self._width]) + del packet[:self._width] plen = len(packet) while plen - len(packet) < uhl: - sp = SignatureSP(packet) + sp = SignatureSP(packet) # type: ignore[abstract] self[sp.__class__.__name__] = sp @@ -228,29 +276,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() @@ -266,17 +314,17 @@ def from_signer(self, sig): class OpaqueSignature(Signature): - def __init__(self): - super(OpaqueSignature, self).__init__() + 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): @@ -289,7 +337,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): @@ -299,65 +347,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] - - i = self.bytes_to_int(_asn[:flen]) - del _asn[:flen] - return i + def __sig__(self) -> bytes: + # return the RFC 3279 encoding: + return utils.encode_dss_signature(self.r, self.s) - if isinstance(sig, bytes): - sig = bytearray(sig) + 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) - # 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): @@ -375,16 +381,54 @@ 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__ = () + __pubfields__: Tuple = () + __pubkey_algo__: Optional[PubKeyAlgorithm] = None @property def __mpis__(self): - for i in self.__pubfields__: - yield i + yield from self.__pubfields__ - def __init__(self): - super(PubKey, self).__init__() + def __init__(self) -> None: + super().__init__() for field in self.__pubfields__: if isinstance(field, tuple): # pragma: no cover field, val = field @@ -396,47 +440,58 @@ 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): - return NotImplemented # pragma: no cover + raise NotImplementedError() # pragma: no cover + + def encrypt(self, symalg: Optional[SymmetricKeyAlgorithm], data: bytes, fpr: Fingerprint) -> CipherText: + raise NotImplementedError() + + 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): - super(OpaquePubKey, self).__init__() + super().__init__() self.data = bytearray() def __iter__(self): yield self.data def __pubkey__(self): - return NotImplemented + raise NotImplementedError() - 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 class RSAPub(PubKey): __pubfields__ = ('n', 'e') + __pubkey_algo__ = PubKeyAlgorithm.RSAEncryptOrSign - def __pubkey__(self): - return rsa.RSAPublicNumbers(self.e, self.n).public_key(default_backend()) + def __pubkey__(self) -> rsa.RSAPublicKey: + return rsa.RSAPublicNumbers(self.e, self.n).public_key() def verify(self, subj, sigbytes, hash_alg): # zero-pad sigbytes if necessary @@ -447,17 +502,23 @@ def verify(self, subj, sigbytes, hash_alg): return False return True - def parse(self, packet): + 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) class DSAPub(PubKey): __pubfields__ = ('p', 'q', 'g', 'y') + __pubkey_algo__ = PubKeyAlgorithm.DSA 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: @@ -466,7 +527,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) @@ -475,11 +536,12 @@ def parse(self, packet): class ElGPub(PubKey): __pubfields__ = ('p', 'g', 'y') + __pubkey_algo__ = PubKeyAlgorithm.ElGamal 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) @@ -515,7 +577,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 @@ -524,7 +586,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) @@ -537,10 +599,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 @@ -551,25 +613,28 @@ def __copy__(self): class ECDSAPub(PubKey): __pubfields__ = ('p',) + __pubkey_algo__ = PubKeyAlgorithm.ECDSA - def __init__(self): - super(ECDSAPub, self).__init__() - self.oid = None + def __init__(self) -> None: + super().__init__() + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.NIST_P256 - def __len__(self): - return len(self.p) + len(encoder.encode(self.oid.value)) - 1 + def __len__(self) -> int: + 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): + def __bytearray__(self) -> bytearray: _b = bytearray() - _b += encoder.encode(self.oid.value)[1:] + _b += bytes(self.oid) _b += self.p.to_mpibytes() return _b - def __copy__(self): - pkt = super(ECDSAPub, self).__copy__() + 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 @@ -580,49 +645,48 @@ def verify(self, subj, sigbytes, hash_alg): return False 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] + def parse(self, packet: bytearray) -> None: + 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: Union[ECPoint, MPI] = 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): __pubfields__ = ('p', ) + __pubkey_algo__ = PubKeyAlgorithm.EdDSA - def __init__(self): - super(EdDSAPub, self).__init__() - self.oid = None + def __init__(self) -> None: + super().__init__() + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.Ed25519 - def __len__(self): - return len(self.p) + len(encoder.encode(self.oid.value)) - 1 + def __len__(self) -> int: + return len(self.p) + len(self.oid) - 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() return _b def __pubkey__(self): return ed25519.Ed25519PublicKey.from_public_bytes(self.p.x) - def __copy__(self): - pkt = super(EdDSAPub, self).__copy__() + 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 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: @@ -631,52 +695,109 @@ def verify(self, subj, sigbytes, hash_alg): return False 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] + def parse(self, packet: bytearray) -> None: + 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: Union[ECPoint, MPI] = ECPoint(packet) + if self.p.format != ECPointFormat.Native: + raise PGPIncompatibleECPointFormatError("Only Native format is valid for EdDSA") + else: + 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 - def __init__(self): - super(ECDHPub, self).__init__() - self.oid = None + def __init__(self) -> None: + super().__init__() + self.oid: Union[bytes, EllipticCurveOID] = EllipticCurveOID.NIST_P256 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: + 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(default_backend()) + return ec.EllipticCurvePublicNumbers(self.p.x, self.p.y, self.oid.curve()).public_key() - 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 - def __copy__(self): - pkt = super(ECDHPub, self).__copy__() + 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 - def parse(self, packet): + def parse(self, packet: bytearray) -> None: """ Algorithm-Specific Fields for ECDH keys: @@ -705,27 +826,164 @@ 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) + + if isinstance(self.oid, EllipticCurveOID): + self.p: Union[ECPoint, MPI] = ECPoint(packet) + 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: + raise PGPIncompatibleECPointFormatError("Only Standard format is valid for this curve") + else: + self.p = MPI(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") 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. -class String2Key(Field): + 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 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 + 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 @@ -829,163 +1087,231 @@ 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): - return self._encalg - @encalg.register(int) - @encalg.register(SymmetricKeyAlgorithm) - def encalg_int(self, val): - 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, + argon2_time: int = 1, + argon2_parallelism: int = 4, + argon2_memory_exp: int = 21, + ): + 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)}") + 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 + 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._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: + 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) + s2k._a2_t = self._a2_t + s2k._a2_p = self._a2_p + s2k._a2_m = self._a2_m + return s2k @sdproperty - def specifier(self): - return self._specifier - - @specifier.register(int) - @specifier.register(String2KeyType) - def specifier_int(self, val): - self._specifier = String2KeyType(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): - return self._gnuext - - @gnuext.register(int) - @gnuext.register(S2KGNUExtension) - def gnuext_int(self, val): - 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): + def halg(self) -> HashAlgorithm: return self._halg - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): + @halg.register + def halg_set(self, val: Union[HashAlgorithm, int]) -> None: self._halg = HashAlgorithm(val) @sdproperty - def count(self): - return (16 + (self._count & 15)) << ((self._count >> 4) + 6) - - @count.register(int) - def count_int(self, val): - if val < 0 or val > 255: # pragma: no cover - raise ValueError("count must be between 0 and 256") - self._count = val - - def __init__(self): - super(String2Key, self).__init__() - self.usage = 0 - self.encalg = 0 - self.specifier = 0 - self.iv = None - - # specifier-specific fields - # simple, salted, iterated - self.halg = 0 + def salt(self) -> bytes: + if self._type.salt_length == 0: + return b'' + if self._salt is None: + self._salt = os.urandom(self._type.salt_length) + return self._salt + + @salt.register + def salt_bytes(self, val: Union[bytes, bytearray]) -> None: + 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) - # salted, iterated - self.salt = bytearray() - - # iterated - self.count = 0 - - # GNU extension default type: ignored if specifier != GNUExtension - self.gnuext = 1 - - # GNU extension smartcard - self.scserial = None + @property + def gnuext(self) -> Optional[S2KGNUExtension]: + return self._gnupg_extension - def __bytearray__(self): + @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() - _bytes.append(self.usage) - if bool(self): - _bytes.append(self.encalg) - _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) - if self.iv is not None: - _bytes += self.iv - return _bytes - - def _experimental_bytearray(self, _bytes): - if self.specifier == String2KeyType.GNUExtension: - _bytes += b'\x00GNU' - _bytes.append(self.gnuext) - if self.scserial: - _bytes.append(len(self.scserial)) - _bytes += self.scserial + 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 + 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): + def __len__(self) -> int: return len(self.__bytearray__()) - def __bool__(self): - return self.usage in [254, 255] - - def __nonzero__(self): - return self.__bool__() - - def __copy__(self): - s2k = String2Key() - s2k.usage = self.usage - s2k.encalg = self.encalg - s2k.specifier = self.specifier - s2k.gnuext = self.gnuext - 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, iv=True): - self.usage = packet[0] + def parse(self, packet: bytearray) -> None: + self._type = String2KeyType(packet[0]) + if self._type is String2KeyType.Unknown: + self._opaque_type: int = packet[0] del packet[0] - if bool(self): - self.encalg = packet[0] - del packet[0] + if self._type is String2KeyType.GNUExtension: + return self._parse_gnu_extension(packet) - self.specifier = packet[0] + if self._type in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated}: + self._halg = HashAlgorithm(packet[0]) del packet[0] - if self.specifier == 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 == String2KeyType.Iterated: - self.count = packet[0] - del packet[0] - - if iv: - self.iv = packet[:(self.encalg.block_size // 8)] - del packet[:(self.encalg.block_size // 8)] + if self._type.salt_length > 0: + self._salt = bytes(packet[:self._type.salt_length]) + del packet[:self._type.salt_length] + + 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})") + 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 _experimental_parse(self, packet, iv=True): + 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 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. @@ -1001,89 +1327,223 @@ def _experimental_parse(self, packet, iv=True): - 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._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] - if self.gnuext == S2KGNUExtension.Smartcard: - slen = min(packet[0], 16) - del packet[0] - self.scserial = packet[:slen] - del packet[:slen] + 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): - ##TODO: raise an exception if self.usage is not 254 or 255 - keylen = self.encalg.key_size - hashlen = self.halg.digest_size * 8 + def derive_key(self, passphrase: Union[str, bytes], keylen_bits: int) -> bytes: + if self._type not in {String2KeyType.Simple, String2KeyType.Salted, String2KeyType.Iterated, String2KeyType.Argon2}: + raise NotImplementedError(f"Cannot derive key from S2KSpecifier {self._type!r}") - ctx = int(math.ceil((keylen / hashlen))) + if not isinstance(passphrase, bytes): + passphrase = passphrase.encode('utf-8') - # Simple S2K - always done - hsalt = b'' - if isinstance(passphrase, bytes): - hpass = passphrase - else: - hpass = 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) - # salted, iterated S2K - if self.specifier >= String2KeyType.Salted: - hsalt = bytes(self.salt) + hashlen = self._halg.digest_size * 8 - count = len(hsalt + hpass) - if self.specifier == String2KeyType.Iterated and self.count > len(hsalt + hpass): - count = self.count + ctx = int(math.ceil((keylen_bits / hashlen))) - hcount = (count // len(hsalt + hpass)) - hleft = count - (hcount * len(hsalt + hpass)) + base_count = len(self.salt + passphrase) + count = base_count + if self._type is String2KeyType.Iterated and self._count > count: + count = self._count - hashdata = ((hsalt + hpass) * hcount) + (hsalt + hpass)[:hleft] + 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) - _h.update(hashdata) + _h = self._halg.hasher + _h.update(b'\x00' * i + (self.salt + passphrase) * hcount + (self.salt + passphrase)[:hleft]) h.append(_h) - # GC some stuff - del hsalt - del hpass - 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_bits // 8)] -class ECKDF(Field): +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, an AEAD mode, and an IV. """ - o a variable-length field containing KDF parameters, - formatted as follows: - - - a one-octet size of the following fields; values 0 and - 0xff are reserved for future extensions - - a one-octet value 01, reserved for future extensions + @sdproperty + def encalg(self) -> SymmetricKeyAlgorithm: + return self._encalg - - a one-octet hash function ID used with a KDF + @encalg.register + def encalg_int(self, val: int) -> None: + if isinstance(val, SymmetricKeyAlgorithm): + self._encalg: SymmetricKeyAlgorithm = val + else: + self._encalg = SymmetricKeyAlgorithm(val) - - a one-octet algorithm ID for the symmetric algorithm - used to wrap the symmetric key used for the message - encryption; see Section 8 for details - """ - @sdproperty - def halg(self): - return self._halg + @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._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 - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - self._halg = HashAlgorithm(val) + 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 encalg(self): + 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 + + def __init__(self, key_version: int) -> None: + super().__init__() + 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 + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes.append(self.usage) + if bool(self): + 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: + conditionals += self.iv + if self.key_version == 6: + _bytes.append(len(conditionals)) + _bytes += conditionals + return _bytes + + def __len__(self) -> int: + return len(self.__bytearray__()) + + def __bool__(self) -> bool: + # 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) + s2k.usage = self.usage + s2k.encalg = self.encalg + s2k._specifier = copy.copy(self._specifier) + + s2k.iv = self.iv + return s2k + + def parse(self, packet: bytearray) -> None: + self.usage = S2KUsage(packet[0]) + 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] + + if self.usage is S2KUsage.AEAD: + 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 + if ivlen: + self.iv = packet[:(ivlen)] + del packet[:(ivlen)] + + def derive_key(self, passphrase) -> bytes: + 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) + + +class ECKDF(Field): + """ + o a variable-length field containing KDF parameters, + formatted as follows: + + - a one-octet size of the following fields; values 0 and + 0xff are reserved for future extensions + + - a one-octet value 01, reserved for future extensions + + - a one-octet hash function ID used with a KDF + + - a one-octet algorithm ID for the symmetric algorithm + used to wrap the symmetric key used for the message + encryption; see Section 8 for details + """ + @sdproperty + def halg(self): + return self._halg + + @halg.register(int) + @halg.register(HashAlgorithm) + def halg_int(self, val): + self._halg = HashAlgorithm(val) + + @sdproperty + def encalg(self): return self._encalg @encalg.register(int) @@ -1092,11 +1552,11 @@ def encalg_int(self, val): self._encalg = SymmetricKeyAlgorithm(val) def __init__(self): - super(ECKDF, self).__init__() + super().__init__() self.halg = 0 self.encalg = 0 - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() _bytes.append(len(self) - 1) _bytes.append(0x01) @@ -1107,7 +1567,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 @@ -1121,12 +1581,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) @@ -1134,50 +1594,52 @@ def derive_key(self, s, curve, pkalg, fingerprint): 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) class PrivKey(PubKey): - __privfields__ = () + __privfields__: Tuple = () @property def __mpis__(self): - for i in super(PrivKey, self).__mpis__: - yield i + yield from super().__mpis__ + yield from self.__privfields__ - for i in self.__privfields__: - yield i + def __init__(self, key_version: int = 4) -> None: + super().__init__() - def __init__(self): - super(PrivKey, self).__init__() - - self.s2k = String2Key() + self.key_version = key_version + self.s2k = String2Key(key_version) self.encbytes = bytearray() self.chksum = bytearray() for field in self.__privfields__: setattr(self, field, MPI(0)) - def __bytearray__(self): + 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) -> bytearray: _bytes = bytearray() - _bytes += super(PrivKey, self).__bytearray__() + _bytes += super().__bytearray__() _bytes += self.s2k.__bytearray__() if self.s2k: _bytes += self.encbytes else: - for field in self.__privfields__: - _bytes += getattr(self, field).to_mpibytes() + self._append_private_fields(_bytes) - if self.s2k.usage == 0: + if self.s2k.usage is S2KUsage.Unprotected and self.key_version == 4: # checksum is only appropriate for v4 keys: _bytes += self.chksum 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 +1649,8 @@ def __len__(self): return nbytes def __copy__(self): - pk = super(PrivKey, self).__copy__() + 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) @@ -1198,49 +1661,124 @@ 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: Optional[Union[int, EllipticCurveOID]]) -> None: """Generate a new PrivKey""" def _compute_chksum(self): "Calculate the key checksum" - def publen(self): - return super(PrivKey, self).__len__() - - def encrypt_keyblob(self, passphrase, enc_alg, hash_alg): - # PGPy will only ever use iterated and salted S2k mode - self.s2k.usage = 254 + 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, + 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 - self.s2k.specifier = String2KeyType.Iterated - 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 - - # 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 + passed_s2kspec: bool + if s2kspec is not None: + passed_s2kspec = True + 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: + 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() pt = bytearray() - for pf in self.__privfields__: - 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() - - # encrypt - self.encbytes = bytearray(_encrypt(bytes(pt), bytes(sessionkey), enc_alg, bytes(self.s2k.iv))) + self._append_private_fields(pt) + + 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 self.clear() @abc.abstractmethod - def decrypt_keyblob(self, passphrase): + 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], + packet_type: PacketType, + creation_time: Optional[datetime]) -> Optional[bytearray]: if not self.s2k: # pragma: no cover # not encrypted - return + 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 @@ -1255,15 +1793,15 @@ 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(): + 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!") - 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!") @@ -1271,9 +1809,48 @@ def decrypt_keyblob(self, passphrase): return bytearray(pt) def sign(self, sigdata, hash_alg): - return NotImplemented # pragma: no cover + raise NotImplementedError() # pragma: no cover - def clear(self): + 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]: + """ + 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__: delattr(self, field) @@ -1282,14 +1859,15 @@ def clear(self): class OpaquePrivKey(PrivKey, OpaquePubKey): # pragma: no cover def __privkey__(self): - return NotImplemented + raise NotImplementedError() - def _generate(self, key_size): - # return NotImplemented + def _generate(self, key_size_or_oid: Optional[Union[int, EllipticCurveOID]]) -> None: raise NotImplementedError() - def decrypt_keyblob(self, passphrase): - return NotImplemented + def decrypt_keyblob(self, passphrase: Union[str, bytes], + packet_type: PacketType = PacketType.SecretKey, + creation_time: Optional[datetime] = None) -> None: + raise NotImplementedError() class RSAPriv(PrivKey, RSAPub): @@ -1300,18 +1878,24 @@ 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 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, default_backend()) + pk = rsa.generate_private_key(65537, key_size) pkn = pk.private_numbers() self.n = MPI(pkn.public_numbers.n) @@ -1331,8 +1915,8 @@ def _generate(self, key_size): self._compute_chksum() - def parse(self, packet): - super(RSAPriv, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1341,7 +1925,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] @@ -1349,22 +1933,36 @@ def parse(self, packet): ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super(RSAPriv, self).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 self.d = MPI(kb) self.p = MPI(kb) 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 - def sign(self, sigdata, hash_alg): + 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',) @@ -1372,18 +1970,24 @@ 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 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, default_backend()) + pk = dsa.generate_private_key(key_size) pkn = pk.private_numbers() self.p = MPI(pkn.public_numbers.parameter_numbers.p) @@ -1397,8 +2001,8 @@ def _generate(self, key_size): self._compute_chksum() - def parse(self, packet): - super(DSAPriv, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1407,17 +2011,21 @@ 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] - def decrypt_keyblob(self, passphrase): - kb = super(DSAPriv, self).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 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 @@ -1435,11 +2043,11 @@ 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): - super(ElGPriv, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.s2k.parse(packet) if not self.s2k: @@ -1448,17 +2056,21 @@ 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] - def decrypt_keyblob(self, passphrase): - kb = super(ElGPriv, self).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 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 @@ -1468,44 +2080,54 @@ 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): + 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, oid): + 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!") - self.oid = EllipticCurveOID(oid) - - if not self.oid.can_gen: - raise ValueError("Curve not currently supported: {}".format(oid.name)) + 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}") + self.oid = oid + 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) self._compute_chksum() - def parse(self, packet): - super(ECDSAPriv, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.s2k.parse(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: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super(ECDSAPriv, self).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 self.s = MPI(kb) def sign(self, sigdata, hash_alg): @@ -1523,14 +2145,22 @@ 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: Optional[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 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}") + self.oid = oid + else: + self.oid = params - if self.oid != EllipticCurveOID.Ed25519: - raise ValueError("EdDSA only supported with {}".format(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() x = pk.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) @@ -1542,46 +2172,144 @@ def _generate(self, oid): ))) self._compute_chksum() - def parse(self, packet): - super(EdDSAPriv, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.s2k.parse(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: ##TODO: this needs to be bounded to the length of the encrypted key material self.encbytes = packet - def decrypt_keyblob(self, passphrase): - kb = super(EdDSAPriv, self).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 self.s = MPI(kb) 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) -class ECDHPriv(ECDSAPriv, ECDHPub): - def __bytearray__(self): +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], + 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] + + 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) _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 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) @@ -1590,16 +2318,24 @@ def __len__(self): 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) else: return ECDSAPriv.__privkey__(self) - def _generate(self, oid): - _oid = EllipticCurveOID(oid) - if _oid == EllipticCurveOID.Curve25519: + 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}") + else: + _oid = params + + 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 @@ -1614,20 +2350,21 @@ def _generate(self, oid): ), 'little')) self._compute_chksum() else: - ECDSAPriv._generate(self, oid) - self.kdf.halg = self.oid.kdf_halg - self.kdf.encalg = self.oid.kek_alg + ECDSAPriv._generate(self, _oid) + 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) - def parse(self, packet): + def parse(self, packet: bytearray) -> None: ECDHPub.parse(self, packet) self.s2k.parse(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: @@ -1637,153 +2374,180 @@ def parse(self, packet): 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)}") -class CipherText(MPIs): - def __init__(self): - super(CipherText, self).__init__() - for i in self.__mpis__: - setattr(self, i, MPI(0)) + if not isinstance(self.oid, EllipticCurveOID): + raise TypeError(f"ECDH: Cannot decrypt with unknown curve({self.oid!r})") - @classmethod - @abc.abstractmethod - def encrypt(cls, encfn, *args): - """create and populate a concrete CipherText class instance""" + 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) - @abc.abstractmethod - def decrypt(self, decfn, *args): - """decrypt the ciphertext contained in this CipherText instance""" + # derive the wrapping key + z = self.kdf.derive_key(s, self.oid, PubKeyAlgorithm.ECDH, fpr) - def __bytearray__(self): - _bytes = bytearray() - for i in self: - _bytes += i.to_mpibytes() - return _bytes + # 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 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 +class NativeCFRGXPriv(PrivKey, NativeCFRGXPub): + def __privkey__(self) -> Union[x25519.X25519PrivateKey, x448.X448PrivateKey]: + return self._raw_privkey - def decrypt(self, decfn, *args): - return decfn(*args) + def clear(self) -> None: + if hasattr(self, '_raw_privkey'): + delattr(self, '_raw_privkey') - def parse(self, packet): - self.me_mod_n = MPI(packet) + @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)) -class ElGCipherText(CipherText): - __mpis__ = ('gk_mod_p', 'myk_mod_p') + 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() - @classmethod - def encrypt(cls, encfn, *args): - raise NotImplementedError() + def parse(self, packet: bytearray) -> None: + NativeCFRGXPub.parse(self, packet) + # parse s2k business + self.s2k.parse(packet) - def decrypt(self, decfn, *args): - raise NotImplementedError() + 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 parse(self, packet): - self.gk_mod_p = MPI(packet) - self.myk_mod_p = MPI(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") -class ECDHCipherText(CipherText): - __mpis__ = ('p',) + 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 - @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. + self._raw_privkey = self._native_private_type.from_private_bytes(kb[:self._private_length]) + del kb[:self._private_length] - 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). + if self.s2k.usage in [254, 255]: + self.chksum = kb + del kb - 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 + 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)}") - # m may need to be PKCS5-padded - padder = PKCS7(64).padder() - m = padder.update(_m) + padder.finalize() + if ct._sym_algo is None and get_symalg: + raise TypeError("Asked for symmetric algorithm but none was present") - km = pk.keymaterial - ct = cls() + 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) - # 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(), default_backend()) - 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__()) + return (ct._sym_algo, cleartext) - # 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, default_backend()) +class X25519Priv(NativeCFRGXPriv, X25519Pub): + @property + def _private_length(self) -> int: + return 32 - return ct + @property + def _native_private_type(self) -> Union[Type[x25519.X25519PrivateKey], Type[x448.X448PrivateKey]]: + return x25519.X25519PrivateKey - 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(default_backend()) - # 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) +class X448Priv(NativeCFRGXPriv, X448Pub): + @property + def _private_length(self) -> int: + return 56 - # unwrap and unpad m - _m = aes_key_unwrap(z, self.c, default_backend()) + @property + def _native_private_type(self) -> Union[Type[x25519.X25519PrivateKey], Type[x448.X448PrivateKey]]: + return x448.X448PrivateKey - padder = PKCS7(64).unpadder() - return padder.update(_m) + padder.finalize() - def __init__(self): - super(ECDHCipherText, self).__init__() +class CipherText(MPIs): + def __init__(self) -> None: + super().__init__() + for i in self.__mpis__: + setattr(self, i, MPI(0)) + + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + for i in self: + _bytes += i.to_mpibytes() + return _bytes + + +class RSACipherText(CipherText): + __mpis__ = ('me_mod_n', ) + + 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) + + +class ElGCipherText(CipherText): + __mpis__ = ('gk_mod_p', 'myk_mod_p') + + def parse(self, packet: bytearray) -> None: + self.gk_mod_p = MPI(packet) + self.myk_mod_p = MPI(packet) + + +class ECDHCipherText(CipherText): + __mpis__ = ('p',) + + 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 @@ -1791,3 +2555,113 @@ def parse(self, packet): 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 cd494137..695392ee 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -1,28 +1,48 @@ """ packet.py """ +from __future__ import annotations + import abc import binascii import calendar import copy -import hashlib import os import warnings 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.asymmetric import padding +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 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 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 from .fields import OpaquePubKey from .fields import OpaquePrivKey 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 @@ -32,7 +52,10 @@ from .types import Public from .types import Sub from .types import VersionedPacket +from .types import VersionedHeader +from ..constants import PacketType +from ..constants import EllipticCurveOID from ..constants import CompressionAlgorithm from ..constants import HashAlgorithm from ..constants import PubKeyAlgorithm @@ -40,56 +63,106 @@ from ..constants import SymmetricKeyAlgorithm from ..constants import TrustFlags from ..constants import TrustLevel +from ..constants import AEADMode from ..decorators import sdproperty from ..errors import PGPDecryptionError +from ..errors import PGPEncryptionError +from ..errors import PGPError -from ..symenc import _decrypt -from ..symenc import _encrypt +from ..symenc import _cfb_decrypt +from ..symenc import _cfb_encrypt +from ..symenc import AEAD from ..types import Fingerprint +from ..types import KeyID __all__ = ['PKESessionKey', 'PKESessionKeyV3', + 'PKESessionKeyV6', 'Signature', 'SignatureV4', + 'SignatureV6', 'SKESessionKey', 'SKESessionKeyV4', + 'SKESessionKeyV6', 'OnePassSignature', 'OnePassSignatureV3', + 'OnePassSignatureV6', 'PrivKey', 'PubKey', 'PubKeyV4', + 'PubKeyV6', 'PrivKeyV4', + 'PrivKeyV6', 'PrivSubKey', 'PrivSubKeyV4', + 'PrivSubKeyV6', 'CompressedData', 'SKEData', 'Marker', + 'Padding', 'LiteralData', 'Trust', 'UserID', 'PubSubKey', 'PubSubKeyV4', + 'PubSubKeyV6', 'UserAttribute', 'IntegrityProtectedSKEData', 'IntegrityProtectedSKEDataV1', + 'IntegrityProtectedSKEDataV2', 'MDC'] class PKESessionKey(VersionedPacket): - __typeid__ = 0x01 + __typeid__ = PacketType.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): + def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: raise NotImplementedError() @abc.abstractmethod - def encrypt_sk(self, pk, symalg, symkey): + 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 + @abc.abstractproperty + 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() + elif self._pkalg is PubKeyAlgorithm.X25519: + self.ct = X25519CipherText() + elif self._pkalg is PubKeyAlgorithm.X448: + self.ct = X448CipherText() + class PKESessionKeyV3(PKESessionKey): """ @@ -157,42 +230,35 @@ 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') - - @sdproperty - def pkalg(self): - return self._pkalg - - @pkalg.register(int) - @pkalg.register(PubKeyAlgorithm) - def pkalg_int(self, val): - self._pkalg = PubKeyAlgorithm(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 + @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) - def __init__(self): - super(PKESessionKeyV3, self).__init__() - self.encrypter = bytearray(8) - self.pkalg = 0 - self.ct = None + def __init__(self) -> None: + super().__init__() + self._encrypter = None + self.symalg: Optional[SymmetricKeyAlgorithm] = None def __bytearray__(self): _bytes = bytearray() - _bytes += super(PKESessionKeyV3, self).__bytearray__() - _bytes += binascii.unhexlify(self.encrypter.encode()) - _bytes += bytearray([self.pkalg]) + _bytes += super().__bytearray__() + 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: + _bytes.append(self.pkalg) + _bytes += self.ct.__bytearray__() if self.ct is not None else b'\x00' * (self.header.length - 10) return _bytes @@ -201,92 +267,212 @@ 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: sk.ct = copy.copy(self.ct) return sk - def decrypt_sk(self, pk): - if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: - # 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 + def decrypt_sk(self, pk: PrivKey) -> Tuple[Optional[SymmetricKeyAlgorithm], bytes]: + 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") - decrypter = pk.keymaterial.__privkey__().decrypt - decargs = (ct, padding.PKCS1v15(),) + return pk.keymaterial.decrypt(self.ct, pk.fingerprint, True) - elif self.pkalg == PubKeyAlgorithm.ECDH: - decrypter = pk - decargs = () + 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') - 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. - """ + self.ct = pk.keymaterial.encrypt(symalg, symkey, pk.fingerprint) - symalg = SymmetricKeyAlgorithm(m[0]) - del m[0] + self.update_hlen() - symkey = m[:symalg.key_size // 8] - del m[:symalg.key_size // 8] + def parse(self, packet): + super().parse(packet) + self.encrypter = packet[:8] + del packet[:8] - checksum = self.bytes_to_int(m[:2]) - del m[:2] + self.pkalg = packet[0] + del packet[0] - if not sum(symkey) % 65536 == checksum: # pragma: no cover - raise PGPDecryptionError("{:s} decryption failed".format(self.pkalg.name)) + if self.ct is not None: + self.ct.parse(packet) + + else: # pragma: no cover + del packet[:(self.header.length - 10)] - return (symalg, symkey) - def encrypt_sk(self, pk, symalg, symkey): - m = bytearray(self.int_to_bytes(symalg) + symkey) - m += self.int_to_bytes(sum(bytearray(symkey)) % 65536, 2) +class PKESessionKeyV6(PKESessionKey): + __ver__ = 6 - if self.pkalg == PubKeyAlgorithm.RSAEncryptOrSign: - encrypter = pk.keymaterial.__pubkey__().encrypt - encargs = (bytes(m), padding.PKCS1v15(),) + def __init__(self) -> None: + super().__init__() + self._encrypter: Optional[Fingerprint] = None - elif self.pkalg == PubKeyAlgorithm.ECDH: - encrypter = pk - encargs = (bytes(m),) + @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: - raise NotImplementedError(self.pkalg) + _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 - self.ct = self.ct.encrypt(encrypter, *encargs) + 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 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) + + 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: + 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): - super(PKESessionKeyV3, self).parse(packet) - self.encrypter = packet[:8] - del packet[:8] + 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 - 18)] + del packet[:(self.header.length - (2 + fplen + 1))] class Signature(VersionedPacket): - __typeid__ = 0x02 + __typeid__ = PacketType.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.__subpacket_width__) + 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() + elif self.pubalg is PubKeyAlgorithm.Ed25519: + self.signature = Ed25519Signature() + elif self.pubalg is PubKeyAlgorithm.Ed448: + self.signature = Ed448Signature() + 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() + + @abc.abstractproperty + def signer(self) -> Optional[Union[KeyID, Fingerprint]]: + ... + + @abc.abstractmethod + def canonical_bytes(self) -> bytearray: + ... class SignatureV4(Signature): @@ -340,77 +526,177 @@ class SignatureV4(Signature): """ __ver__ = 4 - @sdproperty - def sigtype(self): - return self._sigtype + @property + 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 __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes += self.int_to_bytes(self.sigtype) + if self.pubalg is PubKeyAlgorithm.Unknown: + _bytes.append(self._opaque_pubalg) + else: + _bytes.append(self.pubalg) + 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__() - @sigtype.register(int) - @sigtype.register(SignatureType) - def sigtype_int(self, val): - self._sigtype = SignatureType(val) + return _bytes - @sdproperty - def pubalg(self): - return self._pubalg + def canonical_bytes(self) -> bytearray: + '''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() + 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: + _body.append(self._opaque_pubalg) + else: + _body.append(self.pubalg) + 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 + _body += self.signature.__bytearray__() - @pubalg.register(int) - @pubalg.register(PubKeyAlgorithm) - def pubalg_int(self, val): - self._pubalg = PubKeyAlgorithm(val) + _hdr = bytearray() + _hdr += b'\x88' + _hdr += self.int_to_bytes(len(_body), minlen=4) + return _hdr + _body - sigs = { - PubKeyAlgorithm.RSAEncryptOrSign: RSASignature, - PubKeyAlgorithm.RSAEncrypt: RSASignature, - PubKeyAlgorithm.RSASign: RSASignature, - PubKeyAlgorithm.DSA: DSASignature, - PubKeyAlgorithm.ECDSA: ECDSASignature, - PubKeyAlgorithm.EdDSA: EdDSASignature, - } + def __copy__(self) -> SignatureV4: + spkt = SignatureV4() + 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 + if self._halg is HashAlgorithm.Unknown: + spkt._opaque_halg = self._opaque_halg - self.signature = sigs.get(self.pubalg, OpaqueSignature)() + spkt.subpackets = copy.copy(self.subpackets) + spkt.hash2 = copy.copy(self.hash2) + spkt.signature = copy.copy(self.signature) - @sdproperty - def halg(self): - return self._halg + return spkt - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - try: - self._halg = HashAlgorithm(val) + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.sigtype = packet[0] + del packet[0] - except ValueError: # pragma: no cover - self._halg = val + self.pubalg = packet[0] + del packet[0] - @property - def signature(self): - return self._signature + self.halg = packet[0] + del packet[0] - @signature.setter - def signature(self, val): - self._signature = val + self.subpackets.parse(packet) + + self.hash2 = packet[:2] + del packet[:2] + + 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 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): - return self.subpackets['Issuer'][-1].issuer + def signer(self) -> Optional[Fingerprint]: + if 'IssuerFingerprint' in self.subpackets: + return self.subpackets['IssuerFingerprint'][-1].issuer_fingerprint + 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 + @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(Signature, self).__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 @@ -428,6 +714,7 @@ def canonical_bytes(self): 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) @@ -435,8 +722,10 @@ def canonical_bytes(self): _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=2) # empty unhashed subpackets + _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() @@ -445,7 +734,7 @@ def canonical_bytes(self): return _hdr + _body def __copy__(self): - spkt = SignatureV4() + spkt = SignatureV6() spkt.header = copy.copy(self.header) spkt._sigtype = self._sigtype spkt._pubalg = self._pubalg @@ -453,16 +742,13 @@ def __copy__(self): 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 update_hlen(self): - self.subpackets.update_hlen() - super(SignatureV4, self).update_hlen() - def parse(self, packet): - super(Signature, self).parse(packet) + super().parse(packet) self.sigtype = packet[0] del packet[0] @@ -477,19 +763,50 @@ def parse(self, 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__ = 0x03 + __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: @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() @@ -546,43 +863,40 @@ class SKESessionKeyV4(SKESessionKey): """ __ver__ = 4 - @property - def symalg(self): - return self.s2k.encalg - - def __init__(self): - super(SKESessionKeyV4, self).__init__() - self.s2k = String2Key() + def __init__(self) -> None: + super().__init__() self.ct = bytearray() - def __bytearray__(self): + def __bytearray__(self) -> bytearray: _bytes = bytearray() - _bytes += super(SKESessionKeyV4, self).__bytearray__() - _bytes += self.s2k.__bytearray__()[1:] + _bytes += super().__bytearray__() + _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): - super(SKESessionKeyV4, self).parse(packet) - # prepend a valid usage identifier so this parses correctly - packet.insert(0, 255) - self.s2k.parse(packet, iv=False) + def parse(self, packet: bytearray) -> None: + super().parse(packet) + 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): + 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 @@ -590,7 +904,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]) @@ -598,22 +912,207 @@ def decrypt_sk(self, passphrase): return symalg, bytes(m) - def encrypt_sk(self, passphrase, sk): - # 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) + 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.s2kspec.derive_key(passphrase, self.symalg.key_size) del passphrase - self.ct = _encrypt(self.int_to_bytes(self.symalg) + sk, esk, self.symalg) + # 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) + bytes(sk), esk, self.symalg) + + # update header length and return sk + 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): - __typeid__ = 0x04 + '''Holds common members of various OPS packet versions''' + __typeid__ = PacketType.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): """ @@ -655,176 +1154,223 @@ class OnePassSignatureV3(OnePassSignature): __ver__ = 3 @sdproperty - def sigtype(self): - return self._sigtype + def signer(self) -> KeyID: + return self._signer - @sigtype.register(int) - @sigtype.register(SignatureType) - def sigtype_int(self, val): - self._sigtype = SignatureType(val) + @signer.register + def signer_set(self, val: Union[bytearray, bytes, str, KeyID, Fingerprint]) -> None: + self._signer = KeyID(val) - @sdproperty - def pubalg(self): - return self._pubalg + def __init__(self) -> None: + super().__init__() + self._signer = KeyID(b'\x00' * 8) - @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() + def __bytearray__(self) -> bytearray: + _bytes = bytearray() + _bytes += super().__bytearray__() + _bytes.append(self.sigtype) + if self.halg is HashAlgorithm.Unknown: + _bytes.append(self._opaque_halg) + else: + _bytes.append(self.halg) + _bytes.append(self.pubalg) + _bytes += bytes(self.signer) + _bytes.append(int(not self.nested)) + return _bytes - elif self._pubalg == PubKeyAlgorithm.DSA: - self.signature = DSASignature() + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.sigtype = packet[0] + del packet[0] - @sdproperty - def halg(self): - return self._halg + self.halg = packet[0] + del packet[0] - @halg.register(int) - @halg.register(HashAlgorithm) - def halg_int(self, val): - try: - self._halg = HashAlgorithm(val) + self.pubalg = packet[0] + del packet[0] - except ValueError: # pragma: no cover - self._halg = val + self.signer = packet[:8] + del packet[:8] - @sdproperty - def signer(self): - return self._signer + self.nested = (packet[0] == 0) + del packet[0] - @signer.register(str) - @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') +class OnePassSignatureV6(OnePassSignature): + __ver__ = 6 - def __init__(self): - super(OnePassSignatureV3, self).__init__() - self._sigtype = None - self._halg = None - self._pubalg = None - self._signer = b'\x00' * 8 - self.nested = False + def __init__(self) -> None: + super().__init__() + self._signer = Fingerprint(b'\x00' * 32, version=6) + self._salt: Optional[bytes] = None - def __bytearray__(self): + @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(OnePassSignatureV3, self).__bytearray__() - _bytes += bytearray([self.sigtype]) - _bytes += bytearray([self.halg]) - _bytes += bytearray([self.pubalg]) - _bytes += binascii.unhexlify(self.signer.encode("latin-1")) - _bytes += bytearray([int(self.nested)]) + _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): - super(OnePassSignatureV3, self).parse(packet) - self.sigtype = packet[0] + def parse(self, packet: bytearray) -> None: + super().parse(packet) + self.sigtype = SignatureType(packet[0]) del packet[0] - self.halg = packet[0] + self.halg = HashAlgorithm(packet[0]) del packet[0] - self.pubalg = packet[0] + self.pubalg = PubKeyAlgorithm(packet[0]) del packet[0] - self.signer = packet[:8] - del packet[:8] - - self.nested = (packet[0] == 1) + saltsize: int = packet[0] del packet[0] + self.salt = bytes(packet[:saltsize]) + del packet[:saltsize] + self._signer = Fingerprint(bytes(packet[:32])) + del packet[:32] -class PrivKey(VersionedPacket, Primary, Private): - __typeid__ = 0x05 - __ver__ = 0 + self.nested = (packet[0] == 0) + del packet[0] class PubKey(VersionedPacket, Primary, Public): - __typeid__ = 0x06 + __typeid__ = PacketType.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): + 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 - 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) - - _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 + @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 + + if self.pkalg in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: + 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.__ver__) + elif self.pkalg in {PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}: + 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.__ver__) + elif self.pkalg is PubKeyAlgorithm.ECDH: + 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__) + 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__) @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): + 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. - fp = hashlib.new('sha1') + fp = HashAlgorithm.SHA1.hasher plen = self.keymaterial.publen() bcde_len = self.int_to_bytes(6 + plen, 2) @@ -838,41 +1384,97 @@ 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]) # and return the digest - return Fingerprint(fp.hexdigest().upper()) + return Fingerprint(fp.finalize(), version=4) - def __init__(self): - super(PubKeyV4, self).__init__() + def __init__(self) -> None: + super().__init__() self.created = datetime.now(timezone.utc) - self.pkalg = 0 - self.keymaterial = None + + 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) + if self.pkalg is PubKeyAlgorithm.Unknown: + _bytes.append(self._opaque_pkalg) + else: + _bytes.append(self.pkalg) + _bytes += self.keymaterial.__bytearray__() + return _bytes + + def parse(self, packet: bytearray) -> None: + super().parse(packet) + + self.created = packet[:4] + del packet[:4] + + self.pkalg = packet[0] + del packet[0] + + # bound keymaterial to the remaining length of the packet + pend = self.header.length - 6 + if self.keymaterial is not None: + self.keymaterial.parse(packet[:pend]) + 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(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.int_to_bytes(self.keymaterial.publen(), 4) _bytes += self.keymaterial.__bytearray__() return _bytes - def __copy__(self): - pk = self.__class__() - pk.header = copy.copy(self.header) - pk.created = self.created - pk.pkalg = self.pkalg - pk.keymaterial = copy.copy(self.keymaterial) + @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 - return pk + plen = self.keymaterial.publen() + bcde_len = self.int_to_bytes(10 + plen, 4) - def verify(self, subj, sigbytes, hash_alg): - return self.keymaterial.verify(subj, sigbytes, hash_alg) + # 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(PubKeyV4, self).parse(packet) + super().parse(packet) self.created = packet[:4] del packet[:4] @@ -880,73 +1482,158 @@ def parse(self, packet): self.pkalg = packet[0] del packet[0] - # bound keymaterial to the remaining length of the packet - pend = self.header.length - 6 + # 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 + + @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: + 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, + enc_alg: Optional[SymmetricKeyAlgorithm] = None, + hash_alg: Optional[HashAlgorithm] = None, + s2kspec: Optional[S2KSpecifier] = 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, aead_mode=aead_mode, + packet_type=self.__typeid__, + creation_time=self._created) + del passphrase + self.update_hlen() + + 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, packet_type=self.__typeid__, + creation_time=self._created) + 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) + + 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) + + 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 = self.keymaterial._raw_pubkey + + pk.update_hlen() + + 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 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))) + self._extract_pubkey(pk) + return pk - 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) +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 - @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, enc_alg, hash_alg): - self.keymaterial.encrypt_keyblob(passphrase, enc_alg, hash_alg) - 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) + 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(VersionedPacket, Sub, Private): - __typeid__ = 0x07 +class PrivSubKey(PrivKey, Sub): + __typeid__ = PacketType.SecretSubKey __ver__ = 0 @@ -954,6 +1641,10 @@ class PrivSubKeyV4(PrivSubKey, PrivKeyV4): __ver__ = 4 +class PrivSubKeyV6(PrivSubKey, PrivKeyV6): + __ver__ = 6 + + class CompressedData(Packet): """ 5.6. Compressed Data Packet (Tag 8) @@ -984,7 +1675,7 @@ class CompressedData(Packet): BZip2-compressed packets are compressed using the BZip2 [BZ2] algorithm. """ - __typeid__ = 0x08 + __typeid__ = PacketType.CompressedData @sdproperty def calg(self): @@ -996,13 +1687,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 +1704,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] @@ -1068,15 +1759,15 @@ class SKEData(Packet): incorrect. See the "Security Considerations" section for hints on the proper use of this "quick check". """ - __typeid__ = 0x09 + __typeid__ = PacketType.SymmetricallyEncryptedData 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,13 +1777,15 @@ 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] - 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 = _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 @@ -1106,26 +1799,57 @@ 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 class Marker(Packet): - __typeid__ = 0x0a + __typeid__ = PacketType.Marker 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] + + +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] @@ -1176,7 +1900,7 @@ class LiteralData(Packet): normal line endings). These should be converted to native line endings by the receiving software. """ - __typeid__ = 0x0B + __typeid__ = PacketType.LiteralData @sdproperty def mtime(self): @@ -1208,7 +1932,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 +1940,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 +1959,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] @@ -1267,7 +1991,7 @@ class Trust(Packet): transferred to other users, and they SHOULD be ignored on any input other than local keyring files. """ - __typeid__ = 0x0C + __typeid__ = PacketType.Trust @sdproperty def trustlevel(self): @@ -1284,25 +2008,27 @@ 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(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] @@ -1321,16 +2047,16 @@ class UserID(Packet): restrictions on its content. The packet length in the header specifies the length of the User ID. """ - __typeid__ = 0x0D + __typeid__ = PacketType.UserID 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 +2069,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') @@ -1356,7 +2082,7 @@ def parse(self, packet): class PubSubKey(VersionedPacket, Sub, Public): - __typeid__ = 0x0E + __typeid__ = PacketType.PublicSubKey __ver__ = 0 @@ -1364,6 +2090,10 @@ class PubSubKeyV4(PubSubKey, PubKeyV4): __ver__ = 4 +class PubSubKeyV6(PubSubKey, PubKeyV6): + __ver__ = 6 + + class UserAttribute(Packet): """ 5.12. User Attribute Packet (Tag 17) @@ -1397,7 +2127,7 @@ class UserAttribute(Packet): not recognize. Subpacket types 100 through 110 are reserved for private or experimental use. """ - __typeid__ = 0x11 + __typeid__ = PacketType.UserAttribute @property def image(self): @@ -1406,17 +2136,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,13 +2154,17 @@ def parse(self, packet): def update_hlen(self): self.subpackets.update_hlen() - super(UserAttribute, self).update_hlen() + super().update_hlen() class IntegrityProtectedSKEData(VersionedPacket): - __typeid__ = 0x12 + __typeid__ = PacketType.SymmetricallyEncryptedIntegrityProtectedData __ver__ = 0 + @abc.abstractmethod + def decrypt(self, key: bytes, alg: Optional[SymmetricKeyAlgorithm]) -> bytearray: + raise NotImplementedError() + class IntegrityProtectedSKEDataV1(IntegrityProtectedSKEData): """ @@ -1535,12 +2269,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,28 +2284,31 @@ 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] - 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() - 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__() - self.ct = _encrypt(data, key, alg) + 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 = _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() + _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 @@ -1587,6 +2324,142 @@ def decrypt(self, key, alg): 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) @@ -1614,16 +2487,16 @@ class MDC(Packet): in the data hash. While this is a bit restrictive, it reduces complexity. """ - __typeid__ = 0x13 + __typeid__ = PacketType.ModificationDetectionCode 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..ee11562a 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 @@ -10,29 +12,47 @@ from datetime import timedelta from datetime import timezone +from enum import IntFlag + +from typing import Optional, Type, Union, List, Set + from .types import EmbeddedSignatureHeader from .types import Signature +from ..types import VersionedHeader +from ..types import AEADCiphersuiteList + from ...constants import CompressionAlgorithm from ...constants import Features as _Features from ...constants import HashAlgorithm 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 +from ...constants import SigSubpacketType +from ...constants import SignatureType from ...constants import SymmetricKeyAlgorithm +from ...constants import AEADMode from ...decorators import sdproperty 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', 'ByteFlag', 'Boolean', + 'FingerprintSubpacket', 'CreationTime', 'SignatureExpirationTime', 'ExportableCertification', @@ -52,62 +72,57 @@ 'Policy', 'KeyFlags', 'SignersUserID', - 'SubkeyBindingSignature', 'ReasonForRevocation', 'Features', 'EmbeddedSignature', 'IssuerFingerprint', 'IntendedRecipient', - 'AttestedCertifications'] + 'AttestedCertifications', + 'PreferredAEADCiphersuites', + ] class URI(Signature): @sdproperty - def uri(self): + def uri(self) -> str: return self._uri - @uri.register(str) - @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): - super(URI, self).__init__() + def __init__(self) -> None: + super().__init__() self.uri = "" - def __bytearray__(self): - _bytes = super(URI, self).__bytearray__() + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() _bytes += self.uri.encode() return _bytes - def parse(self, packet): - super(URI, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.uri = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] class FlagList(Signature): - __flags__ = None + __flags__: Optional[Type] = None @sdproperty 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!") @@ -118,52 +133,50 @@ 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] class ByteFlag(Signature): - __flags__ = None + __flags__: Optional[Type] = None @sdproperty 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): 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 +184,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] @@ -179,38 +192,71 @@ 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): - super(Boolean, self).__init__() + def __init__(self) -> None: + super().__init__() self.bflag = False - def __bytearray__(self): - _bytes = super(Boolean, self).__bytearray__() - _bytes += self.int_to_bytes(int(self.bflag)) + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() + _bytes.append(int(self.bflag)) return _bytes - def __bool__(self): + def __bool__(self) -> bool: return self.bflag - def __nonzero__(self): - return self.__bool__() - - def parse(self, packet): - super(Boolean, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.bflag = packet[:1] 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 @@ -221,37 +267,37 @@ class CreationTime(Signature): MUST be present in the hashed area. """ - __typeid__ = 0x02 + __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): - super(CreationTime, self).__init__() + def __init__(self) -> None: + super().__init__() self.created = datetime.now(timezone.utc) - def __bytearray__(self): - _bytes = super(CreationTime, self).__bytearray__() + def __bytearray__(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) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.created = packet[:4] del packet[:4] @@ -266,35 +312,35 @@ 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): + 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): - super(SignatureExpirationTime, self).__init__() + def __init__(self) -> None: + super().__init__() self.expires = 0 - def __bytearray__(self): - _bytes = super(SignatureExpirationTime, self).__bytearray__() + def __bytearray__(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) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.expires = packet[:4] del packet[:4] @@ -329,7 +375,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): @@ -351,46 +397,46 @@ 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): + 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): - super(TrustSignature, self).__init__() + def __init__(self) -> None: + super().__init__() self.level = 0 self.amount = 0 - def __bytearray__(self): - _bytes = super(TrustSignature, self).__bytearray__() + 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): - super(TrustSignature, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.level = packet[:1] del packet[:1] self.amount = packet[:1] @@ -411,32 +457,31 @@ 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): + def regex(self) -> str: return self._regex - @regex.register(str) - @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): - super(RegularExpression, self).__init__() + def __init__(self) -> None: + super().__init__() self.regex = r'' - def __bytearray__(self): - _bytes = super(RegularExpression, self).__bytearray__() + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() _bytes += self.regex.encode() return _bytes - def parse(self, packet): - super(RegularExpression, self).parse(packet) + def parse(self, packet: bytearray) -> None: + super().parse(packet) self.regex = packet[:(self.header.length - 1)] del packet[:(self.header.length - 1)] @@ -454,7 +499,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): @@ -468,7 +513,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): @@ -484,7 +529,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 @@ -511,20 +556,21 @@ 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): 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): @@ -547,7 +593,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): @@ -558,20 +603,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] @@ -581,46 +626,47 @@ def parse(self, packet): class Issuer(Signature): - __typeid__ = 0x10 + __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(Issuer, self).__init__() - self.issuer = bytearray() + super().__init__() + self.issuer = bytearray(b'\x00' * 8) def __bytearray__(self): - _bytes = super(Issuer, self).__bytearray__() - _bytes += binascii.unhexlify(self._issuer.encode()) + _bytes = super().__bytearray__() + _bytes += bytes(self._issuer) return _bytes def parse(self, packet): - super(Issuer, self).parse(packet) + super().parse(packet) self.issuer = packet[:8] del packet[:8] class NotationData(Signature): - __typeid__ = 0x14 + __typeid__ = SigSubpacketType.NotationData @sdproperty 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): @@ -630,7 +676,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 +688,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 @@ -657,13 +701,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 +716,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]) @@ -686,30 +730,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 SubkeyBindingSignature(Signature): - __typeid__ = 0x18 - -class PrimaryUserID(SubkeyBindingSignature): - __typeid__ = 0x19 +class PrimaryUserID(Signature): + __typeid__ = SigSubpacketType.PrimaryUserID @sdproperty def primary(self): @@ -724,43 +764,39 @@ 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 def __bool__(self): return self.primary - 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] 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): return self._userid - @userid.register(str) @userid.register(str) def userid_str(self, val): self._userid = val @@ -770,22 +806,22 @@ 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)] class ReasonForRevocation(Signature): - __typeid__ = 0x1D + __typeid__ = SigSubpacketType.ReasonForRevocation @sdproperty def code(self): @@ -804,7 +840,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 @@ -814,18 +849,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)] @@ -833,7 +868,7 @@ def parse(self, packet): class Features(ByteFlag): - __typeid__ = 0x1E + __typeid__ = SigSubpacketType.Features __flags__ = _Features @@ -841,30 +876,37 @@ class Features(ByteFlag): class EmbeddedSignature(Signature): - __typeid__ = 0x20 + __typeid__ = SigSubpacketType.EmbeddedSignature @sdproperty - def _sig(self): + def _sig(self) -> SignaturePacket: return self._sigpkt @_sig.setter - def _sig(self, val): + 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): + 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 @@ -880,164 +922,99 @@ 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): - super(EmbeddedSignature, self).__init__() + def __init__(self) -> None: + super().__init__() from ..packets import SignatureV4 self._sigpkt = SignatureV4() self._sigpkt.header = EmbeddedSignatureHeader() - def __bytearray__(self): - return super(EmbeddedSignature, self).__bytearray__() + self._sigpkt.__bytearray__() + def __bytearray__(self) -> bytearray: + return super().__bytearray__() + self._sigpkt.__bytearray__() - def parse(self, packet): - super(EmbeddedSignature, self).parse(packet) - self._sig.parse(packet) + def parse(self, packet: bytearray) -> None: + from ..types import Packet + super().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] -class IssuerFingerprint(Signature): - ''' - (from RFC4880bis-07) - 5.2.3.28. Issuer Fingerprint + 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 - (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. - ''' - __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) - - @sdproperty - 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): - 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() - - def __init__(self): - super(IssuerFingerprint, self).__init__() - self.version = 4 - self._issuer_fpr = "" - - def __bytearray__(self): - _bytes = super(IssuerFingerprint, self).__bytearray__() - _bytes += self.int_to_bytes(self.version) - _bytes += self.issuer_fingerprint.__bytes__() - return _bytes - - def parse(self, packet): - super(IssuerFingerprint, self).parse(packet) - self.version = packet[:1] - del packet[:1] - - 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 - - self.issuer_fingerprint = packet[:fpr_len] - del packet[:fpr_len] - - -class IntendedRecipient(Signature): - ''' - (from RFC4880bis-08) - 5.2.3.29. Intended Recipient +class IssuerFingerprint(FingerprintSubpacket): + '''(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 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. - - 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__ = 0x23 + __typeid__ = SigSubpacketType.IssuerFingerprint @sdproperty - def version(self): - return self._version + def issuer_fingerprint(self) -> Fingerprint: + return self.fingerprint - @version.register(int) - def version_int(self, val): - self._version = val + @issuer_fingerprint.register + def issuer_fingerprint_set(self, val: Union[str, bytes, bytearray]) -> None: + self.fingerprint = val - @version.register(bytearray) - def version_bytearray(self, val): - self.version = self.bytes_to_int(val) - @sdproperty - def intended_recipient(self): - return self._intended_recipient +class IntendedRecipient(IssuerFingerprint): + '''(from crypto-refresh-08) + 5.2.3.36. Intended Recipient Fingerprint - @intended_recipient.register(str) - @intended_recipient.register(str) - @intended_recipient.register(Fingerprint) - def intended_recipient_str(self, val): - self._intended_recipient = Fingerprint(val) + (1 octet key version number, N octets of fingerprint) - @intended_recipient.register(bytearray) - def intended_recipient_bytearray(self, val): - self.intended_recipient = ''.join('{:02x}'.format(c) for c in val).upper() + 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). - def __init__(self): - super(IntendedRecipient, self).__init__() - self.version = 4 - self._intended_recipient = "" + Note that the length N of the fingerprint for a version 4 key is + 20 octets; for a version 6 key N is 32. - def __bytearray__(self): - _bytes = super(IntendedRecipient, self).__bytearray__() - _bytes += self.int_to_bytes(self.version) - _bytes += self.intended_recipient.__bytes__() - return _bytes - - def parse(self, packet): - super(IntendedRecipient, self).parse(packet) - self.version = packet[:1] - del packet[:1] + An implementation SHOULD generate this subpacket when creating a + signed and encrypted message. + ''' + __typeid__ = SigSubpacketType.IntendedRecipientFingerprint - 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 + @sdproperty + def intended_recipient(self) -> Fingerprint: + return self.fingerprint - self.intended_recipient = packet[:fpr_len] - del packet[:fpr_len] + @intended_recipient.register + def intended_recipient_set(self, val: Union[str, bytes, bytearray]) -> None: + self.fingerprint = val class AttestedCertifications(Signature): @@ -1114,7 +1091,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): @@ -1126,15 +1103,76 @@ 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)] + + +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)] diff --git a/pgpy/packet/subpackets/types.py b/pgpy/packet/subpackets/types.py index 1765b2bd..ce966399 100644 --- a/pgpy/packet/subpackets/types.py +++ b/pgpy/packet/subpackets/types.py @@ -2,6 +2,12 @@ """ import abc +from typing import Optional, Union + +from ...constants import PacketType +from ...constants import SigSubpacketType +from ...constants import AttributeType + from ..types import VersionedHeader from ...decorators import sdproperty @@ -19,119 +25,124 @@ 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): - super(Header, self).__init__() + 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 class EmbeddedSignatureHeader(VersionedHeader): - def __bytearray__(self): + def __bytearray__(self) -> bytearray: return bytearray([self.version]) - def parse(self, packet): - self.tag = 2 - super(EmbeddedSignatureHeader, self).parse(packet) + def parse(self, packet: bytearray) -> None: + self.typeid = PacketType.Signature + super().parse(packet) class SubPacket(Dispatchable): __headercls__ = Header - def __init__(self): - super(SubPacket, self).__init__() + def __init__(self) -> None: + super().__init__() self.header = Header() 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__ - 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): - return "<{} [0x{:02x}] at 0x{:x}>".format(self.__class__.__name__, self.header.typeid, id(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) class Signature(SubPacket): - __typeid__ = -1 + __typeid__: Optional[SigSubpacketType] = None + + # allow one parameter for MetaDispatchable initialization: + def __init__(self, _: Optional[bytes] = None) -> None: + super().__init__() class UserAttribute(SubPacket): - __typeid__ = -1 + __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 @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): - super(Opaque, self).__init__() - self.payload = b'' + def __init__(self) -> None: + super().__init__() + self.payload = bytearray(b'') - def __bytearray__(self): - _bytes = super(Opaque, self).__bytearray__() + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() _bytes += self.payload return _bytes - def parse(self, packet): - super(Opaque, self).parse(packet) + def parse(self, packet: bytearray) -> None: + 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..5620d0e6 100644 --- a/pgpy/packet/subpackets/userattribute.py +++ b/pgpy/packet/subpackets/userattribute.py @@ -2,8 +2,11 @@ """ import struct +from typing import Union + from .types import UserAttribute +from ...constants import AttributeType from ...constants import ImageEncoding from ...decorators import sdproperty @@ -46,57 +49,58 @@ 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): + 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 - 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): + 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): - super(Image, self).__init__() - self.version = 1 - self.iencoding = 1 + def __init__(self) -> None: + super().__init__() + self.version: int = 1 + self.iencoding: ImageEncoding = ImageEncoding.JPEG self.image = bytearray() - def __bytearray__(self): - _bytes = super(Image, self).__bytearray__() + 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(' None: + super().parse(packet) with memoryview(packet) as _head: _, self.version, self.iencoding, _, _, _ = struct.unpack_from(' PacketType: + return self._typeid - @tag.register(int) - @tag.register(PacketTag) - def tag_int(self, val): - _tag = (val & 0x3F) if self._lenfmt 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): - super(Header, self).__init__() - self.tag = 0x00 + def __init__(self) -> None: + super().__init__() + self._typeid = PacketType.Invalid - 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]) + def __bytearray__(self) -> bytearray: + tag = 0x80 | (0x40 if self._openpgp_format else 0x00) + 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 += self.encode_length(self.length, self._lenfmt, self.llen) + _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 @@ -99,13 +110,13 @@ def parse(self, packet): :param packet: raw packet bytes """ - self._lenfmt = ((packet[0] & 0x40) >> 6) - self.tag = packet[0] - if self._lenfmt == 0: + self._openpgp_format = bool(packet[0] & 0x40) + self.typeid = packet[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: @@ -115,25 +126,25 @@ 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): - super(VersionedHeader, self).__init__() + def __init__(self) -> None: + super().__init__() self.version = 0 - def __bytearray__(self): - _bytes = bytearray(super(VersionedHeader, self).__bytearray__()) + 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: - super(VersionedHeader, self).parse(packet) + def parse(self, packet: bytearray) -> None: # pragma: no cover + if self.typeid is PacketType.Invalid: + super().parse(packet) if self.version == 0: self.version = packet[0] @@ -141,70 +152,71 @@ def parse(self, packet): # pragma: no cover class Packet(Dispatchable): - __typeid__ = -1 - __headercls__ = Header + __typeid__: Optional[Union[PacketType, DispatchGuidance]] = None + __headercls__: Type[Header] = Header - def __init__(self, _=None): - super(Packet, self).__init__() + 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): + 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): - return "<{cls:s} [tag {tag:02d}] at 0x{id:x}>".format(cls=self.__class__.__name__, tag=self.header.tag, id=id(self)) + def __repr__(self) -> str: + return f'<{self.__class__.__name__} [type {self.header.typeid:02}] at 0x{id(self):x}>' - def update_hlen(self): + def update_hlen(self) -> None: self.header.length = len(self.__bytearray__()) - len(self.header) @abc.abstractmethod - def parse(self, packet): - if self.header.tag == 0: + def parse(self, packet: bytearray) -> None: + if self.header.typeid is PacketType.Invalid: self.header.parse(packet) class VersionedPacket(Packet): + __typeid__: Union[PacketType, DispatchGuidance] = DispatchGuidance.NoDispatch __headercls__ = VersionedHeader - def __init__(self): - super(VersionedPacket, self).__init__() - if isinstance(self.__ver__, int): + def __init__(self) -> None: + super().__init__() + if isinstance(self.__ver__, int) and isinstance(self.header, VersionedHeader): self.header.version = self.__ver__ - def __repr__(self): - 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)) + def __repr__(self) -> str: + if not isinstance(self.header, VersionedHeader): + raise TypeError(f"VersionedPacket should have VersionedHeader, instead it has {type(self.header)}") + return f"<{self.__class__.__name__} [type {self.header.typeid:02}][v{self.header.version}] at 0x{id(self):x}>" 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): - super(Opaque, self).__init__() + def __init__(self) -> None: + super().__init__() self.payload = b'' - def __bytearray__(self): - _bytes = super(Opaque, self).__bytearray__() + def __bytearray__(self) -> bytearray: + _bytes = super().__bytearray__() _bytes += self.payload return _bytes - def parse(self, packet): # pragma: no cover - super(Opaque, self).parse(packet) + def parse(self, packet: bytearray) -> None: # pragma: no cover + super().parse(packet) pend = self.header.length if hasattr(self.header, 'version'): pend -= 1 @@ -214,8 +226,10 @@ def parse(self, packet): # pragma: no cover # key marker classes for convenience -class Key(object): - pass +class Key: + @abc.abstractproperty + def pkalg(self) -> PubKeyAlgorithm: + """The public key algorithm of the key""" class Public(Key): @@ -223,7 +237,17 @@ class Public(Key): class Private(Key): - pass + @abc.abstractmethod + 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): @@ -234,12 +258,8 @@ class Sub(Key): pass -# This is required for class MPI to work in both Python 2 and 3 -long = int - - -class MPI(long): - def __new__(cls, num): +class MPI(int): + def __new__(cls, num: Union[bytes, bytearray, int]) -> MPI: mpi = num if isinstance(num, (bytes, bytearray)): @@ -252,34 +272,48 @@ def __new__(cls, num): mpi = MPIs.bytes_to_int(num[:fl]) del num[:fl] - return super(MPI, cls).__new__(cls, mpi) + 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__ = () + __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))) 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 diff --git a/pgpy/pgp.py b/pgpy/pgp.py index f34a25fb..9bf60a33 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -2,12 +2,11 @@ this is where the armorable PGP block objects live """ +from __future__ import annotations + 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 @@ -20,21 +19,28 @@ from datetime import datetime, timezone +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 from .constants import Features 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 PacketType from .constants import PubKeyAlgorithm from .constants import RevocationKeyClass from .constants import RevocationReason from .constants import SignatureType from .constants import SymmetricKeyAlgorithm from .constants import SecurityIssues +from .constants import AEADMode +from .constants import String2KeyType from .decorators import KeyAction @@ -46,9 +52,15 @@ 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 PubKeyV6 +from .packet import PrivKeyV6 +from .packet import PrivSubKeyV6 from .packet import Public from .packet import Sub from .packet import UserID @@ -57,64 +69,84 @@ 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 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 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 + +from .packet.fields import ECDSAPub, EdDSAPub, ECDHPub, Ed25519Pub, Ed448Pub, X25519Pub, X448Pub +from .packet.fields import PrivKey as field_PrivKey +from .packet.fields import NativeEdDSAPub, NativeCFRGXPub from .packet.types import Opaque +from .packet.types import VersionedHeader 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 from .types import SorteDeque +from .types import PGPMagicClass __all__ = ['PGPSignature', + 'PGPSignatures', 'PGPUID', 'PGPMessage', 'PGPKey', 'PGPKeyring'] -class PGPSignature(Armorable, ParentRef, PGPObject): - _reason_for_revocation = collections.namedtuple('ReasonForRevocation', ['code', 'comment']) +class PGPSignature(Armorable, ParentRef): + ReasonForRevocation = collections.namedtuple('ReasonForRevocation', ['code', 'comment']) @property 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 self._signature is None: + 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 self._signature is None: + return None if 'PreferredCompressionAlgorithms' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredCompressionAlgorithms'])).flags - return [] + return None @property 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 @@ -122,63 +154,77 @@ def embedded(self): return self.parent is not None @property - def expires_at(self): + def emit_crc(self) -> bool: + return self._signature is not None and self._signature.__ver__ < 6 + + @property + 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 return None @property - def exportable(self): + 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']))) 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 self._signature is None: + return None if 'Features' in self._signature.subpackets: return next(iter(self._signature.subpackets['Features'])).flags - return set() + return None @property 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 self._signature is None: + 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): + 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): + 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`` """ @@ -189,47 +235,57 @@ 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. """ + if self._signature is None: + raise ValueError("wanted key_algorithm of uninitizalized PGPSignature") return self._signature.pubalg @property - def key_expiration(self): + 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 @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 self._signature is None: + return None if 'KeyFlags' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyFlags'])).flags - return set() + return None @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 self._signature is None: + return None if 'PreferredKeyServer' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_PreferredKeyServer'])).uri - return '' + 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 self._signature is None: + return None if 'KeyServerPreferences' in self._signature.subpackets: return next(iter(self._signature.subpackets['h_KeyServerPreferences'])).flags - return [] + return None @property - def magic(self): + def magic(self) -> PGPMagicClass: return "SIGNATURE" @property @@ -237,37 +293,47 @@ 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']) + if self._signature is None: + return {} + 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 self._signature is None: + return None if 'Policy' in self._signature.subpackets: return next(iter(self._signature.subpackets['Policy'])).uri - return '' + return None @property - def revocable(self): + 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): + 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._reason_for_revocation(subpacket.code, subpacket.string) + return self.ReasonForRevocation(subpacket.code, subpacket.string) return None @property @@ -288,20 +354,24 @@ def attested_certifications(self): return ret @property - def signer(self): + 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 - 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 self._signature is None: + return None if 'IssuerFingerprint' in self._signature.subpackets: return next(iter(self._signature.subpackets['IssuerFingerprint'])).issuer_fingerprint - return '' + return None @property def intended_recipients(self): @@ -311,27 +381,33 @@ 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): + 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 - 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', hashed=True, created=created) - sigpkt.subpackets.addnew('Issuer', _issuer=signer) + 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 = signer.version + sigpkt.subpackets.addnew('CreationTime', critical=True, hashed=True, created=created) + if signer.version <= 4: + sigpkt.subpackets.addnew('Issuer', _issuer=signer.keyid) + sigpkt.subpackets.addnew('IssuerFingerprint', issuer_fingerprint=signer) sigpkt.sigtype = sigtype sigpkt.pubalg = pkalg @@ -342,7 +418,7 @@ def new(cls, sigtype, pkalg, halg, signer, created=None): sig._signature = sigpkt return sig - def __init__(self): + def __init__(self) -> None: """ PGPSignature objects represent OpenPGP compliant signatures. @@ -352,56 +428,77 @@ def __init__(self): PGPSignature implements the ``__bytes__`` method, the output of which will be the signature object in OpenPGP-compliant binary format. """ - super(PGPSignature, self).__init__() - self._signature = None + super().__init__() + 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): + 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): + def __copy__(self) -> PGPSignature: # because the default shallow copy isn't actually all that useful, # and deepcopy does too much work - sig = super(PGPSignature, self).__copy__() + sig = super().__copy__() # sig = PGPSignature() # sig.ascii_headers = self.ascii_headers.copy() 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.digest() in self.attested_certifications - - def hashdata(self, subject): + 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") _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 + + 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 @@ -415,8 +512,10 @@ def hashdata(self, subject): """ 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: """ @@ -424,6 +523,8 @@ def hashdata(self, subject): 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, @@ -448,7 +549,7 @@ def hashdata(self, subject): _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}: """ @@ -457,13 +558,18 @@ def hashdata(self, subject): 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: _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}: """ @@ -493,13 +599,16 @@ def hashdata(self, subject): 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 - _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}: @@ -519,6 +628,8 @@ def hashdata(self, subject): 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: @@ -550,27 +661,27 @@ def hashdata(self, subject): """ 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) 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): - 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: + if self._signature is None: + raise TypeError("called make_onepass on an uninitialized PGPSignature") + return self._signature.make_onepass() - def parse(self, packet): + def parse(self, packet: bytearray) -> None: unarmored = self.ascii_unarmor(packet) data = unarmored['body'] @@ -581,15 +692,80 @@ def parse(self, packet): self.ascii_headers = unarmored['headers'] # load *one* packet from data - pkt = Packet(data) - if pkt.header.tag == PacketTag.Signature: + pkt = Packet(data) # type: ignore[abstract] + if pkt.header.typeid is PacketType.Signature: 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): + '''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) -> PGPMagicClass: + return "SIGNATURE" + + def __bytearray__(self) -> bytearray: + b = bytearray() + for sig in self._sigs: + 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'] + + 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: + # this is safe to do because of how MetaDispatchable works: + pkt = Packet(data) # type: ignore[abstract] + if pkt.header.typeid is PacketType.Signature: + if isinstance(pkt, Opaque): + # skip unrecognized version. + pass + 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: {type(pkt)}") class PGPUID(ParentRef): @@ -597,88 +773,103 @@ class PGPUID(ParentRef): def __sig__(self): return list(self._signatures) - def _splitstring(self): + 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 "", "", "" - rfc2822 = 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).groupdict() - - return (rfc2822['name'], rfc2822['comment'] or "", rfc2822['email'] or "") - - @property - def name(self): - """If this is a User ID, the stored name. If this is not a User ID, this will be an empty string.""" + 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: + return self._uid.uid, None, None + + 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) -> 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): + 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): + 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] @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``. """ 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``. """ 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. """ - 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 @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``. """ @@ -690,24 +881,26 @@ def selfsig(self): elif sig.signer: if self.parent.fingerprint == sig.signer: return sig + 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. + This will be a set of all of the key ids or fingerprints of the keys that signed this User ID or Attribute. """ - return set(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): + 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 ''' @@ -719,7 +912,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: @@ -746,7 +939,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. @@ -754,7 +947,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. @@ -786,23 +979,23 @@ 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. PGPUID implements the ``__format__`` method for User IDs, returning a string in the format 'name (comment) ', leaving out any comment or email fields that are not present. """ - super(PGPUID, self).__init__() - self._uid = None + super().__init__() + 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 @@ -824,7 +1017,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: @@ -832,18 +1027,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() @@ -852,53 +1047,67 @@ def __copy__(self): uid |= copy.copy(sig) return uid - def __format__(self, format_spec): - 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) + def __format__(self, format_spec: str) -> str: + if isinstance(self._uid, UserID): + return self._uid.uid - raise NotImplementedError + raise NotImplementedError() -class PGPMessage(Armorable, PGPObject): +class PGPMessage(Armorable): @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 - 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 set(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 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): - """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. @@ -906,40 +1115,41 @@ 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 - def magic(self): + def magic(self) -> PGPMagicClass: 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': + if isinstance(self._message, (str, bytes, bytearray)): 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) @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) + def signers(self) -> Set[Union[KeyID, Fingerprint]]: + """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 - 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' @@ -950,9 +1160,9 @@ def type(self): if isinstance(self._message, (SKEData, IntegrityProtectedSKEData)): return 'encrypted' - raise NotImplementedError + raise NotImplementedError() - def __init__(self): + def __init__(self) -> None: """ PGPMessage objects represent OpenPGP message compositions. @@ -965,14 +1175,15 @@ 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__() - self._compression = CompressionAlgorithm.Uncompressed - self._message = None - self._mdc = None - self._signatures = SorteDeque() - self._sessionkeys = [] + super().__init__() + 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]] = [] + self.format: Literal['t', 'u', 'b', 'm'] = 'b' - def __bytearray__(self): + def __bytearray__(self) -> bytearray: if self.is_compressed: comp = CompressedData() comp.calg = self._compression @@ -985,22 +1196,22 @@ def __bytearray__(self): _bytes += pkt.__bytearray__() return _bytes - 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}" + def __str__(self) -> str: + if isinstance(self._message, (bytes, bytearray, str)): + 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 = 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, 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': @@ -1010,15 +1221,14 @@ 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: ##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 @@ -1029,8 +1239,8 @@ def __iter__(self): for sig in self._signatures: yield sig - def __or__(self, other): - if isinstance(other, Marker): + def __or__(self, other) -> PGPMessage: + if isinstance(other, (Marker, Padding)): return self if isinstance(other, CompressedData): @@ -1066,6 +1276,7 @@ def __or__(self, other): 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 @@ -1077,10 +1288,14 @@ 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): - msg = super(PGPMessage, self).__copy__() + def __copy__(self) -> PGPMessage: + msg = super().__copy__() msg._compression = self._compression msg._message = copy.copy(self._message) msg._mdc = copy.copy(self._mdc) @@ -1094,7 +1309,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[Literal['t', 'u', 'b', 'm']] = None, + sensitive: bool = False, + compression: CompressionAlgorithm = CompressionAlgorithm.ZIP, + file: bool = False, + encoding: Optional[str] = None) -> PGPMessage: """ Create a new PGPMessage object. @@ -1120,27 +1341,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) @@ -1153,9 +1371,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 @@ -1163,7 +1381,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 @@ -1176,9 +1394,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 @@ -1186,7 +1401,13 @@ 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, + aead_mode: Optional[AEADMode] = None, + salt: Optional[bytes] = None) -> PGPMessage: """ encrypt(passphrase, [sessionkey=None,] **prefs) @@ -1208,27 +1429,40 @@ 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.count = skesk.s2k.halg.tuned_count + 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 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 msg = PGPMessage() | skesk if not self.is_encrypted: - skedata = IntegrityProtectedSKEDataV1() - skedata.encrypt(sessionkey, cipher_algo, self.__bytes__()) + 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: @@ -1236,7 +1470,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. @@ -1245,16 +1479,16 @@ 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): + except (TypeError, ValueError, NotImplementedError, PGPDecryptionError, InvalidTag) as e: continue else: @@ -1266,9 +1500,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']))) @@ -1280,9 +1516,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 @@ -1290,10 +1529,10 @@ def parse(self, packet): else: while len(data) > 0: - self |= Packet(data) + self |= Packet(data) # type: ignore[abstract] -class PGPKey(Armorable, ParentRef, PGPObject): +class PGPKey(Armorable, ParentRef): """ 11.1. Transferable Public Keys @@ -1409,6 +1648,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 @@ -1417,7 +1662,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: @@ -1426,27 +1671,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: @@ -1455,19 +1700,31 @@ 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 - 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_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA}: - return self._key.keymaterial.oid + if self._key is None: + return None + if isinstance(self._key.keymaterial, (ECDSAPub, EdDSAPub, ECDHPub)): + if isinstance(self._key.keymaterial.oid, EllipticCurveOID): + return self._key.keymaterial.oid + else: + # this is an unknown elliptic curve + return 0 + if isinstance(self._key.keymaterial, (NativeEdDSAPub, NativeCFRGXPub)): + 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 param = next(iter(self._key.keymaterial)) if isinstance(param, bytearray): @@ -1475,9 +1732,13 @@ def key_size(self): return param.bit_length() @property - def magic(self): - 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): @@ -1495,7 +1756,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 @@ -1536,37 +1797,39 @@ 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 - for sig in iter(sig for sig in self._signatures - if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])): - yield sig + 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): - """A ``set`` of key ids of keys that were used to sign this key""" - return {sig.signer for sig in self.__sig__} + def signers(self) -> Set[Union[KeyID, Fingerprint]]: + """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) @property 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): + 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 - 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 ] @@ -1585,7 +1848,10 @@ 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: Optional[Union[int, EllipticCurveOID]] = None, + created: Optional[datetime] = None, + version: int = 4) -> PGPKey: """ Generate a new PGP key @@ -1607,11 +1873,16 @@ def new(cls, key_algorithm, key_size, created=None): 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 - def __init__(self): + def __init__(self) -> None: """ PGPKey objects represent OpenPGP compliant keys along with all of their associated data. @@ -1624,17 +1895,19 @@ 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__() - self._key = None - self._children = collections.OrderedDict() + super().__init__() + self._key: Optional[PubKey] = None + self._children = FingerprintDict["PGPKey"]() self._signatures = SorteDeque() - self._uids = SorteDeque() + self._uids: collections.deque[PGPUID] = SorteDeque() self._sibling = None - self._self_verified = 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 @@ -1651,7 +1924,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)) @@ -1659,12 +1932,12 @@ 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.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 @@ -1674,27 +1947,28 @@ 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: other._parent = self - self._children[other.fingerprint.keyid] = other + self._children[other.fingerprint] = other elif isinstance(other, PGPSignature): 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 + esig = PGPSignature() | es._sig esig._parent = other self._signatures.insort(esig) 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( @@ -1712,8 +1986,8 @@ def __or__(self, other, from_sib=False): return self - def __copy__(self): - key = super(PGPKey, self).__copy__() + def __copy__(self) -> PGPKey: + key = super().__copy__() key._key = copy.copy(self._key) for uid in self._uids: @@ -1731,7 +2005,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. @@ -1739,13 +2014,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) @@ -1757,13 +2033,45 @@ 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] + + 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', []) + + # 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()): - sk._key.protect(passphrase, enc_alg, hash_alg) + 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 isinstance(sk._key, PrivKey): + sk._key.protect(passphrase, **prefs) 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. @@ -1808,16 +2116,18 @@ 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, selfsign=True, **prefs): + def add_uid(self, uid: PGPUID, selfsign: bool = True, **prefs) -> None: """ Add a User ID to this key. @@ -1835,7 +2145,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. @@ -1847,7 +2157,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. @@ -1863,7 +2173,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. @@ -1882,59 +2192,75 @@ 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 = 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 npk.created = key._key.created npk.keymaterial = key._key.keymaterial 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 bsig = self.bind(key, **prefs) key |= bsig - def _get_key_flags(self, user=None): - 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)) + @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 - return {KeyFlags.Certify} | (user.selfsig.key_flags if user.selfsig else set()) + 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 - def _sign(self, subject, sig, **prefs): + def _sign(self, subject: PGPSubject, 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 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 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 @@ -1943,14 +2269,15 @@ def _sign(self, subject, sig, **prefs): 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): - 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") @@ -1970,7 +2297,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) @@ -1986,17 +2313,25 @@ def _sign(self, subject, sig, **prefs): sig._signature.sigtype = SignatureType.Standalone 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) + 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() 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: - raise NotImplementedError(self.key_algorithm) sig._signature.signature.from_signer(_sig) sig._signature.update_hlen() @@ -2004,7 +2339,7 @@ def _sign(self, subject, sig, **prefs): 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. @@ -2038,6 +2373,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) @@ -2050,12 +2388,12 @@ 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) @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) @@ -2121,20 +2459,22 @@ 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.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._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) 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) @@ -2154,6 +2494,8 @@ 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) + 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 @@ -2175,6 +2517,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, critical=True, flags=usage) + if keyserver_flags is not None: sig._signature.subpackets.addnew('KeyServerPreferences', hashed=True, flags=keyserver_flags) @@ -2186,19 +2531,21 @@ 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.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: 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.digest()) + attestations.add(h.finalize()) elif isinstance(attestation, (bytes, bytearray)) and len(attestation) == sig.hash_algorithm.digest_size: attestations.add(attestation) else: @@ -2208,6 +2555,9 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs): ) 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) @@ -2258,7 +2608,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) @@ -2287,7 +2637,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) @@ -2304,7 +2654,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. @@ -2328,25 +2678,33 @@ 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)) + 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._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 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 - if key.key_algorithm.can_sign and prefs.pop('crosssign', True): - subkeyid = key.fingerprint.keyid + 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 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, created=created) if crosssig is None: if usage is None: @@ -2358,14 +2716,14 @@ def bind(self, key, **prefs): 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: @@ -2377,15 +2735,64 @@ 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: 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): + def self_verified(self) -> SecurityIssues: warnings.warn("TODO: Self-sigs verification is not yet working because self-sigs are not parsed!!!") return SecurityIssues.OK @@ -2394,10 +2801,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)) @@ -2408,10 +2817,29 @@ 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 verify(self, subject, signature=None): + 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: PGPSubject, + signature: Optional[PGPSignature] = None) -> SignatureVerification: """ Verify a subject with a signature using this key. @@ -2421,7 +2849,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)): @@ -2429,15 +2857,16 @@ 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): - _ids = {self.fingerprint.keyid} | set(self.subkeys) + def _filter_sigs(sigs: Iterable[PGPSignature]) -> Iterator[PGPSignature]: for sig in sigs: - if sig.signer in _ids: + if self.issuer_matches(sig): yield sig # 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)) @@ -2459,7 +2888,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: @@ -2468,10 +2897,13 @@ 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 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: @@ -2488,18 +2920,19 @@ def _filter_sigs(sigs): 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) 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, + max_featureset: Optional[Features] = None) -> PGPMessage: + """Encrypt a PGPMessage using this key. :param message: The message to encrypt. :type message: :py:obj:`PGPMessage` @@ -2524,39 +2957,74 @@ 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) - 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) - cipher_algo = prefs.pop('cipher', pref_cipher) + if self._key is None: + 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 + # 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 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 cipher_algo not in uid.selfsig.cipherprefs: + 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 + + 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 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: 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 @@ -2577,12 +3045,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") @@ -2616,16 +3086,17 @@ 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 not in {PacketType.Trust, PacketType.Padding, + PacketType.Marker}, iter(functools.partial(_getpkt, data), None)) def pktgrouper(): - class PktGrouper(object): - def __init__(self): - self.last = None + class PktGrouper: + def __init__(self) -> None: + self.last: Optional[str] = None - def __call__(self, pkt): - if pkt.header.tag != PacketTag.Signature: - self.last = '{:02X}_{:s}'.format(id(pkt), pkt.__class__.__name__) + def __call__(self, pkt) -> Optional[str]: + if pkt.header.typeid is not PacketType.Signature: + self.last = f'{id(pkt):02X}_{pkt.__class__.__name__}' return self.last return PktGrouper() @@ -2670,19 +3141,16 @@ 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 -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 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() @@ -2701,8 +3169,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: @@ -2742,7 +3209,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 @@ -2772,7 +3239,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: @@ -2796,8 +3264,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) @@ -2854,7 +3321,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. @@ -2870,7 +3337,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) @@ -2883,4 +3353,16 @@ 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) + + +# 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) + 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) + ] diff --git a/pgpy/sopgpy.py b/pgpy/sopgpy.py new file mode 100755 index 00000000..73638a7e --- /dev/null +++ b/pgpy/sopgpy.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK +'''OpenPGP Interoperability Test Suite Generic Functionality using PGPy + +Author: Daniel Kahn Gillmor +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 +import os +import codecs +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 +from argparse import Namespace, _SubParsersAction, ArgumentParser + +from cryptography.hazmat.backends import openssl + +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()}{self.cryptodome_version}', + 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'), + 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]: + 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 + # the order we prefer them: + _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] + + def _maybe_armor(self, armor: bool, data: pgpy.types.Armorable) -> bytes: + if (armor): + return str(data).encode('ascii') + else: + 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 + 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] = {} + 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 sop.SOPInvalidDataType('cert {handle} is not an OpenPGP transferable secret key (maybe certificate?)') + 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 + # 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) + 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] = [], + keypassword: Optional[bytes] = None, + 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 + first: bool = True + + 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, + 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, + pgpy.packet.types.AEADCiphersuiteList, + ]] = { + '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, + } + + 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 + + prefs['primary'] = True + for uid in uids: + 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) + 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() + subflags.add(pgpy.constants.KeyFlags.EncryptCommunications) + 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.decode('utf-8')) + return self._maybe_armor(armor, primary) + + 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) + + 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]]: + 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 is sop.SOPSigType.text: + 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: + 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 + 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) + + micalg: Optional[str] = None + if wantmicalg: + if len(hashalgs) != 1: + micalg = '' + else: + micalg = f'pgp-{hashalgs.pop().name.lower()}' + + 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]: + self.raise_on_unknown_options(**kwargs) + if not signers: + raise sop.SOPMissingRequiredArgument('needs at least one OpenPGP certificate') + signatures = self._get_pgp_signatures(sig) + certs: MutableMapping[str, pgpy.PGPKey] = self._get_certs(signers) + + ret: List[sop.SOPSigResult] = self._check_sigs(certs, data, signatures, start, end) + if not ret: + raise sop.SOPNoSignature("No good signature found") + return ret + + 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] = {}, + 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: Literal['t', 'u', 'b', 'm'] + + 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(): + 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: + 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 + + 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')) or \ + (profile is not None and profile.name == 'rfc4880'): + 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: + for c in sig.cipherprefs: + keyciphers.add(c) + ciphers &= keyciphers + for c in self._cipherprefs: + if c in ciphers: + cipher = c + break + # AES128 is MTI in the upcoming revision to RFC 4880: + if cipher is None: + cipher = pgpy.constants.SymmetricKeyAlgorithm.AES128 + sessionkey = cipher.gen_key() + + 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 = 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(): + 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 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 + 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], + 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) + 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, + 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] = {} + # FIXME!!! + if wantsessionkey: + raise sop.SOPUnsupportedOption('sopgpy does not support --session-key-out yet') + 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: + 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: + 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 + if isinstance(out, str): + ret = out.encode('utf8') + else: + ret = bytes(out) + 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 ] + 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 + 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) + out = msg.message + if isinstance(out, str): + ret = out.encode('utf8') + else: + ret = bytes(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) + + 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: + if label is sop.SOPArmorLabel.message: + obj = pgpy.PGPMessage.from_blob(data) + elif label is sop.SOPArmorLabel.key: + obj, _ = pgpy.PGPKey.from_blob(data) + 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 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) + 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.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) + 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: + raise sop.SOPInvalidDataType(f'{e}') + return str(obj).encode('ascii') + + def dearmor(self, data: bytes, **kwargs: Namespace) -> bytes: + self.raise_on_unknown_options(**kwargs) + try: + key: pgpy.PGPKey + key, _ = pgpy.PGPKey.from_blob(data) + return bytes(key) + except: + pass + try: + sig: pgpy.PGPSignatures = pgpy.PGPSignatures.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 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 = 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 + ) -> 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 = msg.message + if isinstance(outmsg, str): + outmsg = outmsg.encode("utf-8") + return (bytes(outmsg), sigresults) + + +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() diff --git a/pgpy/symenc.py b/pgpy/symenc.py index 77537e48..e8d011fc 100644 --- a/pgpy/symenc.py +++ b/pgpy/symenc.py @@ -1,21 +1,33 @@ """ symenc.py """ -from cryptography.exceptions import UnsupportedAlgorithm -from cryptography.hazmat.backends import default_backend +from typing import Optional, Union +from types import ModuleType + +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 AEADMode, SymmetricKeyAlgorithm from .errors import PGPDecryptionError from .errors import PGPEncryptionError from .errors import PGPInsecureCipherError -__all__ = ['_encrypt', - '_decrypt'] +AES_Cryptodome: Optional[ModuleType] +try: + from Cryptodome.Cipher import AES as AES_Cryptodome +except ModuleNotFoundError: + AES_Cryptodome = None +__all__ = ['_cfb_encrypt', + '_cfb_decrypt', + 'AEAD'] -def _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) @@ -26,7 +38,7 @@ def _encrypt(pt, key, alg, iv=None): 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 @@ -35,7 +47,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: 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 @@ -47,10 +59,62 @@ def _decrypt(ct, key, alg, iv=None): 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 else: return bytearray(decryptor.update(ct) + decryptor.finalize()) + + +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, 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, 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) + + def decrypt(self, nonce: bytes, data: bytes, associated_data: Optional[bytes] = None) -> bytes: + return self._aead.decrypt(nonce, data, associated_data) diff --git a/pgpy/types.py b/pgpy/types.py index 187d3984..55bf4287 100644 --- a/pgpy/types.py +++ b/pgpy/types.py @@ -1,6 +1,6 @@ """ types.py """ -from __future__ import division +from __future__ import annotations import abc import base64 @@ -14,44 +14,111 @@ import warnings import weakref -from enum import EnumMeta from enum import IntEnum +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', 'Field', 'Fingerprint', - 'FlagEnum', - 'FlagEnumMeta', + 'FingerprintDict', + 'FingerprintValue', 'Header', + 'KeyID', 'MetaDispatchable', 'Dispatchable', + 'DispatchGuidance', 'SignatureVerification', - 'Fingerprint', 'SorteDeque'] +PGPMagicClass = Literal['SIGNATURE', 'MESSAGE', 'PUBLIC KEY BLOCK', 'PRIVATE KEY BLOCK'] + + +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(metaclass=abc.ABCMeta): +class Armorable(PGPObject, metaclass=abc.ABCMeta): __crc24_init = 0x0B704CE __crc24_poly = 0x1864CFB __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: # - 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 @@ -62,22 +129,38 @@ class Armorable(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) @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 + @property + def emit_crc(self) -> bool: + return True + @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 +170,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 +178,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, List[str]]]]: """ Takes an ASCII-armored PGP block and returns the decoded byte value. @@ -110,20 +196,18 @@ def ascii_unarmor(text): :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_ascii(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(',') @@ -140,13 +224,15 @@ def ascii_unarmor(text): 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 @@ -157,9 +243,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 @@ -171,7 +254,7 @@ def crc24(data): return crc & 0xFFFFFF @abc.abstractproperty - def magic(self): + def magic(self) -> PGPMagicClass: """The magic string identifier for the current PGP type""" @classmethod @@ -203,18 +286,22 @@ 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): + 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): @@ -224,7 +311,7 @@ def __copy__(self): return obj -class ParentRef(object): +class ParentRef: # mixin class to handle weak-referencing a parent object @property def _parent(self): @@ -245,79 +332,20 @@ def parent(self): return self._parent def __init__(self): - super(ParentRef, self).__init__() + super().__init__() self._parent = None -class PGPObject(metaclass=abc.ABCMeta): - - @staticmethod - def int_byte_len(i): - return (i.bit_length() + 7) // 8 - - @staticmethod - def bytes_to_int(b, order='big'): # 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 - """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 - - # 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): - if text is None or isinstance(text, str): - return text - - return text.decode('utf-8') - - @abc.abstractmethod - def parse(self, packet): - """this method is too abstract to understand""" - - @abc.abstractmethod - def __bytearray__(self): - """ - Returns the contents of concrete subclasses in a binary format that can be understood by other OpenPGP - implementations - """ - - def __bytes__(self): - """ - 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): + 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) @@ -327,24 +355,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) -> bytes: 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] @@ -378,7 +405,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] @@ -386,13 +413,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 @@ -408,17 +433,22 @@ 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): - super(Header, self).__init__() + 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): + "Identify classes that should be left alone by PGPy's internal dispatch mechanism" + NoDispatch = -1 class MetaDispatchable(abc.ABCMeta): @@ -426,15 +456,15 @@ 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 A RootClass is successfully registered if the following things are true: - it inherits (directly or indirectly) from Dispatchable - - __typeid__ == -1 + - __typeid__ is None """ - _registry = {} + _registry: Dict[Union[Tuple[Type, Optional[IntEnum]], Tuple[Type, IntEnum, int]], Type] = {} """ _registry is the Dispatchable class registry. It uses the following format: @@ -467,14 +497,14 @@ 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)): + 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 @@ -548,21 +578,23 @@ 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__ = None + __ver__: Optional[int] = None -class SignatureVerification(object): +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): @@ -605,22 +637,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(SignatureVerification, self).__init__() - self._subjects = [] + super().__init__() + 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 @@ -628,39 +660,80 @@ def __bool__(self): for sigsub in self._subjects ) - def __nonzero__(self): - return self.__bool__() - - 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): + ''' + 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) -class FlagEnumMeta(EnumMeta): - def __and__(self, other): - return { f for f in iter(self) if f.value & other } + def __int__(self) -> int: + return int.from_bytes(bytes(self), byteorder='big', signed=False) - def __rand__(self, other): # pragma: no cover - return self & other + def __hash__(self) -> int: + return hash(str(self)) + def __bytes__(self) -> bytes: + return binascii.a2b_hex(self) -namespace = FlagEnumMeta.__prepare__('FlagEnum', (IntEnum,)) -FlagEnum = FlagEnumMeta('FlagEnum', (IntEnum,), namespace) + def __repr__(self) -> str: + return f"KeyID({self})" class Fingerprint(str): @@ -670,26 +743,66 @@ 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: + 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): + def shortid(self) -> str: + if self._version != 4: + raise ValueError("shortid can only ever be used on version 4 keys") return self[-8:] - def __new__(cls, content): + @sdproperty + def version(self) -> int: + '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 - # validate input before continuing: this should be a string of 40 hex digits + if isinstance(content, (bytes, bytearray)): + 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]+$', content): - raise ValueError('Fingerprint must be a string of 40 hex digits') - return str.__new__(cls, content) + 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) + 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): + def __eq__(self, other: object) -> bool: if isinstance(other, Fingerprint): - return str(self) == str(other) + 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 @@ -698,23 +811,30 @@ 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 - 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): - 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): + 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)], @@ -722,22 +842,20 @@ 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): """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 @@ -750,7 +868,63 @@ 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) + + +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 diff --git a/requirements.txt b/requirements.txt index e2a25384..f135d792 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ +argon2_cffi cryptography>=3.3.2 -pyasn1 +sop>=0.5.1[sopgpy] +pycryptodomex[eax] diff --git a/setup.cfg b/setup.cfg index af26cfdd..a38c02cd 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 @@ -41,16 +43,23 @@ 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 - pyasn1 python_requires = >=3.6 # doc_requires = # sphinx # sphinx-better-theme +[options.extras_require] +sopgpy = sop>=0.5.1 +eax = pycryptodomex [build_sphinx] source-dir = docs/source build-dir = docs/build all_files = 1 + +[options.entry_points] +console_scripts = + sopgpy = pgpy.sopgpy:main 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/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("") 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_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_01_packetfields.py b/tests/test_01_packetfields.py index fcd9a411..b6be65a3 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 @@ -51,14 +52,14 @@ ] -class TestHeaders(object): +class TestHeaders: @pytest.mark.parametrize('pheader', pkt_headers) def test_packet_header(self, pheader): b = 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) @@ -197,7 +198,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 +268,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[:] @@ -297,7 +298,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,20 +309,20 @@ 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])] -class TestString2Key(object): +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 @@ -329,15 +330,15 @@ 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.iv == _iv + assert s._specifier._type is String2KeyType.Simple + assert s.iv == _iv[:s.encalg.block_size//8] @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 @@ -345,16 +346,16 @@ 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.iv == _iv + 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) def test_iterated_string2key(self, is2k): b = is2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(is2k) assert len(is2k) == 0 @@ -362,17 +363,17 @@ 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.iv == _iv + 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) def test_gnu_extension_string2key(self, gnus2k): b = gnus2k[:] - s = String2Key() + s = String2Key(key_version=4) s.parse(gnus2k) assert len(gnus2k) == 0 @@ -381,7 +382,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_01_types.py b/tests/test_01_types.py index c425c96f..d666c7f0 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'), } @@ -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): @@ -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' diff --git a/tests/test_02_packets.py b/tests/test_02_packets.py index 0e31d641..353da5c4 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 @@ -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 diff --git a/tests/test_03_armor.py b/tests/test_03_armor.py index e32ee1e9..7b2df8cd 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)), @@ -186,26 +187,26 @@ 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', set()), + ('features', None), ('hash2', b'\xc4\x24'), - ('hashprefs', []), + ('hashprefs', None), ('hash_algorithm', HashAlgorithm.SHA512), ('is_expired', False), ('key_algorithm', PubKeyAlgorithm.RSAEncryptOrSign), - ('key_flags', set()), - ('keyserver', ''), - ('keyserverprefs', []), + ('key_flags', None), + ('keyserver', None), + ('keyserverprefs', None), ('magic', "SIGNATURE"), ('notation', {}), - ('policy_uri', ''), + ('policy_uri', None), ('revocable', True), ('revocation_key', None), - ('signer', 'FCAE54F74BA27CF7'), + ('signer', KeyID('FCAE54F74BA27CF7')), ('type', SignatureType.BinaryDocument)], 'tests/testdata/blocks/eccpubkey.asc': @@ -297,7 +298,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 +339,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..d5421718 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' @@ -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) @@ -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_04_copy.py b/tests/test_04_copy.py index 366ff743..812f5c25 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 @@ -37,11 +36,14 @@ 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): - 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): diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 8a801c9c..4a713f13 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,11 +16,12 @@ 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 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 @@ -35,12 +36,12 @@ 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')) ] -class TestPGPMessage(object): +class TestPGPMessage: @staticmethod def gpg_message(msg): ret = None @@ -71,7 +72,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 +107,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 +123,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 @@ -237,13 +238,13 @@ 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 # - 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: @@ -288,7 +289,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] @@ -346,10 +347,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.pgpy_features 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 @@ -427,8 +428,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 @@ -462,7 +462,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]) @@ -511,7 +511,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 @@ -621,10 +621,11 @@ 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 = {} - msgs = {} +class TestPGPKey_Actions: + 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 @@ -851,7 +852,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, @@ -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) diff --git a/tests/test_06_compatibility.py b/tests/test_06_compatibility.py new file mode 100644 index 00000000..63b8668f --- /dev/null +++ b/tests/test_06_compatibility.py @@ -0,0 +1,62 @@ +# coding=utf-8 +""" ensure that we don't crash on surprising messages +""" +from typing import Optional + +import pytest + +from pgpy import PGPKey, PGPMessage, PGPSignatures +from pgpy.types import SignatureVerification +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') + 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 :)' + sigs = PGPSignatures.from_file(f'tests/testdata/compatibility/{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: + k:PGPKey + (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 + (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 + (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') + 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 299f3456..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,16 +78,17 @@ 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], 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] ] -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 +102,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 @@ -139,14 +142,14 @@ 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) -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 @@ -164,18 +167,18 @@ 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') - 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): @@ -343,7 +346,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 +355,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): @@ -380,7 +383,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): @@ -390,7 +393,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 +412,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") 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] diff --git a/tests/test_12_crypto_refresh.py b/tests/test_12_crypto_refresh.py new file mode 100644 index 00000000..2866602a --- /dev/null +++ b/tests/test_12_crypto_refresh.py @@ -0,0 +1,77 @@ +# coding=utf-8 +""" Verify samples from draft-ietf-openpgp-crypto-refresh-10 +""" + +from typing import Dict, Optional, Tuple +from types import ModuleType + +import pytest + +from warnings import warn + +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') + 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 + 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') + assert msg.is_encrypted + 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!' + + @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') + 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') + 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/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) diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index da12d079..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 @@ -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 @@ -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 @@ -134,8 +133,8 @@ 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) - sig._signature.subpackets['h_CreationTime'][-1].created = datetime(2014, 8, 6, 23, 28, 51) + 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) sig._signature.hash2 = hashlib.new('sha512', hdata).digest()[:2] 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 + +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 +=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 + +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 +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----- + +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 +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----- + +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 +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----- + +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 +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----- + +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 +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----- 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----- diff --git a/tox.ini b/tox.ini index 0956986e..8da29019 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] @@ -25,20 +25,19 @@ passenv = LD_LIBRARY_PATH PATH deps = - cryptography>=2.6 + -rrequirements.txt 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 - git+https://github.com/SecurityInnovation/pytest-order.git@07ceb36233fb083275f34d5c8abbd3e35cd00158#egg=pytest-order + pytest-order +extras = + eax 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