diff --git a/src/wp_drbg.c b/src/wp_drbg.c
index ba516772..a92188f8 100644
--- a/src/wp_drbg.c
+++ b/src/wp_drbg.c
@@ -149,6 +149,9 @@ static void wp_drbg_free(wp_DrbgCtx* ctx)
}
static int wp_drbg_uninstantiate(wp_DrbgCtx* ctx);
+static int wp_drbg_reseed(wp_DrbgCtx* ctx, int predResist,
+ const unsigned char* entropy, size_t entropyLen,
+ const unsigned char* addIn, size_t addInLen);
/**
* Instantiate a new DRBG.
@@ -295,12 +298,20 @@ static int wp_drbg_uninstantiate(wp_DrbgCtx* ctx)
* Generate random data.
*
* @param [in, out] ctx DRBG context object.
+ * @param [out] out Buffer to hold generated random data.
+ * @param [in] outLen Number of random bytes to generate.
* @param [in] strength Strength in bits required.
- * @param [in] predResist Prediction resistance required.
- * @param [in] addIn Additional input data to seed with.
+ * @param [in] predResist Prediction resistance required. When set, the
+ * DRBG is reseeded with fresh entropy before
+ * generating (SP 800-90A 9.3.1). Honored only when
+ * built with WP_HAVE_DRBG_RESEED; otherwise
+ * ignored.
+ * @param [in] addIn Additional input data. Ignored: wolfCrypt has no
+ * public generate API that accepts additional
+ * input on a live WC_RNG.
* @param [in] addInLen Length of additional input data in bytes.
* @return 1 on success.
- * @return 0 on success.
+ * @return 0 on failure.
*/
static int wp_drbg_generate(wp_DrbgCtx* ctx, unsigned char* out,
size_t outLen, unsigned int strength, int predResist,
@@ -311,8 +322,6 @@ static int wp_drbg_generate(wp_DrbgCtx* ctx, unsigned char* out,
WOLFPROV_ENTER(WP_LOG_COMP_RNG, "wp_drbg_generate");
- (void)predResist;
-
if (strength > WP_DRBG_STRENGTH) {
ok = 0;
}
@@ -326,29 +335,50 @@ static int wp_drbg_generate(wp_DrbgCtx* ctx, unsigned char* out,
ok = 0;
}
#endif
-#if 0
+
+ if (ok && (outLen > 0xFFFFFFFFU)) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, "Request length is too big");
+ ok = 0;
+ }
+
+ /* wolfCrypt exposes no public generate API that accepts additional input on
+ * a live WC_RNG (wc_RNG_GenerateBlock passes NULL to Hash_DRBG_Generate),
+ * so addIn must be ignored. Per SP 800-90A additional_input on Generate is
+ * optional, so dropping it is standards-compliant. */
if (ok && (addInLen > 0)) {
- rc = wc_RNG_DRBG_Reseed(ctx->rng, addIn, addInLen);
- if (rc != 0) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, "Additional data ignored");
+ (void)addIn;
+ }
+
+ /* SP 800-90A 9.3.1: prediction resistance requires reseeding with fresh
+ * entropy immediately before generating. wolfCrypt's public generate API
+ * has no PR flag, so emulate it via the in-place reseed path (fresh OS
+ * entropy, no caller-supplied material). Only available where a true
+ * reseed exists; without it the fallback re-instantiates the whole DRBG,
+ * so predResist is ignored. */
+#ifdef WP_HAVE_DRBG_RESEED
+ if (ok && predResist) {
+ if (!wp_drbg_reseed(ctx, 1, NULL, 0, NULL, 0)) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG,
+ "Prediction resistance reseed failed");
ok = 0;
}
}
#else
- (void)addIn;
- (void)addInLen;
+ (void)predResist;
#endif
- if (ok && (outLen > 0xFFFFFFFFU)) {
- ok = 0;
- }
+
if (ok) {
rc = wc_RNG_GenerateBlock(ctx->rng, out, (word32)outLen);
if (rc != 0) {
- WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_RNG_GenerateBlock", rc);
+ WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wc_RNG_GenerateBlock",
+ rc);
ok = 0;
}
}
- WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
+ WOLFPROV_LEAVE(WP_LOG_COMP_RNG, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__),
+ ok);
return ok;
}
@@ -366,7 +396,7 @@ static int wp_drbg_generate(wp_DrbgCtx* ctx, unsigned char* out,
* @param [in] addInLen Length of additional input data in bytes.
* @param [in] params Other parameters.
* @return 1 on success.
- * @return 0 on success.
+ * @return 0 on failure.
*/
static int wp_drbg_reseed(wp_DrbgCtx* ctx, int predResist,
const unsigned char* entropy, size_t entropyLen,
@@ -504,7 +534,7 @@ static int wp_drbg_reseed(wp_DrbgCtx* ctx, int predResist,
*
* @param [in, out] ctx DRBG context object.
* @return 1 on success.
- * @return 0 on success.
+ * @return 0 on failure.
*/
static int wp_drbg_enable_locking(wp_DrbgCtx* ctx)
{
@@ -539,7 +569,7 @@ static int wp_drbg_enable_locking(wp_DrbgCtx* ctx)
*
* @param [in, out] ctx DRBG context object.
* @return 1 on success.
- * @return 0 on success.
+ * @return 0 on failure.
*/
static int wp_drbg_lock(wp_DrbgCtx* ctx)
{
@@ -690,6 +720,7 @@ static int wp_drbg_set_ctx_params(wp_DrbgCtx* ctx, const OSSL_PARAM params[])
*
* @param [in] ctx DRBG context object. Unused.
* @return 1 on success.
+ * @return 0 on failure.
*/
static int wp_drbg_verify_zeroization(wp_DrbgCtx* ctx)
{
diff --git a/src/wp_pbkdf2.c b/src/wp_pbkdf2.c
index e53dcabe..f1a0a4f3 100644
--- a/src/wp_pbkdf2.c
+++ b/src/wp_pbkdf2.c
@@ -41,6 +41,17 @@
OSSL_PARAM_uint64(OSSL_KDF_PARAM_ITER, NULL)
+/* SP 800-132 lower bounds applied for the PKCS5=0 opt-in, matching OpenSSL's
+ * pbkdf2 KDF (providers/implementations/kdfs/pbkdf2.c). Only key length, salt
+ * length and iteration count are enforced. OpenSSL's handling of a minimum
+ * password length varies across versions (e.g. 3.5 accepts an empty password
+ * even with the checks enabled), so that bound is intentionally not applied
+ * here. FIPS enforcement is left to wolfCrypt's wc_PBKDF2_ex. */
+#define WP_PBKDF2_MIN_KEY_LEN_BITS 112
+#define WP_PBKDF2_MIN_ITERATIONS 1000
+#define WP_PBKDF2_MIN_SALT_LEN (128 / 8)
+
+
/**
* The PBKDF2 context structure.
* Includes everything for PBKDF2.
@@ -64,7 +75,7 @@ typedef struct wp_Pbkdf2Ctx {
size_t saltSz;
/** Number of iterations. */
uint64_t iterations;
- /** Used for PKCS#5. */
+ /** SP 800-132 lower-bound checks enabled (set when PKCS5 parameter is 0). */
int pkcs5;
/** PKCS12 key usage byte used in derivation. */
int keyUse;
@@ -263,6 +274,29 @@ static int wp_kdf_pbkdf2_derive(wp_Pbkdf2Ctx* ctx, unsigned char* key,
ok = 0;
}
+ /* PKCS5 == 0 opts into the SP 800-132 lower bounds, matching OpenSSL's
+ * pbkdf2 KDF: minimum key length, salt length and iteration count. The
+ * minimum-password-length check is omitted as OpenSSL's behaviour there is
+ * version-dependent. Applied here for the non-FIPS opt-in case; in a FIPS
+ * build the equivalent SP 800-132 checks are performed inside
+ * wc_PBKDF2_ex, so they are not duplicated here. */
+ if (ok && ctx->pkcs5) {
+ if ((keyLen * 8) < WP_PBKDF2_MIN_KEY_LEN_BITS) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_PBKDF2, "PBKDF2 key size too small");
+ ok = 0;
+ }
+ if (ok && (ctx->saltSz < WP_PBKDF2_MIN_SALT_LEN)) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_PBKDF2, "PBKDF2 salt too small");
+ ok = 0;
+ }
+ if (ok && (ctx->iterations < WP_PBKDF2_MIN_ITERATIONS)) {
+ WOLFPROV_MSG_DEBUG(WP_LOG_COMP_PBKDF2,
+ "PBKDF2 iteration count too low");
+ ok = 0;
+ }
+ }
+ /* wc_PBKDF2_ex fails when ctx->iterations is 0. */
+
if (ok) {
int rc;
@@ -297,8 +331,20 @@ static int wp_kdf_pbkdf2_set_ctx_params(wp_Pbkdf2Ctx* ctx,
WOLFPROV_ENTER(WP_LOG_COMP_PBKDF2, "wp_kdf_pbkdf2_set_ctx_params");
ok = wp_pbkdf2_base_set_ctx_params(ctx, params);
- if (ok && !wp_params_get_int(params, OSSL_KDF_PARAM_PKCS5, &ctx->pkcs5)) {
- ok = 0;
+ if (ok && (params != NULL)) {
+ const OSSL_PARAM* p = OSSL_PARAM_locate_const(params,
+ OSSL_KDF_PARAM_PKCS5);
+ if (p != NULL) {
+ int pkcs5;
+ if (!OSSL_PARAM_get_int(p, &pkcs5)) {
+ ok = 0;
+ }
+ else {
+ /* PKCS5 == 0 opts into the SP 800-132 lower-bound checks; a
+ * non-zero (legacy PKCS#5) value disables them. */
+ ctx->pkcs5 = (pkcs5 == 0);
+ }
+ }
}
WOLFPROV_LEAVE(WP_LOG_COMP_PBKDF2, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
@@ -382,10 +428,14 @@ static int wp_kdf_pkcs12_derive(wp_Pbkdf2Ctx* ctx, unsigned char* key,
if (ok) {
int rc;
+ /* OpenSSL's PKCS12KDF derive loop always runs at least once, so an
+ * iteration count of 0 produces the same output as 1. wolfCrypt
+ * rejects iterations <= 0, so map 0 to 1 to match OpenSSL. */
+ int iterations = (ctx->iterations == 0) ? 1 : (int)ctx->iterations;
PRIVATE_KEY_UNLOCK();
rc = wc_PKCS12_PBKDF_ex(key, ctx->password, (int)ctx->passwordSz,
- ctx->salt, (int)ctx->saltSz, (int)ctx->iterations, (int)keyLen,
+ ctx->salt, (int)ctx->saltSz, iterations, (int)keyLen,
ctx->mdType, ctx->keyUse, NULL);
PRIVATE_KEY_LOCK();
if (rc != 0) {
diff --git a/test/include.am b/test/include.am
index e404ad56..8a0e9528 100644
--- a/test/include.am
+++ b/test/include.am
@@ -29,6 +29,7 @@ test_unit_test_SOURCES = \
test/test_kbkdf.c \
test/test_logging.c \
test/test_pbe.c \
+ test/test_pbkdf2.c \
test/test_pkey.c \
test/test_pkcs7_x509.c \
test/test_mlkem.c \
diff --git a/test/test_pbkdf2.c b/test/test_pbkdf2.c
new file mode 100644
index 00000000..a5652e94
--- /dev/null
+++ b/test/test_pbkdf2.c
@@ -0,0 +1,295 @@
+/* test_pbkdf2.c
+ *
+ * Copyright (C) 2006-2025 wolfSSL Inc.
+ *
+ * This file is part of wolfProvider.
+ *
+ * wolfProvider 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfProvider 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 wolfProvider. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "unit.h"
+
+#if defined(WP_HAVE_PBE) && defined(WP_HAVE_SHA256)
+
+/*
+ * Run an EVP_KDF derive for the named KDF with the given parameters.
+ *
+ * Returns 0 when the derive call ran to completion (whether it succeeded or
+ * was rejected); the EVP_KDF_derive() return value is stored in *deriveRet.
+ * Returns 1 only on test-infrastructure failure (fetch / ctx allocation).
+ */
+static int test_kdf_derive(OSSL_LIB_CTX* libCtx, const char* name,
+ OSSL_PARAM* params, unsigned char* out, size_t outLen, int* deriveRet)
+{
+ int err = 0;
+ EVP_KDF* kdf = NULL;
+ EVP_KDF_CTX* kctx = NULL;
+
+ *deriveRet = -1;
+
+ kdf = EVP_KDF_fetch(libCtx, name, NULL);
+ if (kdf == NULL) {
+ PRINT_MSG("Failed to fetch KDF");
+ err = 1;
+ }
+ if (err == 0) {
+ kctx = EVP_KDF_CTX_new(kdf);
+ if (kctx == NULL) {
+ PRINT_MSG("Failed to create KDF context");
+ err = 1;
+ }
+ }
+ if (err == 0) {
+ *deriveRet = EVP_KDF_derive(kctx, out, outLen, params);
+ }
+
+ EVP_KDF_CTX_free(kctx);
+ EVP_KDF_free(kdf);
+ return err;
+}
+
+/* Derive a PKCS12KDF key with the given iteration count on one provider. */
+static int pkcs12_derive(OSSL_LIB_CTX* libCtx, uint64_t iter,
+ unsigned char* out, size_t outLen, int* deriveRet)
+{
+ static unsigned char pass[] = "password";
+ static unsigned char salt[16] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+ };
+ int id = 1;
+ char digest[] = "SHA256";
+ OSSL_PARAM params[6];
+ OSSL_PARAM* p = params;
+
+ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD,
+ pass, sizeof(pass) - 1);
+ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT,
+ salt, sizeof(salt));
+ *p++ = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ITER, &iter);
+ *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, digest, 0);
+ *p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS12_ID, &id);
+ *p = OSSL_PARAM_construct_end();
+
+ return test_kdf_derive(libCtx, "PKCS12KDF", params, out, outLen, deriveRet);
+}
+
+/*
+ * PKCS12KDF: OpenSSL and wolfProvider must produce the same key, including the
+ * iterations==0 edge case (both run a single iteration), and iterations==0
+ * must equal iterations==1.
+ */
+static int test_pkcs12_kdf(void)
+{
+ int err = 0;
+ int i;
+ uint64_t iters[3] = { 0, 1, 2048 };
+ unsigned char oKey[24];
+ unsigned char wKey[24];
+ unsigned char wKey1[24];
+ size_t outLen = sizeof(oKey);
+
+ for (i = 0; (err == 0) && (i < 3); i++) {
+ int oRet = 0;
+ int wRet = 0;
+
+ memset(oKey, 0, outLen);
+ memset(wKey, 0, outLen);
+
+ err = pkcs12_derive(osslLibCtx, iters[i], oKey, outLen, &oRet);
+ if (err == 0) {
+ err = pkcs12_derive(wpLibCtx, iters[i], wKey, outLen, &wRet);
+ }
+ if ((err == 0) && ((oRet <= 0) || (wRet <= 0))) {
+ PRINT_MSG("PKCS12KDF derive failed");
+ err = 1;
+ }
+ if ((err == 0) && (memcmp(oKey, wKey, outLen) != 0)) {
+ PRINT_BUFFER("OpenSSL key", oKey, outLen);
+ PRINT_BUFFER("wolfProvider key", wKey, outLen);
+ err = 1;
+ }
+ }
+
+ /* iterations==0 must match iterations==1 for wolfProvider. */
+ if (err == 0) {
+ int rc0 = 0;
+ int rc1 = 0;
+
+ memset(wKey, 0, outLen);
+ memset(wKey1, 0, outLen);
+
+ err = pkcs12_derive(wpLibCtx, 0, wKey, outLen, &rc0);
+ if (err == 0) {
+ err = pkcs12_derive(wpLibCtx, 1, wKey1, outLen, &rc1);
+ }
+ if ((err == 0) && ((rc0 <= 0) || (rc1 <= 0) ||
+ (memcmp(wKey, wKey1, outLen) != 0))) {
+ PRINT_MSG("PKCS12KDF iterations==0 differs from iterations==1");
+ err = 1;
+ }
+ }
+
+ return err;
+}
+
+/* Derive a PBKDF2 key on one provider, optionally setting the PKCS5 param. */
+static int pbkdf2_derive(OSSL_LIB_CTX* libCtx, uint64_t iter,
+ const unsigned char* salt, size_t saltLen, int setPkcs5, int pkcs5,
+ unsigned char* out, size_t outLen, int* deriveRet)
+{
+ static unsigned char pass[] = "password";
+ char digest[] = "SHA256";
+ OSSL_PARAM params[6];
+ OSSL_PARAM* p = params;
+
+ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD,
+ pass, sizeof(pass) - 1);
+ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT,
+ (void*)salt, saltLen);
+ *p++ = OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_ITER, &iter);
+ *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, digest, 0);
+ if (setPkcs5) {
+ *p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS5, &pkcs5);
+ }
+ *p = OSSL_PARAM_construct_end();
+
+ return test_kdf_derive(libCtx, "PBKDF2", params, out, outLen, deriveRet);
+}
+
+/*
+ * Derive on both providers and check they agree: same accept/reject decision,
+ * the decision matches expectOk, and the keys are identical when both succeed.
+ */
+static int pbkdf2_compare(uint64_t iter, const unsigned char* salt,
+ size_t saltLen, int setPkcs5, int pkcs5, size_t outLen, int expectOk)
+{
+ int err = 0;
+ int oRet = 0;
+ int wRet = 0;
+ unsigned char oKey[64];
+ unsigned char wKey[64];
+
+ memset(oKey, 0, sizeof(oKey));
+ memset(wKey, 0, sizeof(wKey));
+
+ err = pbkdf2_derive(osslLibCtx, iter, salt, saltLen, setPkcs5, pkcs5,
+ oKey, outLen, &oRet);
+ if (err == 0) {
+ err = pbkdf2_derive(wpLibCtx, iter, salt, saltLen, setPkcs5, pkcs5,
+ wKey, outLen, &wRet);
+ }
+
+ /* Both providers must agree on accept vs reject. */
+ if ((err == 0) && ((oRet > 0) != (wRet > 0))) {
+ PRINT_MSG("PBKDF2 OpenSSL/wolfProvider accept-reject mismatch");
+ err = 1;
+ }
+ /* And that decision must be what the case expects. */
+ if ((err == 0) && ((oRet > 0) != (expectOk != 0))) {
+ PRINT_MSG("PBKDF2 result not as expected");
+ err = 1;
+ }
+ /* When both succeed, the derived keys must be identical. */
+ if ((err == 0) && (oRet > 0) && (memcmp(oKey, wKey, outLen) != 0)) {
+ PRINT_BUFFER("OpenSSL key", oKey, outLen);
+ PRINT_BUFFER("wolfProvider key", wKey, outLen);
+ err = 1;
+ }
+
+ return err;
+}
+
+/*
+ * PBKDF2: OpenSSL and wolfProvider must agree, including the SP 800-132
+ * lower bounds that are enforced only for the PKCS5=0 opt-in.
+ */
+static int test_pbkdf2_bounds(void)
+{
+ int err = 0;
+ static const unsigned char salt16[16] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+ };
+ static const unsigned char salt8[8] = {
+ 1, 2, 3, 4, 5, 6, 7, 8
+ };
+
+ /* Default (no PKCS5): standard parameters, keys match. */
+ PRINT_MSG("PBKDF2 default params");
+ err = pbkdf2_compare(2048, salt16, sizeof(salt16), 0, 0, 32, 1);
+
+ /* Default (no PKCS5): low iteration count still accepted (checks off). */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 default, low iteration count accepted");
+ err = pbkdf2_compare(10, salt16, sizeof(salt16), 0, 0, 32, 1);
+ }
+
+ /* PKCS5=0 opt-in: compliant parameters, keys match. */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 PKCS5=0 compliant");
+ err = pbkdf2_compare(2000, salt16, sizeof(salt16), 1, 0, 32, 1);
+ }
+
+ /* PKCS5=0 opt-in: iteration count < 1000 rejected by both. */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 PKCS5=0 iteration count too low");
+ err = pbkdf2_compare(10, salt16, sizeof(salt16), 1, 0, 32, 0);
+ }
+
+ /* PKCS5=0 opt-in: salt < 128 bits rejected by both. */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 PKCS5=0 salt too short");
+ err = pbkdf2_compare(2000, salt8, sizeof(salt8), 1, 0, 32, 0);
+ }
+
+ /* PKCS5=0 opt-in: key < 112 bits (13 bytes) rejected by both. */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 PKCS5=0 key too short");
+ err = pbkdf2_compare(2000, salt16, sizeof(salt16), 1, 0, 13, 0);
+ }
+
+ /* PKCS5=1: legacy mode disables the checks, low iteration accepted. */
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 PKCS5=1 disables checks");
+ err = pbkdf2_compare(10, salt16, sizeof(salt16), 1, 1, 32, 1);
+ }
+
+ return err;
+}
+
+int test_pbkdf2(void *data)
+{
+ int err = 0;
+
+ (void)data;
+
+ PRINT_MSG("PKCS12KDF OpenSSL vs wolfProvider");
+ err = test_pkcs12_kdf();
+
+ if (err == 0) {
+ PRINT_MSG("PBKDF2 OpenSSL vs wolfProvider");
+ err = test_pbkdf2_bounds();
+ }
+
+ return err;
+}
+
+#endif /* WP_HAVE_PBE && WP_HAVE_SHA256 */
diff --git a/test/unit.c b/test/unit.c
index b0d38f70..a4035e10 100644
--- a/test/unit.c
+++ b/test/unit.c
@@ -468,6 +468,9 @@ TEST_CASE test_case[] = {
#ifdef WP_HAVE_PBE
#if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS)
TEST_DECL(test_pbe, NULL),
+ #ifdef WP_HAVE_SHA256
+ TEST_DECL(test_pbkdf2, NULL),
+ #endif
#endif
#endif
diff --git a/test/unit.h b/test/unit.h
index a459ca1b..6d6f780d 100644
--- a/test/unit.h
+++ b/test/unit.h
@@ -466,6 +466,9 @@ int test_ec_fromdata_oversize(void* data);
#ifdef WP_HAVE_PBE
int test_pbe(void *data);
+#if defined(WP_HAVE_SHA256)
+int test_pbkdf2(void *data);
+#endif
#endif /* WP_HAVE_PBE */
#if defined(WP_HAVE_ED25519) || defined(WP_HAVE_ED448)