support for Ed25519 signatures

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>
This commit is contained in:
rot42
2019-07-20 00:30:25 +02:00
committed by Daniel Kahn Gillmor
parent 53c6c3ba94
commit d601655c27
14 changed files with 206 additions and 16 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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:

View File

@@ -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))

View File

@@ -1,4 +1,4 @@
cryptography>=2.5
cryptography>=2.6
enum34
pyasn1
six>=1.9.0

View File

@@ -22,7 +22,7 @@ with open('README.rst') as readme:
_requires = [
'cryptography>=2.5',
'cryptography>=2.6',
'pyasn1',
'six>=1.9.0',
]

View File

@@ -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"

View File

@@ -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
]

14
tests/testdata/keys/ecc.2.pub.asc vendored Normal file
View File

@@ -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
tests/testdata/keys/ecc.2.sec.asc vendored Normal file
View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYIAB0WIQR/D5etU51R90sBi9EGLmrFIF2HHgUCXMILLAAKCRAGLmrFIF2H
HmI3AQC70mikthv4+gadsD7E8STKEPKtrkGaWTLQ6Q+U9Rc7sAD/dkUfbeRB7UvU
COB64/0yz5kupCYt5+an6jH85yWNTgc=
=RSff
-----END PGP SIGNATURE-----

View File

@@ -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