Skip to content

Commit

Permalink
ed25519 support
Browse files Browse the repository at this point in the history
  • Loading branch information
reaperhulk committed Sep 28, 2018
1 parent 1717f8c commit bfcf205
Show file tree
Hide file tree
Showing 6 changed files with 373 additions and 15 deletions.
76 changes: 76 additions & 0 deletions docs/hazmat/primitives/asymmetric/ed25519.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.. hazmat::

Ed25519 signing
===============

.. currentmodule:: cryptography.hazmat.primitives.asymmetric.ed25519


Ed25519 is a signing algorithm using `EdDSA`_ and `Curve25519`_. If you do not
have legacy interoperability concerns then you should strongly consider using
this signature algorithm.


Signing & Verification
~~~~~~~~~~~~~~~~~~~~~~

.. doctest::

>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
>>> private_key = Ed25519PrivateKey.generate()
>>> signature = private_key.sign(b"my authenticated message")
>>> public_key = private_key.public_key()
>>> # Raises InvalidSignature if verification fails
>>> public_key.verify(signature, b"my authenticated message")

Key interfaces
~~~~~~~~~~~~~~

.. class:: Ed25519PrivateKey

.. versionadded:: 2.4

.. classmethod:: generate()

Generate an Ed25519 private key.

:returns: :class:`Ed25519PrivateKey`

.. method:: public_key()

:returns: :class:`Ed25519PublicKey`

.. method:: sign(data)

:param bytes data: The data to sign.

:returns bytes: The 64 byte signature.

.. class:: Ed25519PublicKey

.. versionadded:: 2.4

.. classmethod:: from_public_bytes(data)

:param bytes data: 32 byte public key.

:returns: :class:`Ed25519PublicKey`

.. method:: public_bytes()

:returns bytes: The raw bytes of the public key.

.. method:: verify(signature, data)

:param bytes signature: The signature to verify.

:param bytes data: The data to verify.

:raises cryptography.exceptions.InvalidSignature: Raised when the
signature cannot be verified.



.. _`EdDSA`: https://en.wikipedia.org/wiki/EdDSA
.. _`Curve25519`: https://en.wikipedia.org/wiki/Curve25519
1 change: 1 addition & 0 deletions docs/hazmat/primitives/asymmetric/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private key is able to decrypt it.

x25519
ec
ed25519
rsa
dh
dsa
Expand Down
71 changes: 56 additions & 15 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
from cryptography.hazmat.backends.openssl.ec import (
_EllipticCurvePrivateKey, _EllipticCurvePublicKey
)
from cryptography.hazmat.backends.openssl.ed25519 import (
_Ed25519PrivateKey, _Ed25519PublicKey
)
from cryptography.hazmat.backends.openssl.encode_asn1 import (
_CRL_ENTRY_EXTENSION_ENCODE_HANDLERS,
_CRL_EXTENSION_ENCODE_HANDLERS, _EXTENSION_ENCODE_HANDLERS,
Expand Down Expand Up @@ -1935,6 +1938,13 @@ def x25519_load_public_bytes(self, data):
backend.openssl_assert(res == 1)
return _X25519PublicKey(self, evp_pkey)

def _generic_25519_load_private_bytes(self, prefix, data, keycls):
bio = self._bytes_to_bio(prefix + data)
evp_pkey = backend._lib.d2i_PrivateKey_bio(bio.bio, self._ffi.NULL)
self.openssl_assert(evp_pkey != self._ffi.NULL)
evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free)
return keycls(self, evp_pkey)

