Skip to content

Commit

Permalink
support for Ed25519 signatures
Browse files Browse the repository at this point in the history
This increases the versioned dependency on the cryptography module to
2.6, since that is the version that provides the necessary ed25519
functionality.

We also add a "pure" 25519 OpenPGP certificate for testing purposes.

Closes #221, #222, #247

Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  • Loading branch information
rot42 authored and dkg committed Aug 1, 2019
1 parent 53c6c3b commit d601655
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 16 deletions.
8 changes: 4 additions & 4 deletions pgpy/_curves.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 3 additions & 5 deletions pgpy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -268,15 +265,16 @@ def can_gen(self):
return self in {PubKeyAlgorithm.RSAEncryptOrSign,
PubKeyAlgorithm.DSA,
PubKeyAlgorithm.ECDSA,
PubKeyAlgorithm.ECDH}
PubKeyAlgorithm.ECDH,
PubKeyAlgorithm.EdDSA}

@property
def can_encrypt(self): # pragma: no cover
return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal, PubKeyAlgorithm.ECDH}

@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):
Expand Down
127 changes: 127 additions & 0 deletions pgpy/packet/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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

Expand Down Expand Up @@ -71,13 +72,15 @@
'RSASignature',
'DSASignature',
'ECDSASignature',
'EdDSASignature',
'PubKey',
'OpaquePubKey',
'RSAPub',
'DSAPub',
'ElGPub',
'ECPoint',
'ECDSAPub',
'EdDSAPub',
'ECDHPub',
'String2Key',
'ECKDF',
Expand All @@ -87,6 +90,7 @@
'DSAPriv',
'ElGPriv',
'ECDSAPriv',
'EdDSAPriv',
'ECDHPriv',
'CipherText',
'RSACipherText',
Expand Down Expand Up @@ -351,6 +355,21 @@ def from_signer(self, sig):
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__ = ()

Expand Down Expand Up @@ -571,6 +590,57 @@ def parse(self, packet):
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',)

Expand Down Expand Up @@ -1434,6 +1504,63 @@ def sign(self, sigdata, hash_alg):
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)
Expand Down
7 changes: 5 additions & 2 deletions pgpy/packet/packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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
Expand Down Expand Up @@ -364,7 +365,7 @@ def pubalg_int(self, val):
PubKeyAlgorithm.RSASign: RSASignature,
PubKeyAlgorithm.DSA: DSASignature,
PubKeyAlgorithm.ECDSA: ECDSASignature,
PubKeyAlgorithm.EdDSA: ECDSASignature,}
PubKeyAlgorithm.EdDSA: EdDSASignature,}

self.signature = sigs.get(self.pubalg, OpaqueSignature)()

Expand Down Expand Up @@ -762,6 +763,7 @@ def pkalg_int(self, val):
(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,
Expand All @@ -771,6 +773,7 @@ def pkalg_int(self, val):
(False, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPriv,
(False, PubKeyAlgorithm.ECDSA): ECDSAPriv,
(False, PubKeyAlgorithm.ECDH): ECDHPriv,
(False, PubKeyAlgorithm.EdDSA): EdDSAPriv,
}

k = (self.public, self.pkalg)
Expand Down Expand Up @@ -878,7 +881,7 @@ def pubkey(self):
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:
Expand Down
2 changes: 1 addition & 1 deletion pgpy/pgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,7 @@ def key_size(self):
"""*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))
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cryptography>=2.5
cryptography>=2.6
enum34
pyasn1
six>=1.9.0
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


_requires = [
'cryptography>=2.5',
'cryptography>=2.6',
'pyasn1',
'six>=1.9.0',
]
Expand Down
17 changes: 16 additions & 1 deletion tests/test_05_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,16 @@ 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),
(PubKeyAlgorithm.DSA, 1024),
(PubKeyAlgorithm.ElGamal, 1024),
(PubKeyAlgorithm.ECDSA, EllipticCurveOID.SECP256K1),
(PubKeyAlgorithm.ECDH, EllipticCurveOID.Brainpool_P256),
(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519),
(PubKeyAlgorithm.ECDH, EllipticCurveOID.Curve25519),)


Expand Down Expand Up @@ -889,3 +891,16 @@ def test_sign_encrypted_message(self, sf, cipher):
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"
2 changes: 2 additions & 0 deletions tests/test_99_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ def test_reg_bug_157(monkeypatch):
_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
]
Expand Down
14 changes: 14 additions & 0 deletions tests/testdata/keys/ecc.2.pub.asc
Original file line number Diff line number Diff line change
@@ -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-----
15 changes: 15 additions & 0 deletions tests/testdata/keys/ecc.2.sec.asc
Original file line number Diff line number Diff line change
@@ -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-----
9 changes: 9 additions & 0 deletions tests/testdata/messages/message.ecdh.cv25519.asc
Original file line number Diff line number Diff line change
@@ -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-----
7 changes: 7 additions & 0 deletions tests/testdata/signatures/ecc.2.sig.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----

iHUEABYIAB0WIQR/D5etU51R90sBi9EGLmrFIF2HHgUCXMILLAAKCRAGLmrFIF2H
HmI3AQC70mikthv4+gadsD7E8STKEPKtrkGaWTLQ6Q+U9Rc7sAD/dkUfbeRB7UvU
COB64/0yz5kupCYt5+an6jH85yWNTgc=
=RSff
-----END PGP SIGNATURE-----
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d601655

Please sign in to comment.