Add HKDF support
parent
0f2ceea12c
commit
d7041ea176
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)[:]
|
||||
Loading…
Reference in New Issue