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 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..f681cb55 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; @@ -2684,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) { @@ -2699,17 +2729,14 @@ 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); return ok; } -#ifdef WOLFSSL_ENCRYPTED_KEYS +#ifdef WP_HAVE_PKCS8_ENC /** * Get the Encrypted PKCS#8 encoding size for the key. * @@ -2728,10 +2755,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. */ @@ -2785,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, @@ -2861,11 +2897,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 +2941,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..8a533bad 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. * @@ -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) { @@ -2957,11 +2966,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 +3026,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_internal.c b/src/wp_internal.c index 6dc3a171..ea98bfe0 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -925,9 +925,18 @@ int wp_cipher_from_params(const OSSL_PARAM params[], int* cipher, 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, @@ -939,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; } } @@ -987,6 +1004,13 @@ 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 @@ -1000,6 +1024,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; 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..3536aac4 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. * @@ -3555,13 +3555,22 @@ 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; } } 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 +3580,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 +3615,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 +3627,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..da5baf82 100644 --- a/test/test_dh.c +++ b/test/test_dh.c @@ -19,6 +19,7 @@ */ #include "unit.h" +#include #include #include #include @@ -361,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; @@ -396,11 +402,58 @@ 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; } -#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_ecc.c b/test/test_ecc.c index ca137cc8..ef272e60 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -992,6 +992,22 @@ 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); + } + 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); return err; diff --git a/test/test_ecx.c b/test/test_ecx.c index 8f4391e1..309d3d97 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -149,9 +149,22 @@ 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 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", + osslLibCtx); + } + if (err == 0) { + PRINT_MSG("EncryptedPrivateKeyInfo PEM: wolfProvider -> OpenSSL"); + 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", @@ -163,6 +176,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..a2f57f9f 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -2427,6 +2427,22 @@ 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); + } + 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); 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 c5ebb0d2..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 @@ -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); @@ -362,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);