Add HKDF support

pull/73/head
Martijn de Milliano 2025-11-12 11:37:18 +01:00
parent 0f2ceea12c
commit d7041ea176
3 changed files with 418 additions and 1 deletions

View File

@ -376,6 +376,7 @@ def get_features(local_wolfssl, features):
features["CHACHA20_POLY1305"] = 1 if '#define HAVE_CHACHA' and '#define HAVE_POLY1305' in defines else 0
features["ML_DSA"] = 1 if '#define HAVE_DILITHIUM' in defines else 0
features["ML_KEM"] = 1 if '#define WOLFSSL_HAVE_MLKEM' in defines else 0
features["HKDF"] = 1 if "#define HAVE_HKDF" in defines else 0
if '#define HAVE_FIPS' in defines:
if not fips:
@ -491,6 +492,7 @@ def build_ffi(local_wolfssl, features):
int CHACHA20_POLY1305_ENABLED = """ + str(features["CHACHA20_POLY1305"]) + """;
int ML_KEM_ENABLED = """ + str(features["ML_KEM"]) + """;
int ML_DSA_ENABLED = """ + str(features["ML_DSA"]) + """;
int HKDF_ENABLED = """ + str(features["HKDF"]) + """;
"""
ffibuilder.set_source( "wolfcrypt._ffi", init_source_string,
@ -528,6 +530,7 @@ def build_ffi(local_wolfssl, features):
extern int CHACHA20_POLY1305_ENABLED;
extern int ML_KEM_ENABLED;
extern int ML_DSA_ENABLED;
extern int HKDF_ENABLED;
typedef unsigned char byte;
typedef unsigned int word32;
@ -884,6 +887,26 @@ def build_ffi(local_wolfssl, features):
int wc_ed448_priv_size(ed448_key* key);
"""
if features["HKDF"]:
cdef += """
int wc_HKDF(int type, const byte* inKey, word32 inKeySz,
const byte* salt, word32 saltSz,
const byte* info, word32 infoSz,
byte* out, word32 outSz);
int wc_HKDF_Extract(int type, const byte* salt, word32 saltSz,
const byte* inKey, word32 inKeySz, byte* out);
int wc_HKDF_Extract_ex(int type, const byte* salt, word32 saltSz,
const byte* inKey, word32 inKeySz, byte* out,
void* heap, int devId);
int wc_HKDF_Expand(int type, const byte* inKey, word32 inKeySz,
const byte* info, word32 infoSz,
byte* out, word32 outSz);
int wc_HKDF_Expand_ex(int type, const byte* inKey, word32 inKeySz,
const byte* info, word32 infoSz,
byte* out, word32 outSz,
void* heap, int devId);
"""
if features["PWDBASED"]:
cdef += """
int wc_PBKDF2(byte* output, const byte* passwd, int pLen,
@ -1018,7 +1041,8 @@ def main(ffibuilder):
"RSA_PSS": 1,
"CHACHA20_POLY1305": 1,
"ML_KEM": 1,
"ML_DSA": 1
"ML_DSA": 1,
"HKDF": 1,
}
# Ed448 requires SHAKE256, which isn't part of the Windows build, yet.

260
tests/test_hkdf.py 100644
View File

@ -0,0 +1,260 @@
# test_hkdf.py
#
# Copyright (C) 2025 wolfSSL Inc.
#
# This file is part of wolfSSL. (formerly known as CyaSSL)
#
# wolfSSL is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# wolfSSL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
# pylint: disable=redefined-outer-name
import pytest
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.hkdf import HKDF, HKDF_Extract, HKDF_Expand
from wolfcrypt.hashes import HmacSha, HmacSha256
# Skip the whole module if required features are not available.
pytestmark = pytest.mark.skipif(
not (_lib.HKDF_ENABLED and _lib.SHA256_ENABLED and _lib.HMAC_ENABLED),
reason="HKDF/SHA256/HMAC not enabled in the underlying wolfCrypt library",
)
def test_hkdf_rfc5869_case1_full():
"""
RFC 5869 Test Case 1 (SHA-256).
"""
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
salt = bytes.fromhex("000102030405060708090a0b0c")
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
length = 42
expected_okm = bytes.fromhex(
"3cb25f25faacd57a90434f64d0362f2a"
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
"34007208d5b887185865"
)
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
assert isinstance(okm, bytes)
assert len(okm) == length
assert okm == expected_okm
def test_hkdf_rfc5869_case1_split_extract_expand():
"""
Same vector as above but exercised via HKDF_Extract and HKDF_Expand.
Verifies the PRK (pseudorandom key) and the final OKM.
"""
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
salt = bytes.fromhex("000102030405060708090a0b0c")
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
length = 42
expected_prk = bytes.fromhex(
"077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"
)
expected_okm = bytes.fromhex(
"3cb25f25faacd57a90434f64d0362f2a"
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
"34007208d5b887185865"
)
prk = HKDF_Extract(HmacSha256, salt, ikm)
assert isinstance(prk, bytes)
assert prk == expected_prk
okm = HKDF_Expand(HmacSha256, prk, info, length)
assert isinstance(okm, bytes)
assert len(okm) == length
assert okm == expected_okm
def test_hkdf_rfc5869_case2_full_and_split():
"""
RFC 5869 Test Case 2 (SHA-256) - longer inputs/outputs
"""
ikm = bytes(range(0x00, 0x00 + 80))
salt = bytes(range(0x60, 0x60 + 80))
info = bytes(range(0xB0, 0xB0 + 80))
length = 82
expected_prk = bytes.fromhex(
"06a6b88c5853361a06104c9ceb35b45c"
"ef760014904671014a193f40c15fc244"
)
expected_okm = bytes.fromhex(
"b11e398dc80327a1c8e7f78c596a4934"
"4f012eda2d4efad8a050cc4c19afa97c"
"59045a99cac7827271cb41c65e590e09"
"da3275600c2f09b8367793a9aca3db71"
"cc30c58179ec3e87c14c01d5c1f3434f"
"1d87"
)
# Full
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
assert isinstance(okm, bytes)
assert len(okm) == length
assert okm == expected_okm
# Split: check PRK then expand
prk = HKDF_Extract(HmacSha256, salt, ikm)
assert prk == expected_prk
okm2 = HKDF_Expand(HmacSha256, prk, info, length)
assert okm2 == expected_okm
def test_hkdf_rfc5869_case3_full_and_split():
"""
RFC 5869 Test Case 3 (SHA-256) - zero-length salt/info
"""
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
salt = b""
info = b""
length = 42
expected_prk = bytes.fromhex(
"19ef24a32c717b167f33a91d6f648bdf"
"96596776afdb6377ac434c1c293ccb04"
)
expected_okm = bytes.fromhex(
"8da4e775a563c18f715f802a063c5a31"
"b8a11f5c5ee1879ec3454e5f3c738d2d"
"9d201395faa4b61a96c8"
)
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
assert okm == expected_okm
prk = HKDF_Extract(HmacSha256, salt, ikm)
assert prk == expected_prk
okm2 = HKDF_Expand(HmacSha256, prk, info, length)
assert okm2 == expected_okm
def test_hkdf_rfc5869_case4_sha1_full_and_split():
"""
RFC 5869 Test Case 4 (SHA-1) - basic test
"""
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b")
salt = bytes.fromhex("000102030405060708090a0b0c")
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
length = 42
expected_prk = bytes.fromhex("9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243")
expected_okm = bytes.fromhex(
"085a01ea1b10f36933068b56efa5ad81"
"a4f14b822f5b091568a9cdd4f155fda2"
"c22e422478d305f3f896"
)
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
assert okm == expected_okm
prk = HKDF_Extract(HmacSha, salt, ikm)
assert prk == expected_prk
okm2 = HKDF_Expand(HmacSha, prk, info, length)
assert okm2 == expected_okm
def test_hkdf_rfc5869_case5_sha1_long_full_and_split():
"""
RFC 5869 Test Case 5 (SHA-1) - longer inputs/outputs
"""
ikm = bytes(range(0x00, 0x00 + 80))
salt = bytes(range(0x60, 0x60 + 80))
info = bytes(range(0xB0, 0xB0 + 80))
length = 82
expected_prk = bytes.fromhex("8adae09a2a307059478d309b26c4115a224cfaf6")
expected_okm = bytes.fromhex(
"0bd770a74d1160f7c9f12cd5912a06eb"
"ff6adcae899d92191fe4305673ba2ffe"
"8fa3f1a4e5ad79f3f334b3b202b2173c"
"486ea37ce3d397ed034c7f9dfeb15c5e"
"927336d0441f4c4300e2cff0d0900b52"
"d3b4"
)
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
assert okm == expected_okm
prk = HKDF_Extract(HmacSha, salt, ikm)
assert prk == expected_prk
okm2 = HKDF_Expand(HmacSha, prk, info, length)
assert okm2 == expected_okm
def test_hkdf_rfc5869_case6_sha1_zero_salt_info():
"""
RFC 5869 Test Case 6 (SHA-1) - zero-length salt/info
"""
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
salt = b""
info = b""
length = 42
expected_prk = bytes.fromhex("da8c8a73c7fa77288ec6f5e7c297786aa0d32d01")
expected_okm = bytes.fromhex(
"0ac1af7002b3d761d1e55298da9d0506"
"b9ae52057220a306e07b6b87e8df21d0"
"ea00033de03984d34918"
)
prk = HKDF_Extract(HmacSha, salt, ikm)
assert prk == expected_prk
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
assert okm == expected_okm
okm2 = HKDF_Expand(HmacSha, prk, info, length)
assert okm2 == expected_okm
def test_hkdf_rfc5869_case7_sha1_salt_not_provided():
"""
RFC 5869 Test Case 7 (SHA-1) - salt not provided (defaults to zeros),
zero-length info.
"""
ikm = bytes.fromhex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c")
info = b""
length = 42
expected_prk = bytes.fromhex("2adccada18779e7c2077ad2eb19d3f3e731385dd")
expected_okm = bytes.fromhex(
"2c91117204d745f3500d636a62f64f0a"
"b3bae548aa53d423b0d1f27ebba6f5e5"
"673a081d70cce7acfc48"
)
# For Extract: when salt is not provided, pass b"" (wc_HKDF_Extract treats
# empty salt as zeros).
# Some implementations treat "not provided" as explicit None;
# wc_HKDF_Extract expects salt pointer and length, so passing empty salt
# (length 0) is equivalent to RFC specification (salt = HashLen zeros).
prk = HKDF_Extract(HmacSha, None, ikm)
assert prk == expected_prk
okm = HKDF(HmacSha, ikm, salt=None, info=info, out_len=length)
assert okm == expected_okm
okm2 = HKDF_Expand(HmacSha, prk, info, length)
assert okm2 == expected_okm

133
wolfcrypt/hkdf.py 100644
View File

@ -0,0 +1,133 @@
# hkdf.py
#
# Copyright (C) 2025 wolfSSL Inc.
#
# This file is part of wolfSSL. (formerly known as CyaSSL)
#
# wolfSSL is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# wolfSSL is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
# pylint: disable=no-member,no-name-in-module
from wolfcrypt._ffi import ffi as _ffi
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.exceptions import WolfCryptError
from wolfcrypt.utils import t2b
if _lib.HKDF_ENABLED:
def HKDF(hash_cls, in_key, salt=None, info=None, out_len=None):
"""
Perform HKDF Extract-and-Expand in one call (wraps wc_HKDF).
Parameters:
- hash_cls: hash class, see `wolfcrypt.hashes`.
- in_key: input key material (IKM) as bytes or str.
- salt: optional salt value (bytes or str). If None, treated as empty.
- info: optional context/application info (bytes or str). If None,
treated as empty.
- out_len: desired length of output keying material (bytes). If None,
defaults to the digest size of the hash.
Returns:
- bytes object containing the derived key of length `out_len`.
Raises:
- WolfCryptError on failure.
- ValueError for invalid arguments.
"""
in_key = t2b(in_key)
salt = b"" if salt is None else t2b(salt)
info = b"" if info is None else t2b(info)
if out_len is None:
out_len = hash_cls.digest_size
out = _ffi.new("byte[%d]" % out_len)
ret = _lib.wc_HKDF(
hash_cls._type,
in_key,
len(in_key),
salt,
len(salt),
info,
len(info),
out,
out_len,
)
if ret != 0:
raise WolfCryptError("HKDF error (%d)" % ret)
return _ffi.buffer(out, out_len)[:]
def HKDF_Extract(hash_cls, salt, in_key):
"""
HKDF-Extract: PRK = HMAC-Hash(salt, IKM)
Wraps wc_HKDF_Extract.
Parameters:
- hash_cls: hash class, see `wolfcrypt.hashes`.
- salt: bytes/str (can be None -> treated as empty).
- in_key: input key material (IKM) as bytes/str.
Returns:
- PRK as bytes (length == hash digest size).
Raises WolfCryptError on failure.
"""
salt = b"" if salt is None else t2b(salt)
in_key = t2b(in_key)
out_len = hash_cls.digest_size
out = _ffi.new("byte[%d]" % out_len)
ret = _lib.wc_HKDF_Extract(hash_cls._type, salt, len(salt), in_key, len(in_key), out)
if ret != 0:
raise WolfCryptError("HKDF_Extract error (%d)" % ret)
return _ffi.buffer(out, out_len)[:]
def HKDF_Expand(hash_cls, prk, info, out_len):
"""
HKDF-Expand: OKM = HKDF-Expand(PRK, info, L)
Wraps wc_HKDF_Expand.
Parameters:
- hash_cls: hash class, see `wolfcrypt.hashes`.
- prk: pseudorandom key (output from HKDF-Extract) as bytes/str.
- info: optional context/application info (bytes/str). If None, treated as empty.
- out_len: length of output keying material in bytes.
Returns:
- OKM as bytes of length `out_len`.
Raises WolfCryptError on failure.
"""
prk = t2b(prk)
info = b"" if info is None else t2b(info)
if out_len is None or out_len <= 0:
raise ValueError("out_len must be a positive integer")
out = _ffi.new("byte[%d]" % out_len)
ret = _lib.wc_HKDF_Expand(
hash_cls._type, prk, len(prk), info, len(info), out, out_len
)
if ret != 0:
raise WolfCryptError("HKDF_Expand error (%d)" % ret)
return _ffi.buffer(out, out_len)[:]