From 052feba28025625c5850719cd88af695332044b1 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 21 Apr 2023 11:30:32 -0600 Subject: [PATCH] Convert hashes to Rust --- .../hazmat/backends/openssl/backend.py | 6 - .../hazmat/backends/openssl/hashes.py | 88 ---------- .../bindings/_rust/openssl/__init__.pyi | 2 + .../hazmat/bindings/_rust/openssl/hashes.pyi | 17 ++ src/cryptography/hazmat/primitives/hashes.py | 76 ++++----- src/rust/src/backend/hashes.rs | 154 ++++++++++++++++++ src/rust/src/backend/mod.rs | 3 + tests/hazmat/primitives/utils.py | 1 - 8 files changed, 205 insertions(+), 142 deletions(-) delete mode 100644 src/cryptography/hazmat/backends/openssl/hashes.py create mode 100644 src/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi create mode 100644 src/rust/src/backend/hashes.rs diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 71215e6b4c24..6176d16d97fd 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -30,7 +30,6 @@ _EllipticCurvePrivateKey, _EllipticCurvePublicKey, ) -from cryptography.hazmat.backends.openssl.hashes import _HashContext from cryptography.hazmat.backends.openssl.hmac import _HMACContext from cryptography.hazmat.backends.openssl.poly1305 import ( _POLY1305_KEY_SIZE, @@ -274,11 +273,6 @@ def hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool: return self.hash_supported(algorithm) - def create_hash_ctx( - self, algorithm: hashes.HashAlgorithm - ) -> hashes.HashContext: - return _HashContext(self, algorithm) - def cipher_supported(self, cipher: CipherAlgorithm, mode: Mode) -> bool: if self._fips_enabled: # FIPS mode requires AES. TripleDES is disallowed/deprecated in diff --git a/src/cryptography/hazmat/backends/openssl/hashes.py b/src/cryptography/hazmat/backends/openssl/hashes.py deleted file mode 100644 index 370407aac58d..000000000000 --- a/src/cryptography/hazmat/backends/openssl/hashes.py +++ /dev/null @@ -1,88 +0,0 @@ -# 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 - -import typing - -from cryptography.exceptions import UnsupportedAlgorithm, _Reasons -from cryptography.hazmat.primitives import hashes - -if typing.TYPE_CHECKING: - from cryptography.hazmat.backends.openssl.backend import Backend - - -class _HashContext(hashes.HashContext): - def __init__( - self, backend: Backend, algorithm: hashes.HashAlgorithm, ctx=None - ) -> None: - self._algorithm = algorithm - - self._backend = backend - - if ctx is None: - ctx = self._backend._lib.EVP_MD_CTX_new() - ctx = self._backend._ffi.gc( - ctx, self._backend._lib.EVP_MD_CTX_free - ) - evp_md = self._backend._evp_md_from_algorithm(algorithm) - if evp_md == self._backend._ffi.NULL: - raise UnsupportedAlgorithm( - "{} is not a supported hash on this backend.".format( - algorithm.name - ), - _Reasons.UNSUPPORTED_HASH, - ) - res = self._backend._lib.EVP_DigestInit_ex( - ctx, evp_md, self._backend._ffi.NULL - ) - self._backend.openssl_assert(res != 0) - - self._ctx = ctx - - @property - def algorithm(self) -> hashes.HashAlgorithm: - return self._algorithm - - def copy(self) -> _HashContext: - copied_ctx = self._backend._lib.EVP_MD_CTX_new() - copied_ctx = self._backend._ffi.gc( - copied_ctx, self._backend._lib.EVP_MD_CTX_free - ) - res = self._backend._lib.EVP_MD_CTX_copy_ex(copied_ctx, self._ctx) - self._backend.openssl_assert(res != 0) - return _HashContext(self._backend, self.algorithm, ctx=copied_ctx) - - def update(self, data: bytes) -> None: - data_ptr = self._backend._ffi.from_buffer(data) - res = self._backend._lib.EVP_DigestUpdate( - self._ctx, data_ptr, len(data) - ) - self._backend.openssl_assert(res != 0) - - def finalize(self) -> bytes: - if isinstance(self.algorithm, hashes.ExtendableOutputFunction): - # extendable output functions use a different finalize - return self._finalize_xof() - else: - buf = self._backend._ffi.new( - "unsigned char[]", self._backend._lib.EVP_MAX_MD_SIZE - ) - outlen = self._backend._ffi.new("unsigned int *") - res = self._backend._lib.EVP_DigestFinal_ex(self._ctx, buf, outlen) - self._backend.openssl_assert(res != 0) - self._backend.openssl_assert( - outlen[0] == self.algorithm.digest_size - ) - return self._backend._ffi.buffer(buf)[: outlen[0]] - - def _finalize_xof(self) -> bytes: - buf = self._backend._ffi.new( - "unsigned char[]", self.algorithm.digest_size - ) - res = self._backend._lib.EVP_DigestFinalXOF( - self._ctx, buf, self.algorithm.digest_size - ) - self._backend.openssl_assert(res != 0) - return self._backend._ffi.buffer(buf)[: self.algorithm.digest_size] diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi index aceb859c63c7..07fa9d7b9320 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi @@ -7,6 +7,7 @@ import typing from cryptography.hazmat.bindings._rust.openssl import ( ed448, ed25519, + hashes, x448, x25519, ) @@ -14,6 +15,7 @@ from cryptography.hazmat.bindings._rust.openssl import ( __all__ = [ "openssl_version", "raise_openssl_error", + "hashes", "ed448", "ed25519", "x448", diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi new file mode 100644 index 000000000000..ca5f42a00615 --- /dev/null +++ b/src/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi @@ -0,0 +1,17 @@ +# 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. + +import typing + +from cryptography.hazmat.primitives import hashes + +class Hash(hashes.HashContext): + def __init__( + self, algorithm: hashes.HashAlgorithm, backend: typing.Any = None + ) -> None: ... + @property + def algorithm(self) -> hashes.HashAlgorithm: ... + def update(self, data: bytes) -> None: ... + def finalize(self) -> bytes: ... + def copy(self) -> Hash: ... diff --git a/src/cryptography/hazmat/primitives/hashes.py b/src/cryptography/hazmat/primitives/hashes.py index c4b7d1060ada..b6a7ff140e68 100644 --- a/src/cryptography/hazmat/primitives/hashes.py +++ b/src/cryptography/hazmat/primitives/hashes.py @@ -7,8 +7,31 @@ import abc import typing -from cryptography import utils -from cryptography.exceptions import AlreadyFinalized +from cryptography.hazmat.bindings._rust import openssl as rust_openssl + +__all__ = [ + "HashAlgorithm", + "HashContext", + "Hash", + "ExtendableOutputFunction", + "SHA1", + "SHA512_224", + "SHA512_256", + "SHA224", + "SHA256", + "SHA384", + "SHA512", + "SHA3_224", + "SHA3_256", + "SHA3_384", + "SHA3_512", + "SHAKE128", + "SHAKE256", + "MD5", + "BLAKE2b", + "BLAKE2s", + "SM3", +] class HashAlgorithm(metaclass=abc.ABCMeta): @@ -62,57 +85,16 @@ def copy(self) -> HashContext: """ +Hash = rust_openssl.hashes.Hash +HashContext.register(Hash) + + class ExtendableOutputFunction(metaclass=abc.ABCMeta): """ An interface for extendable output functions. """ -class Hash(HashContext): - _ctx: typing.Optional[HashContext] - - def __init__( - self, - algorithm: HashAlgorithm, - backend: typing.Any = None, - ctx: typing.Optional[HashContext] = None, - ) -> None: - if not isinstance(algorithm, HashAlgorithm): - raise TypeError("Expected instance of hashes.HashAlgorithm.") - self._algorithm = algorithm - - if ctx is None: - from cryptography.hazmat.backends.openssl.backend import ( - backend as ossl, - ) - - self._ctx = ossl.create_hash_ctx(self.algorithm) - else: - self._ctx = ctx - - @property - def algorithm(self) -> HashAlgorithm: - return self._algorithm - - def update(self, data: bytes) -> None: - if self._ctx is None: - raise AlreadyFinalized("Context was already finalized.") - utils._check_byteslike("data", data) - self._ctx.update(data) - - def copy(self) -> Hash: - if self._ctx is None: - raise AlreadyFinalized("Context was already finalized.") - return Hash(self.algorithm, ctx=self._ctx.copy()) - - def finalize(self) -> bytes: - if self._ctx is None: - raise AlreadyFinalized("Context was already finalized.") - digest = self._ctx.finalize() - self._ctx = None - return digest - - class SHA1(HashAlgorithm): name = "sha1" digest_size = 20 diff --git a/src/rust/src/backend/hashes.rs b/src/rust/src/backend/hashes.rs new file mode 100644 index 000000000000..807890365265 --- /dev/null +++ b/src/rust/src/backend/hashes.rs @@ -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. + +use crate::buf::CffiBuf; +use crate::error::{CryptographyError, CryptographyResult}; +use std::borrow::Cow; + +#[pyo3::prelude::pyclass(module = "cryptography.hazmat.bindings._rust.openssl.hashes")] +struct Hash { + #[pyo3(get)] + algorithm: pyo3::Py, + ctx: Option, +} + +impl Hash { + fn get_ctx(&self, py: pyo3::Python<'_>) -> CryptographyResult<&openssl::hash::Hasher> { + if let Some(ctx) = self.ctx.as_ref() { + return Ok(ctx); + }; + Err(CryptographyError::from(pyo3::PyErr::from_value( + py.import(pyo3::intern!(py, "cryptography.exceptions"))? + .call_method1( + pyo3::intern!(py, "AlreadyFinalized"), + ("Context was already finalized.",), + )?, + ))) + } + + fn get_mut_ctx( + &mut self, + py: pyo3::Python<'_>, + ) -> CryptographyResult<&mut openssl::hash::Hasher> { + if let Some(ctx) = self.ctx.as_mut() { + return Ok(ctx); + } + Err(CryptographyError::from(pyo3::PyErr::from_value( + py.import(pyo3::intern!(py, "cryptography.exceptions"))? + .call_method1( + pyo3::intern!(py, "AlreadyFinalized"), + ("Context was already finalized.",), + )?, + ))) + } +} + +#[pyo3::pymethods] +impl Hash { + #[new] + #[pyo3(signature = (algorithm, backend=None))] + fn new( + py: pyo3::Python<'_>, + algorithm: &pyo3::PyAny, + backend: Option<&pyo3::PyAny>, + ) -> CryptographyResult { + let _ = backend; + let hash_algorithm_class = py + .import(pyo3::intern!(py, "cryptography.hazmat.primitives.hashes"))? + .getattr(pyo3::intern!(py, "HashAlgorithm"))?; + if !algorithm.is_instance(hash_algorithm_class)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "Expected instance of hashes.HashAlgorithm.", + ), + )); + } + + let name = algorithm + .getattr(pyo3::intern!(py, "name"))? + .extract::<&str>()?; + let openssl_name = if name == "blake2b" || name == "blake2s" { + let digest_size = algorithm + .getattr(pyo3::intern!(py, "digest_size"))? + .extract::()?; + Cow::Owned(format!("{}{}", name, digest_size * 8)) + } else { + Cow::Borrowed(name) + }; + + let md = match openssl::hash::MessageDigest::from_name(&openssl_name) { + Some(md) => md, + None => { + let exceptions_module = py.import(pyo3::intern!(py, "cryptography.exceptions"))?; + let reason = exceptions_module + .getattr(pyo3::intern!(py, "_Reasons"))? + .getattr(pyo3::intern!(py, "UNSUPPORTED_HASH"))?; + return Err(CryptographyError::from(pyo3::PyErr::from_value( + exceptions_module.call_method1( + pyo3::intern!(py, "UnsupportedAlgorithm"), + ( + format!("{} is not a supported hash on this backend", name), + reason, + ), + )?, + ))); + } + }; + let ctx = openssl::hash::Hasher::new(md)?; + + Ok(Hash { + algorithm: algorithm.into(), + ctx: Some(ctx), + }) + } + + fn update(&mut self, py: pyo3::Python<'_>, data: CffiBuf<'_>) -> CryptographyResult<()> { + self.get_mut_ctx(py)?.update(data.as_bytes())?; + Ok(()) + } + + fn finalize<'p>( + &mut self, + py: pyo3::Python<'p>, + ) -> CryptographyResult<&'p pyo3::types::PyBytes> { + #[cfg(not(any(CRYPTOGRAPHY_IS_LIBRESSL, CRYPTOGRAPHY_IS_BORINGSSL)))] + { + let xof_class = py + .import(pyo3::intern!(py, "cryptography.hazmat.primitives.hashes"))? + .getattr(pyo3::intern!(py, "ExtendableOutputFunction"))?; + let algorithm = self.algorithm.clone_ref(py); + let algorithm = algorithm.as_ref(py); + if algorithm.is_instance(xof_class)? { + let ctx = self.get_mut_ctx(py)?; + let digest_size = algorithm + .getattr(pyo3::intern!(py, "digest_size"))? + .extract::()?; + let result = pyo3::types::PyBytes::new_with(py, digest_size, |b| { + ctx.finish_xof(b).unwrap(); + Ok(()) + })?; + self.ctx = None; + return Ok(result); + } + } + + let data = self.get_mut_ctx(py)?.finish()?; + self.ctx = None; + Ok(pyo3::types::PyBytes::new(py, &data)) + } + + fn copy(&self, py: pyo3::Python<'_>) -> CryptographyResult { + Ok(Hash { + algorithm: self.algorithm.clone_ref(py), + ctx: Some(self.get_ctx(py)?.clone()), + }) + } +} + +pub(crate) fn create_module(py: pyo3::Python<'_>) -> pyo3::PyResult<&pyo3::prelude::PyModule> { + let m = pyo3::prelude::PyModule::new(py, "hashes")?; + m.add_class::()?; + + Ok(m) +} diff --git a/src/rust/src/backend/mod.rs b/src/rust/src/backend/mod.rs index d2d8cd478548..c4095a03d5f9 100644 --- a/src/rust/src/backend/mod.rs +++ b/src/rust/src/backend/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod ed25519; #[cfg(all(not(CRYPTOGRAPHY_IS_LIBRESSL), not(CRYPTOGRAPHY_IS_BORINGSSL)))] pub(crate) mod ed448; +pub(crate) mod hashes; #[cfg(any(not(CRYPTOGRAPHY_IS_LIBRESSL), CRYPTOGRAPHY_LIBRESSL_370_OR_GREATER))] pub(crate) mod utils; #[cfg(any(not(CRYPTOGRAPHY_IS_LIBRESSL), CRYPTOGRAPHY_LIBRESSL_370_OR_GREATER))] @@ -24,5 +25,7 @@ pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult< #[cfg(all(not(CRYPTOGRAPHY_IS_LIBRESSL), not(CRYPTOGRAPHY_IS_BORINGSSL)))] module.add_submodule(x448::create_module(module.py())?)?; + module.add_submodule(hashes::create_module(module.py())?)?; + Ok(()) } diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 282744e80eaa..637c1eaa67f2 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -209,7 +209,6 @@ def base_hash_test(backend, algorithm, digest_size): assert m.algorithm.digest_size == digest_size m_copy = m.copy() assert m != m_copy - assert m._ctx != m_copy._ctx m.update(b"abc") copy = m.copy()