From cd05e2ac5b8336dac78a0dd73ec9a7baa06208c6 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Wed, 27 Feb 2019 12:33:07 +0800 Subject: ed25519 support (#4114) * ed25519 support * review feedback --- CHANGELOG.rst | 2 + docs/hazmat/primitives/asymmetric/ed25519.rst | 159 ++++++++++++++ docs/hazmat/primitives/asymmetric/index.rst | 1 + docs/spelling_wordlist.txt | 1 + src/_cffi_src/openssl/cryptography.py | 3 + .../hazmat/backends/openssl/backend.py | 44 ++++ .../hazmat/backends/openssl/ed25519.py | 154 ++++++++++++++ .../hazmat/primitives/asymmetric/ed25519.py | 80 +++++++ tests/hazmat/primitives/test_ed25519.py | 229 +++++++++++++++++++++ tests/hazmat/primitives/test_serialization.py | 64 ++++++ tests/wycheproof/test_eddsa.py | 45 ++++ 11 files changed, 782 insertions(+) create mode 100644 docs/hazmat/primitives/asymmetric/ed25519.rst create mode 100644 src/cryptography/hazmat/backends/openssl/ed25519.py create mode 100644 src/cryptography/hazmat/primitives/asymmetric/ed25519.py create mode 100644 tests/hazmat/primitives/test_ed25519.py create mode 100644 tests/wycheproof/test_eddsa.py 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 @@ -1284,6 +1284,70 @@ class TestKeySerializationEncryptionTypes(object): BestAvailableEncryption(b"") +@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"]), + ) -- cgit v1.2.3