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)