Skip to content

Commit e9ebda0

Browse files
authored
Merge pull request #73 from mjdemilliano/hdkf
Add HKDF support
2 parents 99aed59 + d7041ea commit e9ebda0

File tree

3 files changed

+418
-1
lines changed

3 files changed

+418
-1
lines changed

scripts/build_ffi.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ def get_features(local_wolfssl, features):
378378
features["CHACHA20_POLY1305"] = 1 if '#define HAVE_CHACHA' and '#define HAVE_POLY1305' in defines else 0
379379
features["ML_DSA"] = 1 if '#define HAVE_DILITHIUM' in defines else 0
380380
features["ML_KEM"] = 1 if '#define WOLFSSL_HAVE_MLKEM' in defines else 0
381+
features["HKDF"] = 1 if "#define HAVE_HKDF" in defines else 0
381382

382383
if '#define HAVE_FIPS' in defines:
383384
if not fips:
@@ -494,6 +495,7 @@ def build_ffi(local_wolfssl, features):
494495
int CHACHA20_POLY1305_ENABLED = """ + str(features["CHACHA20_POLY1305"]) + """;
495496
int ML_KEM_ENABLED = """ + str(features["ML_KEM"]) + """;
496497
int ML_DSA_ENABLED = """ + str(features["ML_DSA"]) + """;
498+
int HKDF_ENABLED = """ + str(features["HKDF"]) + """;
497499
"""
498500

499501
ffibuilder.set_source( "wolfcrypt._ffi", init_source_string,
@@ -532,6 +534,7 @@ def build_ffi(local_wolfssl, features):
532534
extern int CHACHA20_POLY1305_ENABLED;
533535
extern int ML_KEM_ENABLED;
534536
extern int ML_DSA_ENABLED;
537+
extern int HKDF_ENABLED;
535538
536539
typedef unsigned char byte;
537540
typedef unsigned int word32;
@@ -907,6 +910,26 @@ def build_ffi(local_wolfssl, features):
907910
int wc_ed448_priv_size(ed448_key* key);
908911
"""
909912

