aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2019-02-27 12:33:07 +0800
committerAlex Gaynor <alex.gaynor@gmail.com>2019-02-26 23:33:07 -0500
commitcd05e2ac5b8336dac78a0dd73ec9a7baa06208c6 (patch)
tree7af9a249383425af9f8622f9f29a54371463b10a
parent6a022794b0b9cc7940e860f76eb450e05d883b15 (diff)
downloadcryptography-cd05e2ac5b8336dac78a0dd73ec9a7baa06208c6.tar.gz
cryptography-cd05e2ac5b8336dac78a0dd73ec9a7baa06208c6.tar.bz2
cryptography-cd05e2ac5b8336dac78a0dd73ec9a7baa06208c6.zip
ed25519 support (#4114)
* ed25519 support * review feedback
-rw-r--r--CHANGELOG.rst2
-rw-r--r--docs/hazmat/primitives/asymmetric/ed25519.rst159
-rw-r--r--docs/hazmat/primitives/asymmetric/index.rst1
-rw-r--r--docs/spelling_wordlist.txt1
-rw-r--r--src/_cffi_src/openssl/cryptography.py3
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py44
-rw-r--r--src/cryptography/hazmat/backends/openssl/ed25519.py154
-rw-r--r--src/cryptography/hazmat/primitives/asymmetric/ed25519.py80
-rw-r--r--tests/hazmat/primitives/test_ed25519.py229
-rw-r--r--tests/hazmat/primitives/test_serialization.py64
-rw-r--r--tests/wycheproof/test_eddsa.py45
11 files changed, 782 insertions, 0 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7c3f3bfe..8bca9b2b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,8 @@ Changelog
* **BACKWARDS INCOMPATIBLE**: Removed ``cryptography.x509.Certificate.serial``,
which had been deprecated for nearly 3 years. Use
:attr:`~cryptography.x509.Certificate.serial_number` instead.
+* Added support for :doc:`/hazmat/primitives/asymmetric/ed25519` when using
+ OpenSSL 1.1.1.
* Add support for easily mapping an object identifier to its elliptic curve
class via
:func:`~cryptography.hazmat.primitives.asymmetric.ec.get_curve_for_oid`.
diff --git a/docs/hazmat/primitives/asymmetric/ed25519.rst b/docs/hazmat/primitives/asymmetric/ed25519.rst
new file mode 100644
index 00000000..8893fbbd
--- /dev/null
+++ b/docs/hazmat/primitives/asymmetric/ed25519.rst
@@ -0,0 +1,159 @@
+.. hazmat::
+
+Ed25519 signing
+===============
+
+.. currentmodule:: cryptography.hazmat.primitives.asymmetric.ed25519
+
+
+Ed25519 is an elliptic curve 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.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.6
+
+ .. classmethod:: generate()
+
+ Generate an Ed25519 private key.
+
+ :returns: :class:`Ed25519PrivateKey`
+
+ .. classmethod:: from_private_bytes(data)
+
+ :param data: 32 byte private key.
+ :type data: :term:`bytes-like`
+
+ :returns: :class:`Ed25519PrivateKey`
+
+ .. doctest::
+
+ >>> from cryptography.hazmat.primitives import serialization
+ >>> from cryptography.hazmat.primitives.asymmetric import ed25519
+ >>> private_key = ed25519.Ed25519PrivateKey.generate()
+ >>> private_bytes = private_key.private_bytes(
+ ... encoding=serialization.Encoding.Raw,
+ ... format=serialization.PrivateFormat.Raw,
+ ... encryption_algorithm=serialization.NoEncryption()
+ ... )
+ >>> loaded_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_bytes)
+
+
+ .. method:: public_key()
+
+ :returns: :class:`Ed25519PublicKey`
+
+ .. method:: sign(data)
+
+ :param bytes data: The data to sign.
+
+ :returns bytes: The 64 byte signature.
+
+ .. method:: private_bytes(encoding, format, encryption_algorithm)
+
+ Allows serialization of the key to bytes. Encoding (
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
+ format (
+ :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`
+ or
+ :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
+ ) are chosen to define the exact serialization.
+
+ :param encoding: A value from the
+ :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
+
+ :param format: A value from the
+ :class:`~cryptography.hazmat.primitives.serialization.PrivateFormat`
+ enum. If the ``encoding`` is
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
+ then ``format`` must be
+ :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.Raw`
+ , otherwise it must be
+ :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8`.
+
+ :param encryption_algorithm: An instance of an object conforming to the
+ :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption`
+ interface.
+
+ :return bytes: Serialized key.
+
+.. class:: Ed25519PublicKey
+
+ .. versionadded:: 2.6
+
+ .. classmethod:: from_public_bytes(data)
+
+ :param bytes data: 32 byte public key.
+
+ :returns: :class:`Ed25519PublicKey`
+
+ .. doctest::
+
+ >>> from cryptography.hazmat.primitives import serialization
+ >>> from cryptography.hazmat.primitives.asymmetric import ed25519
+ >>> private_key = ed25519.Ed25519PrivateKey.generate()
+ >>> public_key = private_key.public_key()
+ >>> public_bytes = public_key.public_bytes(
+ ... encoding=serialization.Encoding.Raw,
+ ... format=serialization.PublicFormat.Raw
+ ... )
+ >>> loaded_public_key = ed25519.Ed25519PublicKey.from_public_bytes(public_bytes)
+
+ .. method:: public_bytes(encoding, format)
+
+ Allows serialization of the key to bytes. Encoding (
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`,
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`, or
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`) and
+ format (
+ :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`
+ or
+ :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
+ ) are chosen to define the exact serialization.
+
+ :param encoding: A value from the
+ :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
+
+ :param format: A value from the
+ :class:`~cryptography.hazmat.primitives.serialization.PublicFormat`
+ enum. If the ``encoding`` is
+ :attr:`~cryptography.hazmat.primitives.serialization.Encoding.Raw`
+ then ``format`` must be
+ :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.Raw`
+ , otherwise it must be
+ :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`.
+
+ :returns bytes: The public key bytes.
+
+ .. 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
diff --git a/docs/hazmat/primitives/asymmetric/index.rst b/docs/hazmat/primitives/asymmetric/index.rst
index 1561c59f..5e5bdb9a 100644
--- a/docs/hazmat/primitives/asymmetric/index.rst
+++ b/docs/hazmat/primitives/asymmetric/index.rst
@@ -23,6 +23,7 @@ private key is able to decrypt it.
.. toctree::
:maxdepth: 1
+ ed25519
x25519
x448
ec
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 7e08f124..c9b4777e 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -53,6 +53,7 @@ hostname
idna
indistinguishability
initialisms
+interoperability
interoperable
introspectability
invariants
diff --git a/src/_cffi_src/openssl/cryptography.py b/src/_cffi_src/openssl/cryptography.py
index 23ce87cf..4124dcb8 100644
--- a/src/_cffi_src/openssl/cryptography.py
+++ b/src/_cffi_src/openssl/cryptography.py
@@ -62,6 +62,8 @@ INCLUDES = """
(OPENSSL_VERSION_NUMBER < 0x101000af || CRYPTOGRAPHY_IS_LIBRESSL)
#define CRYPTOGRAPHY_OPENSSL_LESS_THAN_111 \
(OPENSSL_VERSION_NUMBER < 0x10101000 || CRYPTOGRAPHY_IS_LIBRESSL)
+#define CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B \
+ (OPENSSL_VERSION_NUMBER < 0x10101020 || CRYPTOGRAPHY_IS_LIBRESSL)
"""
TYPES = """
@@ -72,6 +74,7 @@ static const int CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER;
static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_102I;
static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_102;
static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111;
+static const int CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B;
static const int CRYPTOGRAPHY_IS_LIBRESSL;
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index e2a5fd78..61f597a3 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -37,6 +37,9 @@ from cryptography.hazmat.backends.openssl.dsa import (
from cryptography.hazmat.backends.openssl.ec import (
_EllipticCurvePrivateKey, _EllipticCurvePublicKey
)
+from cryptography.hazmat.backends.openssl.ed25519 import (
+ _ED25519_KEY_SIZE, _Ed25519PrivateKey, _Ed25519PublicKey
+)
from cryptography.hazmat.backends.openssl.encode_asn1 import (
_CRL_ENTRY_EXTENSION_ENCODE_HANDLERS,
_CRL_EXTENSION_ENCODE_HANDLERS, _EXTENSION_ENCODE_HANDLERS,
@@ -511,6 +514,9 @@ class Backend(object):
self.openssl_assert(dh_cdata != self._ffi.NULL)
dh_cdata = self._ffi.gc(dh_cdata, self._lib.DH_free)
return _DHPrivateKey(self, dh_cdata, evp_pkey)
+ elif key_type == getattr(self._lib, "EVP_PKEY_ED25519", None):
+ # EVP_PKEY_ED25519 is not present in OpenSSL < 1.1.1
+ return _Ed25519PrivateKey(self, evp_pkey)
elif key_type == getattr(self._lib, "EVP_PKEY_X448", None):
# EVP_PKEY_X448 is not present in OpenSSL < 1.1.1
return _X448PrivateKey(self, evp_pkey)
@@ -548,6 +554,9 @@ class Backend(object):
self.openssl_assert(dh_cdata != self._ffi.NULL)
dh_cdata = self._ffi.gc(dh_cdata, self._lib.DH_free)
return _DHPublicKey(self, dh_cdata, evp_pkey)
+ elif key_type == getattr(self._lib, "EVP_PKEY_ED25519", None):
+ # EVP_PKEY_ED25519 is not present in OpenSSL < 1.1.1
+ return _Ed25519PublicKey(self, evp_pkey)
elif key_type == getattr(self._lib, "EVP_PKEY_X448", None):
# EVP_PKEY_X448 is not present in OpenSSL < 1.1.1
return _X448PublicKey(self, evp_pkey)
@@ -2188,6 +2197,41 @@ class Backend(object):
def x448_supported(self):
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111
+ def ed25519_supported(self):
+ return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B
+
+ def ed25519_load_public_bytes(self, data):
+ utils._check_bytes("data", data)
+
+ if len(data) != _ED25519_KEY_SIZE:
+ raise ValueError("An Ed25519 public key is 32 bytes long")
+
+ evp_pkey = self._lib.EVP_PKEY_new_raw_public_key(
+ self._lib.NID_ED25519, self._ffi.NULL, data, len(data)
+ )
+ 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):
+ if len(data) != _ED25519_KEY_SIZE:
+ raise ValueError("An Ed25519 private key is 32 bytes long")
+
+ utils._check_byteslike("data", data)
+ data_ptr = self._ffi.from_buffer(data)
+ evp_pkey = self._lib.EVP_PKEY_new_raw_private_key(
+ self._lib.NID_ED25519, self._ffi.NULL, data_ptr, len(data)
+ )
+ self.openssl_assert(evp_pkey != self._ffi.NULL)
+ evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free)
+
+ return _Ed25519PrivateKey(self, evp_pkey)
+
+ def ed25519_generate_key(self):
+ evp_pkey = self._evp_pkey_keygen_gc(self._lib.NID_ED25519)
+ return _Ed25519PrivateKey(self, evp_pkey)
+
def derive_scrypt(self, key_material, salt, length, n, r, p):
buf = self._ffi.new("unsigned char[]", length)
key_material_ptr = self._ffi.from_buffer(key_material)
diff --git a/src/cryptography/hazmat/backends/openssl/ed25519.py b/src/cryptography/hazmat/backends/openssl/ed25519.py
new file mode 100644
index 00000000..15c1b1ec
--- /dev/null
+++ b/src/cryptography/hazmat/backends/openssl/ed25519.py
@@ -0,0 +1,154 @@
+# 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 import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import (
+ Ed25519PrivateKey, Ed25519PublicKey
+)
+
+_ED25519_KEY_SIZE = 32
+_ED25519_SIG_SIZE = 64
+
+
+@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, encoding, format):
+ if (
+ encoding is serialization.Encoding.Raw or
+ format is serialization.PublicFormat.Raw
+ ):
+ if (
+ encoding is not serialization.Encoding.Raw or
+ format is not serialization.PublicFormat.Raw
+ ):
+ raise ValueError(
+ "When using Raw both encoding and format must be Raw"
+ )
+
+ return self._raw_public_bytes()
+
+ if (
+ encoding in serialization._PEM_DER and
+ format is not serialization.PublicFormat.SubjectPublicKeyInfo
+ ):
+ raise ValueError(
+ "format must be SubjectPublicKeyInfo when encoding is PEM or "
+ "DER"
+ )
+
+ return self._backend._public_key_bytes(
+ encoding, format, self, self._evp_pkey, None
+ )
+
+ def _raw_public_bytes(self):
+ buf = self._backend._ffi.new("unsigned char []", _ED25519_KEY_SIZE)
+ buflen = self._backend._ffi.new("size_t *", _ED25519_KEY_SIZE)
+ res = self._backend._lib.EVP_PKEY_get_raw_public_key(
+ self._evp_pkey, buf, buflen
+ )
+ self._backend.openssl_assert(res == 1)
+ self._backend.openssl_assert(buflen[0] == _ED25519_KEY_SIZE)
+ return self._backend._ffi.buffer(buf, _ED25519_KEY_SIZE)[:]
+
+ 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):
+ buf = self._backend._ffi.new("unsigned char []", _ED25519_KEY_SIZE)
+ buflen = self._backend._ffi.new("size_t *", _ED25519_KEY_SIZE)
+ res = self._backend._lib.EVP_PKEY_get_raw_public_key(
+ self._evp_pkey, buf, buflen
+ )
+ self._backend.openssl_assert(res == 1)
+ self._backend.openssl_assert(buflen[0] == _ED25519_KEY_SIZE)
+ public_bytes = self._backend._ffi.buffer(buf)[:]
+ return self._backend.ed25519_load_public_bytes(public_bytes)
+
+ 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[]", _ED25519_SIG_SIZE)
+ 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] == _ED25519_SIG_SIZE)
+ return self._backend._ffi.buffer(buf, buflen[0])[:]
+
+ def private_bytes(self, encoding, format, encryption_algorithm):
+ if (
+ encoding is serialization.Encoding.Raw or
+ format is serialization.PublicFormat.Raw
+ ):
+ if (
+ format is not serialization.PrivateFormat.Raw or
+ encoding is not serialization.Encoding.Raw or not
+ isinstance(encryption_algorithm, serialization.NoEncryption)
+ ):
+ raise ValueError(
+ "When using Raw both encoding and format must be Raw "
+ "and encryption_algorithm must be NoEncryption"
+ )
+
+ return self._raw_private_bytes()
+
+ if (
+ encoding in serialization._PEM_DER and
+ format is not serialization.PrivateFormat.PKCS8
+ ):
+ raise ValueError(
+ "format must be PKCS8 when encoding is PEM or DER"
+ )
+
+ return self._backend._private_key_bytes(
+ encoding, format, encryption_algorithm, self._evp_pkey, None
+ )
+
+ def _raw_private_bytes(self):
+ buf = self._backend._ffi.new("unsigned char []", _ED25519_KEY_SIZE)
+ buflen = self._backend._ffi.new("size_t *", _ED25519_KEY_SIZE)
+ res = self._backend._lib.EVP_PKEY_get_raw_private_key(
+ self._evp_pkey, buf, buflen
+ )
+ self._backend.openssl_assert(res == 1)
+ self._backend.openssl_assert(buflen[0] == _ED25519_KEY_SIZE)
+ return self._backend._ffi.buffer(buf, _ED25519_KEY_SIZE)[:]
diff --git a/src/cryptography/hazmat/primitives/asymmetric/ed25519.py b/src/cryptography/hazmat/primitives/asymmetric/ed25519.py
new file mode 100644
index 00000000..96be9c58
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/asymmetric/ed25519.py
@@ -0,0 +1,80 @@
+# 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, encoding, format):
+ """
+ The serialized bytes of the public key.
+ """
+
+ @abc.abstractmethod
+ def verify(self, signature, data):
+ """
+ Verify the signature.
+ """
+
+
+@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
+ 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_private_bytes(data)
+
+ @abc.abstractmethod
+ def public_key(self):
+ """
+ The Ed25519PublicKey derived from the private key.
+ """
+
+ @abc.abstractmethod
+ def private_bytes(self, encoding, format, encryption_algorithm):
+ """
+ The serialized bytes of the private key.
+ """
+
+ @abc.abstractmethod
+ def sign(self, data):
+ """
+ Signs the data.
+ """
diff --git a/tests/hazmat/primitives/test_ed25519.py b/tests/hazmat/primitives/test_ed25519.py
new file mode 100644
index 00000000..8a2d3b07
--- /dev/null
+++ b/tests/hazmat/primitives/test_ed25519.py
@@ -0,0 +1,229 @@
+# 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 binascii
+import os
+
+import pytest
+
+from cryptography.exceptions import InvalidSignature, _Reasons
+from cryptography.hazmat.backends.interfaces import DHBackend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import (
+ Ed25519PrivateKey, Ed25519PublicKey
+)
+
+from ...utils import (
+ load_vectors_from_file, raises_unsupported_algorithm
+)
+
+
+def load_ed25519_vectors(vector_data):
+ """
+ djb's ed25519 vectors are structured as a colon delimited array:
+ 0: secret key (32 bytes) + public key (32 bytes)
+ 1: public key (32 bytes)
+ 2: message (0+ bytes)
+ 3: signature + message (64+ bytes)
+ """
+ data = []
+ for line in vector_data:
+ secret_key, public_key, message, signature, _ = line.split(':')
+ secret_key = secret_key[0:64]
+ signature = signature[0:128]
+ data.append({
+ "secret_key": secret_key,
+ "public_key": public_key,
+ "message": message,
+ "signature": signature
+ })
+ return data
+
+
+@pytest.mark.supported(
+ only_if=lambda backend: not backend.ed25519_supported(),
+ skip_message="Requires OpenSSL without Ed25519 support"
+)
+@pytest.mark.requires_backend_interface(interface=DHBackend)
+def test_ed25519_unsupported(backend):
+ with raises_unsupported_algorithm(
+ _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
+ ):
+ Ed25519PublicKey.from_public_bytes(b"0" * 32)
+
+ with raises_unsupported_algorithm(
+ _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
+ ):
+ Ed25519PrivateKey.from_private_bytes(b"0" * 32)
+
+ with raises_unsupported_algorithm(
+ _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
+ ):
+ Ed25519PrivateKey.generate()
+
+
+@pytest.mark.supported(
+ only_if=lambda backend: backend.ed25519_supported(),
+ skip_message="Requires OpenSSL with Ed25519 support"
+)
+@pytest.mark.requires_backend_interface(interface=DHBackend)
+class TestEd25519Signing(object):
+ @pytest.mark.parametrize(
+ "vector",
+ load_vectors_from_file(
+ os.path.join("asymmetric", "Ed25519", "sign.input"),
+ load_ed25519_vectors
+ )
+ )
+ def test_sign_verify_input(self, vector, backend):
+ sk = binascii.unhexlify(vector["secret_key"])
+ pk = binascii.unhexlify(vector["public_key"])
+ message = binascii.unhexlify(vector["message"])
+ signature = binascii.unhexlify(vector["signature"])
+ private_key = Ed25519PrivateKey.from_private_bytes(sk)
+ computed_sig = private_key.sign(message)
+ assert computed_sig == signature
+ public_key = private_key.public_key()
+ assert public_key.public_bytes(
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
+ ) == pk
+ public_key.verify(signature, message)
+
+ def test_invalid_signature(self, backend):
+ key = Ed25519PrivateKey.generate()
+ signature = key.sign(b"test data")
+ with pytest.raises(InvalidSignature):
+ key.public_key().verify(signature, b"wrong data")
+
+ with pytest.raises(InvalidSignature):
+ key.public_key().verify(b"0" * 64, b"test data")
+
+ def test_generate(self, backend):
+ key = Ed25519PrivateKey.generate()
+ assert key
+ assert key.public_key()
+
+ def test_load_public_bytes(self, backend):
+ public_key = Ed25519PrivateKey.generate().public_key()
+ public_bytes = public_key.public_bytes(
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
+ )
+ public_key2 = Ed25519PublicKey.from_public_bytes(public_bytes)
+ assert public_bytes == public_key2.public_bytes(
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
+ )
+
+ def test_invalid_type_public_bytes(self, backend):
+ with pytest.raises(TypeError):
+ Ed25519PublicKey.from_public_bytes(object())
+
+ def test_invalid_type_private_bytes(self, backend):
+ with pytest.raises(TypeError):
+ Ed25519PrivateKey.from_private_bytes(object())
+
+ def test_invalid_length_from_public_bytes(self, backend):
+ with pytest.raises(ValueError):
+ Ed25519PublicKey.from_public_bytes(b"a" * 31)
+ with pytest.raises(ValueError):
+ Ed25519PublicKey.from_public_bytes(b"a" * 33)
+
+ def test_invalid_length_from_private_bytes(self, backend):
+ with pytest.raises(ValueError):
+ Ed25519PrivateKey.from_private_bytes(b"a" * 31)
+ with pytest.raises(ValueError):
+ Ed25519PrivateKey.from_private_bytes(b"a" * 33)
+
+ def test_invalid_private_bytes(self, backend):
+ key = Ed25519PrivateKey.generate()
+ with pytest.raises(ValueError):
+ key.private_bytes(
+ serialization.Encoding.Raw,
+ serialization.PrivateFormat.Raw,
+ None
+ )
+
+ with pytest.raises(ValueError):
+ key.private_bytes(
+ serialization.Encoding.Raw,
+ serialization.PrivateFormat.PKCS8,
+ None
+ )
+
+ with pytest.raises(ValueError):
+ key.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.Raw,
+ serialization.NoEncryption()
+ )
+
+ def test_invalid_public_bytes(self, backend):
+ key = Ed25519PrivateKey.generate().public_key()
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.Raw,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.PKCS1
+ )
+
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.Raw
+ )
+
+ @pytest.mark.parametrize(
+ ("encoding", "fmt", "encryption", "passwd", "load_func"),
+ [
+ (
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.BestAvailableEncryption(b"password"),
+ b"password",
+ serialization.load_pem_private_key
+ ),
+ (
+ serialization.Encoding.DER,
+ serialization.PrivateFormat.PKCS8,
+ serialization.BestAvailableEncryption(b"password"),
+ b"password",
+ serialization.load_der_private_key
+ ),
+ (
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ None,
+ serialization.load_pem_private_key
+ ),
+ (
+ serialization.Encoding.DER,
+ serialization.PrivateFormat.PKCS8,
+ serialization.NoEncryption(),
+ None,
+ serialization.load_der_private_key
+ ),
+ ]
+ )
+ def test_round_trip_private_serialization(self, encoding, fmt, encryption,
+ passwd, load_func, backend):
+ key = Ed25519PrivateKey.generate()
+ serialized = key.private_bytes(encoding, fmt, encryption)
+ loaded_key = load_func(serialized, passwd, backend)
+ assert isinstance(loaded_key, Ed25519PrivateKey)
+
+ def test_buffer_protocol(self, backend):
+ private_bytes = os.urandom(32)
+ key = Ed25519PrivateKey.from_private_bytes(bytearray(private_bytes))
+ assert key.private_bytes(
+ serialization.Encoding.Raw,
+ serialization.PrivateFormat.Raw,
+ serialization.NoEncryption()
+ ) == private_bytes
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
index 5dd72489..52074bf9 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -1285,6 +1285,70 @@ class TestKeySerializationEncryptionTypes(object):
@pytest.mark.supported(
+ only_if=lambda backend: backend.ed25519_supported(),
+ skip_message="Requires OpenSSL with Ed25519 support"
+)
+class TestEd25519Serialization(object):
+ def test_load_der_private_key(self, backend):
+ data = load_vectors_from_file(
+ os.path.join("asymmetric", "Ed25519", "ed25519-pkcs8-enc.der"),
+ lambda derfile: derfile.read(),
+ mode="rb"
+ )
+ unencrypted = load_vectors_from_file(
+ os.path.join("asymmetric", "Ed25519", "ed25519-pkcs8.der"),
+ lambda derfile: derfile.read(),
+ mode="rb"
+ )
+ key = load_der_private_key(data, b"password", backend)
+ assert key.private_bytes(
+ Encoding.DER, PrivateFormat.PKCS8, NoEncryption()
+ ) == unencrypted
+
+ def test_load_pem_private_key(self, backend):
+ data = load_vectors_from_file(
+ os.path.join("asymmetric", "Ed25519", "ed25519-pkcs8-enc.pem"),
+ lambda pemfile: pemfile.read(),
+ mode="rb"
+ )
+ unencrypted = load_vectors_from_file(
+ os.path.join("asymmetric", "Ed25519", "ed25519-pkcs8.pem"),
+ lambda pemfile: pemfile.read(),
+ mode="rb"
+ )
+ key = load_pem_private_key(data, b"password", backend)
+ assert key.private_bytes(
+ Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
+ ) == unencrypted
+
+ @pytest.mark.parametrize(
+ ("key_path", "encoding", "loader"),
+ [
+ (
+ ["Ed25519", "ed25519-pub.pem"],
+ Encoding.PEM,
+ load_pem_public_key
+ ),
+ (
+ ["Ed25519", "ed25519-pub.der"],
+ Encoding.DER,
+ load_der_public_key
+ ),
+ ]
+ )
+ def test_load_public_key(self, key_path, encoding, loader, backend):
+ data = load_vectors_from_file(
+ os.path.join("asymmetric", *key_path),
+ lambda pemfile: pemfile.read(),
+ mode="rb"
+ )
+ public_key = loader(data, backend)
+ assert public_key.public_bytes(
+ encoding, PublicFormat.SubjectPublicKeyInfo
+ ) == data
+
+
+@pytest.mark.supported(
only_if=lambda backend: backend.x448_supported(),
skip_message="Requires OpenSSL with X448 support"
)
diff --git a/tests/wycheproof/test_eddsa.py b/tests/wycheproof/test_eddsa.py
new file mode 100644
index 00000000..40cb49a3
--- /dev/null
+++ b/tests/wycheproof/test_eddsa.py
@@ -0,0 +1,45 @@
+# 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 binascii
+
+import pytest
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.backends.interfaces import DHBackend
+from cryptography.hazmat.primitives.asymmetric.ed25519 import (
+ Ed25519PublicKey
+)
+
+
+@pytest.mark.supported(
+ only_if=lambda backend: backend.ed25519_supported(),
+ skip_message="Requires OpenSSL with Ed25519 support"
+)
+@pytest.mark.requires_backend_interface(interface=DHBackend)
+@pytest.mark.wycheproof_tests(
+ "eddsa_test.json",
+)
+def test_ed25519_signature(backend, wycheproof):
+ # We want to fail if/when wycheproof adds more edwards curve tests
+ # so we can add them as well.
+ assert wycheproof.testgroup["key"]["curve"] == "edwards25519"
+
+ key = Ed25519PublicKey.from_public_bytes(
+ binascii.unhexlify(wycheproof.testgroup["key"]["pk"])
+ )
+
+ if wycheproof.valid or wycheproof.acceptable:
+ key.verify(
+ binascii.unhexlify(wycheproof.testcase["sig"]),
+ binascii.unhexlify(wycheproof.testcase["msg"]),
+ )
+ else:
+ with pytest.raises(InvalidSignature):
+ key.verify(
+ binascii.unhexlify(wycheproof.testcase["sig"]),
+ binascii.unhexlify(wycheproof.testcase["msg"]),
+ )