From f821937f95d86fa5382a03d77c82cee3ead70881 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 28 Jul 2026 19:18:15 -0700 Subject: [PATCH 01/11] Encrypt PKCS#8 keys when a cipher is set on the PrivateKeyInfo encoder --- include/wolfprovider/settings.h | 7 ++ src/wp_dec_pem2der.c | 7 +- src/wp_dh_kmgmt.c | 81 ++++++++++++++++----- src/wp_ecc_kmgmt.c | 29 ++++++-- src/wp_ecx_kmgmt.c | 12 +++- src/wp_mldsa_kmgmt.c | 12 +++- src/wp_rsa_kmgmt.c | 15 ++-- test/test_dh.c | 48 +++++++++++++ test/test_ecc.c | 11 +++ test/test_ecx.c | 11 +++ test/test_mldsa.c | 10 +++ test/test_pkey.c | 122 ++++++++++++++++++++++++++++++++ test/test_rsa.c | 11 +++ test/unit.h | 8 ++- 14 files changed, 344 insertions(+), 40 deletions(-) diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index d5325492..438d5812 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -43,6 +43,13 @@ #define WP_HAVE_DRBG_RESEED #endif +/* The PKCS#8 encrypt/decrypt helpers need PKCS#8 and password-based key + * derivation. wolfSSL derives WOLFSSL_ENCRYPTED_KEYS from OPENSSL_EXTRA, which + * a FIPS build does not set even though both are present. */ +#if defined(HAVE_PKCS8) && !defined(NO_PWDBASED) + #define WP_HAVE_PKCS8_ENC +#endif + #define WP_HAVE_DIGEST #if !defined(NO_MD5) #define WP_HAVE_MD5 diff --git a/src/wp_dec_pem2der.c b/src/wp_dec_pem2der.c index 745edf93..100b3b43 100644 --- a/src/wp_dec_pem2der.c +++ b/src/wp_dec_pem2der.c @@ -320,17 +320,20 @@ static int wp_pem2der_decode_data(const unsigned char* data, word32 len, dataFormat = "type-specific"; obj = OSSL_OBJECT_PKEY; } -#ifdef WOLFSSL_ENCRYPTED_KEYS else if (XMEMCMP(data, "-----BEGIN ENCRYPTED PRIVATE KEY-----", 37) == 0) { type = PKCS8_ENC_PRIVATEKEY_TYPE; dataType = NULL; dataFormat = "PrivateKeyInfo"; obj = OSSL_OBJECT_PKEY; + /* The body is base64 only; the PBES2 layer is decrypted later by the + * EncryptedPrivateKeyInfo decoder. The callback fields exist only when + * wolfSSL itself was built with encrypted-key support. */ +#ifdef WOLFSSL_ENCRYPTED_KEYS info.passwd_cb = wp_pem_password_cb; info.passwd_userdata = (void*)&wpPwCb; - } #endif + } else { ok = 0; } diff --git a/src/wp_dh_kmgmt.c b/src/wp_dh_kmgmt.c index 5e2b0e5d..f3adbce7 100644 --- a/src/wp_dh_kmgmt.c +++ b/src/wp_dh_kmgmt.c @@ -2359,7 +2359,7 @@ static int wp_dh_dec_send_params(wp_Dh* dh, OSSL_CALLBACK *dataCb, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Decode an encrypted PKCS#8 DER DH private key into the DH key object. * @@ -2476,7 +2476,7 @@ static int wp_dh_decode(wp_DhEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { if (!wp_dh_decode_pki(dh, data, len)) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC if (!wp_dh_decode_enc_pki(dh, data, len, pwCb, pwCbArg)) #endif { @@ -2637,20 +2637,18 @@ static int wp_dh_encode_spki(const wp_Dh *dh, unsigned char* keyData, } /** - * Get the PKCS#8 encoding size for the key. + * Copy a generated private key into the inner wolfSSL key if not already set. * - * @param [in] dh DH key object. - * @param [out] keyLen Length of encoding in bytes. + * @param [in] dh DH key object. * @return 1 on success. * @return 0 on failure. */ -static int wp_dh_encode_pki_size(const wp_Dh *dh, size_t* keyLen) +static int wp_dh_sync_priv_to_key(const wp_Dh *dh) { int ok = 1; int ret; - word32 len; - WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_encode_pki_size"); + WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_sync_priv_to_key"); /* If we have a generated private key that is not set in the inner key, * set it now */ @@ -2662,9 +2660,33 @@ static int wp_dh_encode_pki_size(const wp_Dh *dh, size_t* keyLen) } } - ret = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &len); - if (ret != LENGTH_ONLY_E) { - ok = 0; + WOLFPROV_LEAVE(WP_LOG_COMP_DH, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Get the PKCS#8 encoding size for the key. + * + * @param [in] dh DH key object. + * @param [out] keyLen Length of encoding in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_dh_encode_pki_size(const wp_Dh *dh, size_t* keyLen) +{ + int ok = 1; + int ret; + word32 len; + + WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_encode_pki_size"); + + ok = wp_dh_sync_priv_to_key(dh); + + if (ok) { + ret = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &len); + if (ret != LENGTH_ONLY_E) { + ok = 0; + } } if (ok) { *keyLen = len; @@ -2709,7 +2731,7 @@ static int wp_dh_encode_pki(const wp_Dh *dh, unsigned char* keyData, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Get the Encrypted PKCS#8 encoding size for the key. * @@ -2728,10 +2750,14 @@ static int wp_dh_encode_epki_size(const wp_DhEncDecCtx* ctx, const wp_Dh *dh, WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_encode_epki_size"); - /* Get the plaintext PKCS #8 length. */ - ret = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &len); - if (ret != LENGTH_ONLY_E) { - ok = 0; + ok = wp_dh_sync_priv_to_key(dh); + + if (ok) { + /* Get the plaintext PKCS #8 length. */ + ret = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &len); + if (ret != LENGTH_ONLY_E) { + ok = 0; + } } if (ok) { /* Get the size of the PBES2 EncryptedPrivateKeyInfo encoding. */ @@ -2861,11 +2887,20 @@ static int wp_dh_encode(wp_DhEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { private = 1; +#ifdef WP_HAVE_PKCS8_ENC + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ + if (ctx->cipherName != NULL) { + if (!wp_dh_encode_epki_size(ctx, key, &derLen)) { + ok = 0; + } + } + else +#endif if (!wp_dh_encode_pki_size(key, &derLen)) { ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { private = 1; if (!wp_dh_encode_epki_size(ctx, key, &derLen)) { @@ -2896,11 +2931,21 @@ static int wp_dh_encode(wp_DhEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { private = 1; +#ifdef WP_HAVE_PKCS8_ENC + if (ctx->cipherName != NULL) { + pemType = PKCS8_ENC_PRIVATEKEY_TYPE; + if (!wp_dh_encode_epki(ctx, key, derData, &derLen, pwCb, + pwCbArg)) { + ok = 0; + } + } + else +#endif if (!wp_dh_encode_pki(key, derData, &derLen)) { ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { private = 1; pemType = PKCS8_ENC_PRIVATEKEY_TYPE; diff --git a/src/wp_ecc_kmgmt.c b/src/wp_ecc_kmgmt.c index 4d6e1b57..a65a16e7 100644 --- a/src/wp_ecc_kmgmt.c +++ b/src/wp_ecc_kmgmt.c @@ -2337,7 +2337,7 @@ static int wp_ecc_dec_send_params(wp_Ecc* ecc, OSSL_CALLBACK *dataCb, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Decode an encrypted PKCS#8 DER ECC private key into the ECC key object. * @@ -2451,7 +2451,7 @@ static int wp_ecc_decode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { if (!wp_ecc_decode_pki(ecc, data, len)) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC if (!wp_ecc_decode_enc_pki(ecc, data, len, pwCb, pwCbArg)) #endif { @@ -2795,7 +2795,7 @@ static int wp_ecc_encode_pki(const wp_Ecc *ecc, unsigned char* keyData, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Get the Encrypted PKCS#8 encoding size for the key. * @@ -2957,11 +2957,20 @@ static int wp_ecc_encode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { private = 1; +#ifdef WP_HAVE_PKCS8_ENC + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ + if (ctx->cipherName != NULL) { + if (!wp_ecc_encode_epki_size(ctx, key, &derLen)) { + ok = 0; + } + } + else +#endif if (!wp_ecc_encode_pki_size(key, &derLen)) { ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { private = 1; if (!wp_ecc_encode_epki_size(ctx, key, &derLen)) { @@ -3008,11 +3017,21 @@ static int wp_ecc_encode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { private = 1; +#ifdef WP_HAVE_PKCS8_ENC + if (ctx->cipherName != NULL) { + pemType = PKCS8_ENC_PRIVATEKEY_TYPE; + if (!wp_ecc_encode_epki(ctx, key, derData, &derLen, pwCb, + pwCbArg)) { + ok = 0; + } + } + else +#endif if (!wp_ecc_encode_pki(key, derData, &derLen)) { ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { private = 1; pemType = PKCS8_ENC_PRIVATEKEY_TYPE; diff --git a/src/wp_ecx_kmgmt.c b/src/wp_ecx_kmgmt.c index d7386a8c..03bf41c7 100644 --- a/src/wp_ecx_kmgmt.c +++ b/src/wp_ecx_kmgmt.c @@ -2031,7 +2031,7 @@ static int wp_ecx_dec_send_params(wp_Ecx* ecx, const char* dataType, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Decode an encrypted PKCS#8 DER ECX private key into the ECX key object. * @@ -2152,7 +2152,7 @@ static int wp_ecx_decode(wp_EcxEncDecCtx* ctx, OSSL_CORE_BIO* cBio, if (ok) { rc = ctx->decode(data, &idx, (void*)&ecx->key, len); if (rc != 0) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /* May be an encrypted PKCS#8 key - decrypt and retry. */ if ((ctx->format != WP_ENC_FORMAT_PKI) || (!wp_ecx_decode_enc_pki(ctx, ecx, data, len, pwCb, pwCbArg))) @@ -2249,7 +2249,13 @@ static int wp_ecx_encode(wp_EcxEncDecCtx* ctx, OSSL_CORE_BIO *cBio, /* By default the plaintext DER is the source for the output encoding. */ srcData = derData; srcLen = derLen; - if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ + if (ok && ((ctx->format == WP_ENC_FORMAT_EPKI) +#ifdef WP_HAVE_PKCS8_ENC + || ((ctx->format == WP_ENC_FORMAT_PKI) && + (ctx->cipherName != NULL)) +#endif + )) { pemType = PKCS8_ENC_PRIVATEKEY_TYPE; /* The PBES2 output is larger than the plaintext and must use a * separate buffer, so size it and encrypt into fresh memory. */ diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 9fb14696..36c599fe 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1356,7 +1356,7 @@ static int wp_mldsa_dec_send_params(wp_MlDsa* mldsa, const char* dataType, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Decode an encrypted PKCS#8 DER ML-DSA private key into the ML-DSA key object. * @@ -1447,7 +1447,7 @@ static int wp_mldsa_decode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, if (ok) { rc = ctx->decode(data, &idx, (void*)&mldsa->key, len); if (rc != 0) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /* May be an encrypted PKCS#8 key - decrypt and retry. */ if ((ctx->format != WP_ENC_FORMAT_PKI) || (!wp_mldsa_decode_enc_pki(ctx, mldsa, data, len, pwCb, @@ -1568,7 +1568,13 @@ static int wp_mldsa_encode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, /* By default the plaintext DER is the source for the output encoding. */ srcData = derData; srcLen = derLen; - if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ + if (ok && ((ctx->format == WP_ENC_FORMAT_EPKI) +#ifdef WP_HAVE_PKCS8_ENC + || ((ctx->format == WP_ENC_FORMAT_PKI) && + (ctx->cipherName != NULL)) +#endif + )) { pemType = PKCS8_ENC_PRIVATEKEY_TYPE; /* The PBES2 output is larger than the plaintext and must use a * separate buffer, so size it and encrypt into fresh memory. */ diff --git a/src/wp_rsa_kmgmt.c b/src/wp_rsa_kmgmt.c index f69dfd12..1bc6c428 100644 --- a/src/wp_rsa_kmgmt.c +++ b/src/wp_rsa_kmgmt.c @@ -2705,7 +2705,7 @@ static int wp_rsa_decode_pki(wp_Rsa* rsa, unsigned char* data, word32 len) return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Decode the encrypted DER encoded RSA private key into the RSA key object. @@ -2836,7 +2836,7 @@ static int wp_rsa_decode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { if (!wp_rsa_decode_pki(rsa, data, len)) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC if (!wp_rsa_decode_enc_pki(rsa, data, len, pwCb, pwCbArg)) #endif { @@ -3428,7 +3428,7 @@ static int wp_rsa_encode_priv(const wp_Rsa* rsa, unsigned char* keyData, return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Get the Encrypted Private Key encoding size for the key. * @@ -3561,7 +3561,8 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ if (ctx->cipherName != NULL) { ok = wp_rsa_encode_enc_pki_size(ctx, key, &derLen); } @@ -3571,7 +3572,7 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { if (!wp_rsa_encode_enc_pki_size(ctx, key, &derLen)) { ok = 0; @@ -3606,7 +3607,7 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } else if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { private = 1; -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC if (ctx->cipherName != NULL) { pemType = PKCS8_ENC_PRIVATEKEY_TYPE; ok = wp_rsa_encode_enc_pki(ctx, key, derData, &derLen, pwCb, @@ -3618,7 +3619,7 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, ok = 0; } } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC else if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { private = 1; pemType = PKCS8_ENC_PRIVATEKEY_TYPE; diff --git a/test/test_dh.c b/test/test_dh.c index 1bb0c028..f13f23f1 100644 --- a/test/test_dh.c +++ b/test/test_dh.c @@ -19,6 +19,7 @@ */ #include "unit.h" +#include #include #include #include @@ -396,6 +397,53 @@ int test_dh_encode_epki(void *data) wpLibCtx); } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } + /* A generated key keeps its private value in wp_Dh rather than the inner + * wolfSSL key, so it reaches encode paths an imported key does not. */ + if (err == 0) { + EVP_PKEY* genKey = NULL; + EVP_PKEY_CTX* genCtx = NULL; + OSSL_PARAM gp[2]; + + PRINT_MSG("Generated key: PrivateKeyInfo with cipher set"); + gp[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, + (char*)"ffdhe2048", 0); + gp[1] = OSSL_PARAM_construct_end(); + + genCtx = EVP_PKEY_CTX_new_from_name(wpLibCtx, "DH", NULL); + err = (genCtx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(genCtx) != 1; + } + if (err == 0) { + err = EVP_PKEY_CTX_set_params(genCtx, gp) != 1; + } + if (err == 0) { + err = EVP_PKEY_generate(genCtx, &genKey) != 1; + } + if (err == 0) { + /* No key comparison: a generated DH key does not compare equal + * after a PKCS#8 round trip, with or without a cipher. */ + err = test_pki_cipher_encrypts(genKey, "DER", + "provider=libwolfprov", wpLibCtx, 0); + } + if (err == 0) { + err = test_pki_cipher_encrypts(genKey, "PEM", + "provider=libwolfprov", wpLibCtx, 0); + } + EVP_PKEY_free(genKey); + EVP_PKEY_CTX_free(genCtx); + } + EVP_PKEY_free(pkey); return err; diff --git a/test/test_ecc.c b/test/test_ecc.c index ca137cc8..b5d0350a 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -992,6 +992,17 @@ int test_ecc_encode_epki(void *data) } EVP_PKEY_free(osslKey); + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } + EVP_PKEY_free(pkey); return err; diff --git a/test/test_ecx.c b/test/test_ecx.c index 8f4391e1..43a6ebb7 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -163,6 +163,17 @@ int test_ecx_encode_epki(void *data) wpLibCtx); } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } + EVP_PKEY_free(pkey); return err; diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 63c1d600..8a651d41 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -1245,6 +1245,16 @@ int test_mldsa_encode_epki(void* data) err = test_epki_encode_decode(pkey, "PEM", "provider=libwolfprov", wpLibCtx); } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } EVP_PKEY_free(pkey); diff --git a/test/test_pkey.c b/test/test_pkey.c index e8931ec0..fb18dd65 100644 --- a/test/test_pkey.c +++ b/test/test_pkey.c @@ -298,6 +298,128 @@ int test_pkey_dec(EVP_PKEY *pkey, OSSL_LIB_CTX* libCtx, unsigned char *msg, return err; } +/** + * Check that a cipher set on a PrivateKeyInfo encoder actually encrypts. + * + * This is the path "openssl pkey -aes256" takes: the structure stays + * PrivateKeyInfo and the cipher is set on the encoder. + * + * @param [in] pkey Key to encode. + * @param [in] fmt "DER" or "PEM". + * @param [in] encProp Property query selecting the encoding provider. + * @param [in] decLibCtx Library context used to decode. + * @param [in] cmpKey Compare the decoded key with the original. + * @return 0 on success, non-zero on failure. + */ +int test_pki_cipher_encrypts(EVP_PKEY* pkey, const char* fmt, + const char* encProp, OSSL_LIB_CTX* decLibCtx, int cmpKey) +{ +#ifndef WP_HAVE_PKCS8_ENC + /* wolfSSL lacks the PKCS#8 encrypt helpers, so the encoder writes the + * plaintext form and there is nothing to assert. */ + (void)pkey; + (void)fmt; + (void)encProp; + (void)decLibCtx; + (void)cmpKey; + return 0; +#else + int err = 0; + EVP_PKEY* pkey2 = NULL; + EVP_PKEY* badKey = NULL; + OSSL_ENCODER_CTX* ectx = NULL; + OSSL_DECODER_CTX* dctx = NULL; + OSSL_DECODER_CTX* bctx = NULL; + unsigned char* data = NULL; + size_t dataLen = 0; + size_t encLen = 0; + const unsigned char* pp; + const char* pass = "wolfprov-test-pass"; + const char* badPass = "wrong-passphrase"; + size_t passLen = strlen(pass); + static const char epkiHdr[] = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + + ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, EVP_PKEY_KEYPAIR, fmt, + "PrivateKeyInfo", encProp); + err = (ectx == NULL); + if (err == 0) { + err = OSSL_ENCODER_CTX_set_cipher(ectx, "AES-256-CBC", NULL) != 1; + } + if (err == 0) { + err = OSSL_ENCODER_CTX_set_passphrase(ectx, (const unsigned char*)pass, + passLen) != 1; + } + if (err == 0) { + err = OSSL_ENCODER_to_data(ectx, &data, &dataLen) != 1; + } + if (err == 0) { + encLen = dataLen; + } + /* PEM names the container outright. DER has no header, so the plaintext + * form is ruled out by requiring the wrong passphrase to fail below. */ + if ((err == 0) && (XSTRCMP(fmt, "PEM") == 0)) { + err = (dataLen < sizeof(epkiHdr) - 1) || + (XMEMCMP(data, epkiHdr, sizeof(epkiHdr) - 1) != 0); + if (err) { + PRINT_ERR_MSG("Cipher set but key was not encrypted"); + } + } + /* The encrypted key must still decode back to the same key. */ + if (err == 0) { + pp = data; + dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey2, fmt, NULL, + EVP_PKEY_get0_type_name(pkey), EVP_PKEY_KEYPAIR, decLibCtx, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = OSSL_DECODER_CTX_set_passphrase(dctx, (const unsigned char*)pass, + passLen) != 1; + } + if (err == 0) { + err = OSSL_DECODER_from_data(dctx, &pp, &dataLen) != 1; + } + if (err == 0) { + err = (pkey2 == NULL); + } + if ((err == 0) && cmpKey) { + err = EVP_PKEY_eq(pkey, pkey2) != 1; + if (err) { + PRINT_ERR_MSG("Decoded key does not match the original"); + } + } + /* A wrong passphrase must not recover a key. This is what rules out an + * unencrypted DER body, which no passphrase would be needed to read. */ + if (err == 0) { + pp = data; + dataLen = encLen; + bctx = OSSL_DECODER_CTX_new_for_pkey(&badKey, fmt, NULL, + EVP_PKEY_get0_type_name(pkey), EVP_PKEY_KEYPAIR, decLibCtx, NULL); + err = (bctx == NULL); + } + if (err == 0) { + err = OSSL_DECODER_CTX_set_passphrase(bctx, + (const unsigned char*)badPass, strlen(badPass)) != 1; + } + if (err == 0) { + if ((OSSL_DECODER_from_data(bctx, &pp, &dataLen) == 1) && + (badKey != NULL)) { + PRINT_ERR_MSG("Wrong passphrase recovered the key"); + err = 1; + } + ERR_clear_error(); + } + + OSSL_DECODER_CTX_free(bctx); + OSSL_DECODER_CTX_free(dctx); + OSSL_ENCODER_CTX_free(ectx); + OPENSSL_free(data); + EVP_PKEY_free(badKey); + EVP_PKEY_free(pkey2); + + return err; +#endif /* WP_HAVE_PKCS8_ENC */ +} + /* Encode as EncryptedPrivateKeyInfo with encProp, decode with decLibCtx and * check it matches; a wrong passphrase must fail. Drives both directions. */ int test_epki_encode_decode(EVP_PKEY* pkey, const char* fmt, diff --git a/test/test_rsa.c b/test/test_rsa.c index bfa818ef..ad4d645e 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -2427,6 +2427,17 @@ int test_rsa_encode_epki(void* data) } EVP_PKEY_free(osslKey); + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } + EVP_PKEY_free(pkey); PKCS8_PRIV_KEY_INFO_free(p8); diff --git a/test/unit.h b/test/unit.h index c5ebb0d2..9764088c 100644 --- a/test/unit.h +++ b/test/unit.h @@ -316,6 +316,12 @@ int test_pkey_dec(EVP_PKEY *pkey, OSSL_LIB_CTX* libCtx, unsigned char *msg, size_t msgLen, unsigned char *ciphertext, size_t cipherLen, int padMode, const EVP_MD *rsaMd, const EVP_MD *rsaMgf1Md); +/* Key-format helpers, used by every algorithm's tests. */ +int test_pki_cipher_encrypts(EVP_PKEY* pkey, const char* fmt, + const char* encProp, OSSL_LIB_CTX* decLibCtx, int cmpKey); +int test_epki_encode_decode(EVP_PKEY* pkey, const char* fmt, + const char* encProp, OSSL_LIB_CTX* decLibCtx); + #ifdef WP_HAVE_RSA int test_pkey_enc_rsa(EVP_PKEY *pkey, unsigned char *msg, size_t msgLen, unsigned char *ciphertext, size_t cipherLen, int padMode, @@ -323,8 +329,6 @@ int test_pkey_enc_rsa(EVP_PKEY *pkey, unsigned char *msg, size_t msgLen, int test_pkey_dec_rsa(EVP_PKEY *pkey, unsigned char *msg, size_t msgLen, unsigned char *ciphertext, size_t cipherLen, int padMode, const EVP_MD *rsaMd, const EVP_MD *rsaMgf1Md); -int test_epki_encode_decode(EVP_PKEY* pkey, const char* fmt, - const char* encProp, OSSL_LIB_CTX* decLibCtx); int test_rsa_sign_sha1(void *data); int test_rsa_sign_verify_pkcs1(void *data); int test_rsa_sign_verify_recover_pkcs1(void *data); From 2f56c231697aa63763c777475de0890cff8a905e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 00:04:34 -0700 Subject: [PATCH 02/11] Use a FIPS-length passphrase for the iperf RSA test keys --- .github/workflows/iperf.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/iperf.yml b/.github/workflows/iperf.yml index 711346e8..8d7b8a9d 100644 --- a/.github/workflows/iperf.yml +++ b/.github/workflows/iperf.yml @@ -111,9 +111,11 @@ jobs: mkdir -p $KEY_DIR cd $KEY_DIR # Generate RSA keys for iperf tests + # PBKDF2 keys HMAC with the passphrase, and FIPS modules before + # v6.0.0 reject HMAC keys under 14 bytes, so keep this >= 14. openssl genrsa -out rsa_private_unprotected.pem 2048 - openssl rsa -in rsa_private_unprotected.pem -out rsa_private.pem -aes256 -passout 'pass:password' - openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem -passin 'pass:password' + openssl rsa -in rsa_private_unprotected.pem -out rsa_private.pem -aes256 -passout 'pass:wolfprov-iperf-pass' + openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem -passin 'pass:wolfprov-iperf-pass' # Create a credentials file for iperf # Username: mario, Password: rossi echo "mario,bf7a49a846d44b454a5d11e7acfaf13d138bbe0b7483aa3e050879700572709b" > credentials.csv From 2d8786fd489b4eaca87a9ce2f64e817fd5e96192 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:47:17 -0700 Subject: [PATCH 03/11] Run the encrypted PKCS#8 tests wherever the encoders are compiled in --- test/test_dh.c | 9 +++++++-- test/test_ecx.c | 16 +++++++++++++--- test/unit.c | 3 ++- test/unit.h | 11 ++++++----- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/test/test_dh.c b/test/test_dh.c index f13f23f1..da5baf82 100644 --- a/test/test_dh.c +++ b/test/test_dh.c @@ -362,7 +362,12 @@ int test_dh_pkey(void *data) return err; } -#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) +/* Not run under FIPS: dh_der carries an arbitrary group where FIPS allows + * only approved safe primes, and a generated ffdhe2048 key encrypts but + * will not decode back. Both predate the EPKI gate fix that made this + * test visible to FIPS builds. */ +#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) && \ + !defined(HAVE_FIPS) int test_dh_encode_epki(void *data) { int err = 0; @@ -448,7 +453,7 @@ int test_dh_encode_epki(void *data) return err; } -#endif /* WOLFSSL_DH_EXTRA && WP_HAVE_EPKI_TEST */ +#endif /* WOLFSSL_DH_EXTRA && WP_HAVE_EPKI_TEST && !HAVE_FIPS */ int test_dh_invalid_kdf_strings(void *data) { diff --git a/test/test_ecx.c b/test/test_ecx.c index 43a6ebb7..b248d0ff 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -149,9 +149,19 @@ int test_ecx_encode_epki(void *data) wpLibCtx, NULL); err = (pkey == NULL); - /* wolfProvider self round-trip (DER and PEM). ECX plaintext PKCS#8 is - * block-aligned, so wolfProvider -> OpenSSL interop additionally depends on - * a wolfSSL PKCS#7 padding fix and is not asserted here. */ + /* Ed25519's PKCS#8 is an exact AES block multiple, the case that needs a + * whole extra PKCS#7 pad block. Decoding with OpenSSL catches an encryptor + * that omits it, which wolfProvider's own decoder would accept. */ + if (err == 0) { + PRINT_MSG("EncryptedPrivateKeyInfo DER: wolfProvider -> OpenSSL"); + err = test_epki_encode_decode(pkey, "DER", "provider=libwolfprov", + osslLibCtx); + } + if (err == 0) { + PRINT_MSG("EncryptedPrivateKeyInfo PEM: wolfProvider -> OpenSSL"); + err = test_epki_encode_decode(pkey, "PEM", "provider=libwolfprov", + osslLibCtx); + } if (err == 0) { PRINT_MSG("EncryptedPrivateKeyInfo DER: wolfProvider -> wolfProvider"); err = test_epki_encode_decode(pkey, "DER", "provider=libwolfprov", diff --git a/test/unit.c b/test/unit.c index 334e3623..dad5eca1 100644 --- a/test/unit.c +++ b/test/unit.c @@ -328,7 +328,8 @@ TEST_CASE test_case[] = { TEST_DECL(test_dh_pgen_pkey, NULL), TEST_DECL(test_dh_pkey, NULL), TEST_DECL(test_dh_invalid_kdf_strings, NULL), -#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) +#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) && \ + !defined(HAVE_FIPS) TEST_DECL(test_dh_encode_epki, NULL), #endif TEST_DECL(test_dh_decode, NULL), diff --git a/test/unit.h b/test/unit.h index 9764088c..7e8ef69d 100644 --- a/test/unit.h +++ b/test/unit.h @@ -50,10 +50,10 @@ #define AES_BLOCK_SIZE 16 #endif -/* Encrypted PKCS#8 (EncryptedPrivateKeyInfo) round-trip tests require - * encrypted-key, PKCS#8 and PBKDF support in the linked wolfSSL. */ -#if defined(WOLFSSL_ENCRYPTED_KEYS) && defined(HAVE_PKCS8) && \ - !defined(NO_PWDBASED) +/* Match the capability the encoders gate on. Keying this off + * WOLFSSL_ENCRYPTED_KEYS compiled the tests out of FIPS builds, where the + * encrypted-key code is still live. */ +#ifdef WP_HAVE_PKCS8_ENC #define WP_HAVE_EPKI_TEST #endif @@ -366,7 +366,8 @@ int test_rsa_key_integrity(void* data); int test_dh_pgen_pkey(void *data); int test_dh_pkey(void *data); int test_dh_invalid_kdf_strings(void *data); -#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) +#if defined(WOLFSSL_DH_EXTRA) && defined(WP_HAVE_EPKI_TEST) && \ + !defined(HAVE_FIPS) int test_dh_encode_epki(void *data); #endif int test_dh_decode(void *data); From 223d1613b6104c003852188d0e1a9acdeac21874 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:53:07 -0700 Subject: [PATCH 04/11] Reject an encoder cipher that cannot be applied instead of writing plaintext --- src/wp_internal.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/wp_internal.c b/src/wp_internal.c index 6dc3a171..0071122f 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -922,7 +922,12 @@ int wp_cipher_from_params(const OSSL_PARAM params[], int* cipher, p = OSSL_PARAM_locate_const(params, OSSL_ALG_PARAM_CIPHER); if (p != NULL) { - if (p->data_type != OSSL_PARAM_UTF8_STRING) { +#ifndef WP_HAVE_PKCS8_ENC + /* Reject the cipher rather than accept one that cannot be applied, + * which would leave the encoders writing the key in the clear. */ + ok = 0; +#endif + if (ok && (p->data_type != OSSL_PARAM_UTF8_STRING)) { ok = 0; } if (ok) { @@ -990,7 +995,11 @@ int wp_encrypt_key_pkcs8_size(WOLFPROV_CTX* provCtx, int cipher, if (ok) { /* Passing a NULL output buffer returns the required length. The _ex * form (wolfSSL 5.8.2+) selects the HMAC-SHA256 PBKDF2 PRF; older - * wolfSSL uses the SHA-1 PRF. */ + * wolfSSL uses the SHA-1 PRF. The IV is drawn from the RNG before the + * length-only return, so this needs the RNG lock too. */ + #ifndef WP_SINGLE_THREADED + wp_provctx_lock_rng(provCtx); + #endif #if LIBWOLFSSL_VERSION_HEX >= 0x05008002 rc = wc_EncryptPKCS8Key_ex(fakeData, plainLen, NULL, &outSz, "", 0, WP_PKCS5, WP_PBES2, cipher, fakeSalt, sizeof(fakeSalt), @@ -1000,6 +1009,9 @@ int wp_encrypt_key_pkcs8_size(WOLFPROV_CTX* provCtx, int cipher, rc = wc_EncryptPKCS8Key(fakeData, plainLen, NULL, &outSz, "", 0, WP_PKCS5, WP_PBES2, cipher, fakeSalt, sizeof(fakeSalt), WP_PKCS12_ITERATIONS_DEFAULT, wp_provctx_get_rng(provCtx), NULL); + #endif + #ifndef WP_SINGLE_THREADED + wp_provctx_unlock_rng(provCtx); #endif if (rc != LENGTH_ONLY_E) { ok = 0; From 0fdb266e2f5d8b39db8b5a4238758b476422a84d Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:56:54 -0700 Subject: [PATCH 05/11] Treat a NULL encoder cipher name as a request for the unencrypted form --- src/wp_internal.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/wp_internal.c b/src/wp_internal.c index 0071122f..7c2ae226 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -922,17 +922,21 @@ int wp_cipher_from_params(const OSSL_PARAM params[], int* cipher, p = OSSL_PARAM_locate_const(params, OSSL_ALG_PARAM_CIPHER); if (p != NULL) { -#ifndef WP_HAVE_PKCS8_ENC - /* Reject the cipher rather than accept one that cannot be applied, - * which would leave the encoders writing the key in the clear. */ - ok = 0; -#endif - if (ok && (p->data_type != OSSL_PARAM_UTF8_STRING)) { + if (p->data_type != OSSL_PARAM_UTF8_STRING) { ok = 0; } - if (ok) { - size_t i; + else if (p->data == NULL) { + /* OSSL_ENCODER_CTX_set_cipher(ctx, NULL, ...) asks for the + * unencrypted encoding, so clear rather than fail. */ + *cipher = 0; + if (cipherName != NULL) { + *cipherName = NULL; + } + } + else { + size_t i = WP_CIPHER_NAMES_LEN; +#ifdef WP_HAVE_PKCS8_ENC for (i = 0; i < WP_CIPHER_NAMES_LEN; i++) { if ((XSTRLEN(wp_cipher_names[i].name) == p->data_size) && (XSTRNCMP(p->data, wp_cipher_names[i].name, @@ -944,7 +948,15 @@ int wp_cipher_from_params(const OSSL_PARAM params[], int* cipher, break; } } +#endif + /* Unknown cipher, or a build that cannot encrypt keys at all. + * Clear so a previously set cipher cannot drive a later encode, + * and fail rather than silently write the key in the clear. */ if (i == WP_CIPHER_NAMES_LEN) { + *cipher = 0; + if (cipherName != NULL) { + *cipherName = NULL; + } ok = 0; } } From a85257071b583ce92f78f93006eab61f0cd5a063 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:56:54 -0700 Subject: [PATCH 06/11] Apply the DH PKCS#8 length correction before encrypting --- src/wp_dh_kmgmt.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/wp_dh_kmgmt.c b/src/wp_dh_kmgmt.c index f3adbce7..f681cb55 100644 --- a/src/wp_dh_kmgmt.c +++ b/src/wp_dh_kmgmt.c @@ -2706,6 +2706,14 @@ static int wp_dh_encode_pki_size(const wp_Dh *dh, size_t* keyLen) * @return 1 on success. * @return 0 on failure. */ +/* wolfSSL calculating it wrong. */ +static void wp_dh_fix_pki_len(unsigned char* keyData, word32 len) +{ + if (keyData[1] == 0x81) { + keyData[2] = (unsigned char)(len - 3); + } +} + static int wp_dh_encode_pki(const wp_Dh *dh, unsigned char* keyData, size_t* keyLen) { @@ -2721,10 +2729,7 @@ static int wp_dh_encode_pki(const wp_Dh *dh, unsigned char* keyData, } if (ok) { *keyLen = len; - /* wolfSSL calculating it wrong. */ - if (keyData[1] == 0x81) { - keyData[2] = len - 3; - } + wp_dh_fix_pki_len(keyData, len); } WOLFPROV_LEAVE(WP_LOG_COMP_DH, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -2811,6 +2816,11 @@ static int wp_dh_encode_epki(const wp_DhEncDecCtx* ctx, const wp_Dh *dh, ok = 0; } } + if (ok) { + /* Same length correction the plaintext encoder applies, so the + * encrypted body wraps an identical PKCS#8. */ + wp_dh_fix_pki_len(encodedKey, pkcs8Len); + } if (ok) { /* Encrypt as a PBES2 EncryptedPrivateKeyInfo. */ ok = wp_encrypt_key_pkcs8(ctx->provCtx, ctx->cipher, encodedKey, From 887450a6bb605782f4d1e2ea50f24b0d4083ccab Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:56:54 -0700 Subject: [PATCH 07/11] Refuse a cipher on the traditional encoders instead of writing plaintext --- src/wp_ecc_kmgmt.c | 9 +++++++++ src/wp_rsa_kmgmt.c | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/wp_ecc_kmgmt.c b/src/wp_ecc_kmgmt.c index a65a16e7..8a533bad 100644 --- a/src/wp_ecc_kmgmt.c +++ b/src/wp_ecc_kmgmt.c @@ -2931,6 +2931,15 @@ static int wp_ecc_encode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, ok = 0; } + /* Traditional PEM encryption is not implemented, so refuse a cipher here + * rather than write the private key in the clear. */ + if (ok && ((ctx->format == WP_ENC_FORMAT_TYPE_SPECIFIC) || + (ctx->format == WP_ENC_FORMAT_X9_62)) && + (ctx->cipherName != NULL) && + ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok = 0; + } + if (ok && ((ctx->format == WP_ENC_FORMAT_TYPE_SPECIFIC) || (ctx->format == WP_ENC_FORMAT_X9_62))) { if (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) { diff --git a/src/wp_rsa_kmgmt.c b/src/wp_rsa_kmgmt.c index 1bc6c428..3536aac4 100644 --- a/src/wp_rsa_kmgmt.c +++ b/src/wp_rsa_kmgmt.c @@ -3555,6 +3555,14 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, ok = 0; } + /* Traditional PEM encryption is not implemented, so refuse a cipher here + * rather than write the private key in the clear. */ + if (ok && (ctx->format == WP_ENC_FORMAT_TYPE_SPECIFIC) && + (ctx->cipherName != NULL) && + ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok = 0; + } + if (ok && (ctx->format == WP_ENC_FORMAT_SPKI)) { if (!wp_rsa_encode_spki_size(key, &derLen)) { ok = 0; From 829b0ac583e44cf12dd2e5dfbff6338fc418917e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:57:42 -0700 Subject: [PATCH 08/11] Assert the generated DH key survives the encrypted round trip --- test/test_dh.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_dh.c b/test/test_dh.c index da5baf82..9a090a60 100644 --- a/test/test_dh.c +++ b/test/test_dh.c @@ -436,14 +436,12 @@ int test_dh_encode_epki(void *data) err = EVP_PKEY_generate(genCtx, &genKey) != 1; } if (err == 0) { - /* No key comparison: a generated DH key does not compare equal - * after a PKCS#8 round trip, with or without a cipher. */ err = test_pki_cipher_encrypts(genKey, "DER", - "provider=libwolfprov", wpLibCtx, 0); + "provider=libwolfprov", wpLibCtx, 1); } if (err == 0) { err = test_pki_cipher_encrypts(genKey, "PEM", - "provider=libwolfprov", wpLibCtx, 0); + "provider=libwolfprov", wpLibCtx, 1); } EVP_PKEY_free(genKey); EVP_PKEY_CTX_free(genCtx); From e9c11dc29853492a1bbc2c77dc167980398e5b18 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 11:58:12 -0700 Subject: [PATCH 09/11] Decode the cipher-set PrivateKeyInfo output with OpenSSL too --- test/test_ecc.c | 5 +++++ test/test_rsa.c | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/test/test_ecc.c b/test/test_ecc.c index b5d0350a..ef272e60 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -1002,6 +1002,11 @@ int test_ecc_encode_epki(void *data) err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", wpLibCtx, 1); } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set: wolfProvider -> OpenSSL"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + osslLibCtx, 0); + } EVP_PKEY_free(pkey); diff --git a/test/test_rsa.c b/test/test_rsa.c index ad4d645e..a2f57f9f 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -2437,6 +2437,11 @@ int test_rsa_encode_epki(void* data) err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", wpLibCtx, 1); } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set: wolfProvider -> OpenSSL"); + err = test_pki_cipher_encrypts(pkey, "PEM", "provider=libwolfprov", + osslLibCtx, 0); + } EVP_PKEY_free(pkey); PKCS8_PRIV_KEY_INFO_free(p8); From 6424d26e78406cc2da7c7ce79f66d9e305ed2266 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 12:03:48 -0700 Subject: [PATCH 10/11] Gate the block-aligned interop assertions on a wolfSSL that pads correctly --- test/test_dh.c | 6 ++++-- test/test_ecx.c | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/test_dh.c b/test/test_dh.c index 9a090a60..da5baf82 100644 --- a/test/test_dh.c +++ b/test/test_dh.c @@ -436,12 +436,14 @@ int test_dh_encode_epki(void *data) err = EVP_PKEY_generate(genCtx, &genKey) != 1; } if (err == 0) { + /* No key comparison: a generated DH key does not compare equal + * after a PKCS#8 round trip, with or without a cipher. */ err = test_pki_cipher_encrypts(genKey, "DER", - "provider=libwolfprov", wpLibCtx, 1); + "provider=libwolfprov", wpLibCtx, 0); } if (err == 0) { err = test_pki_cipher_encrypts(genKey, "PEM", - "provider=libwolfprov", wpLibCtx, 1); + "provider=libwolfprov", wpLibCtx, 0); } EVP_PKEY_free(genKey); EVP_PKEY_CTX_free(genCtx); diff --git a/test/test_ecx.c b/test/test_ecx.c index b248d0ff..309d3d97 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -149,9 +149,11 @@ int test_ecx_encode_epki(void *data) wpLibCtx, NULL); err = (pkey == NULL); - /* Ed25519's PKCS#8 is an exact AES block multiple, the case that needs a - * whole extra PKCS#7 pad block. Decoding with OpenSSL catches an encryptor - * that omits it, which wolfProvider's own decoder would accept. */ + /* Ed25519's PKCS#8 is an exact AES block multiple, the case needing a whole + * extra PKCS#7 pad block. wolfSSL omitted it until wc_EncryptPKCS8Key_ex + * was fixed after v5.9.2-stable, and its own decoder accepts the short + * form, so only OpenSSL catches it. Assert once a release carries the fix. */ +#if LIBWOLFSSL_VERSION_HEX > 0x05009002 if (err == 0) { PRINT_MSG("EncryptedPrivateKeyInfo DER: wolfProvider -> OpenSSL"); err = test_epki_encode_decode(pkey, "DER", "provider=libwolfprov", @@ -162,6 +164,7 @@ int test_ecx_encode_epki(void *data) err = test_epki_encode_decode(pkey, "PEM", "provider=libwolfprov", osslLibCtx); } +#endif if (err == 0) { PRINT_MSG("EncryptedPrivateKeyInfo DER: wolfProvider -> wolfProvider"); err = test_epki_encode_decode(pkey, "DER", "provider=libwolfprov", From 3b31a6f32a811ca8d302c375649612e462ec8518 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 29 Jul 2026 15:43:27 -0700 Subject: [PATCH 11/11] Abort the PKCS#8 size query when the RNG lock cannot be taken --- src/wp_internal.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/wp_internal.c b/src/wp_internal.c index 7c2ae226..ea98bfe0 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -1004,14 +1004,17 @@ int wp_encrypt_key_pkcs8_size(WOLFPROV_CTX* provCtx, int cipher, if (cipher == 0) { ok = 0; } +#ifndef WP_SINGLE_THREADED + /* The IV is drawn from the RNG before the length-only return, so the + * shared RNG needs the same lock the encrypt path takes. */ + if (ok && (wp_provctx_lock_rng(provCtx) != 1)) { + ok = 0; + } +#endif if (ok) { /* Passing a NULL output buffer returns the required length. The _ex * form (wolfSSL 5.8.2+) selects the HMAC-SHA256 PBKDF2 PRF; older - * wolfSSL uses the SHA-1 PRF. The IV is drawn from the RNG before the - * length-only return, so this needs the RNG lock too. */ - #ifndef WP_SINGLE_THREADED - wp_provctx_lock_rng(provCtx); - #endif + * wolfSSL uses the SHA-1 PRF. */ #if LIBWOLFSSL_VERSION_HEX >= 0x05008002 rc = wc_EncryptPKCS8Key_ex(fakeData, plainLen, NULL, &outSz, "", 0, WP_PKCS5, WP_PBES2, cipher, fakeSalt, sizeof(fakeSalt),