From fe7787e4c7e1b3fe0024d9ee8da788259ad536bc Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 7 Jul 2023 18:28:25 +0200 Subject: [PATCH] Add support for ChaCha20 with LibreSSL --- src/_cffi_src/build_openssl.py | 1 + src/_cffi_src/openssl/chacha.py | 40 +++++++++ .../hazmat/backends/openssl/backend.py | 16 +++- .../hazmat/backends/openssl/ciphers.py | 89 ++++++++++++++++++- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/_cffi_src/openssl/chacha.py diff --git a/src/_cffi_src/build_openssl.py b/src/_cffi_src/build_openssl.py index 361473679ece..73bf02111090 100644 --- a/src/_cffi_src/build_openssl.py +++ b/src/_cffi_src/build_openssl.py @@ -26,6 +26,7 @@ "asn1", "bignum", "bio", + "chacha", "cmac", "crypto", "dh", diff --git a/src/_cffi_src/openssl/chacha.py b/src/_cffi_src/openssl/chacha.py new file mode 100644 index 000000000000..1c6806befd55 --- /dev/null +++ b/src/_cffi_src/openssl/chacha.py @@ -0,0 +1,40 @@ +# 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 annotations + +INCLUDES = """ +#if CRYPTOGRAPHY_IS_LIBRESSL +#include +#endif""" + +TYPES = """ +static const long Cryptography_HAS_CHACHA20_API; +""" + +FUNCTIONS = """ +void Cryptography_CRYPTO_chacha_20(uint8_t *, const uint8_t *, size_t, + const uint8_t[32], const uint8_t[8], + uint64_t); +""" + +CUSTOMIZATIONS = """ +#if CRYPTOGRAPHY_IS_LIBRESSL +static const long Cryptography_HAS_CHACHA20_API = 1; +#else +static const long Cryptography_HAS_CHACHA20_API = 0; +#endif + +#if CRYPTOGRAPHY_IS_LIBRESSL +void Cryptography_CRYPTO_chacha_20(uint8_t *out, const uint8_t *in, + size_t in_len, const uint8_t key[32], + const uint8_t nonce[8], uint64_t counter) { + CRYPTO_chacha_20(out, in, in_len, key, nonce, counter); +} +#else +void (*Cryptography_CRYPTO_chacha_20)(uint8_t *, const uint8_t *, size_t, + const uint8_t[32], const uint8_t[8], + uint64_t) = NULL; +#endif +""" diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index b4294224035a..395043f6f14f 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -12,7 +12,11 @@ from cryptography import utils, x509 from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.backends.openssl import aead -from cryptography.hazmat.backends.openssl.ciphers import _CipherContext +from cryptography.hazmat.backends.openssl.ciphers import ( + _CipherContext, + algorithms, + create_cipher_context, +) from cryptography.hazmat.backends.openssl.cmac import _CMACContext from cryptography.hazmat.backends.openssl.rsa import ( _RSAPrivateKey, @@ -226,6 +230,12 @@ def hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool: return self.hash_supported(algorithm) def cipher_supported(self, cipher: CipherAlgorithm, mode: Mode) -> bool: + # ChaCha20 is supported in LibreSSL by a different API than OpenSSL, + # so checking for the corresponding EVP_CIPHER is not useful + if (self._lib.CRYPTOGRAPHY_IS_LIBRESSL) and isinstance( + cipher, algorithms.ChaCha20 + ): + return True if self._fips_enabled: # FIPS mode requires AES. TripleDES is disallowed/deprecated in # FIPS 140-3. @@ -319,12 +329,12 @@ def _register_default_ciphers(self) -> None: def create_symmetric_encryption_ctx( self, cipher: CipherAlgorithm, mode: Mode ) -> _CipherContext: - return _CipherContext(self, cipher, mode, _CipherContext._ENCRYPT) + return create_cipher_context(self, cipher, mode, encrypt=True) def create_symmetric_decryption_ctx( self, cipher: CipherAlgorithm, mode: Mode ) -> _CipherContext: - return _CipherContext(self, cipher, mode, _CipherContext._DECRYPT) + return create_cipher_context(self, cipher, mode, encrypt=False) def pbkdf2_hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool: return self.hmac_supported(algorithm) diff --git a/src/cryptography/hazmat/backends/openssl/ciphers.py b/src/cryptography/hazmat/backends/openssl/ciphers.py index bc42adbd49a5..d7ca294dad2c 100644 --- a/src/cryptography/hazmat/backends/openssl/ciphers.py +++ b/src/cryptography/hazmat/backends/openssl/ciphers.py @@ -4,6 +4,7 @@ from __future__ import annotations +import abc import typing from cryptography.exceptions import InvalidTag, UnsupportedAlgorithm, _Reasons @@ -14,7 +15,93 @@ from cryptography.hazmat.backends.openssl.backend import Backend -class _CipherContext: +class _CipherContext(metaclass=abc.ABCMeta): + @abc.abstractmethod + def update(self, data: bytes) -> bytes: + """ + Processes the provided bytes through the cipher and returns the results + as bytes. + """ + + @abc.abstractmethod + def update_into(self, data: bytes, buf: bytes) -> int: + """ + Processes the provided bytes and writes the resulting data into the + provided buffer. Returns the number of bytes written. + """ + + @abc.abstractmethod + def finalize(self) -> bytes: + """ + Returns the results of processing the final block as bytes. + """ + + +def create_cipher_context( + backend: Backend, cipher, mode, encrypt: bool +) -> _CipherContext: + if ( + isinstance(cipher, algorithms.ChaCha20) + and backend._lib.Cryptography_HAS_CHACHA20_API + ): + return _CipherContextChaCha(backend, cipher) + else: + operation = ( + _CipherContextEVP._ENCRYPT + if encrypt + else _CipherContextEVP._DECRYPT + ) + return _CipherContextEVP(backend, cipher, mode, operation) + + +class _CipherContextChaCha(_CipherContext): + """ + Cipher context specific to ChaCha20 under LibreSSL + """ + + def __init__(self, backend: Backend, cipher) -> None: + assert isinstance(cipher, algorithms.ChaCha20) + assert backend._lib.Cryptography_HAS_CHACHA20_API + self._backend = backend + self._cipher = cipher + + # The ChaCha20 stream cipher. The key length is 256 bits, the IV is + # 128 bits long. The first 64 bits consists of a counter in + # little-endian order followed by a 64 bit nonce. + self._counter = int.from_bytes(cipher.nonce[:8], byteorder="little") + self._iv_nonce = cipher.nonce[8:] + + def update(self, data: bytes) -> bytes: + buf = bytearray(len(data)) + n = self.update_into(data, buf) + return bytes(buf[:n]) + + def update_into(self, data: bytes, buf: bytes) -> int: + total_data_len = len(data) + if len(buf) < total_data_len: + raise ValueError( + "buffer must be at least {} bytes for this " + "payload".format(len(data)) + ) + + baseoutbuf = self._backend._ffi.from_buffer(buf, require_writable=True) + baseinbuf = self._backend._ffi.from_buffer(data) + + self._backend._lib.Cryptography_CRYPTO_chacha_20( + baseoutbuf, + baseinbuf, + total_data_len, + self._backend._ffi.from_buffer(self._cipher.key), + self._backend._ffi.from_buffer(self._iv_nonce), + self._counter, + ) + return total_data_len + + def finalize(self) -> bytes: + return b"" + + +class _CipherContextEVP(_CipherContext): _ENCRYPT = 1 _DECRYPT = 0 _MAX_CHUNK_SIZE = 2**30 - 1