diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6d9193411a4a..5f8480e92e00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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: diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index ced988855f84..4c94354d346c 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -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. diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 0302d71baa95..2a2349d140a5 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -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: ... diff --git a/src/rust/src/backend/hmac.rs b/src/rust/src/backend/hmac.rs index 28bf75949f7e..cc1b504016e0 100644 --- a/src/rust/src/backend/hmac.rs +++ b/src/rust/src/backend/hmac.rs @@ -45,6 +45,14 @@ impl Hmac { Ok(()) } + pub(crate) fn finalize_bytes( + &mut self, + ) -> CryptographyResult { + 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); @@ -83,7 +91,7 @@ impl Hmac { &mut self, py: pyo3::Python<'p>, ) -> CryptographyResult> { - let data = self.get_mut_ctx()?.finish()?; + let data = self.finalize_bytes()?; self.ctx = None; Ok(pyo3::types::PyBytes::new(py, &data)) } diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 8d56ee5066e5..b575a6ce3618 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -520,12 +520,35 @@ impl Argon2id { #[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.hkdf", name = "HKDF")] struct Hkdf { algorithm: pyo3::Py, - salt: pyo3::Py, + salt: Option>, info: Option>, length: usize, used: bool, } +fn hkdf_extract( + py: pyo3::Python<'_>, + algorithm: &pyo3::Py, + salt: Option<&pyo3::Py>, + key_material: &CffiBuf<'_>, +) -> CryptographyResult { + let algorithm_bound = algorithm.bind(py); + let digest_size = algorithm_bound + .getattr(pyo3::intern!(py, "digest_size"))? + .extract::()?; + 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] @@ -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, @@ -573,15 +590,24 @@ impl Hkdf { }) } + #[staticmethod] + fn extract<'p>( + py: pyo3::Python<'p>, + algorithm: pyo3::Py, + salt: Option>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + 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> { - 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>( @@ -594,7 +620,7 @@ 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), @@ -602,8 +628,7 @@ impl Hkdf { 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) } diff --git a/tests/hazmat/primitives/test_hkdf.py b/tests/hazmat/primitives/test_hkdf.py index ffa3310d2826..86023fa51ed6 100644 --- a/tests/hazmat/primitives/test_hkdf.py +++ b/tests/hazmat/primitives/test_hkdf.py @@ -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 diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index aad324683a81..0557694c96e9 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -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"])