def x25519_load_private_bytes(self, data):
# OpenSSL only has facilities for loading PKCS8 formatted private
# keys using the algorithm identifiers specified in
Expand All @@ -1950,35 +1960,66 @@ def x25519_load_private_bytes(self, data):
# contains an OCTET STRING of length 32! So the last two bytes here
# are \x04\x20, which is an OCTET STRING of length 32.
pkcs8_prefix = b'0.\x02\x01\x000\x05\x06\x03+en\x04"\x04 '
bio = self._bytes_to_bio(pkcs8_prefix + data)
evp_pkey = backend._lib.d2i_PrivateKey_bio(bio.bio, self._ffi.NULL)
self.openssl_assert(evp_pkey != self._ffi.NULL)
evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free)
self.openssl_assert(
self._lib.EVP_PKEY_id(evp_pkey) == self._lib.EVP_PKEY_X25519
return self._generic_25519_load_private_bytes(
pkcs8_prefix, data, _X25519PrivateKey
)
return _X25519PrivateKey(self, evp_pkey)

def x25519_generate_key(self):
evp_pkey_ctx = self._lib.EVP_PKEY_CTX_new_id(
self._lib.NID_X25519, self._ffi.NULL
)
def _generic_25519_generate_key(self, nid, keycls):
evp_pkey_ctx = self._lib.EVP_PKEY_CTX_new_id(nid, self._ffi.NULL)
self.openssl_assert(evp_pkey_ctx != self._ffi.NULL)
evp_pkey_ctx = self._ffi.gc(
evp_pkey_ctx, self._lib.EVP_PKEY_CTX_free
)
evp_pkey_ctx = self._ffi.gc(evp_pkey_ctx, self._lib.EVP_PKEY_CTX_free)
res = self._lib.EVP_PKEY_keygen_init(evp_pkey_ctx)
self.openssl_assert(res == 1)
evp_ppkey = self._ffi.new("EVP_PKEY **")
res = self._lib.EVP_PKEY_keygen(evp_pkey_ctx, evp_ppkey)
self.openssl_assert(res == 1)
self.openssl_assert(evp_ppkey[0] != self._ffi.NULL)
evp_pkey = self._ffi.gc(evp_ppkey[0], self._lib.EVP_PKEY_free)
return _X25519PrivateKey(self, evp_pkey)
return keycls(self, evp_pkey)

def x25519_generate_key(self):
return self._generic_25519_generate_key(
self._lib.NID_X25519, _X25519PrivateKey
)

def x25519_supported(self):
return self._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER

def ed25519_supported(self):
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111

def ed25519_load_public_bytes(self, data):
pkcs8_prefix = b'0.\x02\x01\x000\x05\x06\x03+ep\x04"\x04 '
bio = self._bytes_to_bio(pkcs8_prefix + data)
evp_pkey = backend._lib.d2i_PUBKEY_bio(bio.bio, self._ffi.NULL)
self.openssl_assert(evp_pkey != self._ffi.NULL)
evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free)
return _Ed25519PublicKey(self, evp_pkey)

def ed25519_load_private_bytes(self, data):
# OpenSSL only has facilities for loading PKCS8 formatted private
# keys using the algorithm identifiers specified in
# https://tools.ietf.org/html/draft-ietf-curdle-pkix-03.
# This is the standard PKCS8 prefix for a 32 byte Ed25519 key.
# The form is:
# 0:d=0 hl=2 l= 46 cons: SEQUENCE
# 2:d=1 hl=2 l= 1 prim: INTEGER :00
# 5:d=1 hl=2 l= 5 cons: SEQUENCE
# 7:d=2 hl=2 l= 3 prim: OBJECT :1.3.101.112
# 12:d=1 hl=2 l= 34 prim: OCTET STRING (the key)
# Of course there's a bit more complexity. In reality OCTET STRING
# contains an OCTET STRING of length 32! So the last two bytes here
# are \x04\x20, which is an OCTET STRING of length 32.
pkcs8_prefix = b'0.\x02\x01\x000\x05\x06\x03+ep\x04"\x04 '
return self._generic_25519_load_private_bytes(
pkcs8_prefix, data, _Ed25519PrivateKey
)

def ed25519_generate_key(self):
return self._generic_25519_generate_key(
self._lib.NID_ED25519, _Ed25519PrivateKey
)

def derive_scrypt(self, key_material, salt, length, n, r, p):
buf = self._ffi.new("unsigned char[]", length)
res = self._lib.EVP_PBE_scrypt(
Expand Down
81 changes: 81 additions & 0 deletions src/cryptography/hazmat/backends/openssl/ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import absolute_import, division, print_function

from cryptography import exceptions, utils
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey, Ed25519PublicKey
)


@utils.register_interface(Ed25519PublicKey)
class _Ed25519PublicKey(object):
def __init__(self, backend, evp_pkey):
self._backend = backend
self._evp_pkey = evp_pkey

