From b73ed5a6a3067c832413a6b4c987667a9d545153 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sun, 10 Mar 2019 10:12:00 +0800 Subject: [PATCH] poly1305 support (#4802) * poly1305 support * some more tests * have I mentioned how bad the spellchecker is? * doc improvements * EVP_PKEY_new_raw_private_key copies the key but that's not documented Let's assume that might change and be very defensive * review feedback * add a test that fails on a tag of the correct length but wrong value * docs improvements --- CHANGELOG.rst | 2 + docs/hazmat/primitives/mac/index.rst | 1 + docs/hazmat/primitives/mac/poly1305.rst | 87 ++++++++++++ docs/spelling_wordlist.txt | 2 + src/cryptography/exceptions.py | 1 + .../hazmat/backends/openssl/backend.py | 13 ++ .../hazmat/backends/openssl/poly1305.py | 60 +++++++++ .../hazmat/primitives/poly1305.py | 43 ++++++ tests/hazmat/primitives/test_poly1305.py | 125 ++++++++++++++++++ 9 files changed, 334 insertions(+) create mode 100644 docs/hazmat/primitives/mac/poly1305.rst create mode 100644 src/cryptography/hazmat/backends/openssl/poly1305.py create mode 100644 src/cryptography/hazmat/primitives/poly1305.py create mode 100644 tests/hazmat/primitives/test_poly1305.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 019d257741b7..525c48139ce0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ Changelog ``cryptography.hazmat.primitives.mac.MACContext`` interface. The ``CMAC`` and ``HMAC`` APIs have not changed, but they are no longer registered as ``MACContext`` instances. +* Add support for :class:`~cryptography.hazmat.primitives.poly1305.Poly1305` + when using OpenSSL 1.1.1 or newer. .. _v2-6-1: diff --git a/docs/hazmat/primitives/mac/index.rst b/docs/hazmat/primitives/mac/index.rst index f85eaa0e16c2..8bfe29e30bdf 100644 --- a/docs/hazmat/primitives/mac/index.rst +++ b/docs/hazmat/primitives/mac/index.rst @@ -14,5 +14,6 @@ HMAC?`_ cmac hmac + poly1305 .. _`Use cases for CMAC vs. HMAC?`: https://crypto.stackexchange.com/questions/15721/use-cases-for-cmac-vs-hmac diff --git a/docs/hazmat/primitives/mac/poly1305.rst b/docs/hazmat/primitives/mac/poly1305.rst new file mode 100644 index 000000000000..1d0753c6e831 --- /dev/null +++ b/docs/hazmat/primitives/mac/poly1305.rst @@ -0,0 +1,87 @@ +.. hazmat:: + +Poly1305 +======== + +.. currentmodule:: cryptography.hazmat.primitives.poly1305 + +.. testsetup:: + + key = b"\x01" * 32 + +Poly1305 is an authenticator that takes a 32-byte key and a message and +produces a 16-byte tag. This tag is used to authenticate the message. Each key +**must** only be used once. Using the same key to generate tags for multiple +messages allows an attacker to forge tags. Poly1305 is described in +:rfc:`7539`. + +.. class:: Poly1305(key) + + .. versionadded:: 2.7 + + .. warning:: + + Using the same key to generate tags for multiple messages allows an + attacker to forge tags. Always generate a new key per message you want + to authenticate. If you are using this as a MAC for + symmetric encryption please use + :class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` + instead. + + .. doctest:: + + >>> from cryptography.hazmat.primitives import poly1305 + >>> p = poly1305.Poly1305(key) + >>> p.update(b"message to authenticate") + >>> p.finalize() + b'T\xae\xff3\xbdW\xef\xd5r\x01\xe2n=\xb7\xd2h' + + To check that a given tag is correct use the :meth:`verify` method. + You will receive an exception if the tag is wrong: + + .. doctest:: + + >>> p = poly1305.Poly1305(key) + >>> p.update(b"message to authenticate") + >>> p.verify(b"an incorrect tag") + Traceback (most recent call last): + ... + cryptography.exceptions.InvalidSignature: Value did not match computed tag. + + :param key: Secret key as ``bytes``. + :type key: :term:`bytes-like` + :raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if + the version of OpenSSL ``cryptography`` is compiled against does not + support this algorithm. + + .. method:: update(data) + + :param data: The bytes to hash and authenticate. + :type data: :term:`bytes-like` + :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` + :raises TypeError: This exception is raised if ``data`` is not ``bytes``. + + .. method:: verify(tag) + + Finalize the current context and securely compare the MAC to + ``tag``. + + :param bytes tag: The bytes to compare against. + :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` + :raises cryptography.exceptions.InvalidSignature: If tag does not + match. + :raises TypeError: This exception is raised if ``tag`` is not + ``bytes``. + + .. method:: finalize() + + Finalize the current context and return the message authentication code + as bytes. + + After ``finalize`` has been called this object can no longer be used + and :meth:`update`, :meth:`verify`, and :meth:`finalize` + will raise an :class:`~cryptography.exceptions.AlreadyFinalized` + exception. + + :return bytes: The message authentication code as bytes. + :raises cryptography.exceptions.AlreadyFinalized: diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index c9b4777eb9ae..47d3730150ac 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,6 +1,7 @@ accessor affine Authenticator +authenticator backend Backends backends @@ -77,6 +78,7 @@ Parallelization personalization pickleable plaintext +Poly pre precompute preprocessor diff --git a/src/cryptography/exceptions.py b/src/cryptography/exceptions.py index 648cf9dfe6bc..1d52d7dcfc5e 100644 --- a/src/cryptography/exceptions.py +++ b/src/cryptography/exceptions.py @@ -19,6 +19,7 @@ class _Reasons(Enum): UNSUPPORTED_X509 = 8 UNSUPPORTED_EXCHANGE_ALGORITHM = 9 UNSUPPORTED_DIFFIE_HELLMAN = 10 + UNSUPPORTED_MAC = 11 class UnsupportedAlgorithm(Exception): diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index b040b80965b2..15eff8379eac 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.ocsp import ( _OCSPRequest, _OCSPResponse ) +from cryptography.hazmat.backends.openssl.poly1305 import ( + _POLY1305_KEY_SIZE, _Poly1305Context +) from cryptography.hazmat.backends.openssl.rsa import ( _RSAPrivateKey, _RSAPublicKey ) @@ -2401,6 +2404,16 @@ def load_key_and_certificates_from_pkcs12(self, data, password): return (key, cert, additional_certificates) + def poly1305_supported(self): + return self._lib.Cryptography_HAS_POLY1305 == 1 + + def create_poly1305_ctx(self, key): + utils._check_byteslike("key", key) + if len(key) != _POLY1305_KEY_SIZE: + raise ValueError("A poly1305 key is 32 bytes long") + + return _Poly1305Context(self, key) + class GetCipherByName(object): def __init__(self, fmt): diff --git a/src/cryptography/hazmat/backends/openssl/poly1305.py b/src/cryptography/hazmat/backends/openssl/poly1305.py new file mode 100644 index 000000000000..25448dd02597 --- /dev/null +++ b/src/cryptography/hazmat/backends/openssl/poly1305.py @@ -0,0 +1,60 @@ +# 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.exceptions import InvalidSignature +from cryptography.hazmat.primitives import constant_time + + +_POLY1305_TAG_SIZE = 16 +_POLY1305_KEY_SIZE = 32 + + +class _Poly1305Context(object): + def __init__(self, backend, key): + self._backend = backend + + key_ptr = self._backend._ffi.from_buffer(key) + # This function copies the key into OpenSSL-owned memory so we don't + # need to retain it ourselves + evp_pkey = self._backend._lib.EVP_PKEY_new_raw_private_key( + self._backend._lib.NID_poly1305, + self._backend._ffi.NULL, key_ptr, len(key) + ) + self._backend.openssl_assert(evp_pkey != self._backend._ffi.NULL) + self._evp_pkey = self._backend._ffi.gc( + evp_pkey, self._backend._lib.EVP_PKEY_free + ) + ctx = self._backend._lib.Cryptography_EVP_MD_CTX_new() + self._backend.openssl_assert(ctx != self._backend._ffi.NULL) + self._ctx = self._backend._ffi.gc( + ctx, self._backend._lib.Cryptography_EVP_MD_CTX_free + ) + res = self._backend._lib.EVP_DigestSignInit( + self._ctx, self._backend._ffi.NULL, self._backend._ffi.NULL, + self._backend._ffi.NULL, self._evp_pkey + ) + self._backend.openssl_assert(res == 1) + + def update(self, data): + data_ptr = self._backend._ffi.from_buffer(data) + res = self._backend._lib.EVP_DigestSignUpdate( + self._ctx, data_ptr, len(data) + ) + self._backend.openssl_assert(res != 0) + + def finalize(self): + buf = self._backend._ffi.new("unsigned char[]", _POLY1305_TAG_SIZE) + outlen = self._backend._ffi.new("size_t *") + res = self._backend._lib.EVP_DigestSignFinal(self._ctx, buf, outlen) + self._backend.openssl_assert(res != 0) + self._backend.openssl_assert(outlen[0] == _POLY1305_TAG_SIZE) + return self._backend._ffi.buffer(buf)[:outlen[0]] + + def verify(self, tag): + mac = self.finalize() + if not constant_time.bytes_eq(mac, tag): + raise InvalidSignature("Value did not match computed tag.") diff --git a/src/cryptography/hazmat/primitives/poly1305.py b/src/cryptography/hazmat/primitives/poly1305.py new file mode 100644 index 000000000000..02b6629dee53 --- /dev/null +++ b/src/cryptography/hazmat/primitives/poly1305.py @@ -0,0 +1,43 @@ +# 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.exceptions import ( + AlreadyFinalized, UnsupportedAlgorithm, _Reasons +) + + +class Poly1305(object): + def __init__(self, key): + from cryptography.hazmat.backends.openssl.backend import backend + if not backend.poly1305_supported(): + raise UnsupportedAlgorithm( + "poly1305 is not supported by this version of OpenSSL.", + _Reasons.UNSUPPORTED_MAC + ) + self._ctx = backend.create_poly1305_ctx(key) + + def update(self, data): + if self._ctx is None: + raise AlreadyFinalized("Context was already finalized.") + utils._check_byteslike("data", data) + self._ctx.update(data) + + def finalize(self): + if self._ctx is None: + raise AlreadyFinalized("Context was already finalized.") + mac = self._ctx.finalize() + self._ctx = None + return mac + + def verify(self, tag): + utils._check_bytes("tag", tag) + if self._ctx is None: + raise AlreadyFinalized("Context was already finalized.") + + ctx, self._ctx = self._ctx, None + ctx.verify(tag) diff --git a/tests/hazmat/primitives/test_poly1305.py b/tests/hazmat/primitives/test_poly1305.py new file mode 100644 index 000000000000..71495ff7e08e --- /dev/null +++ b/tests/hazmat/primitives/test_poly1305.py @@ -0,0 +1,125 @@ +# 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 ( + AlreadyFinalized, InvalidSignature, _Reasons +) +from cryptography.hazmat.primitives.poly1305 import Poly1305 + +from ...utils import ( + load_nist_vectors, load_vectors_from_file, raises_unsupported_algorithm +) + + +@pytest.mark.supported( + only_if=lambda backend: not backend.poly1305_supported(), + skip_message="Requires OpenSSL without poly1305 support" +) +def test_poly1305_unsupported(backend): + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_MAC): + Poly1305(b"0" * 32) + + +@pytest.mark.supported( + only_if=lambda backend: backend.poly1305_supported(), + skip_message="Requires OpenSSL with poly1305 support" +) +class TestPoly1305(object): + @pytest.mark.parametrize( + "vector", + load_vectors_from_file( + os.path.join("poly1305", "rfc7539.txt"), load_nist_vectors + ) + ) + def test_vectors(self, vector, backend): + key = binascii.unhexlify(vector["key"]) + msg = binascii.unhexlify(vector["msg"]) + tag = binascii.unhexlify(vector["tag"]) + poly = Poly1305(key) + poly.update(msg) + assert poly.finalize() == tag + + def test_key_with_no_additional_references(self, backend): + poly = Poly1305(os.urandom(32)) + assert len(poly.finalize()) == 16 + + def test_raises_after_finalize(self, backend): + poly = Poly1305(b"0" * 32) + poly.finalize() + + with pytest.raises(AlreadyFinalized): + poly.update(b"foo") + + with pytest.raises(AlreadyFinalized): + poly.finalize() + + def test_reject_unicode(self, backend): + poly = Poly1305(b"0" * 32) + with pytest.raises(TypeError): + poly.update(u'') + + def test_verify(self, backend): + poly = Poly1305(b"0" * 32) + poly.update(b"msg") + tag = poly.finalize() + + with pytest.raises(AlreadyFinalized): + poly.verify(b"") + + poly2 = Poly1305(b"0" * 32) + poly2.update(b"msg") + poly2.verify(tag) + + def test_invalid_verify(self, backend): + poly = Poly1305(b"0" * 32) + poly.update(b"msg") + with pytest.raises(InvalidSignature): + poly.verify(b"") + + p2 = Poly1305(b"0" * 32) + p2.update(b"msg") + with pytest.raises(InvalidSignature): + p2.verify(b"\x00" * 16) + + def test_verify_reject_unicode(self, backend): + poly = Poly1305(b"0" * 32) + with pytest.raises(TypeError): + poly.verify(u'') + + def test_invalid_key_type(self, backend): + with pytest.raises(TypeError): + Poly1305(object()) + + def test_invalid_key_length(self, backend): + with pytest.raises(ValueError): + Poly1305(b"0" * 31) + + with pytest.raises(ValueError): + Poly1305(b"0" * 33) + + def test_buffer_protocol(self, backend): + key = binascii.unhexlify( + b"1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cb" + b"c207075c0" + ) + msg = binascii.unhexlify( + b"2754776173206272696c6c69672c20616e642074686520736c69746" + b"87920746f7665730a446964206779726520616e642067696d626c65" + b"20696e2074686520776162653a0a416c6c206d696d7379207765726" + b"52074686520626f726f676f7665732c0a416e6420746865206d6f6d" + b"65207261746873206f757467726162652e" + ) + key = bytearray(key) + poly = Poly1305(key) + poly.update(bytearray(msg)) + assert poly.finalize() == binascii.unhexlify( + b"4541669a7eaaee61e708dc7cbcc5eb62" + )