913+
if features["HKDF"]:
914+
cdef += """
915+
int wc_HKDF(int type, const byte* inKey, word32 inKeySz,
916+
const byte* salt, word32 saltSz,
917+
const byte* info, word32 infoSz,
918+
byte* out, word32 outSz);
919+
int wc_HKDF_Extract(int type, const byte* salt, word32 saltSz,
920+
const byte* inKey, word32 inKeySz, byte* out);
921+
int wc_HKDF_Extract_ex(int type, const byte* salt, word32 saltSz,
922+
const byte* inKey, word32 inKeySz, byte* out,
923+
void* heap, int devId);
924+
int wc_HKDF_Expand(int type, const byte* inKey, word32 inKeySz,
925+
const byte* info, word32 infoSz,
926+
byte* out, word32 outSz);
927+
int wc_HKDF_Expand_ex(int type, const byte* inKey, word32 inKeySz,
928+
const byte* info, word32 infoSz,
929+
byte* out, word32 outSz,
930+
void* heap, int devId);
931+
"""
932+
910933
if features["PWDBASED"]:
911934
cdef += """
912935
int wc_PBKDF2(byte* output, const byte* passwd, int pLen,
@@ -1042,7 +1065,8 @@ def main(ffibuilder):
10421065
"RSA_PSS": 1,
10431066
"CHACHA20_POLY1305": 1,
10441067
"ML_KEM": 1,
1045-
"ML_DSA": 1
1068+
"ML_DSA": 1,
1069+
"HKDF": 1,
10461070
}
10471071

10481072
# Ed448 requires SHAKE256, which isn't part of the Windows build, yet.

tests/test_hkdf.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# test_hkdf.py
2+
#
3+
# Copyright (C) 2025 wolfSSL Inc.
4+
#
5+
# This file is part of wolfSSL. (formerly known as CyaSSL)
6+
#
7+
# wolfSSL is free software; you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation; either version 2 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# wolfSSL is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with this program; if not, write to the Free Software
19+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20+
21+
# pylint: disable=redefined-outer-name
22+
23+
import pytest
24+
25+
from wolfcrypt._ffi import lib as _lib
26+
from wolfcrypt.hkdf import HKDF, HKDF_Extract, HKDF_Expand
27+
from wolfcrypt.hashes import HmacSha, HmacSha256
28+
29+
# Skip the whole module if required features are not available.
30+
pytestmark = pytest.mark.skipif(
31+
not (_lib.HKDF_ENABLED and _lib.SHA256_ENABLED and _lib.HMAC_ENABLED),
32+
reason="HKDF/SHA256/HMAC not enabled in the underlying wolfCrypt library",
33+
)
34+
35+
36+
def test_hkdf_rfc5869_case1_full():
37+
"""
38+
RFC 5869 Test Case 1 (SHA-256).
39+
"""
40+
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
41+
salt = bytes.fromhex("000102030405060708090a0b0c")
42+
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
43+
length = 42
44+
45+
expected_okm = bytes.fromhex(
46+
"3cb25f25faacd57a90434f64d0362f2a"
47+
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
48+
"34007208d5b887185865"
49+
)
50+
51+
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
52+
assert isinstance(okm, bytes)
53+
assert len(okm) == length
54+
assert okm == expected_okm
55+
56+
57+
def test_hkdf_rfc5869_case1_split_extract_expand():
58+
"""
59+
Same vector as above but exercised via HKDF_Extract and HKDF_Expand.
60+
Verifies the PRK (pseudorandom key) and the final OKM.
61+
"""
62+
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
63+
salt = bytes.fromhex("000102030405060708090a0b0c")
64+
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
65+
length = 42
66+
67+
expected_prk = bytes.fromhex(
68+
"077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"
69+
)
70+
expected_okm = bytes.fromhex(
71+
"3cb25f25faacd57a90434f64d0362f2a"
72+
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
73+
"34007208d5b887185865"
74+
)
75+
76+
prk = HKDF_Extract(HmacSha256, salt, ikm)
77+
assert isinstance(prk, bytes)
78+
assert prk == expected_prk
79+
80+
okm = HKDF_Expand(HmacSha256, prk, info, length)
81+
assert isinstance(okm, bytes)
82+
assert len(okm) == length
83+
assert okm == expected_okm
84+
85+
86+
def test_hkdf_rfc5869_case2_full_and_split():
87+
"""
88+
RFC 5869 Test Case 2 (SHA-256) - longer inputs/outputs
89+
"""
90+
ikm = bytes(range(0x00, 0x00 + 80))
91+
salt = bytes(range(0x60, 0x60 + 80))
92+
info = bytes(range(0xB0, 0xB0 + 80))
93+
length = 82
94+
95+
expected_prk = bytes.fromhex(
96+
"06a6b88c5853361a06104c9ceb35b45c"
97+
"ef760014904671014a193f40c15fc244"
98+
)
99+
expected_okm = bytes.fromhex(
100+
"b11e398dc80327a1c8e7f78c596a4934"
101+
"4f012eda2d4efad8a050cc4c19afa97c"
102+
"59045a99cac7827271cb41c65e590e09"
103+
"da3275600c2f09b8367793a9aca3db71"
104+
"cc30c58179ec3e87c14c01d5c1f3434f"
105+
"1d87"
106+
)
107+
108+
# Full
109+
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
110+
assert isinstance(okm, bytes)
111+
assert len(okm) == length
112+
assert okm == expected_okm
113+
114+
# Split: check PRK then expand
115+
prk = HKDF_Extract(HmacSha256, salt, ikm)
116+
assert prk == expected_prk
117+
118+
okm2 = HKDF_Expand(HmacSha256, prk, info, length)
119+
assert okm2 == expected_okm
120+
121+
122+
def test_hkdf_rfc5869_case3_full_and_split():
123+
"""
124+
RFC 5869 Test Case 3 (SHA-256) - zero-length salt/info
125+
"""
126+
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
127+
salt = b""
128+
info = b""
129+
length = 42
130+
131+
expected_prk = bytes.fromhex(
132+
"19ef24a32c717b167f33a91d6f648bdf"
133+
"96596776afdb6377ac434c1c293ccb04"
134+
)
135+
expected_okm = bytes.fromhex(
136+
"8da4e775a563c18f715f802a063c5a31"
137+
"b8a11f5c5ee1879ec3454e5f3c738d2d"
138+
"9d201395faa4b61a96c8"
139+
)
140+
141+
okm = HKDF(HmacSha256, ikm, salt=salt, info=info, out_len=length)
142+
assert okm == expected_okm
143+
144+
prk = HKDF_Extract(HmacSha256, salt, ikm)
145+
assert prk == expected_prk
146+
147+
okm2 = HKDF_Expand(HmacSha256, prk, info, length)
148+
assert okm2 == expected_okm
149+
150+
151+
def test_hkdf_rfc5869_case4_sha1_full_and_split():
152+
"""
153+
RFC 5869 Test Case 4 (SHA-1) - basic test
154+
"""
155+
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b")
156+
salt = bytes.fromhex("000102030405060708090a0b0c")
157+
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
158+
length = 42
159+
160+
expected_prk = bytes.fromhex("9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243")
161+
expected_okm = bytes.fromhex(
162+
"085a01ea1b10f36933068b56efa5ad81"
163+
"a4f14b822f5b091568a9cdd4f155fda2"
164+
"c22e422478d305f3f896"
165+
)
166+
167+
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
168+
assert okm == expected_okm
169+
170+
prk = HKDF_Extract(HmacSha, salt, ikm)
171+
assert prk == expected_prk
172+
173+
okm2 = HKDF_Expand(HmacSha, prk, info, length)
174+
assert okm2 == expected_okm
175+
176+
177+
def test_hkdf_rfc5869_case5_sha1_long_full_and_split():
178+
"""
179+
RFC 5869 Test Case 5 (SHA-1) - longer inputs/outputs
180+
"""
181+
ikm = bytes(range(0x00, 0x00 + 80))
182+
salt = bytes(range(0x60, 0x60 + 80))
183+
info = bytes(range(0xB0, 0xB0 + 80))
184+
length = 82
185+
186+
expected_prk = bytes.fromhex("8adae09a2a307059478d309b26c4115a224cfaf6")
187+
expected_okm = bytes.fromhex(
188+
"0bd770a74d1160f7c9f12cd5912a06eb"
189+
"ff6adcae899d92191fe4305673ba2ffe"
190+
"8fa3f1a4e5ad79f3f334b3b202b2173c"
191+
"486ea37ce3d397ed034c7f9dfeb15c5e"
192+
"927336d0441f4c4300e2cff0d0900b52"
193+
"d3b4"
194+
)
195+
196+
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
197+
assert okm == expected_okm
198+
199+
prk = HKDF_Extract(HmacSha, salt, ikm)
200+
assert prk == expected_prk
201+
202+
okm2 = HKDF_Expand(HmacSha, prk, info, length)
203+
assert okm2 == expected_okm
204+
205+
206+
def test_hkdf_rfc5869_case6_sha1_zero_salt_info():
207+
"""
208+
RFC 5869 Test Case 6 (SHA-1) - zero-length salt/info
209+
"""
210+
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
211+
salt = b""
212+
info = b""
213+
length = 42
214+
215+
expected_prk = bytes.fromhex("da8c8a73c7fa77288ec6f5e7c297786aa0d32d01")
216+
expected_okm = bytes.fromhex(
217+
"0ac1af7002b3d761d1e55298da9d0506"
218+
"b9ae52057220a306e07b6b87e8df21d0"
219+
"ea00033de03984d34918"
220+
)
221+
222+
prk = HKDF_Extract(HmacSha, salt, ikm)
223+
assert prk == expected_prk
224+
225+
okm = HKDF(HmacSha, ikm, salt=salt, info=info, out_len=length)
226+
assert okm == expected_okm
227+
228+
okm2 = HKDF_Expand(HmacSha, prk, info, length)
229+
assert okm2 == expected_okm
230+
231+
232+
def test_hkdf_rfc5869_case7_sha1_salt_not_provided():
233+
"""
234+
RFC 5869 Test Case 7 (SHA-1) - salt not provided (defaults to zeros),
235+
zero-length info.
236+
"""
237+
ikm = bytes.fromhex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c")
238+
info = b""
239+
length = 42
240+
241+
expected_prk = bytes.fromhex("2adccada18779e7c2077ad2eb19d3f3e731385dd")
242+
expected_okm = bytes.fromhex(
243+
"2c91117204d745f3500d636a62f64f0a"
244+
"b3bae548aa53d423b0d1f27ebba6f5e5"
245+
"673a081d70cce7acfc48"
246+
)
247+
248+
# For Extract: when salt is not provided, pass b"" (wc_HKDF_Extract treats
249+
# empty salt as zeros).
250+
# Some implementations treat "not provided" as explicit None;
251+
# wc_HKDF_Extract expects salt pointer and length, so passing empty salt
252+
# (length 0) is equivalent to RFC specification (salt = HashLen zeros).
253+
prk = HKDF_Extract(HmacSha, None, ikm)
254+
assert prk == expected_prk
255+
256+
okm = HKDF(HmacSha, ikm, salt=None, info=info, out_len=length)
257+
assert okm == expected_okm
258+
259+
okm2 = HKDF_Expand(HmacSha, prk, info, length)
260+
assert okm2 == expected_okm

0 commit comments

Comments
 (0)