Skip to content

Commit f031a9d

Browse files
committed
SecurityReview FND 40.2 + 36.1 + 6.4 + 10.1 + 15.1 + 26.7 + 11.3 + 43.2: integrity, PCT, zeroize, CMAC/SHAKE/AES-KW CASTs, DH PCT + configurable DRBG_SHA512_SEED_LEN, ML-DSA sign privateKeyReadEnable parity, FIPS CAST benchmark deliverable
Adds wolfcrypt/benchmark/fips_cast_bench.c, a small program that measures wc_RunCast_fips() execution time per CAST to help operators of resource- constrained operational environments (DSP, MCU) budget module power-on latency. Compiled only when FIPS is enabled (BUILD_FIPS in benchmark/include.am). Output is a per-CAST table (mean / stddev / min / max in ms) plus a one-pass total. Sample run on x86_64 shows SLH-DSA at ~42 ms and DH-Primitive-Z at ~26 ms as the dominant CASTs - the same algorithms whose CAST cost on a constrained DSP can determine whether boot is 5 minutes or 50. Citations in the file: FIPS 140-3 sec 7.10 (Self-Tests), IG 10.3.A (algorithm-by-algorithm CAST coverage), ISO/IEC 19790:2012 sec 7.10.2 (Conditional self-test execution). Note: the benchmark currently reports failures for FIPS_CAST_ECC_PRIMITIVE_Z and FIPS_CAST_ECDSA when invoked via wc_RunAllCast_fips() / wc_RunCast_fips(). The same algorithms work correctly when exercised through normal lazy AlgoAllowed wrapper paths (testwolfcrypt and make check both pass), so this is a pre-existing CAST-chain dependency-ordering bug exposed by the benchmark for the first time. Tracked as a separate follow-up; the benchmark deliverable is checked in despite this so customers can use the non-affected CAST timings (which include all the expensive ones the boot-time concern targets).
1 parent d00a137 commit f031a9d

11 files changed

Lines changed: 498 additions & 13 deletions

File tree

fips-hash.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ then
1313
fi
1414

1515
OUT=$(./wolfcrypt/test/testwolfcrypt | sed -n 's/hash = \(.*\)/\1/p')
16-
NEWHASH=$(echo "$OUT" | cut -c1-64)
16+
# FIPS v7.0.0+ uses HMAC-SHA-512 (128 hex chars); older FIPS versions
17+
# use HMAC-SHA-256 (64 hex chars). Take the whole captured hash; the
18+
# static_assert on sizeof(verifyCore) guards against wrong length at
19+
# compile time after this script runs.
20+
NEWHASH=$(echo "$OUT" | head -n1 | tr -d '[:space:]')
1721
if test -n "$NEWHASH"
1822
then
1923
cp wolfcrypt/src/fips_test.c wolfcrypt/src/fips_test.c.bak

tests/api/test_mldsa.c

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,9 +752,20 @@ int test_wc_dilithium_sign_pubonly_fails(void)
752752
/* Import only the public key into a fresh key object. */
753753
ExpectIntEQ(wc_dilithium_import_public(pubBuf, pubLen, pubOnlyKey), 0);
754754

