diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2018-11-23 10:44:37 +0800 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2018-11-22 20:44:37 -0600 |
commit | 6f88e01af8f5d6db7082d155f3faf88dfb48e864 (patch) | |
tree | 42fb14caa9d24a6eca1ae9d07b69a4a502e5c200 | |
parent | 579dfcf48f013dddfd3447e6dc38cfdc0b17145c (diff) | |
download | cryptography-6f88e01af8f5d6db7082d155f3faf88dfb48e864.tar.gz cryptography-6f88e01af8f5d6db7082d155f3faf88dfb48e864.tar.bz2 cryptography-6f88e01af8f5d6db7082d155f3faf88dfb48e864.zip |
X448 support (#4580)
* x448 support
This work was originally authored by derwolfe
* update docs to have a more useful derived key length
* error if key is not a valid length in from_public_bytes
* one more
* switch to using evp_pkey_keygen_gc for x448 keygen
* review feedback
* switch to using evp_pkey_derive
* nit fix
-rw-r--r-- | docs/hazmat/primitives/asymmetric/index.rst | 1 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/x448.rst | 104 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 29 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/x448.py | 55 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/asymmetric/x448.py | 61 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_x448.py | 127 |
6 files changed, 377 insertions, 0 deletions
diff --git a/docs/hazmat/primitives/asymmetric/index.rst b/docs/hazmat/primitives/asymmetric/index.rst index 173faa9e..1561c59f 100644 --- a/docs/hazmat/primitives/asymmetric/index.rst +++ b/docs/hazmat/primitives/asymmetric/index.rst @@ -24,6 +24,7 @@ private key is able to decrypt it. :maxdepth: 1 x25519 + x448 ec rsa dh diff --git a/docs/hazmat/primitives/asymmetric/x448.rst b/docs/hazmat/primitives/asymmetric/x448.rst new file mode 100644 index 00000000..057b7b50 --- /dev/null +++ b/docs/hazmat/primitives/asymmetric/x448.rst @@ -0,0 +1,104 @@ +.. hazmat:: + +X448 key exchange +=================== + +.. currentmodule:: cryptography.hazmat.primitives.asymmetric.x448 + + +X448 is an elliptic curve `Diffie-Hellman key exchange`_ using `Curve448`_. +It allows two parties to jointly agree on a shared secret using an insecure +channel. + + +Exchange Algorithm +~~~~~~~~~~~~~~~~~~ + +For most applications the ``shared_key`` should be passed to a key +derivation function. This allows mixing of additional information into the +key, derivation of multiple keys, and destroys any structure that may be +present. + +.. doctest:: + + >>> from cryptography.hazmat.backends import default_backend + >>> from cryptography.hazmat.primitives import hashes + >>> from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey + >>> from cryptography.hazmat.primitives.kdf.hkdf import HKDF + >>> # Generate a private key for use in the exchange. + >>> private_key = X448PrivateKey.generate() + >>> # In a real handshake the peer_public_key will be received from the + >>> # other party. For this example we'll generate another private key and + >>> # get a public key from that. Note that in a DH handshake both peers + >>> # must agree on a common set of parameters. + >>> peer_public_key = X448PrivateKey.generate().public_key() + >>> shared_key = private_key.exchange(peer_public_key) + >>> # Perform key derivation. + >>> derived_key = HKDF( + ... algorithm=hashes.SHA256(), + ... length=32, + ... salt=None, + ... info=b'handshake data', + ... backend=default_backend() + ... ).derive(shared_key) + >>> # For the next handshake we MUST generate another private key. + >>> private_key_2 = X448PrivateKey.generate() + >>> peer_public_key_2 = X448PrivateKey.generate().public_key() + >>> shared_key_2 = private_key_2.exchange(peer_public_key_2) + >>> derived_key_2 = HKDF( + ... algorithm=hashes.SHA256(), + ... length=32, + ... salt=None, + ... info=b'handshake data', + ... backend=default_backend() + ... ).derive(shared_key_2) + +Key interfaces +~~~~~~~~~~~~~~ + +.. class:: X448PrivateKey + + .. versionadded:: 2.5 + + .. classmethod:: generate() + + Generate an X448 private key. + + :returns: :class:`X448PrivateKey` + + .. method:: public_key() + + :returns: :class:`X448PublicKey` + + .. method:: exchange(peer_public_key) + + :param X448PublicKey peer_public_key: The public key for the + peer. + + :returns bytes: A shared key. + +.. class:: X448PublicKey + + .. versionadded:: 2.5 + + .. classmethod:: from_public_bytes(data) + + :param bytes data: 56 byte public key. + + :returns: :class:`X448PublicKey` + + .. doctest:: + + >>> from cryptography.hazmat.primitives.asymmetric import x448 + >>> private_key = x448.X448PrivateKey.generate() + >>> public_key = private_key.public_key() + >>> public_bytes = public_key.public_bytes() + >>> loaded_public_key = x448.X448PublicKey.from_public_bytes(public_bytes) + + .. method:: public_bytes() + + :returns bytes: The raw bytes of the public key. + + +.. _`Diffie-Hellman key exchange`: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange +.. _`Curve448`: https://en.wikipedia.org/wiki/Curve448 diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index d00f6133..f0b09dac 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -55,6 +55,9 @@ from cryptography.hazmat.backends.openssl.rsa import ( from cryptography.hazmat.backends.openssl.x25519 import ( _X25519PrivateKey, _X25519PublicKey ) +from cryptography.hazmat.backends.openssl.x448 import ( + _X448PrivateKey, _X448PublicKey +) from cryptography.hazmat.backends.openssl.x509 import ( _Certificate, _CertificateRevocationList, _CertificateSigningRequest, _RevokedCertificate @@ -2080,6 +2083,32 @@ class Backend(object): def x25519_supported(self): return self._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER + def x448_load_public_bytes(self, data): + if len(data) != 56: + raise ValueError("An X448 public key is 56 bytes long") + + evp_pkey = self._lib.EVP_PKEY_new_raw_public_key( + self._lib.NID_X448, 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 _X448PublicKey(self, evp_pkey) + + def x448_load_private_bytes(self, data): + evp_pkey = self._lib.EVP_PKEY_new_raw_private_key( + self._lib.NID_X448, 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 _X448PrivateKey(self, evp_pkey) + + def x448_generate_key(self): + evp_pkey = self._evp_pkey_keygen_gc(self._lib.NID_X448) + return _X448PrivateKey(self, evp_pkey) + + def x448_supported(self): + return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111 + def derive_scrypt(self, key_material, salt, length, n, r, p): buf = self._ffi.new("unsigned char[]", length) res = self._lib.EVP_PBE_scrypt( diff --git a/src/cryptography/hazmat/backends/openssl/x448.py b/src/cryptography/hazmat/backends/openssl/x448.py new file mode 100644 index 00000000..a10aa821 --- /dev/null +++ b/src/cryptography/hazmat/backends/openssl/x448.py @@ -0,0 +1,55 @@ +# 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 utils +from cryptography.hazmat.backends.openssl.utils import _evp_pkey_derive +from cryptography.hazmat.primitives.asymmetric.x448 import ( + X448PrivateKey, X448PublicKey +) + +_X448_KEY_SIZE = 56 + + +@utils.register_interface(X448PublicKey) +class _X448PublicKey(object): + def __init__(self, backend, evp_pkey): + self._backend = backend + self._evp_pkey = evp_pkey + + def public_bytes(self): + buf = self._backend._ffi.new("unsigned char []", _X448_KEY_SIZE) + buflen = self._backend._ffi.new("size_t *", _X448_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] == _X448_KEY_SIZE) + return self._backend._ffi.buffer(buf, _X448_KEY_SIZE)[:] + + +@utils.register_interface(X448PrivateKey) +class _X448PrivateKey(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 []", _X448_KEY_SIZE) + buflen = self._backend._ffi.new("size_t *", _X448_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] == _X448_KEY_SIZE) + return self._backend.x448_load_public_bytes(buf) + + def exchange(self, peer_public_key): + if not isinstance(peer_public_key, X448PublicKey): + raise TypeError("peer_public_key must be X448PublicKey.") + + return _evp_pkey_derive( + self._backend, self._evp_pkey, peer_public_key + ) diff --git a/src/cryptography/hazmat/primitives/asymmetric/x448.py b/src/cryptography/hazmat/primitives/asymmetric/x448.py new file mode 100644 index 00000000..69bfa408 --- /dev/null +++ b/src/cryptography/hazmat/primitives/asymmetric/x448.py @@ -0,0 +1,61 @@ +# 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 X448PublicKey(object): + @classmethod + def from_public_bytes(cls, data): + from cryptography.hazmat.backends.openssl.backend import backend + if not backend.x448_supported(): + raise UnsupportedAlgorithm( + "X448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM + ) + + return backend.x448_load_public_bytes(data) + + @abc.abstractmethod + def public_bytes(self): + """ + The serialized bytes of the public key. + """ + + +@six.add_metaclass(abc.ABCMeta) +class X448PrivateKey(object): + @classmethod + def generate(cls): + from cryptography.hazmat.backends.openssl.backend import backend + if not backend.x448_supported(): + raise UnsupportedAlgorithm( + "X448 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM + ) + return backend.x448_generate_key() + + @classmethod + def _from_private_bytes(cls, data): + from cryptography.hazmat.backends.openssl.backend import backend + return backend.x448_load_private_bytes(data) + + @abc.abstractmethod + def public_key(self): + """ + The serialized bytes of the public key. + """ + + @abc.abstractmethod + def exchange(self, peer_public_key): + """ + Performs a key exchange operation using the provided peer's public key. + """ diff --git a/tests/hazmat/primitives/test_x448.py b/tests/hazmat/primitives/test_x448.py new file mode 100644 index 00000000..71b25341 --- /dev/null +++ b/tests/hazmat/primitives/test_x448.py @@ -0,0 +1,127 @@ +# 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 _Reasons +from cryptography.hazmat.backends.interfaces import DHBackend +from cryptography.hazmat.primitives.asymmetric.x448 import ( + X448PrivateKey, X448PublicKey +) + +from ...utils import ( + load_nist_vectors, load_vectors_from_file, raises_unsupported_algorithm +) + + +@pytest.mark.supported( + only_if=lambda backend: not backend.x448_supported(), + skip_message="Requires OpenSSL without X448 support" +) +@pytest.mark.requires_backend_interface(interface=DHBackend) +def test_x448_unsupported(backend): + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): + X448PublicKey.from_public_bytes(b"0" * 32) + + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): + X448PrivateKey.generate() + + +@pytest.mark.supported( + only_if=lambda backend: backend.x448_supported(), + skip_message="Requires OpenSSL with X448 support" +) +@pytest.mark.requires_backend_interface(interface=DHBackend) +class TestX448Exchange(object): + @pytest.mark.parametrize( + "vector", + load_vectors_from_file( + os.path.join("asymmetric", "X448", "rfc7748.txt"), + load_nist_vectors + ) + ) + def test_rfc7748(self, vector, backend): + private = binascii.unhexlify(vector["input_scalar"]) + public = binascii.unhexlify(vector["input_u"]) + shared_key = binascii.unhexlify(vector["output_u"]) + private_key = X448PrivateKey._from_private_bytes(private) + public_key = X448PublicKey.from_public_bytes(public) + computed_shared_key = private_key.exchange(public_key) + assert computed_shared_key == shared_key + + def test_rfc7748_1000_iteration(self, backend): + old_private = private = public = binascii.unhexlify( + b"05000000000000000000000000000000000000000000000000000000" + b"00000000000000000000000000000000000000000000000000000000" + ) + shared_key = binascii.unhexlify( + b"aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4" + b"af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38" + ) + private_key = X448PrivateKey._from_private_bytes(private) + public_key = X448PublicKey.from_public_bytes(public) + for _ in range(1000): + computed_shared_key = private_key.exchange(public_key) + private_key = X448PrivateKey._from_private_bytes( + computed_shared_key + ) + public_key = X448PublicKey.from_public_bytes(old_private) + old_private = computed_shared_key + + assert computed_shared_key == shared_key + + # These vectors are also from RFC 7748 + # https://tools.ietf.org/html/rfc7748#section-6.2 + @pytest.mark.parametrize( + ("private_bytes", "public_bytes"), + [ + ( + binascii.unhexlify( + b"9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28d" + b"d9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b" + ), + binascii.unhexlify( + b"9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c" + b"22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0" + ) + ), + ( + binascii.unhexlify( + b"1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d" + b"6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d" + ), + binascii.unhexlify( + b"3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b430" + b"27d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609" + ) + ) + ] + ) + def test_public_bytes(self, private_bytes, public_bytes, backend): + private_key = X448PrivateKey._from_private_bytes(private_bytes) + assert private_key.public_key().public_bytes() == public_bytes + public_key = X448PublicKey.from_public_bytes(public_bytes) + assert public_key.public_bytes() == public_bytes + + def test_generate(self, backend): + key = X448PrivateKey.generate() + assert key + assert key.public_key() + + def test_invalid_type_exchange(self, backend): + key = X448PrivateKey.generate() + with pytest.raises(TypeError): + key.exchange(object()) + + def test_invalid_length_from_public_bytes(self, backend): + with pytest.raises(ValueError): + X448PublicKey.from_public_bytes(b"a" * 55) + + with pytest.raises(ValueError): + X448PublicKey.from_public_bytes(b"a" * 57) |