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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Changelog
* Moved :class:`~cryptography.hazmat.primitives.ciphers.algorithms.Camellia`
into :doc:`/hazmat/decrepit/index` and deprecated it in the ``cipher`` module.
It will be removed from the ``cipher`` module in 49.0.0.
* Added :meth:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF.extract`
to :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`. The previous
private implementation will be removed in 49.0.0.

.. _v46-0-2:

Expand Down
27 changes: 27 additions & 0 deletions docs/hazmat/primitives/key-derivation-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,33 @@ HKDF
:raises TypeError: This exception is raised if ``salt`` or ``info`` is not
``bytes``.

.. staticmethod:: extract(algorithm, salt, key_material)

.. versionadded:: 47.0.0

.. note::
Extract is a component of the complete HKDF algorithm.
Unless needed for implementing an existing protocol, users
should ignore this method and use call :meth:`derive`.

:param algorithm: An instance of
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`.
:param bytes salt: A salt. Randomizes the KDF's output. Optional, but
highly recommended. Ideally as many bits of entropy as the security
level of the hash: often that means cryptographically random and as
long as the hash output. Worse (shorter, less entropy) salt values can
still meaningfully contribute to security. May be reused. Does not have
to be secret, but may cause stronger security guarantees if secret; see
:rfc:`5869` and the `HKDF paper`_ for more details. If ``None`` is
explicitly passed a default salt of ``algorithm.digest_size // 8`` null
bytes will be used. See `understanding HKDF`_ for additional detail about
the salt and info parameters.
:param key_material: The input key material.
:type key_material: :term:`bytes-like`
:return bytes: The extracted value.
:raises TypeError: This exception is raised if ``key_material``, ``salt``, or
``algorithm`` are the wrong type.

.. method:: derive(key_material)

:param key_material: The input key material.
Expand Down
4 changes: 4 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class HKDF:
info: bytes | None,
backend: typing.Any = None,
): ...
@staticmethod
def extract(
algorithm: HashAlgorithm, salt: bytes | None, key_material: Buffer
) -> bytes: ...
def derive(self, key_material: Buffer) -> bytes: ...
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...

Expand Down
10 changes: 9 additions & 1 deletion src/rust/src/backend/hmac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ impl Hmac {
Ok(())
}

pub(crate) fn finalize_bytes(
&mut self,
) -> CryptographyResult<cryptography_openssl::hmac::DigestBytes> {
let data = self.get_mut_ctx()?.finish()?;
self.ctx = None;
Ok(data)
}

fn get_ctx(&self) -> CryptographyResult<&cryptography_openssl::hmac::Hmac> {
if let Some(ctx) = self.ctx.as_ref() {
return Ok(ctx);
Expand Down Expand Up @@ -83,7 +91,7 @@ impl Hmac {
&mut self,
py: pyo3::Python<'p>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let data = self.get_mut_ctx()?.finish()?;
let data = self.finalize_bytes()?;
self.ctx = None;
Ok(pyo3::types::PyBytes::new(py, &data))
}
Expand Down
55 changes: 40 additions & 15 deletions src/rust/src/backend/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,12 +520,35 @@ impl Argon2id {
#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.hkdf", name = "HKDF")]
struct Hkdf {
algorithm: pyo3::Py<pyo3::PyAny>,
salt: pyo3::Py<pyo3::types::PyBytes>,
salt: Option<pyo3::Py<pyo3::types::PyBytes>>,
info: Option<pyo3::Py<pyo3::types::PyBytes>>,
length: usize,
used: bool,
}

fn hkdf_extract(
py: pyo3::Python<'_>,
algorithm: &pyo3::Py<pyo3::PyAny>,
salt: Option<&pyo3::Py<pyo3::types::PyBytes>>,
key_material: &CffiBuf<'_>,
) -> CryptographyResult<cryptography_openssl::hmac::DigestBytes> {
let algorithm_bound = algorithm.bind(py);
let digest_size = algorithm_bound
.getattr(pyo3::intern!(py, "digest_size"))?
.extract::<usize>()?;
let salt_bound = salt.map(|s| s.bind(py));
let default_salt = vec![0; digest_size];
let salt_bytes: &[u8] = if let Some(bound) = salt_bound {
bound.as_bytes()
} else {
&default_salt
};

let mut hmac = Hmac::new_bytes(py, salt_bytes, algorithm_bound)?;
hmac.update_bytes(key_material.as_bytes())?;
hmac.finalize_bytes()
}

#[pyo3::pymethods]
impl Hkdf {
#[new]
Expand Down Expand Up @@ -558,12 +581,6 @@ impl Hkdf {
));
}

let salt = if let Some(salt) = salt {
salt
} else {
pyo3::types::PyBytes::new_with(py, digest_size, |_| Ok(()))?.into()
};

Ok(Hkdf {
algorithm,
salt,
Expand All @@ -573,15 +590,24 @@ impl Hkdf {
})
}

#[staticmethod]
fn extract<'p>(
py: pyo3::Python<'p>,
algorithm: pyo3::Py<pyo3::PyAny>,
salt: Option<pyo3::Py<pyo3::types::PyBytes>>,
key_material: CffiBuf<'_>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let prk = hkdf_extract(py, &algorithm, salt.as_ref(), &key_material)?;
Ok(pyo3::types::PyBytes::new(py, &prk))
}

fn _extract<'p>(
&self,
py: pyo3::Python<'p>,
key_material: &[u8],
key_material: CffiBuf<'_>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let algorithm_bound = self.algorithm.bind(py);
let mut hmac = Hmac::new_bytes(py, self.salt.as_bytes(py), algorithm_bound)?;
hmac.update_bytes(key_material)?;
hmac.finalize(py)
let prk = hkdf_extract(py, &self.algorithm, self.salt.as_ref(), &key_material)?;
Ok(pyo3::types::PyBytes::new(py, &prk))
}

fn derive<'p>(
Expand All @@ -594,16 +620,15 @@ impl Hkdf {
}
self.used = true;

let prk = self._extract(py, key_material.as_bytes())?;
let prk = hkdf_extract(py, &self.algorithm, self.salt.as_ref(), &key_material)?;
let mut hkdf_expand = HkdfExpand::new(
py,
self.algorithm.clone_ref(py),
self.length,
self.info.as_ref().map(|i| i.clone_ref(py)),
None,
)?;
let prk_bytes = prk.as_bytes();
let cffi_buf = CffiBuf::from_bytes(py, prk_bytes);
let cffi_buf = CffiBuf::from_bytes(py, &prk);
hkdf_expand.derive(py, cffi_buf)
}

Expand Down
7 changes: 7 additions & 0 deletions tests/hazmat/primitives/test_hkdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ def test_derive_long_output(self, backend):

assert hkdf.derive(ikm) == binascii.unhexlify(vector["okm"])

def test_private_extract_exists(self):
# This was DeprecatedIn47 but we can't raise a warning
# because the scapy tests are fragile butterflies
hkdf = HKDF(hashes.SHA256(), 32, salt=b"0", info=None)
prk = hkdf._extract(b"0") # type:ignore[attr-defined]
assert len(prk) == 32

def test_buffer_protocol(self, backend):
vector = load_vectors_from_file(
os.path.join("KDF", "hkdf-generated.txt"), load_nist_vectors
Expand Down
8 changes: 7 additions & 1 deletion tests/hazmat/primitives/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,14 @@ def hkdf_derive_test(backend, algorithm, params):
backend=backend,
)

okm = hkdf.derive(binascii.unhexlify(params["ikm"]))
prk = HKDF.extract(
algorithm,
binascii.unhexlify(params["salt"]) or None,
binascii.unhexlify(params["ikm"]),
)
assert prk == binascii.unhexlify(params["prk"])

okm = hkdf.derive(binascii.unhexlify(params["ikm"]))
assert okm == binascii.unhexlify(params["okm"])


Expand Down