755-
/* Signing with a public-key-only object must fail. */
755+
/* Signing with a public-key-only object must fail.
756+
*
757+
* In FIPS v7.0.0 mode the ML-DSA sign wrappers enforce the
758+
* privateKeyReadEnable contract (FIPS 140-3 sec 7.10.2 CSP access
759+
* control); without unlocking, the wrapper short-circuits to
760+
* FIPS_PRIVATE_KEY_LOCKED_E before reaching the no-private-key
761+
* detection. Unlock briefly so this test exercises the underlying
762+
* BAD_FUNC_ARG path it is designed to verify. The
763+
* PRIVATE_KEY_UNLOCK / PRIVATE_KEY_LOCK macros expand to no-ops in
764+
* non-FIPS builds. */
765+
PRIVATE_KEY_UNLOCK();
756766
ExpectIntEQ(wc_dilithium_sign_ctx_msg(NULL, 0, msg, sizeof(msg), sig,
757767
&sigLen, pubOnlyKey, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
768+
PRIVATE_KEY_LOCK();
758769

759770
DoExpectIntEQ(wc_FreeRng(&rng), 0);
760771
wc_dilithium_free(pubOnlyKey);
@@ -1236,6 +1247,12 @@ int test_wc_dilithium_sign_vfy(void)
12361247

12371248
ExpectIntEQ(wc_InitRng(&rng), 0);
12381249

1250+
/* FIPS v7.0.0 ML-DSA sign wrappers enforce the privateKeyReadEnable
1251+
* contract (FIPS 140-3 sec 7.10.2 CSP access control); unlock for the
1252+
* duration of this test's signing operations and re-lock at the end.
1253+
* Macros expand to no-ops in non-FIPS builds. */
1254+
PRIVATE_KEY_UNLOCK();
1255+
12391256
#ifndef WOLFSSL_NO_ML_DSA_44
12401257
ExpectIntEQ(wc_dilithium_init(key), 0);
12411258
ExpectIntEQ(wc_dilithium_set_level(key, WC_ML_DSA_44), 0);
@@ -1300,6 +1317,8 @@ int test_wc_dilithium_sign_vfy(void)
13001317
wc_dilithium_free(key);
13011318
#endif
13021319

1320+
PRIVATE_KEY_LOCK();
1321+
13031322
wc_FreeRng(&rng);
13041323
XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER);
13051324
XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER);
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/* fips_cast_bench.c
2+
*
3+
* Copyright (C) 2006-2026 wolfSSL Inc.
4+
*
5+
* This file is part of wolfSSL.
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 3 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-1335, USA
20+
*/
21+
22+
/* FIPS CAST benchmark.
23+
*
24+
* Measures the wall-clock cost of each Conditional Algorithm Self-Test (CAST)
25+
* defined by the wolfCrypt v7.0.0 FIPS module so operators can budget module
26+
* power-on latency on resource-constrained operational environments (DSP,
27+
* MCU) where every additional CAST is directly observable as boot-time delay.
28+
*
29+
* Compiled only when HAVE_FIPS is defined (see wolfcrypt/benchmark/include.am
30+
* BUILD_FIPS gate). Calls wc_RunCast_fips(id) repeatedly per CAST and reports
31+
* mean / stddev / min / max for each, plus total time for one pass over all
32+
* enabled CASTs (the cost paid by callers that invoke wc_RunAllCast_fips() at
33+
* application start).
34+
*
35+
* Citations:
36+
* FIPS 140-3 sec 7.10 (Self-Tests) - CAST framework
37+
* FIPS 140-3 IG 10.3.A - Algorithm-by-algorithm CAST coverage
38+
* ISO/IEC 19790:2012 sec 7.10.2 - Conditional self-test execution
39+
*/
40+
41+
#ifdef HAVE_CONFIG_H
42+
#include <config.h>
43+
#endif
44+
45+
#if !defined(WOLFSSL_USER_SETTINGS) && !defined(WOLFSSL_NO_OPTIONS_H)
46+
#include <wolfssl/options.h>
47+
#endif
48+
#include <wolfssl/wolfcrypt/settings.h> /* also picks up user_settings.h */
49+
50+
#ifdef HAVE_FIPS
51+
52+
#include <wolfssl/version.h>
53+
#include <wolfssl/wolfcrypt/types.h>
54+
#include <wolfssl/wolfcrypt/error-crypt.h>
55+
#include <wolfssl/wolfcrypt/fips_test.h>
56+
57+
#include <stdio.h>
58+
#include <stdlib.h>
59+
#include <string.h>
60+
#include <math.h>
61+
#include <limits.h>
62+
63+
#ifdef _WIN32
64+
#define WIN32_LEAN_AND_MEAN
65+
#include <windows.h>
66+
#else
67+
#include <time.h>
68+
#endif
69+
70+
71+
#define BENCH_DEFAULT_ITERS 10
72+
73+
/* Map FIPS_CAST_* enum value to a printable name. Kept in sync with
74+
* wolfssl/wolfcrypt/fips_test.h FipsCastId enum. */
75+
static const char* cast_name(int id)
76+
{
77+
switch (id) {
78+
case FIPS_CAST_AES_CBC: return "AES-CBC";
79+
case FIPS_CAST_AES_GCM: return "AES-GCM";
80+
case FIPS_CAST_HMAC_SHA1: return "HMAC-SHA-1";
81+
case FIPS_CAST_HMAC_SHA2_256: return "HMAC-SHA2-256";
82+
case FIPS_CAST_HMAC_SHA2_512: return "HMAC-SHA2-512";
83+
case FIPS_CAST_HMAC_SHA3_256: return "HMAC-SHA3-256";
84+
case FIPS_CAST_DRBG: return "DRBG (SHA-256)";
85+
case FIPS_CAST_RSA_SIGN_PKCS1v15: return "RSA-SIGN-PKCS1v15";
86+
case FIPS_CAST_ECC_CDH: return "ECC-CDH";
87+
case FIPS_CAST_ECC_PRIMITIVE_Z: return "ECC-Primitive-Z";
88+
case FIPS_CAST_DH_PRIMITIVE_Z: return "DH-Primitive-Z";
89+
case FIPS_CAST_ECDSA: return "ECDSA";
90+
case FIPS_CAST_KDF_TLS12: return "KDF-TLS12";
91+
case FIPS_CAST_KDF_TLS13: return "KDF-TLS13";
92+
case FIPS_CAST_KDF_SSH: return "KDF-SSH";
93+
#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)
94+
case FIPS_CAST_KDF_SRTP: return "KDF-SRTP";
95+
case FIPS_CAST_ED25519: return "Ed25519";
96+
case FIPS_CAST_ED448: return "Ed448";
97+
case FIPS_CAST_PBKDF2: return "PBKDF2";
98+
#endif
99+
#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(7,0)
100+
case FIPS_CAST_AES_ECB: return "AES-ECB";
101+
case FIPS_CAST_ML_KEM: return "ML-KEM";
102+
case FIPS_CAST_ML_DSA: return "ML-DSA";
103+
case FIPS_CAST_LMS: return "LMS";
104+
case FIPS_CAST_XMSS: return "XMSS";
105+
case FIPS_CAST_DRBG_SHA512: return "DRBG (SHA-512)";
106+
case FIPS_CAST_SLH_DSA: return "SLH-DSA";
107+
case FIPS_CAST_AES_CMAC: return "AES-CMAC";
108+
case FIPS_CAST_SHAKE: return "SHAKE";
109+
case FIPS_CAST_AES_KW: return "AES-KW";
110+
#endif
111+
default: return "(unknown)";
112+
}
113+
}
114+
115+
116+
/* Monotonic clock in nanoseconds. POSIX clock_gettime(CLOCK_MONOTONIC) on
117+
* Unix-like systems; QueryPerformanceCounter on Windows. */
118+
static long long now_ns(void)
119+
{
120+
#ifdef _WIN32
121+
static LARGE_INTEGER freq = { 0 };
122+
LARGE_INTEGER count;
123+
if (freq.QuadPart == 0)
124+
QueryPerformanceFrequency(&freq);
125+
QueryPerformanceCounter(&count);
126+
/* Multiply before divide to keep precision; freq is typically 10MHz. */
127+
return (long long)((count.QuadPart * 1000000000LL) / freq.QuadPart);
128+
#else
129+
struct timespec ts;
130+
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
131+
return 0;
132+
return (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec;
133+
#endif
134+
}
135+
136+
137+
/* Run a single CAST iters times, populate stats (in milliseconds).
138+
* Returns 0 on success, non-zero on first CAST failure. */
139+
static int run_one_cast(int id, int iters,
140+
double* out_mean_ms, double* out_stddev_ms,
141+
double* out_min_ms, double* out_max_ms)
142+
{
143+
int i;
144+
long long total = 0;
145+
long long mn = LLONG_MAX;
146+
long long mx = 0;
147+
long long* samples;
148+
double mean_ns;
149+
double variance_acc = 0.0;
150+
151+
if (iters <= 0)
152+
return BAD_FUNC_ARG;
153+
154+
samples = (long long*)XMALLOC((size_t)iters * sizeof(long long), NULL,
155+
DYNAMIC_TYPE_TMP_BUFFER);
156+
if (samples == NULL)
157+
return MEMORY_E;
158+
159+
for (i = 0; i < iters; i++) {
160+
long long t0, t1, dt;
161+
int rc;
162+
163+
t0 = now_ns();
164+
rc = wc_RunCast_fips(id);
165+
t1 = now_ns();
166+
if (rc != 0) {
167+
XFREE(samples, NULL, DYNAMIC_TYPE_TMP_BUFFER);
168+
return rc;
169+
}
170+
dt = t1 - t0;
171+
if (dt < 0)
172+
dt = 0;
173+
samples[i] = dt;
174+
total += dt;
175+
if (dt < mn)
176+
mn = dt;
177+
if (dt > mx)
178+
mx = dt;
179+
}
180+
181+
mean_ns = (double)total / (double)iters;
182+
for (i = 0; i < iters; i++) {
183+
double d = (double)samples[i] - mean_ns;
184+
variance_acc += d * d;
185+
}
186+
XFREE(samples, NULL, DYNAMIC_TYPE_TMP_BUFFER);
187+
188+
*out_mean_ms = mean_ns / 1.0e6;
189+
*out_stddev_ms = sqrt(variance_acc / (double)iters) / 1.0e6;
190+
*out_min_ms = (double)mn / 1.0e6;
191+
*out_max_ms = (double)mx / 1.0e6;
192+
return 0;
193+
}
194+
195+
196+
static void usage(const char* prog)
197+
{
198+
printf("usage: %s [-i ITERS] [-c CAST_ID] [-l]\n", prog);
199+
printf(" -i ITERS iterations per CAST (default %d)\n",
200+
BENCH_DEFAULT_ITERS);
201+
printf(" -c CAST_ID benchmark only the named CAST id\n");
202+
printf(" -l list CAST ids and names; do not run\n");
203+
printf(" -h show this help\n");
204+
}
205+
206+
207+
int main(int argc, char** argv)
208+
{
209+
int iters = BENCH_DEFAULT_ITERS;
210+
int single = -1;
211+
int list_only = 0;
212+
int i;
213+
int first, last;
214+
int failures = 0;
215+
int run_count = 0;
216+
double total_mean_ms = 0.0;
217+
218+
for (i = 1; i < argc; i++) {
219+
if (XSTRCMP(argv[i], "-i") == 0 && i + 1 < argc) {
220+
iters = atoi(argv[++i]);
221+
if (iters <= 0) {
222+
fprintf(stderr, "-i requires a positive iteration count\n");
223+
return 2;
224+
}
225+
} else if (XSTRCMP(argv[i], "-c") == 0 && i + 1 < argc) {
226+
single = atoi(argv[++i]);
227+
} else if (XSTRCMP(argv[i], "-l") == 0) {
228+
list_only = 1;
229+
} else if (XSTRCMP(argv[i], "-h") == 0
230+
|| XSTRCMP(argv[i], "--help") == 0) {
231+
usage(argv[0]);
232+
return 0;
233+
} else {
234+
fprintf(stderr, "unknown argument: %s\n", argv[i]);
235+
usage(argv[0]);
236+
return 2;
237+
}
238+
}
239+
240+
if (list_only) {
241+
printf("FIPS CAST IDs (FIPS_CAST_COUNT = %d):\n", FIPS_CAST_COUNT);
242+
for (i = 0; i < FIPS_CAST_COUNT; i++)
243+
printf(" %2d %s\n", i, cast_name(i));
244+
return 0;
245+
}
246+
247+
if (single >= 0 && single >= FIPS_CAST_COUNT) {
248+
fprintf(stderr, "CAST id %d out of range (0..%d)\n",
249+
single, FIPS_CAST_COUNT - 1);
250+
return 2;
251+
}
252+
253+
printf("wolfCrypt FIPS CAST benchmark\n");
254+
printf("Library version: %s\n", LIBWOLFSSL_VERSION_STRING);
255+
printf("FIPS_CAST_COUNT: %d\n", FIPS_CAST_COUNT);
256+
printf("Iterations per CAST: %d\n", iters);
257+
printf("Clock: %s\n",
258+
#ifdef _WIN32
259+
"QueryPerformanceCounter"
260+
#else
261+
"clock_gettime(CLOCK_MONOTONIC)"
262+
#endif
263+
);
264+
printf("\n");
265+
266+
/* Prime: run every CAST once via wc_RunAllCast_fips() so each CAST
267+
* reaches FIPS_CAST_STATE_SUCCESS before we begin measuring. This
268+
* isolates the per-CAST KAT runtime cost from the cascading
269+
* recursive-CAST init chain that fires on the first invocation of a
270+
* cold CAST whose KAT internally calls FIPS-wrapped primitives whose
271+
* own CASTs are still in INIT state. Customers calling
272+
* wc_RunAllCast_fips() at boot pay this one-time cost up front, so
273+
* priming here matches that real-world workflow. */
274+
{
275+
int prime_rc = wc_RunAllCast_fips();
276+
if (prime_rc != 0) {
277+
fprintf(stderr,
278+
"wc_RunAllCast_fips() prime returned %d - some CASTs may have failed.\n"
279+
"Per-CAST measurements continue but failed CASTs will report errors.\n\n",
280+
prime_rc);
281+
}
282+
}
283+
284+
printf("ID | Name | Mean(ms) | StdDev(ms) | Min(ms) "
285+
"| Max(ms)\n");
286+
printf("---+---------------------+----------+------------+---------"
287+
"+---------\n");
288+
289+
first = (single >= 0) ? single : 0;
290+
last = (single >= 0) ? single + 1 : FIPS_CAST_COUNT;
291+
292+
for (i = first; i < last; i++) {
293+
double mean_ms = 0, sd_ms = 0, mn_ms = 0, mx_ms = 0;
294+
int rc = run_one_cast(i, iters, &mean_ms, &sd_ms, &mn_ms, &mx_ms);
295+
if (rc != 0) {
296+
printf("%2d | %-19s | FAILED rc=%d (%s)\n",
297+
i, cast_name(i), rc, wc_GetErrorString(rc));
298+
failures++;
299+
continue;
300+
}
301+
printf("%2d | %-19s | %8.3f | %10.3f | %7.3f | %7.3f\n",
302+
i, cast_name(i), mean_ms, sd_ms, mn_ms, mx_ms);
303+
total_mean_ms += mean_ms;
304+
run_count++;
305+
}
306+
307+
printf("\n");
308+
if (run_count > 0) {
309+
printf("Sum of mean CAST times (one wc_RunAllCast_fips() pass): "
310+
"%.3f ms\n", total_mean_ms);
311+
}
312+
if (failures > 0) {
313+
printf("WARN: %d CAST(s) failed.\n", failures);
314+
return 1;
315+
}
316+
return 0;
317+
}
318+
319+
#else /* !HAVE_FIPS */
320+
321+
#include <stdio.h>
322+
323+
int main(void)
324+
{
325+
fprintf(stderr,
326+
"fips_cast_bench: built without HAVE_FIPS - nothing to measure\n");
327+
return 0;
328+
}
329+
330+
#endif /* HAVE_FIPS */

0 commit comments

Comments
 (0)