Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
PYTHON:
- {VERSION: "3.12", NOXSESSION: "flake"}
- {VERSION: "3.12", NOXSESSION: "rust"}
- {VERSION: "3.12", NOXSESSION: "docs", OPENSSL: {TYPE: "openssl", VERSION: "3.2.3"}}
- {VERSION: "3.12", NOXSESSION: "docs", OPENSSL: {TYPE: "openssl", VERSION: "3.4.0"}}
- {VERSION: "3.13", NOXSESSION: "tests"}
- {VERSION: "3.14-dev", NOXSESSION: "tests"}
- {VERSION: "pypy-3.10", NOXSESSION: "tests-nocoverage"}
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Changelog
was raised).
* Added ``unsafe_skip_rsa_key_validation`` keyword-argument to
:func:`~cryptography.hazmat.primitives.serialization.load_ssh_private_key`.
* Added :class:`~cryptography.hazmat.primitives.hashes.XOFHash` to support
repeated :meth:`~cryptography.hazmat.primitives.hashes.XOFHash.squeeze`
operations on extendable output functions.

.. _v44-0-0:

Expand Down
149 changes: 114 additions & 35 deletions docs/hazmat/primitives/cryptographic-hashes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Message digests (Hashing)

:param algorithm: A
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
instance such as those described in
instance such as those described
:ref:`below <cryptographic-hash-algorithms>`.

:raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if the
Expand All @@ -44,14 +44,14 @@ Message digests (Hashing)
.. method:: update(data)