def public_bytes(self):
bio = self._backend._create_mem_bio_gc()
res = self._backend._lib.i2d_PUBKEY_bio(bio, self._evp_pkey)
self._backend.openssl_assert(res == 1)
asn1 = self._backend._read_mem_bio(bio)
# We serialize to the ASN.1 structure defined in
# https://tools.ietf.org/html/draft-ietf-curdle-pkix-03. and
# then take the last 32 bytes, which are the actual key.
return asn1[-32:]

def verify(self, signature, data):
evp_md_ctx = self._backend._lib.Cryptography_EVP_MD_CTX_new()
self._backend.openssl_assert(evp_md_ctx != self._backend._ffi.NULL)
evp_md_ctx = self._backend._ffi.gc(
evp_md_ctx, self._backend._lib.Cryptography_EVP_MD_CTX_free
)
res = self._backend._lib.EVP_DigestVerifyInit(
evp_md_ctx, self._backend._ffi.NULL, self._backend._ffi.NULL,
self._backend._ffi.NULL, self._evp_pkey
)
self._backend.openssl_assert(res == 1)
res = self._backend._lib.EVP_DigestVerify(
evp_md_ctx, signature, len(signature), data, len(data)
)
if res != 1:
self._backend._consume_errors()
raise exceptions.InvalidSignature


@utils.register_interface(Ed25519PrivateKey)
class _Ed25519PrivateKey(object):
def __init__(self, backend, evp_pkey):
self._backend = backend
self._evp_pkey = evp_pkey

def public_key(self):
bio = self._backend._create_mem_bio_gc()
res = self._backend._lib.i2d_PUBKEY_bio(bio, self._evp_pkey)
self._backend.openssl_assert(res == 1)
evp_pkey = self._backend._lib.d2i_PUBKEY_bio(
bio, self._backend._ffi.NULL
)
return _Ed25519PublicKey(self._backend, evp_pkey)

def sign(self, data):
evp_md_ctx = self._backend._lib.Cryptography_EVP_MD_CTX_new()
self._backend.openssl_assert(evp_md_ctx != self._backend._ffi.NULL)
evp_md_ctx = self._backend._ffi.gc(
evp_md_ctx, self._backend._lib.Cryptography_EVP_MD_CTX_free
)
res = self._backend._lib.EVP_DigestSignInit(
evp_md_ctx, self._backend._ffi.NULL, self._backend._ffi.NULL,
self._backend._ffi.NULL, self._evp_pkey
)
self._backend.openssl_assert(res == 1)
buf = self._backend._ffi.new("unsigned char[]", 64)
buflen = self._backend._ffi.new("size_t *", len(buf))
res = self._backend._lib.EVP_DigestSign(
evp_md_ctx, buf, buflen, data, len(data)
)
self._backend.openssl_assert(res == 1)
self._backend.openssl_assert(buflen[0] == 64)
return self._backend._ffi.buffer(buf, buflen[0])[:]
58 changes: 58 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import absolute_import, division, print_function

import abc

import six

from cryptography.exceptions import UnsupportedAlgorithm, _Reasons


@six.add_metaclass(abc.ABCMeta)
class Ed25519PublicKey(object):
@classmethod
def from_public_bytes(cls, data):
from cryptography.hazmat.backends.openssl.backend import backend
if not backend.ed25519_supported():
raise UnsupportedAlgorithm(
"ed25519 is not supported by this version of OpenSSL.",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
)
return backend.ed25519_load_public_bytes(data)

@abc.abstractmethod
def public_bytes(self):
pass

@abc.abstractmethod
def verify(self, signature, data):
pass


@six.add_metaclass(abc.ABCMeta)
class Ed25519PrivateKey(object):
@classmethod
def generate(cls):
from cryptography.hazmat.backends.openssl.backend import backend
if not backend.ed25519_supported():
raise UnsupportedAlgorithm(
"ed25519 is not supported by this version of OpenSSL.",
_Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
)
return backend.ed25519_generate_key()

@classmethod
def from_private_bytes(cls, data):
from cryptography.hazmat.backends.openssl.backend import backend
return backend.ed25519_load_private_bytes(data)

@abc.abstractmethod
def public_key(self):
pass

@abc.abstractmethod
def sign(self, data):
pass
Loading

0 comments on commit bfcf205

Please sign in to comment.