diff --git a/pgpy/_curves.py b/pgpy/_curves.py index a967407..ce6a9f0 100644 --- a/pgpy/_curves.py +++ b/pgpy/_curves.py @@ -26,10 +26,10 @@ def _openssl_get_supported_curves(): # 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 } - # X25519 is always present in cryptography>=2.5 - # The python cryptography lib provides a different interface for this curve, - # so it is handled differently in the ECDHPriv/ECDHPub classes - curves |= {'X25519'} + # 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 diff --git a/pgpy/constants.py b/pgpy/constants.py index ce2e623..68a7627 100644 --- a/pgpy/constants.py +++ b/pgpy/constants.py @@ -59,9 +59,6 @@ class EllipticCurveOID(Enum): #: DJB's fast elliptic curve Curve25519 = ('1.3.6.1.4.1.3029.1.5.1', X25519) #: Twisted Edwards variant of Curve25519 - #: - #: .. warning:: - #: This curve is not currently usable by PGPy 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) @@ -268,7 +265,8 @@ class PubKeyAlgorithm(IntEnum): return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, - PubKeyAlgorithm.ECDH} + PubKeyAlgorithm.ECDH, + PubKeyAlgorithm.EdDSA} @property def can_encrypt(self): # pragma: no cover @@ -276,7 +274,7 @@ class PubKeyAlgorithm(IntEnum): @property def can_sign(self): - return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA} + return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA} @property def deprecated(self): diff --git a/pgpy/packet/fields.py b/pgpy/packet/fields.py index 72ae560..846d106 100644 --- a/pgpy/packet/fields.py +++ b/pgpy/packet/fields.py @@ -27,6 +27,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa 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 padding from cryptography.hazmat.primitives.asymmetric import x25519 @@ -71,6 +72,7 @@ __all__ = ['SubPackets', 'RSASignature', 'DSASignature', 'ECDSASignature', + 'EdDSASignature', 'PubKey', 'OpaquePubKey', 'RSAPub', @@ -78,6 +80,7 @@ __all__ = ['SubPackets', 'ElGPub', 'ECPoint', 'ECDSAPub', + 'EdDSAPub', 'ECDHPub', 'String2Key', 'ECKDF', @@ -87,6 +90,7 @@ __all__ = ['SubPackets', 'DSAPriv', 'ElGPriv', 'ECDSAPriv', + 'EdDSAPriv', 'ECDHPriv', 'CipherText', 'RSACipherText', @@ -351,6 +355,21 @@ class ECDSASignature(DSASignature): self.s = MPI(seq[1]) +class EdDSASignature(DSASignature): + def from_signer(self, sig): + lsig = len(sig) + if lsig % 2 != 0: + raise PGPError("malformed EdDSA signature") + split = lsig // 2 + self.r = MPI(self.bytes_to_int(sig[:split])) + self.s = MPI(self.bytes_to_int(sig[split:])) + + def __sig__(self): + # TODO: change this length when EdDSA can be used with another curve (Ed448) + l = (EllipticCurveOID.Ed25519.key_size + 7) // 8 + return self.int_to_bytes(self.r, l) + self.int_to_bytes(self.s, l) + + class PubKey(MPIs): __pubfields__ = () @@ -571,6 +590,57 @@ class ECDSAPub(PubKey): raise PGPIncompatibleECPointFormat("Only Standard format is valid for ECDSA") +class EdDSAPub(PubKey): + __pubfields__ = ('p', ) + + def __init__(self): + super(EdDSAPub, self).__init__() + self.oid = None + + def __len__(self): + return len(self.p) + len(encoder.encode(self.oid.value)) - 1 + + def __bytearray__(self): + _b = bytearray() + _b += encoder.encode(self.oid.value)[1:] + _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__() + 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.update(subj) + subj = digest.finalize() + try: + self.__pubkey__().verify(sigbytes, subj) + except InvalidSignature: + 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] + + self.p = ECPoint(packet) + if self.p.format != ECPointFormat.Native: + raise PGPIncompatibleECPointFormat("Only Native format is valid for EdDSA") + + class ECDHPub(PubKey): __pubfields__ = ('p',) @@ -1434,6 +1504,63 @@ class ECDSAPriv(PrivKey, ECDSAPub): return self.__privkey__().sign(sigdata, ec.ECDSA(hash_alg)) +class EdDSAPriv(PrivKey, EdDSAPub): + __privfields__ = ('s', ) + + def __privkey__(self): + s = self.int_to_bytes(self.s, (self.oid.key_size + 7) // 8) + return ed25519.Ed25519PrivateKey.from_private_bytes(s) + + 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): + if any(c != 0 for c in self): # pragma: no cover + raise PGPError("Key is already populated!") + + self.oid = EllipticCurveOID(oid) + + if self.oid != EllipticCurveOID.Ed25519: + raise ValueError("EdDSA only supported with {}".format(EllipticCurveOID.Ed25519)) + + pk = ed25519.Ed25519PrivateKey.generate() + x = pk.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) + self.p = ECPoint.from_values(self.oid.key_size, ECPointFormat.Native, x) + self.s = MPI(self.bytes_to_int(pk.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption() + ))) + self._compute_chksum() + + def parse(self, packet): + super(EdDSAPriv, self).parse(packet) + self.s2k.parse(packet) + + if not self.s2k: + self.s = MPI(packet) + if self.s2k.usage == 0: + 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) + del passphrase + 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.update(sigdata) + sigdata = digest.finalize() + return self.__privkey__().sign(sigdata) + + class ECDHPriv(ECDSAPriv, ECDHPub): def __bytearray__(self): _b = ECDHPub.__bytearray__(self) diff --git a/pgpy/packet/packets.py b/pgpy/packet/packets.py index 5060b70..648725a 100644 --- a/pgpy/packet/packets.py +++ b/pgpy/packet/packets.py @@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import padding 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 OpaquePubKey from .fields import OpaquePrivKey @@ -364,7 +365,7 @@ class SignatureV4(Signature): PubKeyAlgorithm.RSASign: RSASignature, PubKeyAlgorithm.DSA: DSASignature, PubKeyAlgorithm.ECDSA: ECDSASignature, - PubKeyAlgorithm.EdDSA: ECDSASignature,} + PubKeyAlgorithm.EdDSA: EdDSASignature,} self.signature = sigs.get(self.pubalg, OpaqueSignature)() @@ -762,6 +763,7 @@ class PubKeyV4(PubKey): (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, @@ -771,6 +773,7 @@ class PubKeyV4(PubKey): (False, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPriv, (False, PubKeyAlgorithm.ECDSA): ECDSAPriv, (False, PubKeyAlgorithm.ECDH): ECDHPriv, + (False, PubKeyAlgorithm.EdDSA): EdDSAPriv, } k = (self.public, self.pkalg) @@ -878,7 +881,7 @@ class PrivKeyV4(PrivKey, PubKeyV4): for pm in self.keymaterial.__pubfields__: setattr(pk.keymaterial, pm, copy.copy(getattr(self.keymaterial, pm))) - if self.pkalg == PubKeyAlgorithm.ECDSA: + if self.pkalg in [PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA]: pk.keymaterial.oid = self.keymaterial.oid if self.pkalg == PubKeyAlgorithm.ECDH: diff --git a/pgpy/pgp.py b/pgpy/pgp.py index bd5b5a8..265fd92 100644 --- a/pgpy/pgp.py +++ b/pgpy/pgp.py @@ -1324,7 +1324,7 @@ class PGPKey(Armorable, ParentRef, PGPObject): """*new in 0.4.1* The size pertaining to this key. ``int`` for non-EC key algorithms; :py:obj:`constants.EllipticCurveOID` for EC keys. """ - if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH}: + if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA}: return self._key.keymaterial.oid # check if keymaterial is not an Opaque class containing a bytearray param = next(iter(self._key.keymaterial)) diff --git a/requirements.txt b/requirements.txt index 30e2600..c24caca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography>=2.5 +cryptography>=2.6 enum34 pyasn1 six>=1.9.0 diff --git a/setup.py b/setup.py index 1a03f85..93b5868 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ with open('README.rst') as readme: _requires = [ - 'cryptography>=2.5', + 'cryptography>=2.6', 'pyasn1', 'six>=1.9.0', ] diff --git a/tests/test_05_actions.py b/tests/test_05_actions.py index 4505a9b..bb52bb3 100644 --- a/tests/test_05_actions.py +++ b/tests/test_05_actions.py @@ -194,7 +194,8 @@ def userphoto(): # TODO: add more keyspecs pkeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024), (PubKeyAlgorithm.DSA, 1024), - (PubKeyAlgorithm.ECDSA, EllipticCurveOID.NIST_P256),) + (PubKeyAlgorithm.ECDSA, EllipticCurveOID.NIST_P256), + (PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519),) skeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024), @@ -202,6 +203,7 @@ skeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024), (PubKeyAlgorithm.ElGamal, 1024), (PubKeyAlgorithm.ECDSA, EllipticCurveOID.SECP256K1), (PubKeyAlgorithm.ECDH, EllipticCurveOID.Brainpool_P256), + (PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519), (PubKeyAlgorithm.ECDH, EllipticCurveOID.Curve25519),) @@ -889,3 +891,16 @@ class TestPGPKey_Actions(object): assert emsg.is_signed assert emsg.is_encrypted assert isinstance(next(iter(emsg)), PGPSignature) + + def test_gpg_ed25519_verify(self, abe): + # test verification of Ed25519 signature generated by GnuPG + pubkey, _ = PGPKey.from_file('tests/testdata/keys/ecc.2.pub.asc') + sig = PGPSignature.from_file('tests/testdata/signatures/ecc.2.sig.asc') + assert pubkey.verify("This is a test signature message", sig) + + def test_gpg_cv25519_decrypt(self, abe): + # test the decryption of X25519 generated by GnuPG + seckey, _ = PGPKey.from_file('tests/testdata/keys/ecc.2.sec.asc') + emsg = PGPMessage.from_file('tests/testdata/messages/message.ecdh.cv25519.asc') + dmsg = seckey.decrypt(emsg) + assert bytes(dmsg.message) == b"This message will have been encrypted" diff --git a/tests/test_99_regressions.py b/tests/test_99_regressions.py index 59125e0..3071ad8 100644 --- a/tests/test_99_regressions.py +++ b/tests/test_99_regressions.py @@ -204,6 +204,8 @@ seckm = [ _seckeys['RSAEncryptOrSign']._key, # RSA private key packet _seckeys['ECDSA']._key, # ECDSA private key packet _seckeys['ECDSA'].subkeys['A81B93FD16BD9806']._key, # ECDH private key packet + _seckeys['EdDSA']._key, # EdDSA private key packet + _seckeys['EdDSA'].subkeys['AFC377493D8E897D']._key, # Curve25519 private key packet _mixed1._key, # RSA private key packet _mixed1.subkeys['B345506C90A428C5']._key, # ECDH Curve25519 private key packet ] diff --git a/tests/testdata/keys/ecc.2.pub.asc b/tests/testdata/keys/ecc.2.pub.asc new file mode 100644 index 0000000..aa00473 --- /dev/null +++ b/tests/testdata/keys/ecc.2.pub.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEXL8mcRYJKwYBBAHaRw8BAQdASFm0/fvw3kw5Vjz+vVjKq2Xhy8aX4WAcr+YF +n72YSHy0PkN1cnZlMjU1MTkgdm9uIFRlc3RLZXkgKGVkMjU1MTkgY3YyNTUxOSkg +PGN1cnZlMjU1MTlAdGVzdC5rZXk+iJAEExYIADgWIQR/D5etU51R90sBi9EGLmrF +IF2HHgUCXL8mcQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAGLmrFIF2H +Hq3uAQCEY5BFGUt+yKQ0nzMv18EPH/AArvctRR4efdTXPAc6JgEAz/Gp4THGptmf +Pn5JALETkH86zNUApJgSWeF/HifRwAi4OARcvyZxEgorBgEEAZdVAQUBAQdAMzdC +zRVztPW7BXo+eZl9J06cclRV6rcw7udWUE4Rvj0DAQgHiHgEGBYIACAWIQR/D5et +U51R90sBi9EGLmrFIF2HHgUCXL8mcQIbDAAKCRAGLmrFIF2HHgK/AP0XT+98jOMo +6G8qoFU7ANLfn4E3bxV0sAH9g/q/rnU7FwEAkpn5woqcil5DexELJoKruxEQ0M84 +3wriYvL2DwktCAs= +=OgJa +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/testdata/keys/ecc.2.sec.asc b/tests/testdata/keys/ecc.2.sec.asc new file mode 100644 index 0000000..62eae3b --- /dev/null +++ b/tests/testdata/keys/ecc.2.sec.asc @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEXL8mcRYJKwYBBAHaRw8BAQdASFm0/fvw3kw5Vjz+vVjKq2Xhy8aX4WAcr+YF +n72YSHwAAQCamKiDL90+GqgU5W7Y/givu0p9aUcW8XAaKh8y8f122A/YtD5DdXJ2 +ZTI1NTE5IHZvbiBUZXN0S2V5IChlZDI1NTE5IGN2MjU1MTkpIDxjdXJ2ZTI1NTE5 +QHRlc3Qua2V5PoiQBBMWCAA4FiEEfw+XrVOdUfdLAYvRBi5qxSBdhx4FAly/JnEC +GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQBi5qxSBdhx6t7gEAhGOQRRlL +fsikNJ8zL9fBDx/wAK73LUUeHn3U1zwHOiYBAM/xqeExxqbZnz5+SQCxE5B/OszV +AKSYElnhfx4n0cAInF0EXL8mcRIKKwYBBAGXVQEFAQEHQDM3Qs0Vc7T1uwV6PnmZ +fSdOnHJUVeq3MO7nVlBOEb49AwEIBwAA/1yFWz6KQ1cHO+dce/Moq56wvUqJxVMB +EGDIFjyxs9IQDyqIeAQYFggAIBYhBH8Pl61TnVH3SwGL0QYuasUgXYceBQJcvyZx +AhsMAAoJEAYuasUgXYceAr8A/RdP73yM4yjobyqgVTsA0t+fgTdvFXSwAf2D+r+u +dTsXAQCSmfnCipyKXkN7EQsmgqu7ERDQzzjfCuJi8vYPCS0ICw== +=siss +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/testdata/messages/message.ecdh.cv25519.asc b/tests/testdata/messages/message.ecdh.cv25519.asc new file mode 100644 index 0000000..039dc6f --- /dev/null +++ b/tests/testdata/messages/message.ecdh.cv25519.asc @@ -0,0 +1,9 @@ +-----BEGIN PGP MESSAGE----- + +hF4Dr8N3ST2OiX0SAQdAjBvn1lUIHo/MoXG0iizRRIs3zmU+q/H2nk8iU+ksgXgw +YxiH/gRKL/QH0ozo4mKQBy9bzY0jt0V1ldVhP4C5nh3YKLLpasHyhn/Hd+QgmKBS +0mABoKVxFzehABvQa1+QRkiegFGTjouEjRik/MCmfyT+kusqnj1dOgY/sAzFJoEy +7duCJ4YsXRrh1VAa+eJsdm7h9eeXRUvbn7QPA6VLnhYeRsQU8cCscHj+jvklSdzs +rR0= +=PzYF +-----END PGP MESSAGE----- diff --git a/tests/testdata/signatures/ecc.2.sig.asc b/tests/testdata/signatures/ecc.2.sig.asc new file mode 100644 index 0000000..1710692 --- /dev/null +++ b/tests/testdata/signatures/ecc.2.sig.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYIAB0WIQR/D5etU51R90sBi9EGLmrFIF2HHgUCXMILLAAKCRAGLmrFIF2H +HmI3AQC70mikthv4+gadsD7E8STKEPKtrkGaWTLQ6Q+U9Rc7sAD/dkUfbeRB7UvU +COB64/0yz5kupCYt5+an6jH85yWNTgc= +=RSff +-----END PGP SIGNATURE----- diff --git a/tox.ini b/tox.ini index 026257d..a68a931 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ max-line-length = 160 [testenv] passenv = HOME ARCHFLAGS LDFLAGS CFLAGS INCLUDE LIB LD_LIBRARY_PATH PATH deps = - cryptography>=2.5 + cryptography>=2.6 enum34 gpg==1.8.0 pyasn1