:param bytes data: The bytes to be hashed.
:raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize`.
:raises cryptography.exceptions.AlreadyFinalized: See :meth:`.finalize`.
:raises TypeError: This exception is raised if ``data`` is not ``bytes``.

.. method:: copy()

Copy this :class:`Hash` instance, usually so that you may call
:meth:`finalize` to get an intermediate digest value while we continue
to call :meth:`update` on the original instance.
:meth:`.finalize` to get an intermediate digest value while we continue
to call :meth:`.update` on the original instance.

:return: A new instance of :class:`Hash` that can be updated
and finalized independently of the original instance.
Expand All @@ -62,11 +62,70 @@ Message digests (Hashing)
Finalize the current context and return the message digest as bytes.

After ``finalize`` has been called this object can no longer be used
and :meth:`update`, :meth:`copy`, and :meth:`finalize` will raise an
and :meth:`.update`, :meth:`.copy`, and :meth:`.finalize` will raise an
:class:`~cryptography.exceptions.AlreadyFinalized` exception.

:return bytes: The message digest as bytes.

.. class:: XOFHash(algorithm)

An extendable output function (XOF) is a cryptographic hash function that
can produce an arbitrary amount of output for a given input. The output
can be obtained by repeatedly calling :meth:`.squeeze` with the desired
length.

.. doctest::

>>> import sys
>>> from cryptography.hazmat.primitives import hashes
>>> digest = hashes.XOFHash(hashes.SHAKE128(digest_size=sys.maxsize))
>>> digest.update(b"abc")
>>> digest.update(b"123")
>>> digest.squeeze(16)
b'\x18\xd6\xbd\xeb5u\x83[@\xfa%/\xdc\xca\x9f\x1b'
>>> digest.squeeze(16)
b'\xc2\xeb\x12\x05\xc3\xf9Bu\x88\xe0\xda\x80FvAV'

:param algorithm: A
:class:`~cryptography.hazmat.primitives.hashes.ExtendableOutputFunction`
instance such as those described
:ref:`below <extendable-output-functions>`. The ``digest_size``
passed is the maximum number of bytes that can be squeezed from the XOF
when using this class.

:raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if the
provided ``algorithm`` is unsupported.

.. method:: update(data)

:param bytes data: The bytes to be hashed.
:raises cryptography.exceptions.AlreadyFinalized: If already squeezed.
:raises TypeError: This exception is raised if ``data`` is not ``bytes``.

.. method:: copy()

Copy this :class:`XOFHash` instance, usually so that you may call
:meth:`.squeeze` to get an intermediate digest value while we continue
to call :meth:`.update` on the original instance.

:return: A new instance of :class:`XOFHash` that can be updated
and squeezed independently of the original instance. If
you copy an instance that has already been squeezed, the copy will
also be in a squeezed state.
:raises cryptography.exceptions.AlreadyFinalized: See :meth:`.squeeze`.

.. method:: squeeze(length)

:param int length: The number of bytes to squeeze.

After :meth:`.squeeze` has been called this object can no longer be updated
and :meth:`.update`, will raise an
:class:`~cryptography.exceptions.AlreadyFinalized` exception.

:return bytes: ``length`` bytes of output from the extendable output function (XOF).
:raises ValueError: If the maximum number of bytes that can be squeezed
has been exceeded.

Comment thread
reaperhulk marked this conversation as resolved.

.. _cryptographic-hash-algorithms:

Expand Down Expand Up @@ -176,36 +235,6 @@ than SHA-2 so at this time most users should choose SHA-2.
SHA3/512 is a cryptographic hash function from the SHA-3 family and is
standardized by NIST. It produces a 512-bit message digest.

.. class:: SHAKE128(digest_size)

.. versionadded:: 2.5

SHAKE128 is an extendable output function (XOF) based on the same core
permutations as SHA3. It allows the caller to obtain an arbitrarily long
digest length. Longer lengths, however, do not increase security or
collision resistance and lengths shorter than 128 bit (16 bytes) will
decrease it.

:param int digest_size: The length of output desired. Must be greater than
zero.

:raises ValueError: If the ``digest_size`` is invalid.

.. class:: SHAKE256(digest_size)

.. versionadded:: 2.5

SHAKE256 is an extendable output function (XOF) based on the same core
permutations as SHA3. It allows the caller to obtain an arbitrarily long
digest length. Longer lengths, however, do not increase security or
collision resistance and lengths shorter than 256 bit (32 bytes) will
decrease it.

:param int digest_size: The length of output desired. Must be greater than
zero.

:raises ValueError: If the ``digest_size`` is invalid.

SHA-1
~~~~~

Expand Down Expand Up @@ -250,6 +279,52 @@ SM3
`draft-sca-cfrg-sm3`_.) This hash should be used for compatibility
purposes where required and is not otherwise recommended for use.

.. _extendable-output-functions:

Extendable Output Functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. class:: SHAKE128(digest_size)

.. versionadded:: 2.5

SHAKE128 is an extendable output function (XOF) based on the same core
permutations as SHA3. It allows the caller to obtain an arbitrarily long
digest length. Longer lengths, however, do not increase security or
collision resistance and lengths shorter than 128 bit (16 bytes) will
decrease it.

This class can be used with :class:`Hash` or :class:`XOFHash`. When used
in :class:`Hash` :meth:`~cryptography.hazmat.primitives.hashes.Hash.finalize`
will return ``digest_size`` bytes. When used in :class:`XOFHash` this
defines the total number of bytes allowed to be squeezed.

:param int digest_size: The length of output desired. Must be greater than
zero.

:raises ValueError: If the ``digest_size`` is invalid.

.. class:: SHAKE256(digest_size)

.. versionadded:: 2.5

SHAKE256 is an extendable output function (XOF) based on the same core
permutations as SHA3. It allows the caller to obtain an arbitrarily long
digest length. Longer lengths, however, do not increase security or
collision resistance and lengths shorter than 256 bit (32 bytes) will
decrease it.

This class can be used with :class:`Hash` or :class:`XOFHash`. When used
in :class:`Hash` :meth:`~cryptography.hazmat.primitives.hashes.Hash.finalize`
will return ``digest_size`` bytes. When used in :class:`XOFHash` this
defines the total number of bytes allowed to be squeezed.

:param int digest_size: The length of output desired. Must be greater than
zero.

:raises ValueError: If the ``digest_size`` is invalid.



Interfaces
~~~~~~~~~~
Expand All @@ -269,6 +344,10 @@ Interfaces

The size of the resulting digest in bytes.

.. class:: ExtendableOutputFunction

An interface applied to hashes that act as extendable output functions (XOFs).
The currently supported XOFs are :class:`SHAKE128` and :class:`SHAKE256`.

.. class:: HashContext

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ CRYPTOGRAPHY_IS_BORINGSSL: bool
CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: bool
CRYPTOGRAPHY_OPENSSL_309_OR_GREATER: bool
CRYPTOGRAPHY_OPENSSL_320_OR_GREATER: bool
CRYPTOGRAPHY_OPENSSL_330_OR_GREATER: bool
CRYPTOGRAPHY_OPENSSL_350_OR_GREATER: bool

class Providers: ...
Expand Down
8 changes: 8 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ class Hash(hashes.HashContext):
def copy(self) -> Hash: ...

def hash_supported(algorithm: hashes.HashAlgorithm) -> bool: ...

class XOFHash:
def __init__(self, algorithm: hashes.ExtendableOutputFunction) -> None: ...
@property
def algorithm(self) -> hashes.ExtendableOutputFunction: ...
def update(self, data: bytes) -> None: ...
def squeeze(self, length: int) -> bytes: ...
def copy(self) -> XOFHash: ...
3 changes: 3 additions & 0 deletions src/cryptography/hazmat/primitives/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"Hash",
"HashAlgorithm",
"HashContext",
"XOFHash",
]


Expand Down Expand Up @@ -87,6 +88,8 @@ def copy(self) -> HashContext:
Hash = rust_openssl.hashes.Hash
HashContext.register(Hash)

XOFHash = rust_openssl.hashes.XOFHash


class ExtendableOutputFunction(metaclass=abc.ABCMeta):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ name = "cryptography_rust"
crate-type = ["cdylib"]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(CRYPTOGRAPHY_OPENSSL_300_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_309_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_350_OR_GREATER)', 'cfg(CRYPTOGRAPHY_IS_LIBRESSL)', 'cfg(CRYPTOGRAPHY_IS_BORINGSSL)', 'cfg(CRYPTOGRAPHY_OSSLCONF, values("OPENSSL_NO_IDEA", "OPENSSL_NO_CAST", "OPENSSL_NO_BF", "OPENSSL_NO_CAMELLIA", "OPENSSL_NO_SEED", "OPENSSL_NO_SM4"))'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(CRYPTOGRAPHY_OPENSSL_300_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_309_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_330_OR_GREATER)', 'cfg(CRYPTOGRAPHY_OPENSSL_350_OR_GREATER)', 'cfg(CRYPTOGRAPHY_IS_LIBRESSL)', 'cfg(CRYPTOGRAPHY_IS_BORINGSSL)', 'cfg(CRYPTOGRAPHY_OSSLCONF, values("OPENSSL_NO_IDEA", "OPENSSL_NO_CAST", "OPENSSL_NO_BF", "OPENSSL_NO_CAMELLIA", "OPENSSL_NO_SEED", "OPENSSL_NO_SM4"))'] }
3 changes: 3 additions & 0 deletions src/rust/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ fn main() {
if version >= 0x3_02_00_00_0 {
println!("cargo:rustc-cfg=CRYPTOGRAPHY_OPENSSL_320_OR_GREATER");
}
if version >= 0x3_03_00_00_0 {
println!("cargo:rustc-cfg=CRYPTOGRAPHY_OPENSSL_330_OR_GREATER");
}
if version >= 0x3_05_00_00_0 {
println!("cargo:rustc-cfg=CRYPTOGRAPHY_OPENSSL_350_OR_GREATER");
}
Expand Down
109 changes: 108 additions & 1 deletion src/rust/src/backend/hashes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,115 @@ impl Hash {
}
}

#[pyo3::pyclass(module = "cryptography.hazmat.bindings._rust.openssl.hashes")]
pub(crate) struct XOFHash {
#[pyo3(get)]
algorithm: pyo3::Py<pyo3::PyAny>,
ctx: openssl::hash::Hasher,
bytes_remaining: u64,
squeezed: bool,
}

impl XOFHash {
pub(crate) fn update_bytes(&mut self, data: &[u8]) -> CryptographyResult<()> {
Comment thread
reaperhulk marked this conversation as resolved.
self.ctx.update(data)?;
Ok(())
}
}

#[pyo3::pymethods]
impl XOFHash {
#[new]
#[pyo3(signature = (algorithm))]
fn new(
py: pyo3::Python<'_>,
algorithm: &pyo3::Bound<'_, pyo3::PyAny>,
) -> CryptographyResult<XOFHash> {
cfg_if::cfg_if! {
if #[cfg(any(
CRYPTOGRAPHY_IS_LIBRESSL,
CRYPTOGRAPHY_IS_BORINGSSL,
not(CRYPTOGRAPHY_OPENSSL_330_OR_GREATER)
))] {
let _ = py;
let _ = algorithm;
Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err((
"Extendable output functions are not supported on LibreSSL or BoringSSL.",
)),
))
} else {
if !algorithm.is_instance(&types::EXTENDABLE_OUTPUT_FUNCTION.get(py)?)? {
return Err(CryptographyError::from(
pyo3::exceptions::PyTypeError::new_err(
"Expected instance of an extendable output function.",
),
));
}
let md = message_digest_from_algorithm(py, algorithm)?;
let ctx = openssl::hash::Hasher::new(md)?;
// We treat digest_size as the maximum total output for this API
let bytes_remaining = algorithm
.getattr(pyo3::intern!(py, "digest_size"))?
.extract::<u64>()?;

Ok(XOFHash {
algorithm: algorithm.clone().unbind(),
ctx,
bytes_remaining,
squeezed: false,
})
}
}
}

fn update(&mut self, data: CffiBuf<'_>) -> CryptographyResult<()> {
if self.squeezed {
return Err(CryptographyError::from(
exceptions::AlreadyFinalized::new_err("Context was already squeezed."),
));
}
self.update_bytes(data.as_bytes())
}
#[cfg(all(
CRYPTOGRAPHY_OPENSSL_330_OR_GREATER,
not(CRYPTOGRAPHY_IS_LIBRESSL),
not(CRYPTOGRAPHY_IS_BORINGSSL),
))]
fn squeeze<'p>(
&mut self,
py: pyo3::Python<'p>,
length: usize,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
self.squeezed = true;
// We treat digest_size as the maximum total output for this API
self.bytes_remaining = self
.bytes_remaining
.checked_sub(length.try_into().unwrap())
.ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err(
"Exceeded maximum squeeze limit specified by digest_size.",
)
})?;
let result = pyo3::types::PyBytes::new_with(py, length, |b| {
self.ctx.squeeze_xof(b).unwrap();
Ok(())
})?;
Ok(result)
}

fn copy(&self, py: pyo3::Python<'_>) -> CryptographyResult<XOFHash> {
Ok(XOFHash {
algorithm: self.algorithm.clone_ref(py),
ctx: self.ctx.clone(),
bytes_remaining: self.bytes_remaining,
squeezed: self.squeezed,
})
}
}

#[pyo3::pymodule]
pub(crate) mod hashes {
#[pymodule_export]
use super::{hash_supported, Hash};
use super::{hash_supported, Hash, XOFHash};
}
4 changes: 4 additions & 0 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ mod _rust {
"CRYPTOGRAPHY_OPENSSL_320_OR_GREATER",
cfg!(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER),
)?;
openssl_mod.add(
"CRYPTOGRAPHY_OPENSSL_330_OR_GREATER",
cfg!(CRYPTOGRAPHY_OPENSSL_330_OR_GREATER),
)?;
openssl_mod.add(
"CRYPTOGRAPHY_OPENSSL_350_OR_GREATER",
cfg!(CRYPTOGRAPHY_OPENSSL_350_OR_GREATER),
Expand Down
Loading