From d8c513f96f1d9cebf5282227b67744c0172a0213 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 11:27:00 -0700 Subject: [PATCH 01/15] Encrypt PKCS#8 keys when a cipher is configured --- .github/workflows/iperf.yml | 6 +- include/wolfprovider/settings.h | 7 ++ src/wp_dec_pem2der.c | 7 +- src/wp_dh_kmgmt.c | 99 ++++++++++++++++++++------ src/wp_ecc_kmgmt.c | 38 ++++++++-- src/wp_ecx_kmgmt.c | 12 +++- src/wp_internal.c | 40 +++++++++-- src/wp_mldsa_kmgmt.c | 12 +++- src/wp_rsa_kmgmt.c | 23 ++++-- test/test_dh.c | 57 ++++++++++++++- test/test_ecc.c | 16 +++++ test/test_ecx.c | 30 +++++++- test/test_mldsa.c | 10 +++ test/test_pkey.c | 122 ++++++++++++++++++++++++++++++++ test/test_rsa.c | 16 +++++ test/unit.c | 3 +- test/unit.h | 19 +++-- 17 files changed, 454 insertions(+), 63 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 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..b544e51b 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; @@ -1080,10 +1107,12 @@ int wp_encrypt_key_pkcs8(WOLFPROV_CTX* provCtx, int cipher, if (ok && (passwordSz > WP_EPKI_PASSWORD_MAX)) { ok = 0; } - if (ok) { #ifndef WP_SINGLE_THREADED - wp_provctx_lock_rng(provCtx); + if (ok && (wp_provctx_lock_rng(provCtx) != 1)) { + ok = 0; + } #endif + if (ok) { /* Generate the PBKDF2 salt. */ rc = wc_RNG_GenerateBlock(rng, salt, sizeof(salt)); if (rc == 0) { @@ -1110,7 +1139,7 @@ int wp_encrypt_key_pkcs8(WOLFPROV_CTX* provCtx, int cipher, ok = 0; } else { - *outLen = (size_t)outSz; + *outLen = (size_t)rc; } } @@ -1505,4 +1534,3 @@ word32 wp_atoc32(const byte* c) { return *(const word32*)c; #endif } - 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); From 327bae65ea118dcd88a0d7b1e049842730d5e97f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 10:52:53 -0700 Subject: [PATCH 02/15] Add SLH-DSA provider operations --- configure.ac | 15 +- include/wolfprovider/alg_funcs.h | 146 +++ include/wolfprovider/settings.h | 59 + src/include.am | 2 + src/wp_slhdsa_kmgmt.c | 2058 ++++++++++++++++++++++++++++++ src/wp_slhdsa_sig.c | 938 ++++++++++++++ src/wp_wolfprov.c | 384 ++++++ 7 files changed, 3600 insertions(+), 2 deletions(-) create mode 100644 src/wp_slhdsa_kmgmt.c create mode 100644 src/wp_slhdsa_sig.c diff --git a/configure.ac b/configure.ac index 38d00698..ebf00198 100644 --- a/configure.ac +++ b/configure.ac @@ -167,7 +167,7 @@ if test "x$ENABLED_SEED_SRC" = "xyes"; then fi AC_ARG_ENABLE([pqc], - [AS_HELP_STRING([--enable-pqc],[Enable both ML-KEM (FIPS 203) and ML-DSA (FIPS 204). Requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+ (default: disabled).])], + [AS_HELP_STRING([--enable-pqc],[Enable ML-KEM (FIPS 203), ML-DSA (FIPS 204) and SLH-DSA (FIPS 205). Requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+ (default: disabled).])], [ ENABLED_PQC=$enableval ], [ ENABLED_PQC=no ] ) @@ -181,12 +181,18 @@ AC_ARG_ENABLE([mldsa], [ ENABLED_MLDSA=$enableval ], [ ENABLED_MLDSA= ] ) +AC_ARG_ENABLE([slhdsa], + [AS_HELP_STRING([--enable-slhdsa],[Enable SLH-DSA (FIPS 205) only (default: disabled).])], + [ ENABLED_SLHDSA=$enableval ], + [ ENABLED_SLHDSA= ] + ) -# --enable-pqc is shorthand for both ML-KEM and ML-DSA, unless one was +# --enable-pqc is shorthand for every post-quantum algorithm, unless one was # explicitly disabled (--enable-pqc --disable-mldsa keeps ML-DSA off). if test "x$ENABLED_PQC" = "xyes"; then if test "x$ENABLED_MLKEM" != "xno"; then ENABLED_MLKEM=yes; fi if test "x$ENABLED_MLDSA" != "xno"; then ENABLED_MLDSA=yes; fi + if test "x$ENABLED_SLHDSA" != "xno"; then ENABLED_SLHDSA=yes; fi fi if test "x$ENABLED_MLKEM" = "xyes"; then AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_MLKEM" @@ -194,6 +200,9 @@ fi if test "x$ENABLED_MLDSA" = "xyes"; then AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_MLDSA" fi +if test "x$ENABLED_SLHDSA" = "xyes"; then + AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_SLHDSA" +fi # Set OpenSSL lib directory for installing libdefault.so if test "x$ENABLED_REPLACE_DEFAULT" = "xyes"; then @@ -251,11 +260,13 @@ echo " * Debug silent mode: $ENABLED_DEBUG_SILENT" echo test "x$ENABLED_MLKEM" = "xyes" || ENABLED_MLKEM=no test "x$ENABLED_MLDSA" = "xyes" || ENABLED_MLDSA=no +test "x$ENABLED_SLHDSA" = "xyes" || ENABLED_SLHDSA=no echo " Features " echo " * User settings: $ENABLED_USERSETTINGS" echo " * Dynamic provider: $ENABLED_DYNAMIC_PROVIDER" echo " * Replace default: $ENABLED_REPLACE_DEFAULT" echo " * ML-KEM (FIPS 203): $ENABLED_MLKEM" echo " * ML-DSA (FIPS 204): $ENABLED_MLDSA" +echo " * SLH-DSA (FIPS 205): $ENABLED_SLHDSA" echo "" echo "---" diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 3d35d115..351b797c 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -185,6 +185,32 @@ typedef void (*DFUNC)(void); #define WP_NAMES_ML_DSA_65 "ML-DSA-65:MLDSA65:2.16.840.1.101.3.4.3.18:id-ml-dsa-65" #define WP_NAMES_ML_DSA_87 "ML-DSA-87:MLDSA87:2.16.840.1.101.3.4.3.19:id-ml-dsa-87" +/* SLH-DSA names (NIST FIPS 205). */ +#define WP_NAMES_SLH_DSA_SHA2_128S \ + "SLH-DSA-SHA2-128s:2.16.840.1.101.3.4.3.20:id-slh-dsa-sha2-128s" +#define WP_NAMES_SLH_DSA_SHA2_128F \ + "SLH-DSA-SHA2-128f:2.16.840.1.101.3.4.3.21:id-slh-dsa-sha2-128f" +#define WP_NAMES_SLH_DSA_SHA2_192S \ + "SLH-DSA-SHA2-192s:2.16.840.1.101.3.4.3.22:id-slh-dsa-sha2-192s" +#define WP_NAMES_SLH_DSA_SHA2_192F \ + "SLH-DSA-SHA2-192f:2.16.840.1.101.3.4.3.23:id-slh-dsa-sha2-192f" +#define WP_NAMES_SLH_DSA_SHA2_256S \ + "SLH-DSA-SHA2-256s:2.16.840.1.101.3.4.3.24:id-slh-dsa-sha2-256s" +#define WP_NAMES_SLH_DSA_SHA2_256F \ + "SLH-DSA-SHA2-256f:2.16.840.1.101.3.4.3.25:id-slh-dsa-sha2-256f" +#define WP_NAMES_SLH_DSA_SHAKE_128S \ + "SLH-DSA-SHAKE-128s:2.16.840.1.101.3.4.3.26:id-slh-dsa-shake-128s" +#define WP_NAMES_SLH_DSA_SHAKE_128F \ + "SLH-DSA-SHAKE-128f:2.16.840.1.101.3.4.3.27:id-slh-dsa-shake-128f" +#define WP_NAMES_SLH_DSA_SHAKE_192S \ + "SLH-DSA-SHAKE-192s:2.16.840.1.101.3.4.3.28:id-slh-dsa-shake-192s" +#define WP_NAMES_SLH_DSA_SHAKE_192F \ + "SLH-DSA-SHAKE-192f:2.16.840.1.101.3.4.3.29:id-slh-dsa-shake-192f" +#define WP_NAMES_SLH_DSA_SHAKE_256S \ + "SLH-DSA-SHAKE-256s:2.16.840.1.101.3.4.3.30:id-slh-dsa-shake-256s" +#define WP_NAMES_SLH_DSA_SHAKE_256F \ + "SLH-DSA-SHAKE-256f:2.16.840.1.101.3.4.3.31:id-slh-dsa-shake-256f" + /* DRBG names. */ #define WP_NAMES_SEED_SRC "SEED-SRC" #define WP_NAMES_CTR_DRBG "CTR-DRBG" @@ -302,6 +328,17 @@ int wp_mldsa_get_sig_size(const wp_MlDsa* mldsa); int wp_mldsa_has_private(const wp_MlDsa* mldsa); int wp_mldsa_get_level(wp_MlDsa* mldsa); +/* Internal SLH-DSA types and functions. */ +typedef struct wp_SlhDsa wp_SlhDsa; + +int wp_slhdsa_up_ref(wp_SlhDsa* slhdsa); +void wp_slhdsa_free(wp_SlhDsa* slhdsa); +void* wp_slhdsa_get_key(wp_SlhDsa* slhdsa); +wolfSSL_Mutex* wp_slhdsa_get_mutex(wp_SlhDsa* slhdsa); +int wp_slhdsa_get_sig_size(const wp_SlhDsa* slhdsa); +int wp_slhdsa_get_n(const wp_SlhDsa* slhdsa); +int wp_slhdsa_get_param(wp_SlhDsa* slhdsa); + /* Internal DH types and functions. */ typedef struct wp_Dh wp_Dh; @@ -406,6 +443,7 @@ extern const OSSL_DISPATCH wp_ed448_signature_functions[]; extern const OSSL_DISPATCH wp_hmac_signature_functions[]; extern const OSSL_DISPATCH wp_cmac_signature_functions[]; extern const OSSL_DISPATCH wp_mldsa_signature_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_signature_functions[]; /* Asymmetric cipher implementations. */ extern const OSSL_DISPATCH wp_rsa_asym_cipher_functions[]; @@ -436,6 +474,18 @@ extern const OSSL_DISPATCH wp_mlx_p384_keymgmt_functions[]; extern const OSSL_DISPATCH wp_mldsa44_keymgmt_functions[]; extern const OSSL_DISPATCH wp_mldsa65_keymgmt_functions[]; extern const OSSL_DISPATCH wp_mldsa87_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_keymgmt_functions[]; /* Key exchange implementations. */ extern const OSSL_DISPATCH wp_ecdh_keyexch_functions[]; @@ -555,6 +605,102 @@ extern const OSSL_DISPATCH wp_mldsa87_pki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_mldsa87_pki_pem_encoder_functions[]; extern const OSSL_DISPATCH wp_mldsa87_epki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_mldsa87_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_128f_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_192f_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_shake_256f_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_128f_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_192f_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256s_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_slhdsa_sha2_256f_epki_pem_encoder_functions[]; /* Storage implementations. */ extern const OSSL_DISPATCH wp_file_store_functions[]; diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 438d5812..03002fa7 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -206,6 +206,7 @@ #if !defined(__has_include) #define WP_MLKEM_HEADER #define WP_MLDSA_HEADER + #define WP_SLHDSA_HEADER #else #if __has_include() #define WP_MLKEM_HEADER @@ -213,6 +214,9 @@ #if __has_include() #define WP_MLDSA_HEADER #endif + #if __has_include() + #define WP_SLHDSA_HEADER + #endif #endif /* wolfSSL must be master or v5.9.2-stable+: a release newer than v5.9.1, or a * dev build carrying wc_mldsa.h. wc_mldsa.h is the deliberate post-v5.9.1 @@ -241,6 +245,58 @@ #define WP_HAVE_ML_DSA_65 #define WP_HAVE_ML_DSA_87 #endif +#if defined(WOLFPROV_HAVE_SLHDSA) && defined(WOLFSSL_HAVE_SLHDSA) && \ + defined(WP_SLHDSA_HEADER) && defined(WP_WOLFSSL_PQC_CAPABLE) && \ + (OPENSSL_VERSION_NUMBER >= 0x30600000L) + #define WP_HAVE_SLHDSA + /* Pulled in for the parameter-set macros: wc_slhdsa.h resolves them from + * the NO_* defaults, so a user_settings.h wolfSSL may not carry them in + * options.h. Which sets exist decides which algorithms get registered. */ + #include + /* A verify-only wolfSSL compiles out keygen, sign and every private-key + * entry point, so the provider offers verify only against it. */ + #ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + #define WP_HAVE_SLHDSA_PRIVATE + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_128S + #define WP_HAVE_SLH_DSA_SHAKE_128S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_128F + #define WP_HAVE_SLH_DSA_SHAKE_128F + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_192S + #define WP_HAVE_SLH_DSA_SHAKE_192S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_192F + #define WP_HAVE_SLH_DSA_SHAKE_192F + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_256S + #define WP_HAVE_SLH_DSA_SHAKE_256S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_256F + #define WP_HAVE_SLH_DSA_SHAKE_256F + #endif + #ifdef WOLFSSL_SLHDSA_SHA2 + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_128S + #define WP_HAVE_SLH_DSA_SHA2_128S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_128F + #define WP_HAVE_SLH_DSA_SHA2_128F + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_192S + #define WP_HAVE_SLH_DSA_SHA2_192S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_192F + #define WP_HAVE_SLH_DSA_SHA2_192F + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_256S + #define WP_HAVE_SLH_DSA_SHA2_256S + #endif + #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_256F + #define WP_HAVE_SLH_DSA_SHA2_256F + #endif + #endif +#endif /* Fail loudly if PQC was requested but the prerequisites are missing, so a * direct ./configure (bypassing the build script's version gate) does not * silently produce a non-PQC build. */ @@ -250,6 +306,9 @@ #if defined(WOLFPROV_HAVE_MLDSA) && !defined(WP_HAVE_MLDSA) #error "ML-DSA requested but unavailable: needs OpenSSL >= 3.6 and wolfSSL master or v5.9.2-stable+ with ML-DSA." #endif +#if defined(WOLFPROV_HAVE_SLHDSA) && !defined(WP_HAVE_SLHDSA) + #error "SLH-DSA requested but unavailable: needs OpenSSL >= 3.6 and wolfSSL master or v5.9.2-stable+ with SLH-DSA." +#endif #if !defined(NO_AES_CBC) && (defined(WP_HAVE_HMAC) || defined(WP_HAVE_CMAC)) #define WP_HAVE_KBKDF #endif diff --git a/src/include.am b/src/include.am index 156e11fc..e43b0087 100644 --- a/src/include.am +++ b/src/include.am @@ -42,6 +42,8 @@ libwolfprov_la_SOURCES += src/wp_mlx_kmgmt.c libwolfprov_la_SOURCES += src/wp_mlx_kem.c libwolfprov_la_SOURCES += src/wp_mldsa_kmgmt.c libwolfprov_la_SOURCES += src/wp_mldsa_sig.c +libwolfprov_la_SOURCES += src/wp_slhdsa_kmgmt.c +libwolfprov_la_SOURCES += src/wp_slhdsa_sig.c libwolfprov_la_SOURCES += src/wp_drbg.c libwolfprov_la_SOURCES += src/wp_seed_src.c libwolfprov_la_SOURCES += src/wp_dec_pem2der.c diff --git a/src/wp_slhdsa_kmgmt.c b/src/wp_slhdsa_kmgmt.c new file mode 100644 index 00000000..6e82890d --- /dev/null +++ b/src/wp_slhdsa_kmgmt.c @@ -0,0 +1,2058 @@ +/* wp_slhdsa_kmgmt.c + * + * Copyright (C) 2006-2026 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 + +#include +#include + +#ifdef WP_HAVE_SLHDSA + +#include + +/* Supported selections (key parts) in this key manager for SLH-DSA. */ +#define WP_SLHDSA_POSSIBLE_SELECTIONS \ + (OSSL_KEYMGMT_SELECT_KEYPAIR | OSSL_KEYMGMT_SELECT_ALL_PARAMETERS) + +/* Largest keygen seed: SK.seed || SK.prf || PK.seed, each n bytes. */ +#define WP_SLHDSA_MAX_SEED_SZ (3 * 32) + +/* SLH-DSA parameter set data. */ +typedef struct wp_SlhDsaData { + enum SlhDsaParam param; + word32 pubKeySize; + word32 privKeySize; + word32 sigSize; + int securityBits; + int securityCategory; + const char* name; +} wp_SlhDsaData; + +/* SLH-DSA key object. */ +struct wp_SlhDsa { + SlhDsaKey key; + const wp_SlhDsaData* data; + +#ifndef WP_SINGLE_THREADED + wolfSSL_Mutex mutex; +#endif + int refCnt; + + WOLFPROV_CTX* provCtx; + + unsigned int hasPub:1; + unsigned int hasPriv:1; +}; + +typedef struct wp_SlhDsa wp_SlhDsa; + +#ifdef WP_HAVE_SLHDSA_PRIVATE +/* SLH-DSA key generation context. */ +typedef struct wp_SlhDsaGenCtx { + WC_RNG rng; + const wp_SlhDsaData* data; + WOLFPROV_CTX* provCtx; + int selection; + unsigned char seed[WP_SLHDSA_MAX_SEED_SZ]; + size_t seedLen; +} wp_SlhDsaGenCtx; +#endif /* WP_HAVE_SLHDSA_PRIVATE */ + + +/* Parameter set tables. Security category follows FIPS 205 Table 2. */ +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S +static const wp_SlhDsaData slhdsaShake128sData = { + SLHDSA_SHAKE128S, + WC_SLHDSA_SHAKE128S_PUB_LEN, + WC_SLHDSA_SHAKE128S_PRIV_LEN, + WC_SLHDSA_SHAKE128S_SIG_LEN, + 128, 1, + "SLH-DSA-SHAKE-128s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F +static const wp_SlhDsaData slhdsaShake128fData = { + SLHDSA_SHAKE128F, + WC_SLHDSA_SHAKE128F_PUB_LEN, + WC_SLHDSA_SHAKE128F_PRIV_LEN, + WC_SLHDSA_SHAKE128F_SIG_LEN, + 128, 1, + "SLH-DSA-SHAKE-128f" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S +static const wp_SlhDsaData slhdsaShake192sData = { + SLHDSA_SHAKE192S, + WC_SLHDSA_SHAKE192S_PUB_LEN, + WC_SLHDSA_SHAKE192S_PRIV_LEN, + WC_SLHDSA_SHAKE192S_SIG_LEN, + 192, 3, + "SLH-DSA-SHAKE-192s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F +static const wp_SlhDsaData slhdsaShake192fData = { + SLHDSA_SHAKE192F, + WC_SLHDSA_SHAKE192F_PUB_LEN, + WC_SLHDSA_SHAKE192F_PRIV_LEN, + WC_SLHDSA_SHAKE192F_SIG_LEN, + 192, 3, + "SLH-DSA-SHAKE-192f" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S +static const wp_SlhDsaData slhdsaShake256sData = { + SLHDSA_SHAKE256S, + WC_SLHDSA_SHAKE256S_PUB_LEN, + WC_SLHDSA_SHAKE256S_PRIV_LEN, + WC_SLHDSA_SHAKE256S_SIG_LEN, + 256, 5, + "SLH-DSA-SHAKE-256s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F +static const wp_SlhDsaData slhdsaShake256fData = { + SLHDSA_SHAKE256F, + WC_SLHDSA_SHAKE256F_PUB_LEN, + WC_SLHDSA_SHAKE256F_PRIV_LEN, + WC_SLHDSA_SHAKE256F_SIG_LEN, + 256, 5, + "SLH-DSA-SHAKE-256f" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S +static const wp_SlhDsaData slhdsaSha2128sData = { + SLHDSA_SHA2_128S, + WC_SLHDSA_SHA2_128S_PUB_LEN, + WC_SLHDSA_SHA2_128S_PRIV_LEN, + WC_SLHDSA_SHA2_128S_SIG_LEN, + 128, 1, + "SLH-DSA-SHA2-128s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F +static const wp_SlhDsaData slhdsaSha2128fData = { + SLHDSA_SHA2_128F, + WC_SLHDSA_SHA2_128F_PUB_LEN, + WC_SLHDSA_SHA2_128F_PRIV_LEN, + WC_SLHDSA_SHA2_128F_SIG_LEN, + 128, 1, + "SLH-DSA-SHA2-128f" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S +static const wp_SlhDsaData slhdsaSha2192sData = { + SLHDSA_SHA2_192S, + WC_SLHDSA_SHA2_192S_PUB_LEN, + WC_SLHDSA_SHA2_192S_PRIV_LEN, + WC_SLHDSA_SHA2_192S_SIG_LEN, + 192, 3, + "SLH-DSA-SHA2-192s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F +static const wp_SlhDsaData slhdsaSha2192fData = { + SLHDSA_SHA2_192F, + WC_SLHDSA_SHA2_192F_PUB_LEN, + WC_SLHDSA_SHA2_192F_PRIV_LEN, + WC_SLHDSA_SHA2_192F_SIG_LEN, + 192, 3, + "SLH-DSA-SHA2-192f" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S +static const wp_SlhDsaData slhdsaSha2256sData = { + SLHDSA_SHA2_256S, + WC_SLHDSA_SHA2_256S_PUB_LEN, + WC_SLHDSA_SHA2_256S_PRIV_LEN, + WC_SLHDSA_SHA2_256S_SIG_LEN, + 256, 5, + "SLH-DSA-SHA2-256s" +}; +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F +static const wp_SlhDsaData slhdsaSha2256fData = { + SLHDSA_SHA2_256F, + WC_SLHDSA_SHA2_256F_PUB_LEN, + WC_SLHDSA_SHA2_256F_PRIV_LEN, + WC_SLHDSA_SHA2_256F_SIG_LEN, + 256, 5, + "SLH-DSA-SHA2-256f" +}; +#endif + + +/** + * Increment reference count for key. + * + * @param [in, out] slhdsa SLH-DSA key object. + * @return 1 on success, 0 on failure. + */ +int wp_slhdsa_up_ref(wp_SlhDsa* slhdsa) +{ +#ifndef WP_SINGLE_THREADED + int ok = 1; + int rc; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_up_ref"); + + rc = wc_LockMutex(&slhdsa->mutex); + if (rc < 0) { + ok = 0; + } + if (ok) { + slhdsa->refCnt++; + wc_UnLockMutex(&slhdsa->mutex); + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +#else + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_up_ref"); + slhdsa->refCnt++; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; +#endif +} + +/** + * Get the wolfSSL SLH-DSA key from the wp_SlhDsa object. + * + * @param [in] slhdsa SLH-DSA key object. + * @return Pointer to wolfSSL SlhDsaKey, returned as void*. + */ +void* wp_slhdsa_get_key(wp_SlhDsa* slhdsa) +{ + return &slhdsa->key; +} + +/** + * Get the mutex from an SLH-DSA key object. + * + * @param [in] slhdsa SLH-DSA key object. + * @return Pointer to wolfSSL mutex object. + */ +wolfSSL_Mutex* wp_slhdsa_get_mutex(wp_SlhDsa* slhdsa) +{ +#ifndef WP_SINGLE_THREADED + return &slhdsa->mutex; +#else + (void)slhdsa; + return NULL; +#endif +} + +/** + * Get the SLH-DSA parameter id for the key. + * + * @param [in] slhdsa SLH-DSA key object. + * @return Parameter id, or -1 when not available. + */ +int wp_slhdsa_get_param(wp_SlhDsa* slhdsa) +{ + int param = -1; + + if ((slhdsa != NULL) && (slhdsa->data != NULL)) { + param = (int)slhdsa->data->param; + } + return param; +} + +/** + * Get the FIPS 205 security parameter n for the key. + * + * @param [in] slhdsa SLH-DSA key object. + * @return n in bytes (16, 24 or 32), or 0 if slhdsa is NULL. + */ +int wp_slhdsa_get_n(const wp_SlhDsa* slhdsa) +{ + if ((slhdsa == NULL) || (slhdsa->data == NULL)) { + return 0; + } + /* The public key is PK.seed || PK.root, n bytes each. */ + return (int)(slhdsa->data->pubKeySize / 2); +} + +/** + * Get the maximum signature size for the key. + * + * @param [in] slhdsa SLH-DSA key object. + * @return Signature size in bytes, or 0 if slhdsa is NULL. + */ +int wp_slhdsa_get_sig_size(const wp_SlhDsa* slhdsa) +{ + if ((slhdsa == NULL) || (slhdsa->data == NULL)) { + return 0; + } + return (int)slhdsa->data->sigSize; +} + + +/** + * Create a new SLH-DSA key object. + * + * @param [in] provCtx Provider context. + * @param [in] data Parameter set data. + * @return New SLH-DSA key object on success, NULL on failure. + */ +static wp_SlhDsa* wp_slhdsa_new(WOLFPROV_CTX* provCtx, + const wp_SlhDsaData* data) +{ + wp_SlhDsa* slhdsa = NULL; + + if (wolfssl_prov_is_running()) { + slhdsa = (wp_SlhDsa*)OPENSSL_zalloc(sizeof(*slhdsa)); + } + if (slhdsa != NULL) { + int ok = 1; + int rc; + + rc = wc_SlhDsaKey_Init(&slhdsa->key, data->param, NULL, INVALID_DEVID); + if (rc != 0) { + ok = 0; + } + #ifndef WP_SINGLE_THREADED + if (ok) { + rc = wc_InitMutex(&slhdsa->mutex); + if (rc != 0) { + wc_SlhDsaKey_Free(&slhdsa->key); + ok = 0; + } + } + #endif + if (ok) { + slhdsa->provCtx = provCtx; + slhdsa->refCnt = 1; + slhdsa->data = data; + } + if (!ok) { + OPENSSL_free(slhdsa); + slhdsa = NULL; + } + } + return slhdsa; +} + +/** + * Dispose of SLH-DSA key object. + * + * @param [in, out] slhdsa SLH-DSA key object. May be NULL. + */ +void wp_slhdsa_free(wp_SlhDsa* slhdsa) +{ + if (slhdsa != NULL) { + int cnt; + #ifndef WP_SINGLE_THREADED + int rc; + + rc = wc_LockMutex(&slhdsa->mutex); + if (rc == 0) { + cnt = --slhdsa->refCnt; + wc_UnLockMutex(&slhdsa->mutex); + } + else { + /* Cannot safely decrement without the lock; keep the object. */ + cnt = slhdsa->refCnt; + } + #else + cnt = --slhdsa->refCnt; + #endif + + if (cnt == 0) { + #ifndef WP_SINGLE_THREADED + wc_FreeMutex(&slhdsa->mutex); + #endif + wc_SlhDsaKey_Free(&slhdsa->key); + OPENSSL_free(slhdsa); + } + } +} + +/** + * Duplicate SLH-DSA key object via raw export/import. + * + * @param [in] src Source SLH-DSA key object. + * @param [in] selection Parts of key (public/private) to duplicate. + * @return New SLH-DSA key object on success, NULL on failure. + */ +static wp_SlhDsa* wp_slhdsa_dup(const wp_SlhDsa* src, int selection) +{ + wp_SlhDsa* dst = NULL; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + word32 pubLen; + word32 privLen; + word32 privAllocLen = 0; + int rc; + int ok = 1; + int dupPub; + int dupPriv; + + if (!wolfssl_prov_is_running() || (src == NULL)) { + return NULL; + } + dupPub = ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && src->hasPub; + dupPriv = ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) + && src->hasPriv; + + dst = wp_slhdsa_new(src->provCtx, src->data); + if (dst == NULL) { + return NULL; + } + + if (dupPub) { + pubLen = src->data->pubKeySize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPublic((SlhDsaKey*)&src->key, pubBuf, + &pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_SlhDsaKey_ImportPublic(&dst->key, pubBuf, pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPub = 1; + } + OPENSSL_free(pubBuf); + pubBuf = NULL; + } + +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ok && dupPriv) { + privAllocLen = src->data->privKeySize; + privLen = privAllocLen; + privBuf = (unsigned char*)OPENSSL_malloc(privAllocLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPrivate((SlhDsaKey*)&src->key, privBuf, + &privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_SlhDsaKey_ImportPrivate(&dst->key, privBuf, privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPriv = 1; + } + /* Zero the full allocation, not just the (possibly-truncated) out len. */ + OPENSSL_clear_free(privBuf, privAllocLen); + } +#else + (void)privBuf; + (void)privLen; + (void)privAllocLen; + (void)dupPriv; +#endif + + if (!ok) { + wp_slhdsa_free(dst); + return NULL; + } + return dst; +} + +/** + * Load an SLH-DSA key from a reference. + * + * @param [in, out] pSlhDsa Pointer to an SLH-DSA key reference. + * @param [in] size Size of reference object. Unused. + * @return SLH-DSA key object on success. + */ +static const wp_SlhDsa* wp_slhdsa_load(const wp_SlhDsa** pSlhDsa, size_t size) +{ + const wp_SlhDsa* slhdsa = *pSlhDsa; + (void)size; + *pSlhDsa = NULL; + return slhdsa; +} + +/** + * Check SLH-DSA key object has the components required. + * + * @param [in] slhdsa SLH-DSA key object. + * @param [in] selection Parts of key required. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_has(const wp_SlhDsa* slhdsa, int selection) +{ + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_has"); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (slhdsa == NULL)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + ok &= slhdsa->hasPub; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok &= slhdsa->hasPriv; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Compare two SLH-DSA keys. + * + * @param [in] a First SLH-DSA key. + * @param [in] b Second SLH-DSA key. + * @param [in] selection Parts of key to compare. + * @return 1 if match, 0 otherwise. + */ +static int wp_slhdsa_match(const wp_SlhDsa* a, const wp_SlhDsa* b, + int selection) +{ + int ok = 1; + int rc; + unsigned char* bufA = NULL; + unsigned char* bufB = NULL; + word32 lenA; + word32 lenB; + word32 allocA = 0; + word32 allocB = 0; + int checked = 0; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_match"); + + if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (a->data->param != b->data->param) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + /* Presence mismatch fails; both-present compares; neither-present skips. */ + if (((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && + (a->hasPub != b->hasPub)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && + a->hasPub && b->hasPub) { + checked = 1; + lenA = a->data->pubKeySize; + lenB = b->data->pubKeySize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPublic((SlhDsaKey*)&a->key, bufA, &lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_SlhDsaKey_ExportPublic((SlhDsaKey*)&b->key, bufB, &lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { + ok = 0; + } + OPENSSL_free(bufA); + OPENSSL_free(bufB); + bufA = NULL; + bufB = NULL; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) && + (a->hasPriv != b->hasPriv)) { + ok = 0; + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) && + a->hasPriv && b->hasPriv) { + checked = 1; + allocA = a->data->privKeySize; + allocB = b->data->privKeySize; + lenA = allocA; + lenB = allocB; + bufA = (unsigned char*)OPENSSL_malloc(allocA); + bufB = (unsigned char*)OPENSSL_malloc(allocB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPrivate((SlhDsaKey*)&a->key, bufA, &lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_SlhDsaKey_ExportPrivate((SlhDsaKey*)&b->key, bufB, &lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (CRYPTO_memcmp(bufA, bufB, lenA) != 0))) { + ok = 0; + } + /* Zero full allocations even if export truncated the out lengths. */ + OPENSSL_clear_free(bufA, allocA); + OPENSSL_clear_free(bufB, allocB); + } +#else + (void)allocA; + (void)allocB; +#endif + /* A public/private selection with no component present in both is not a match. */ + if (ok && !checked && + ((selection & (OSSL_KEYMGMT_SELECT_PUBLIC_KEY | + OSSL_KEYMGMT_SELECT_PRIVATE_KEY)) != 0)) { + ok = 0; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Import an SLH-DSA key from parameters. + * + * @param [in, out] slhdsa SLH-DSA key object. + * @param [in] selection Parts of key to import. + * @param [in] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_import(wp_SlhDsa* slhdsa, int selection, + const OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + unsigned char* privData = NULL; + unsigned char* pubData = NULL; + size_t privLen = 0; + size_t pubLen = 0; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_import"); + + if (!wolfssl_prov_is_running() || (slhdsa == NULL)) { + ok = 0; + } + if (ok && ((selection & WP_SLHDSA_POSSIBLE_SELECTIONS) == 0)) { + ok = 0; + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PRIV_KEY, + &privData, &privLen)) { + ok = 0; + } + /* FIPS 205 priv keys are fixed-size; equality check before word32 cast + * also catches truncation on 64-bit platforms. */ + if (ok && (privData != NULL) && + (privLen != slhdsa->data->privKeySize)) { + ok = 0; + } + if (ok && (privData != NULL)) { + rc = wc_SlhDsaKey_ImportPrivate(&slhdsa->key, privData, + (word32)privLen); + if (rc != 0) { + ok = 0; + } + if (ok) { + /* Unlike ML-DSA, the 4n FIPS 205 private key ends with the + * public key, so importing it yields both halves. */ + slhdsa->hasPriv = 1; + slhdsa->hasPub = 1; + } + } + } +#else + (void)privLen; +#endif + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PUB_KEY, + &pubData, &pubLen)) { + ok = 0; + } + if (ok && (pubData != NULL) && (pubLen != slhdsa->data->pubKeySize)) { + ok = 0; + } + if (ok && (pubData != NULL)) { + rc = wc_SlhDsaKey_ImportPublic(&slhdsa->key, pubData, + (word32)pubLen); + if (rc != 0) { + ok = 0; + } + if (ok) { + slhdsa->hasPub = 1; + } + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + /* Catches a mismatched pub/priv pair that the imports accept separately. + * Only when both were supplied: CheckKey rebuilds the public root, which + * is slow for the 's' parameter sets. */ + if (ok && (privData != NULL) && (pubData != NULL)) { + if (wc_SlhDsaKey_CheckKey(&slhdsa->key) != 0) { + ok = 0; + } + } +#endif + if (!ok && (slhdsa != NULL)) { + /* Clear flags on failure so partial-init state is not advertised. */ + slhdsa->hasPriv = 0; + slhdsa->hasPub = 0; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/* SLH-DSA key parameters for import/export type queries. */ +static const OSSL_PARAM wp_slhdsa_key_params[] = { + /* 0: none */ + OSSL_PARAM_END, + + /* 1: private only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 3: public only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 5: both */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, +}; + +static const OSSL_PARAM* wp_slhdsa_key_types(int selection) +{ + int idx = 0; + int extra = 0; + + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + idx += 3; + extra++; + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) { + idx += 1 + extra; + } +#else + (void)extra; +#endif + return &wp_slhdsa_key_params[idx]; +} + +static const OSSL_PARAM* wp_slhdsa_import_types(int selection) +{ + return wp_slhdsa_key_types(selection); +} + +static const OSSL_PARAM* wp_slhdsa_export_types(int selection) +{ + return wp_slhdsa_key_types(selection); +} + +/** + * Export SLH-DSA key data via callback. + * + * @param [in] slhdsa SLH-DSA key object. + * @param [in] selection Parts of key to export. + * @param [in] paramCb Callback to receive constructed parameters. + * @param [in] cbArg Argument to pass to callback. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_export(wp_SlhDsa* slhdsa, int selection, + OSSL_CALLBACK* paramCb, void* cbArg) +{ + int ok = 1; + int rc; + OSSL_PARAM params[3]; + int paramsSz = 0; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + word32 pubLen = 0; + word32 privLen = 0; + word32 privAllocLen = 0; + int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_export"); + + if (!wolfssl_prov_is_running() || (slhdsa == NULL)) { + ok = 0; + } + XMEMSET(params, 0, sizeof(params)); + + if (ok && expPub && slhdsa->hasPub) { + pubLen = slhdsa->data->pubKeySize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPublic(&slhdsa->key, pubBuf, &pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PUB_KEY, pubBuf, pubLen); + } + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ok && expPriv && slhdsa->hasPriv) { + privAllocLen = slhdsa->data->privKeySize; + privLen = privAllocLen; + privBuf = (unsigned char*)OPENSSL_malloc(privAllocLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_SlhDsaKey_ExportPrivate(&slhdsa->key, privBuf, &privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PRIV_KEY, privBuf, privLen); + } + } +#else + (void)expPriv; + (void)privLen; +#endif + if (ok) { + ok = paramCb(params, cbArg); + } + OPENSSL_free(pubBuf); + /* Zero full allocation in case ExportPrivate truncated privLen. */ + OPENSSL_clear_free(privBuf, privAllocLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Gettable parameters for SLH-DSA key. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of supported gettable parameters. + */ +static const OSSL_PARAM* wp_slhdsa_gettable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_supported_gettable_params[] = { + OSSL_PARAM_int(OSSL_PKEY_PARAM_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_CATEGORY, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_MAX_SIZE, NULL), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), +#ifdef WP_HAVE_SLHDSA_PRIVATE + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), +#endif + OSSL_PARAM_END + }; + (void)provCtx; + return wp_slhdsa_supported_gettable_params; +} + +/** + * Get SLH-DSA key parameters. + * + * @param [in] slhdsa SLH-DSA key object. + * @param [in, out] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_get_params"); + + if (slhdsa == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, (int)slhdsa->data->pubKeySize * 8)) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, slhdsa->data->securityBits)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_CATEGORY); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, slhdsa->data->securityCategory)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_MAX_SIZE); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, (int)slhdsa->data->sigSize)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); + if (p != NULL) { + word32 outLen = slhdsa->data->pubKeySize; + if (!slhdsa->hasPub) { + ok = 0; + } + else if (p->data == NULL) { + /* Size query. */ + p->return_size = outLen; + } + else if (p->data_size < outLen) { + /* Buffer too small: report required size and fail so the + * caller can retry; do not claim a completed export. */ + p->return_size = outLen; + ok = 0; + } + else { + outLen = (word32)p->data_size; + rc = wc_SlhDsaKey_ExportPublic(&slhdsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); + if (p != NULL) { + word32 outLen = slhdsa->data->privKeySize; + if (!slhdsa->hasPriv) { + ok = 0; + } + else if (p->data == NULL) { + p->return_size = outLen; + } + else if (p->data_size < outLen) { + p->return_size = outLen; + ok = 0; + } + else { + outLen = (word32)p->data_size; + rc = wc_SlhDsaKey_ExportPrivate(&slhdsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } +#endif + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Settable parameters for SLH-DSA key. + * + * @param [in] provCtx Provider context. Unused. + * @return Empty parameter list. + */ +static const OSSL_PARAM* wp_slhdsa_settable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_supported_settable_params[] = { + OSSL_PARAM_END + }; + (void)provCtx; + return wp_slhdsa_supported_settable_params; +} + +/** + * Set SLH-DSA key parameters. None supported. + * + * @param [in] slhdsa SLH-DSA key object. Unused. + * @param [in] params Array of parameters. Unused. + * @return 1 always. + */ +static int wp_slhdsa_set_params(wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) +{ + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_set_params"); + (void)slhdsa; + (void)params; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; +} + +/* + * SLH-DSA generation + */ + +#ifdef WP_HAVE_SLHDSA_PRIVATE + +static int wp_slhdsa_gen_set_params(wp_SlhDsaGenCtx* ctx, + const OSSL_PARAM params[]); + +/** + * Create SLH-DSA generation context object. + * + * @param [in] provCtx Provider context. + * @param [in] selection Parts of the key to generate. + * @param [in] params Parameters to set for generation. + * @param [in] data Parameter set data. + * @return New SLH-DSA generation context on success, NULL on failure. + */ +static wp_SlhDsaGenCtx* wp_slhdsa_gen_init_base(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[], const wp_SlhDsaData* data) +{ + wp_SlhDsaGenCtx* ctx = NULL; + + if (wolfssl_prov_is_running() && + ((selection & WP_SLHDSA_POSSIBLE_SELECTIONS) != 0)) { + ctx = (wp_SlhDsaGenCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + int rc; + int ok = 1; + + rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + ok = 0; + } + if (ok) { + ctx->provCtx = provCtx; + ctx->data = data; + ctx->selection = selection; + /* Apply init-time params (e.g. the deterministic keygen seed) so + * the seed and its length validation are honored at init, not + * only via a later gen_set_params call. */ + if (!wp_slhdsa_gen_set_params(ctx, params)) { + ok = 0; + } + } + if (!ok) { + wc_FreeRng(&ctx->rng); + OPENSSL_clear_free(ctx, sizeof(*ctx)); + ctx = NULL; + } + } + return ctx; +} + +/** + * Generate SLH-DSA key pair. + * + * @param [in, out] ctx SLH-DSA generation context. + * @param [in] osslcb Progress callback. Unused. + * @param [in] cbarg Argument for callback. Unused. + * @return SLH-DSA key object on success, NULL on failure. + */ +static wp_SlhDsa* wp_slhdsa_gen(wp_SlhDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, + void* cbarg) +{ + wp_SlhDsa* slhdsa; + int keyPair = (ctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0; + + (void)osslcb; + (void)cbarg; + + slhdsa = wp_slhdsa_new(ctx->provCtx, ctx->data); + if ((slhdsa != NULL) && keyPair) { + int rc; + word32 n = ctx->data->pubKeySize / 2; + + if (ctx->seedLen == (size_t)(3 * n)) { + /* Seed is SK.seed || SK.prf || PK.seed, n bytes each. */ + rc = wc_SlhDsaKey_MakeKeyWithRandom(&slhdsa->key, ctx->seed, n, + ctx->seed + n, n, ctx->seed + (2 * n), n); + } + else { + rc = wc_SlhDsaKey_MakeKey(&slhdsa->key, &ctx->rng); + } + if (rc != 0) { + wp_slhdsa_free(slhdsa); + slhdsa = NULL; + } + else { + slhdsa->hasPub = 1; + slhdsa->hasPriv = 1; + } + } + return slhdsa; +} + +/** + * Set parameters into SLH-DSA generation context. + * + * @param [in] ctx Generation context. + * @param [in] params Array of parameters (SLH-DSA keygen seed). + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_gen_set_params(wp_SlhDsaGenCtx* ctx, + const OSSL_PARAM params[]) +{ + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_gen_set_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_SLH_DSA_SEED); + if (p != NULL) { + void* vp = ctx->seed; + ctx->seedLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->seed), + &ctx->seedLen)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + /* A short seed would silently fall back to RNG keygen, breaking the + * caller's reproducibility contract. Require exactly 3n. */ + if (ctx->seedLen != (size_t)((3 * ctx->data->pubKeySize) / 2)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; +} + +/** + * Settable parameters for SLH-DSA generation context. + * + * @param [in] ctx Generation context. Unused. + * @param [in] provCtx Provider context. Unused. + * @return Array of settable parameters. + */ +static const OSSL_PARAM* wp_slhdsa_gen_settable_params(wp_SlhDsaGenCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_gen_settable[] = { + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_SLH_DSA_SEED, NULL, 0), + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_slhdsa_gen_settable; +} + +/** + * Free SLH-DSA generation context. + * + * @param [in, out] ctx Generation context. + */ +static void wp_slhdsa_gen_cleanup(wp_SlhDsaGenCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + /* ctx holds the deterministic keygen seed; cleanse it. */ + OPENSSL_clear_free(ctx, sizeof(*ctx)); + } +} + +#endif /* WP_HAVE_SLHDSA_PRIVATE */ + +/* + * Per-parameter-set trampolines and dispatch tables + */ + +/* Verify-only builds omit the GEN_* entries entirely rather than stubbing + * them: OpenSSL then fails keygen with "operation not supported for this key + * type" instead of a generic error. */ +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define WP_SLHDSA_GEN_DISPATCH(alg) \ + { OSSL_FUNC_KEYMGMT_GEN_INIT, \ + (DFUNC)wp_##alg##_gen_init }, \ + { OSSL_FUNC_KEYMGMT_GEN_SET_PARAMS, \ + (DFUNC)wp_slhdsa_gen_set_params }, \ + { OSSL_FUNC_KEYMGMT_GEN_SETTABLE_PARAMS, \ + (DFUNC)wp_slhdsa_gen_settable_params }, \ + { OSSL_FUNC_KEYMGMT_GEN, (DFUNC)wp_slhdsa_gen }, \ + { OSSL_FUNC_KEYMGMT_GEN_CLEANUP, \ + (DFUNC)wp_slhdsa_gen_cleanup }, +#define WP_SLHDSA_GEN_TRAMPOLINE(alg, dataName) \ +static wp_SlhDsaGenCtx* wp_##alg##_gen_init(WOLFPROV_CTX* provCtx, \ + int selection, const OSSL_PARAM params[]) \ +{ \ + return wp_slhdsa_gen_init_base(provCtx, selection, params, &dataName); \ +} +#else +#define WP_SLHDSA_GEN_DISPATCH(alg) +#define WP_SLHDSA_GEN_TRAMPOLINE(alg, dataName) +#endif + +#define IMPLEMENT_SLHDSA_KEYMGMT(alg, dataName, algName) \ +static wp_SlhDsa* wp_##alg##_new(WOLFPROV_CTX* provCtx) \ +{ \ + return wp_slhdsa_new(provCtx, &dataName); \ +} \ +static const char* wp_##alg##_query_operation_name(int op) \ +{ \ + (void)op; \ + return algName; \ +} \ +WP_SLHDSA_GEN_TRAMPOLINE(alg, dataName) \ +const OSSL_DISPATCH wp_##alg##_keymgmt_functions[] = { \ + { OSSL_FUNC_KEYMGMT_NEW, \ + (DFUNC)wp_##alg##_new }, \ + { OSSL_FUNC_KEYMGMT_FREE, (DFUNC)wp_slhdsa_free }, \ + { OSSL_FUNC_KEYMGMT_DUP, (DFUNC)wp_slhdsa_dup }, \ + WP_SLHDSA_GEN_DISPATCH(alg) \ + { OSSL_FUNC_KEYMGMT_LOAD, (DFUNC)wp_slhdsa_load }, \ + { OSSL_FUNC_KEYMGMT_GET_PARAMS, \ + (DFUNC)wp_slhdsa_get_params }, \ + { OSSL_FUNC_KEYMGMT_GETTABLE_PARAMS, \ + (DFUNC)wp_slhdsa_gettable_params }, \ + { OSSL_FUNC_KEYMGMT_SET_PARAMS, \ + (DFUNC)wp_slhdsa_set_params }, \ + { OSSL_FUNC_KEYMGMT_SETTABLE_PARAMS, \ + (DFUNC)wp_slhdsa_settable_params }, \ + { OSSL_FUNC_KEYMGMT_HAS, (DFUNC)wp_slhdsa_has }, \ + { OSSL_FUNC_KEYMGMT_MATCH, (DFUNC)wp_slhdsa_match }, \ + { OSSL_FUNC_KEYMGMT_IMPORT, (DFUNC)wp_slhdsa_import }, \ + { OSSL_FUNC_KEYMGMT_IMPORT_TYPES, \ + (DFUNC)wp_slhdsa_import_types }, \ + { OSSL_FUNC_KEYMGMT_EXPORT, (DFUNC)wp_slhdsa_export }, \ + { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, \ + (DFUNC)wp_slhdsa_export_types }, \ + { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ + (DFUNC)wp_##alg##_query_operation_name }, \ + { 0, NULL } \ +}; + +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_128s, slhdsaShake128sData, + "SLH-DSA-SHAKE-128s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_128f, slhdsaShake128fData, + "SLH-DSA-SHAKE-128f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_192s, slhdsaShake192sData, + "SLH-DSA-SHAKE-192s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_192f, slhdsaShake192fData, + "SLH-DSA-SHAKE-192f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_256s, slhdsaShake256sData, + "SLH-DSA-SHAKE-256s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_shake_256f, slhdsaShake256fData, + "SLH-DSA-SHAKE-256f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_128s, slhdsaSha2128sData, + "SLH-DSA-SHA2-128s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_128f, slhdsaSha2128fData, + "SLH-DSA-SHA2-128f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_192s, slhdsaSha2192sData, + "SLH-DSA-SHA2-192s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_192f, slhdsaSha2192fData, + "SLH-DSA-SHA2-192f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_256s, slhdsaSha2256sData, + "SLH-DSA-SHA2-256s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F +IMPLEMENT_SLHDSA_KEYMGMT(slhdsa_sha2_256f, slhdsaSha2256fData, + "SLH-DSA-SHA2-256f") +#endif + +/* + * SLH-DSA encoder/decoder + */ + +/* Extra slack added to the queried DER length before allocating. */ +#define WP_SLHDSA_DER_SLACK 32 + +/* Functions used by the encoder and decoder implementations. */ +typedef int (*WP_SLHDSA_DECODE)(const byte* input, word32* inOutIdx, void* key, + word32 inSz); +typedef int (*WP_SLHDSA_ENCODE)(void* key, byte* output, word32 inLen); + +typedef wp_SlhDsa* (*WP_SLHDSA_NEW)(WOLFPROV_CTX* provCtx); + +/* SLH-DSA encoder and decoder context. */ +typedef struct wp_SlhDsaEncDecCtx { + WP_SLHDSA_DECODE decode; + WP_SLHDSA_ENCODE encode; + WP_SLHDSA_NEW newKey; + + WOLFPROV_CTX* provCtx; + int selection; + + const char* dataType; + int format; + int encoding; + + int cipher; + const char* cipherName; +} wp_SlhDsaEncDecCtx; + +/** + * Create a new SLH-DSA encoder/decoder context. + * + * @param [in] provCtx Provider context. + * @param [in] newKey Function to create parameter-set SLH-DSA key. + * @param [in] dataType Data type name passed to data callback. + * @param [in] format Supported key format. + * @param [in] encoding Data format. + * @param [in] decode Function to decode DER data to a key. + * @param [in] encode Function to encode key to DER data. + * @return New SLH-DSA encoder/decoder context object on success. + * @return NULL on failure. + */ +static wp_SlhDsaEncDecCtx* wp_slhdsa_enc_dec_new(WOLFPROV_CTX* provCtx, + WP_SLHDSA_NEW newKey, const char* dataType, int format, int encoding, + WP_SLHDSA_DECODE decode, WP_SLHDSA_ENCODE encode) +{ + wp_SlhDsaEncDecCtx* ctx = NULL; + + if (wolfssl_prov_is_running()) { + ctx = (wp_SlhDsaEncDecCtx*)OPENSSL_zalloc(sizeof(wp_SlhDsaEncDecCtx)); + } + if (ctx != NULL) { + ctx->decode = decode; + ctx->encode = encode; + ctx->newKey = newKey; + ctx->provCtx = provCtx; + ctx->dataType = dataType; + ctx->format = format; + ctx->encoding = encoding; + } + return ctx; +} + +/** + * Dispose of SLH-DSA encoder/decoder context object. + * + * @param [in, out] ctx SLH-DSA encoder/decoder context object. + */ +static void wp_slhdsa_enc_dec_free(wp_SlhDsaEncDecCtx* ctx) +{ + OPENSSL_free(ctx); +} + +/** + * Return the settable parameters for the SLH-DSA encoder/decoder context. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of parameters with data type. + */ +static const OSSL_PARAM* wp_slhdsa_enc_dec_settable_ctx_params( + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_enc_dec_supported_settables[] = { + OSSL_PARAM_utf8_string(OSSL_ENCODER_PARAM_CIPHER, NULL, 0), + OSSL_PARAM_utf8_string(OSSL_ENCODER_PARAM_PROPERTIES, NULL, 0), + OSSL_PARAM_END, + }; + + (void)provCtx; + return wp_slhdsa_enc_dec_supported_settables; +} + +/** + * Set the SLH-DSA encoder/decoder context parameters. + * + * @param [in, out] ctx SLH-DSA encoder/decoder context object. + * @param [in] params Array of parameters. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_enc_dec_set_ctx_params(wp_SlhDsaEncDecCtx* ctx, + const OSSL_PARAM params[]) +{ + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_enc_dec_set_ctx_params"); + + if (!wp_cipher_from_params(params, &ctx->cipher, &ctx->cipherName)) { + ok = 0; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Construct parameters from SLH-DSA key and pass off to callback. + * + * @param [in] slhdsa SLH-DSA key object. + * @param [in] dataType Data type name passed to the callback. + * @param [in] dataCb Callback to pass SLH-DSA key in parameters to. + * @param [in] dataCbArg Argument to pass to callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_dec_send_params(wp_SlhDsa* slhdsa, const char* dataType, + OSSL_CALLBACK* dataCb, void* dataCbArg) +{ + int ok = 1; + OSSL_PARAM params[4]; + int object_type = OSSL_OBJECT_PKEY; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_dec_send_params"); + + params[0] = OSSL_PARAM_construct_int(OSSL_OBJECT_PARAM_TYPE, &object_type); + params[1] = OSSL_PARAM_construct_utf8_string(OSSL_OBJECT_PARAM_DATA_TYPE, + (char*)dataType, 0); + /* The address of the key object becomes the octet string pointer. */ + params[2] = OSSL_PARAM_construct_octet_string(OSSL_OBJECT_PARAM_REFERENCE, + &slhdsa, sizeof(slhdsa)); + params[3] = OSSL_PARAM_construct_end(); + + if (!dataCb(params, dataCbArg)) { + ok = 0; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +#if defined(WP_HAVE_PKCS8_ENC) && defined(WP_HAVE_SLHDSA_PRIVATE) +/** + * Decode an encrypted PKCS#8 DER SLH-DSA private key into the key object. + * + * @param [in] ctx SLH-DSA encoder/decoder context object. + * @param [in, out] slhdsa SLH-DSA key object. + * @param [in] data DER encoding (decrypted in place). + * @param [in] len Length, in bytes, of DER encoding. + * @param [in] pwCb Password callback. + * @param [in] pwCbArg Argument to pass to password callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_decode_enc_pki(wp_SlhDsaEncDecCtx* ctx, wp_SlhDsa* slhdsa, + unsigned char* data, word32 len, OSSL_PASSPHRASE_CALLBACK* pwCb, + void* pwCbArg) +{ + int ok = 1; + word32 idx = 0; + + WOLFPROV_ENTER_SILENT(WP_LOG_COMP_PQC, WOLFPROV_FUNC_NAME); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + /* Decrypt the PBES2 EncryptedPrivateKeyInfo in place. */ + if (ok && (!wp_decrypt_key_pkcs8(data, &len, pwCb, pwCbArg))) { + ok = 0; + } + /* Decode the recovered plaintext private key. */ + if (ok && (ctx->decode(data, &idx, (void*)&slhdsa->key, len) != 0)) { + ok = 0; + } + + WOLFPROV_LEAVE_SILENT(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif + +/** + * Decode the data in the core BIO. + * + * The parameter set is preset on the created key object so decode only + * succeeds when the DER's algorithm OID matches this decoder's set. + * + * @param [in, out] ctx SLH-DSA encoder/decoder context object. + * @param [in, out] cBio Core BIO to read data from. + * @param [in] selection Parts of key to export. + * @param [in] dataCb Callback to pass SLH-DSA key in parameters to. + * @param [in] dataCbArg Argument to pass to callback. + * @param [in] pwCb Password callback. + * @param [in] pwCbArg Argument to pass to password callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_decode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, + int selection, OSSL_CALLBACK* dataCb, void* dataCbArg, + OSSL_PASSPHRASE_CALLBACK* pwCb, void* pwCbArg) +{ + int ok = 1; + int decoded = 1; + int rc; + unsigned char* data = NULL; + word32 len = 0; + word32 idx = 0; + wp_SlhDsa* slhdsa = NULL; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_decode"); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + + (void)pwCb; + (void)pwCbArg; + + if (ok) { + ctx->selection = selection; + slhdsa = ctx->newKey(ctx->provCtx); + if (slhdsa == NULL) { + ok = 0; + } + } + + if (ok) { + ok = wp_read_der_bio(ctx->provCtx, cBio, &data, &len); + } + if (ok) { + rc = ctx->decode(data, &idx, (void*)&slhdsa->key, len); + if (rc != 0) { +#if defined(WP_HAVE_PKCS8_ENC) && defined(WP_HAVE_SLHDSA_PRIVATE) + /* May be an encrypted PKCS#8 key - decrypt and retry. */ + if ((ctx->format != WP_ENC_FORMAT_PKI) || + (!wp_slhdsa_decode_enc_pki(ctx, slhdsa, data, len, pwCb, + pwCbArg))) +#endif + { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "decode", rc); + ok = 0; + decoded = 0; + } + } + } + /* wc_SlhDsaKey_*Decode overwrites the preset parameter set from the DER's + * OID rather than rejecting a mismatch, so every per-set decoder would + * accept any SLH-DSA key and leave this object describing the wrong set. + * Hand a foreign set back to the chain so the right decoder claims it. */ + if (ok && ((slhdsa->key.params == NULL) || + (slhdsa->key.params->param != slhdsa->data->param))) { + ok = 0; + decoded = 0; + } + if (ok && (ctx->format == WP_ENC_FORMAT_SPKI)) { + slhdsa->hasPub = 1; + } + if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { + slhdsa->hasPriv = 1; + /* The 4n FIPS 205 private key ends with the public key, so a decoded + * private always carries it; confirm from the key's own flags. */ + slhdsa->hasPub = + ((slhdsa->key.flags & WC_SLHDSA_FLAG_PUBLIC) != 0) ? 1 : 0; + } + + OPENSSL_clear_free(data, len); + + if (ok && (!wp_slhdsa_dec_send_params(slhdsa, ctx->dataType, dataCb, + dataCbArg))) { + ok = 0; + } + + if (!ok) { + wp_slhdsa_free(slhdsa); + /* Not our OID: report no error so the decoder chain keeps trying. */ + if (!decoded) { + ok = 1; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Encode the SLH-DSA key. + * + * SLH-DSA keys are large so the DER buffer is sized from a query-length call + * (output == NULL) and then allocated, rather than using a fixed buffer. + * + * @param [in] ctx SLH-DSA encoder/decoder context object. + * @param [in, out] cBio Core BIO to write data to. + * @param [in] slhdsa SLH-DSA key object. + * @param [in] params Key parameters. Unused. + * @param [in] selection Parts of key to encode. Unused. + * @param [in] pwCb Password callback. + * @param [in] pwCbArg Argument to pass to password callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, + const wp_SlhDsa* slhdsa, const OSSL_PARAM* params, int selection, + OSSL_PASSPHRASE_CALLBACK* pwCb, void* pwCbArg) +{ + int ok = 1; + int rc; + BIO* out = wp_corebio_get_bio(ctx->provCtx, cBio); + unsigned char* keyData = NULL; + size_t keyLen = 0; + unsigned char* derData = NULL; + word32 derAllocLen = 0; + size_t derLen = 0; + unsigned char* encData = NULL; + size_t encLen = 0; + unsigned char* srcData; + size_t srcLen; + unsigned char* pemData = NULL; + size_t pemLen = 0; + int pemType = (ctx->format == WP_ENC_FORMAT_SPKI) ? PUBLICKEY_TYPE : + PKCS8_PRIVATEKEY_TYPE; + int private = (ctx->format == WP_ENC_FORMAT_PKI) || + (ctx->format == WP_ENC_FORMAT_EPKI); + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_encode"); + + (void)params; + (void)selection; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (out == NULL)) { + ok = 0; + } + + if (ok) { + rc = ctx->encode((void*)&slhdsa->key, NULL, 0); + if (rc <= 0) { + ok = 0; + } + else { + /* Buffer holds the plaintext PKCS #8 encoding; EPKI encrypts into + * a separate buffer allocated later. */ + derAllocLen = (word32)rc + WP_SLHDSA_DER_SLACK; + } + } + if (ok) { + derData = (unsigned char*)OPENSSL_malloc(derAllocLen); + if (derData == NULL) { + ok = 0; + } + } + if (ok) { + rc = ctx->encode((void*)&slhdsa->key, derData, derAllocLen); + if (rc <= 0) { + ok = 0; + } + else { + derLen = (size_t)rc; + } + } + /* By default the plaintext DER is the source for the output encoding. */ + srcData = derData; + srcLen = derLen; + /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ + if (ok && ((ctx->format == WP_ENC_FORMAT_EPKI) || + ((ctx->format == WP_ENC_FORMAT_PKI) && + (ctx->cipherName != NULL)))) { + 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. */ + if (!wp_encrypt_key_pkcs8_size(ctx->provCtx, ctx->cipher, + (word32)derLen, &encLen)) { + ok = 0; + } + if (ok) { + encData = (unsigned char*)OPENSSL_malloc(encLen); + if (encData == NULL) { + ok = 0; + } + } + if (ok && (!wp_encrypt_key_pkcs8(ctx->provCtx, ctx->cipher, derData, + (word32)derLen, encData, &encLen, pwCb, pwCbArg))) { + ok = 0; + } + if (ok) { + srcData = encData; + srcLen = encLen; + } + } + + if (ok && (ctx->encoding == WP_FORMAT_DER)) { + keyData = srcData; + keyLen = srcLen; + } + else if (ok && (ctx->encoding == WP_FORMAT_PEM)) { + rc = wc_DerToPemEx(srcData, (word32)srcLen, NULL, 0, NULL, pemType); + if (rc <= 0) { + ok = 0; + } + if (ok) { + pemLen = (size_t)rc; + pemData = (unsigned char*)OPENSSL_malloc(pemLen); + if (pemData == NULL) { + ok = 0; + } + } + if (ok) { + rc = wc_DerToPemEx(srcData, (word32)srcLen, pemData, + (word32)pemLen, NULL, pemType); + if (rc <= 0) { + ok = 0; + } + } + if (ok) { + keyLen = pemLen = (size_t)rc; + keyData = pemData; + } + } + if (ok) { + rc = BIO_write(out, keyData, (int)keyLen); + if (rc <= 0) { + ok = 0; + } + } + + if (private) { + if (derData != NULL) { + OPENSSL_clear_free(derData, derAllocLen); + } + OPENSSL_clear_free(pemData, pemLen); + } + else { + OPENSSL_free(derData); + OPENSSL_free(pemData); + } + OPENSSL_clear_free(encData, encLen); + BIO_free(out); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Export the SLH-DSA key object. + * + * @param [in] ctx SLH-DSA encoder/decoder context object. + * @param [in] slhdsa SLH-DSA key object. + * @param [in] size Size of key object. + * @param [in] exportCb Callback to export key. + * @param [in] exportCbArg Argument to pass to callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_slhdsa_export_object(wp_SlhDsaEncDecCtx* ctx, wp_SlhDsa* slhdsa, + size_t size, OSSL_CALLBACK* exportCb, void* exportCbArg) +{ + (void)size; + return wp_slhdsa_export(slhdsa, ctx->selection, exportCb, exportCbArg); +} + +/** + * Return whether the SPKI decoder/encoder handles this part of the key. + * + * @param [in] provCtx Provider context. Unused. + * @param [in] selection Parts of key to handle. + * @return 1 when supported, 0 when not. + */ +static int wp_slhdsa_spki_does_selection(WOLFPROV_CTX* provCtx, int selection) +{ + int ok; + + WOLFPROV_ENTER_SILENT(WP_LOG_COMP_PQC, WOLFPROV_FUNC_NAME); + + (void)provCtx; + + if (selection == 0) { + ok = 1; + } + else { + ok = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + } + + WOLFPROV_LEAVE_SILENT(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Decode a public SLH-DSA key from SPKI DER. + * + * @param [in] input Buffer holding SPKI DER data. + * @param [in, out] inOutIdx On in, index into buffer. On out, index after. + * @param [in, out] key SLH-DSA key object. + * @param [in] inSz Length of buffer in bytes. + * @return 0 on success, negative on error. + */ +static int wp_slhdsa_pub_decode(const byte* input, word32* inOutIdx, void* key, + word32 inSz) +{ + return wc_SlhDsaKey_PublicKeyDecode(input, inOutIdx, (SlhDsaKey*)key, inSz); +} + +/** + * Encode the public part of an SLH-DSA key as SubjectPublicKeyInfo DER. + * + * Pass NULL for output to query the required length. + * + * @param [in] key SLH-DSA key object. + * @param [out] output Buffer to put encoded data in. + * @param [in] inLen Size of buffer in bytes. + * @return Size of encoded data in bytes on success, negative on error. + */ +static int wp_slhdsa_pub_encode(void* key, byte* output, word32 inLen) +{ + return wc_SlhDsaKey_PublicKeyToDer((SlhDsaKey*)key, output, inLen, 1); +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +/** + * Return whether the PKI decoder/encoder handles this part of the key. + * + * @param [in] provCtx Provider context. Unused. + * @param [in] selection Parts of key to handle. + * @return 1 when supported, 0 when not. + */ +static int wp_slhdsa_pki_does_selection(WOLFPROV_CTX* provCtx, int selection) +{ + int ok; + + WOLFPROV_ENTER_SILENT(WP_LOG_COMP_PQC, WOLFPROV_FUNC_NAME); + + (void)provCtx; + + if (selection == 0) { + ok = 1; + } + else { + ok = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + } + + WOLFPROV_LEAVE_SILENT(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Decode a private SLH-DSA key from PKCS8 PrivateKeyInfo DER. + * + * @param [in] input Buffer holding PKCS8 DER data. + * @param [in, out] inOutIdx On in, index into buffer. On out, index after. + * @param [in, out] key SLH-DSA key object. + * @param [in] inSz Length of buffer in bytes. + * @return 0 on success, negative on error. + */ +static int wp_slhdsa_priv_decode(const byte* input, word32* inOutIdx, + void* key, word32 inSz) +{ + return wc_SlhDsaKey_PrivateKeyDecode(input, inOutIdx, (SlhDsaKey*)key, + inSz); +} + +/** + * Encode the private part of an SLH-DSA key as PKCS8 PrivateKeyInfo DER. + * + * Pass NULL for output to query the required length. + * + * @param [in] key SLH-DSA key object. + * @param [out] output Buffer to put encoded data in. + * @param [in] inLen Size of buffer in bytes. + * @return Size of encoded data in bytes on success, negative on error. + */ +static int wp_slhdsa_priv_encode(void* key, byte* output, word32 inLen) +{ + /* RFC 9909 defines no private-only form for SLH-DSA: the 4n private key + * already contains the public half, so KeyToDer is the whole encoding. */ + return wc_SlhDsaKey_KeyToDer((SlhDsaKey*)key, output, inLen); +} +#endif /* WP_HAVE_SLHDSA_PRIVATE */ + +/* + * Per-parameter-set encoder/decoder context constructors and dispatch tables. + */ + +#define IMPLEMENT_SLHDSA_SPKI_DECODER(alg, dataType) \ +static wp_SlhDsaEncDecCtx* wp_##alg##_spki_dec_new(WOLFPROV_CTX* provCtx) \ +{ \ + return wp_slhdsa_enc_dec_new(provCtx, wp_##alg##_new, dataType, \ + WP_ENC_FORMAT_SPKI, 0, wp_slhdsa_pub_decode, NULL); \ +} \ +const OSSL_DISPATCH wp_##alg##_spki_decoder_functions[] = { \ + { OSSL_FUNC_DECODER_NEWCTX, (DFUNC)wp_##alg##_spki_dec_new }, \ + { OSSL_FUNC_DECODER_FREECTX, (DFUNC)wp_slhdsa_enc_dec_free }, \ + { OSSL_FUNC_DECODER_DOES_SELECTION, \ + (DFUNC)wp_slhdsa_spki_does_selection }, \ + { OSSL_FUNC_DECODER_DECODE, (DFUNC)wp_slhdsa_decode }, \ + { OSSL_FUNC_DECODER_EXPORT_OBJECT, (DFUNC)wp_slhdsa_export_object }, \ + { 0, NULL } \ +}; + +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define IMPLEMENT_SLHDSA_PKI_DECODER(alg, dataType) \ +static wp_SlhDsaEncDecCtx* wp_##alg##_pki_dec_new(WOLFPROV_CTX* provCtx) \ +{ \ + return wp_slhdsa_enc_dec_new(provCtx, wp_##alg##_new, dataType, \ + WP_ENC_FORMAT_PKI, 0, wp_slhdsa_priv_decode, NULL); \ +} \ +const OSSL_DISPATCH wp_##alg##_pki_decoder_functions[] = { \ + { OSSL_FUNC_DECODER_NEWCTX, (DFUNC)wp_##alg##_pki_dec_new }, \ + { OSSL_FUNC_DECODER_FREECTX, (DFUNC)wp_slhdsa_enc_dec_free }, \ + { OSSL_FUNC_DECODER_DOES_SELECTION, \ + (DFUNC)wp_slhdsa_pki_does_selection }, \ + { OSSL_FUNC_DECODER_DECODE, (DFUNC)wp_slhdsa_decode }, \ + { OSSL_FUNC_DECODER_EXPORT_OBJECT, (DFUNC)wp_slhdsa_export_object }, \ + { 0, NULL } \ +}; +#else +#define IMPLEMENT_SLHDSA_PKI_DECODER(alg, dataType) +#endif + +#define IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, fmt, enc, dsel) \ +static wp_SlhDsaEncDecCtx* wp_##alg##_##fmt##_##enc##_enc_new( \ + WOLFPROV_CTX* provCtx) \ +{ \ + return wp_slhdsa_enc_dec_new(provCtx, wp_##alg##_new, NULL, \ + WP_ENC_FORMAT_##fmt##_VAL, WP_FORMAT_##enc##_VAL, NULL, \ + WP_SLHDSA_ENC_##fmt##_ENCODE); \ +} \ +const OSSL_DISPATCH wp_##alg##_##fmt##_##enc##_encoder_functions[] = { \ + { OSSL_FUNC_ENCODER_NEWCTX, \ + (DFUNC)wp_##alg##_##fmt##_##enc##_enc_new }, \ + { OSSL_FUNC_ENCODER_FREECTX, (DFUNC)wp_slhdsa_enc_dec_free }, \ + { OSSL_FUNC_ENCODER_SETTABLE_CTX_PARAMS, \ + (DFUNC)wp_slhdsa_enc_dec_settable_ctx_params }, \ + { OSSL_FUNC_ENCODER_SET_CTX_PARAMS, \ + (DFUNC)wp_slhdsa_enc_dec_set_ctx_params }, \ + { OSSL_FUNC_ENCODER_DOES_SELECTION, (DFUNC)dsel }, \ + { OSSL_FUNC_ENCODER_ENCODE, (DFUNC)wp_slhdsa_encode }, \ + { OSSL_FUNC_ENCODER_IMPORT_OBJECT, (DFUNC)wp_slhdsa_import }, \ + { OSSL_FUNC_ENCODER_FREE_OBJECT, (DFUNC)wp_slhdsa_free }, \ + { 0, NULL } \ +}; + +/* Encode-function selectors for the table macro, mapping the lower-case token + * pasted into each table name onto its format/encoding enum value. */ +#ifndef WP_ENC_FORMAT_spki_VAL +#define WP_ENC_FORMAT_spki_VAL WP_ENC_FORMAT_SPKI +#define WP_ENC_FORMAT_pki_VAL WP_ENC_FORMAT_PKI +#define WP_ENC_FORMAT_epki_VAL WP_ENC_FORMAT_EPKI +#define WP_FORMAT_der_VAL WP_FORMAT_DER +#define WP_FORMAT_pem_VAL WP_FORMAT_PEM +#endif +#define WP_SLHDSA_ENC_spki_ENCODE wp_slhdsa_pub_encode +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define WP_SLHDSA_ENC_pki_ENCODE wp_slhdsa_priv_encode +#define WP_SLHDSA_ENC_epki_ENCODE wp_slhdsa_priv_encode +#endif + +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define IMPLEMENT_SLHDSA_PRIVATE_ENCODERS(alg) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, pki, der, \ + wp_slhdsa_pki_does_selection) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, pki, pem, \ + wp_slhdsa_pki_does_selection) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, epki, der, \ + wp_slhdsa_pki_does_selection) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, epki, pem, \ + wp_slhdsa_pki_does_selection) +#else +#define IMPLEMENT_SLHDSA_PRIVATE_ENCODERS(alg) +#endif + +#define IMPLEMENT_SLHDSA_ENC_DEC(alg, dataType) \ + IMPLEMENT_SLHDSA_SPKI_DECODER(alg, dataType) \ + IMPLEMENT_SLHDSA_PKI_DECODER(alg, dataType) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, spki, der, \ + wp_slhdsa_spki_does_selection) \ + IMPLEMENT_SLHDSA_ENCODER_TABLE(alg, spki, pem, \ + wp_slhdsa_spki_does_selection) \ + IMPLEMENT_SLHDSA_PRIVATE_ENCODERS(alg) + +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_128s, "SLH-DSA-SHAKE-128s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_128f, "SLH-DSA-SHAKE-128f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_192s, "SLH-DSA-SHAKE-192s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_192f, "SLH-DSA-SHAKE-192f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_256s, "SLH-DSA-SHAKE-256s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_shake_256f, "SLH-DSA-SHAKE-256f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_128s, "SLH-DSA-SHA2-128s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_128f, "SLH-DSA-SHA2-128f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_192s, "SLH-DSA-SHA2-192s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_192f, "SLH-DSA-SHA2-192f") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_256s, "SLH-DSA-SHA2-256s") +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F +IMPLEMENT_SLHDSA_ENC_DEC(slhdsa_sha2_256f, "SLH-DSA-SHA2-256f") +#endif + +#endif /* WP_HAVE_SLHDSA */ diff --git a/src/wp_slhdsa_sig.c b/src/wp_slhdsa_sig.c new file mode 100644 index 00000000..25e35dd3 --- /dev/null +++ b/src/wp_slhdsa_sig.c @@ -0,0 +1,938 @@ +/* wp_slhdsa_sig.c + * + * Copyright (C) 2006-2026 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 +#include +#include + +#ifdef WP_HAVE_SLHDSA + +#include + +/* FIPS 205 context string limit and the largest additional-randomness (n). */ +#define WP_SLHDSA_CTX_MAX 255 +#define WP_SLHDSA_RND_MAX 32 + +/* wolfSSL has no SLH-DSA streaming API, so bound the buffered message. */ +#define WP_SLHDSA_BUF_MAX (64UL * 1024UL * 1024UL) + +/* SLH-DSA signature context. */ +typedef struct wp_SlhDsaSigCtx { + WOLFPROV_CTX* provCtx; + wp_SlhDsa* slhdsa; +#ifdef WP_HAVE_SLHDSA_PRIVATE + WC_RNG rng; +#endif + unsigned char* mdBuf; + size_t mdLen; + size_t mdCap; + unsigned char* verifySig; + size_t verifySigLen; + unsigned char context[WP_SLHDSA_CTX_MAX]; + size_t contextLen; + unsigned char testEntropy[WP_SLHDSA_RND_MAX]; + size_t testEntropyLen; + unsigned int deterministic; /* Deterministic signing. */ + unsigned int rawMsg; /* Message is already FIPS 205 M'. */ +} wp_SlhDsaSigCtx; + +static int wp_slhdsa_set_ctx_params(wp_SlhDsaSigCtx* ctx, + const OSSL_PARAM params[]); + + +/** + * Append data into the streaming message buffer. + * + * @param [in, out] ctx Signature context. + * @param [in] data Data to append. + * @param [in] dataLen Length of data in bytes. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_buf_append(wp_SlhDsaSigCtx* ctx, + const unsigned char* data, size_t dataLen) +{ + int ok = 1; + size_t needed; + size_t newCap; + size_t doubled; + unsigned char* tmp; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_buf_append"); + + /* A NULL buffer with a non-zero length is a caller error; reject it before + * the copy rather than dereferencing NULL. (NULL + 0 is a valid no-op.) */ + if ((data == NULL) && (dataLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + + needed = ctx->mdLen + dataLen; + if (needed < ctx->mdLen) { + ok = 0; + } + if (ok && (needed > WP_SLHDSA_BUF_MAX)) { + ok = 0; + } + if (ok && (needed > ctx->mdCap)) { + newCap = ctx->mdCap == 0 ? 256 : ctx->mdCap; + while (newCap < needed) { + doubled = newCap * 2; + if (doubled < newCap) { + ok = 0; + break; + } + newCap = doubled; + } + if (ok) { + /* Grow by alloc+copy+zero rather than realloc so we always wipe + * the previous block (message can be signer-confidential). */ + tmp = (unsigned char*)OPENSSL_malloc(newCap); + if (tmp == NULL) { + ok = 0; + } + if (ok) { + if (ctx->mdLen > 0) { + XMEMCPY(tmp, ctx->mdBuf, ctx->mdLen); + } + OPENSSL_clear_free(ctx->mdBuf, ctx->mdCap); + ctx->mdBuf = tmp; + ctx->mdCap = newCap; + } + } + } + if (ok && (dataLen > 0)) { + XMEMCPY(ctx->mdBuf + ctx->mdLen, data, dataLen); + ctx->mdLen += dataLen; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Reset the streaming message buffer length to zero (keeps capacity). + * + * @param [in, out] ctx Signature context. + */ +static void wp_slhdsa_buf_reset(wp_SlhDsaSigCtx* ctx) +{ + /* Wipe stale bytes; ctx reuse across operations must not leak prior msg. */ + if ((ctx->mdBuf != NULL) && (ctx->mdLen > 0)) { + wc_ForceZero(ctx->mdBuf, ctx->mdLen); + } + ctx->mdLen = 0; +} + +/** + * Create a new SLH-DSA signature context object. + * + * @param [in] provCtx Provider context. + * @param [in] propq Property query string. Unused. + * @return New signature context on success, NULL on failure. + */ +static wp_SlhDsaSigCtx* wp_slhdsa_newctx(WOLFPROV_CTX* provCtx, + const char* propq) +{ + wp_SlhDsaSigCtx* ctx = NULL; + + (void)propq; + + if (wolfssl_prov_is_running()) { + ctx = (wp_SlhDsaSigCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } +#ifdef WP_HAVE_SLHDSA_PRIVATE + if (ctx != NULL) { + int rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + OPENSSL_free(ctx); + ctx = NULL; + } + } +#endif + if (ctx != NULL) { + ctx->provCtx = provCtx; + } + return ctx; +} + +/** + * Free an SLH-DSA signature context. + * + * @param [in, out] ctx Signature context. May be NULL. + */ +static void wp_slhdsa_freectx(wp_SlhDsaSigCtx* ctx) +{ + if (ctx != NULL) { +#ifdef WP_HAVE_SLHDSA_PRIVATE + wc_FreeRng(&ctx->rng); +#endif + wp_slhdsa_free(ctx->slhdsa); + OPENSSL_clear_free(ctx->mdBuf, ctx->mdCap); + OPENSSL_free(ctx->verifySig); + OPENSSL_clear_free(ctx, sizeof(*ctx)); + } +} + +/** + * Duplicate an SLH-DSA signature context. + * + * @param [in] src Source signature context. + * @return New signature context on success, NULL on failure. + */ +static wp_SlhDsaSigCtx* wp_slhdsa_dupctx(wp_SlhDsaSigCtx* src) +{ + wp_SlhDsaSigCtx* dst = NULL; + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_dupctx"); + + if (!wolfssl_prov_is_running() || (src == NULL)) { + return NULL; + } + dst = wp_slhdsa_newctx(src->provCtx, NULL); + if (dst == NULL) { + return NULL; + } + if ((src->slhdsa != NULL) && !wp_slhdsa_up_ref(src->slhdsa)) { + ok = 0; + } + if (ok) { + dst->slhdsa = src->slhdsa; + if (src->mdLen > 0) { + ok = wp_slhdsa_buf_append(dst, src->mdBuf, src->mdLen); + } + } + if (ok) { + XMEMCPY(dst->context, src->context, src->contextLen); + dst->contextLen = src->contextLen; + XMEMCPY(dst->testEntropy, src->testEntropy, src->testEntropyLen); + dst->testEntropyLen = src->testEntropyLen; + dst->deterministic = src->deterministic; + dst->rawMsg = src->rawMsg; + } + if (ok && (src->verifySig != NULL)) { + dst->verifySig = OPENSSL_memdup(src->verifySig, src->verifySigLen); + if (dst->verifySig == NULL) { + ok = 0; + } + else { + dst->verifySigLen = src->verifySigLen; + } + } + if (!ok) { + wp_slhdsa_freectx(dst); + dst = NULL; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + dst != NULL); + return dst; +} + +/** + * Common init: take a reference on the key, reset state. + * + * @param [in, out] ctx Signature context. + * @param [in] slhdsa SLH-DSA key (reference taken). + * @param [in] params Parameters to apply. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, + const OSSL_PARAM params[]) +{ + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_init"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + /* NULL reuses the existing key and requires one to be present. */ + if ((slhdsa == NULL) && (ctx->slhdsa == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (slhdsa != NULL) { + if (!wp_slhdsa_up_ref(slhdsa)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + wp_slhdsa_free(ctx->slhdsa); + ctx->slhdsa = slhdsa; + } + wp_slhdsa_buf_reset(ctx); + /* Pure mode is the newctx default and persists across a re-init, as do the + * context string, deterministic flag and test entropy. Resetting the + * encoding here would silently change what a raw-mode caller signs. */ + if (!wp_slhdsa_set_ctx_params(ctx, params)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +static int wp_slhdsa_sign_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, + const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_sign_init"); + ok = wp_slhdsa_init(ctx, slhdsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif + +static int wp_slhdsa_verify_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, + const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_verify_init"); + ok = wp_slhdsa_init(ctx, slhdsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +/** + * One-shot sign of a message. + * + * If sig is NULL, just report the signature size in sigLen. + * + * @param [in] ctx Signature context. + * @param [out] sig Signature buffer. + * @param [in, out] sigLen On in, buffer size; on out, signature length. + * @param [in] sigSize Allocated size of sig. + * @param [in] msg Message to sign. + * @param [in] msgLen Message length. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_sign(wp_SlhDsaSigCtx* ctx, unsigned char* sig, + size_t* sigLen, size_t sigSize, const unsigned char* msg, size_t msgLen) +{ + int ok = 1; + int rc; + word32 sigSz; + /* FIPS 205 permits an empty message; give wolfSSL a valid pointer so a + * NULL+0 message does not become a backend-dependent NULL deref. */ + unsigned char dummy = 0; + const unsigned char* m = msg; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_sign"); + + if ((ctx == NULL) || (ctx->slhdsa == NULL) || (sigLen == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if ((msg == NULL) && (msgLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (m == NULL) { + m = &dummy; + } + + sigSz = (word32)wp_slhdsa_get_sig_size(ctx->slhdsa); + + if (sig == NULL) { + *sigLen = sigSz; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + /* sigSize is the authoritative buffer capacity; fall back to *sigLen only + * when the dispatcher passes SIZE_MAX (matching wp_ecx_sig). */ + if (sigSize == (size_t)-1) { + sigSize = *sigLen; + } + if (sigSize < sigSz) { + ok = 0; + } + /* wolfSSL's SLH-DSA API takes a 32-bit message length. Reject >4 GiB + * messages explicitly rather than silently truncating. */ + if (ok && ((!WP_FITS_WORD32(msgLen)) || + (msgLen > WP_SLHDSA_BUF_MAX))) { + ok = 0; + } + /* Test entropy is sized against the key it was set for, and a re-init can + * swap in a key from another parameter set. wolfSSL reads n bytes, so a + * stale value would pad the randomizer with zeros the caller never gave. */ + if (ok && (ctx->testEntropyLen > 0) && + (ctx->testEntropyLen != (size_t)wp_slhdsa_get_n(ctx->slhdsa))) { + ok = 0; + } + if (ok) { + word32 outLen = sigSz; + SlhDsaKey* key = (SlhDsaKey*)wp_slhdsa_get_key(ctx->slhdsa); + + if (wp_lock(wp_slhdsa_get_mutex(ctx->slhdsa)) != 1) { + ok = 0; + } + if (ok && ctx->rawMsg) { + /* FIPS 205 internal interface: the message already is M', so the + * context string is part of it and must not be applied again. */ + if (ctx->testEntropyLen > 0) { + rc = wc_SlhDsaKey_SignMsgWithRandom(key, m, (word32)msgLen, + sig, &outLen, ctx->testEntropy); + } + else if (ctx->deterministic) { + rc = wc_SlhDsaKey_SignMsgDeterministic(key, m, (word32)msgLen, + sig, &outLen); + } + else { + byte rnd[WP_SLHDSA_RND_MAX]; + int n = wp_slhdsa_get_n(ctx->slhdsa); + + rc = wc_RNG_GenerateBlock(&ctx->rng, rnd, (word32)n); + if (rc == 0) { + rc = wc_SlhDsaKey_SignMsgWithRandom(key, m, + (word32)msgLen, sig, &outLen, rnd); + } + wc_ForceZero(rnd, sizeof(rnd)); + } + } + else if (ok && (ctx->testEntropyLen > 0)) { + rc = wc_SlhDsaKey_SignWithRandom(key, ctx->context, + (byte)ctx->contextLen, m, (word32)msgLen, sig, &outLen, + ctx->testEntropy); + } + else if (ok && ctx->deterministic) { + rc = wc_SlhDsaKey_SignDeterministic(key, ctx->context, + (byte)ctx->contextLen, m, (word32)msgLen, sig, &outLen); + } + else if (ok) { + rc = wc_SlhDsaKey_Sign(key, ctx->context, (byte)ctx->contextLen, + m, (word32)msgLen, sig, &outLen, &ctx->rng); + } + if (ok) { + wp_unlock(wp_slhdsa_get_mutex(ctx->slhdsa)); + } + if (ok && (rc != 0)) { + ok = 0; + } + if (ok) { + *sigLen = outLen; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif /* WP_HAVE_SLHDSA_PRIVATE */ + +/** + * One-shot verify of a signature on a message. + * + * @param [in] ctx Signature context. + * @param [in] sig Signature. + * @param [in] sigLen Signature length. + * @param [in] msg Message. + * @param [in] msgLen Message length. + * @return 1 if signature valid, 0 otherwise. + */ +static int wp_slhdsa_verify(wp_SlhDsaSigCtx* ctx, const unsigned char* sig, + size_t sigLen, const unsigned char* msg, size_t msgLen) +{ + int ok = 1; + int rc; + /* FIPS 205 permits an empty message; give wolfSSL a valid pointer. */ + unsigned char dummy = 0; + const unsigned char* m = msg; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_verify"); + + if ((ctx == NULL) || (ctx->slhdsa == NULL) || (sig == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if ((msg == NULL) && (msgLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (m == NULL) { + m = &dummy; + } + /* wolfSSL's SLH-DSA API takes 32-bit lengths. Reject oversize inputs + * explicitly rather than silently truncating. */ + if ((!WP_FITS_WORD32(sigLen)) || (!WP_FITS_WORD32(msgLen)) || + (msgLen > WP_SLHDSA_BUF_MAX)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + + /* Returns 0 only for a valid signature; SIG_VERIFY_E and every other + * non-zero code is a verification failure. */ + if (wp_lock(wp_slhdsa_get_mutex(ctx->slhdsa)) != 1) { + ok = 0; + } + if (ok && ctx->rawMsg) { + rc = wc_SlhDsaKey_VerifyMsg((SlhDsaKey*)wp_slhdsa_get_key(ctx->slhdsa), + m, (word32)msgLen, sig, (word32)sigLen); + } + else if (ok) { + rc = wc_SlhDsaKey_Verify((SlhDsaKey*)wp_slhdsa_get_key(ctx->slhdsa), + ctx->context, (byte)ctx->contextLen, m, (word32)msgLen, sig, + (word32)sigLen); + } + if (ok) { + wp_unlock(wp_slhdsa_get_mutex(ctx->slhdsa)); + } + if (ok && (rc != 0)) { + ok = 0; + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Reject a pre-hash digest name. FIPS 205 HashSLH-DSA is a distinct algorithm + * and is not implemented here, so a named digest must not be silently ignored. + * + * @param [in] mdName Digest name, may be NULL or empty for pure mode. + * @return 1 when pure mode, 0 when a digest was requested. + */ +static int wp_slhdsa_check_pure(const char* mdName) +{ + return (mdName == NULL) || (mdName[0] == '\0'); +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +static int wp_slhdsa_digest_sign_init(wp_SlhDsaSigCtx* ctx, const char* mdName, + wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_digest_sign_init"); + ok = wp_slhdsa_check_pure(mdName) && wp_slhdsa_init(ctx, slhdsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif + +static int wp_slhdsa_digest_verify_init(wp_SlhDsaSigCtx* ctx, + const char* mdName, wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_digest_verify_init"); + ok = wp_slhdsa_check_pure(mdName) && wp_slhdsa_init(ctx, slhdsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Accumulate message bytes for a streamed sign or verify. + * + * @param [in, out] ctx Signature context. + * @param [in] data Data to append. + * @param [in] dataLen Length of data. + * @return 1 on success, 0 on failure. + */ +static int wp_slhdsa_digest_signverify_update(wp_SlhDsaSigCtx* ctx, + const unsigned char* data, size_t dataLen) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_digest_signverify_update"); + if ((ctx == NULL) || (ctx->slhdsa == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + ok = wp_slhdsa_buf_append(ctx, data, dataLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +static int wp_slhdsa_digest_sign_final(wp_SlhDsaSigCtx* ctx, + unsigned char* sig, size_t* sigLen, size_t sigSize) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_digest_sign_final"); + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + ok = wp_slhdsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif + +static int wp_slhdsa_digest_verify_final(wp_SlhDsaSigCtx* ctx, + const unsigned char* sig, size_t sigLen) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_digest_verify_final"); + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + ok = wp_slhdsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +#if defined(OPENSSL_VERSION_NUMBER) && \ + (OPENSSL_VERSION_NUMBER >= 0x30500000L) +/* OpenSSL 3.5+ signature message API. */ +static int wp_slhdsa_message_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, + const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_message_init"); + ok = wp_slhdsa_init(ctx, slhdsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +#ifdef WP_HAVE_SLHDSA_PRIVATE +static int wp_slhdsa_sign_message_final(wp_SlhDsaSigCtx* ctx, + unsigned char* sig, size_t* sigLen, size_t sigSize) +{ + return wp_slhdsa_digest_sign_final(ctx, sig, sigLen, sigSize); +} +#endif + +static int wp_slhdsa_verify_message_init(wp_SlhDsaSigCtx* ctx, + wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) +{ + if (ctx != NULL) { + OPENSSL_free(ctx->verifySig); + ctx->verifySig = NULL; + ctx->verifySigLen = 0; + } + return wp_slhdsa_message_init(ctx, slhdsa, params); +} + +static int wp_slhdsa_verify_message_final(wp_SlhDsaSigCtx* ctx) +{ + if ((ctx == NULL) || (ctx->verifySig == NULL)) { + return 0; + } + return wp_slhdsa_digest_verify_final(ctx, ctx->verifySig, + ctx->verifySigLen); +} +#endif /* OPENSSL_VERSION_NUMBER >= 0x30500000L */ + +/* DER AlgorithmIdentifier (SEQUENCE { OID }) for each SLH-DSA parameter set. + * FIPS 205 signature algorithms carry no parameters, so the encoding differs + * only in the final OID arc (20 through 31). */ +static const byte wp_slhdsa_aid_prefix[] = { + 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03 +}; + +/* Map a parameter id to its OID arc, following the id-slh-dsa-* assignment + * order: SHA2 128s..256f are .20-.25, SHAKE 128s..256f are .26-.31. */ +static int wp_slhdsa_aid_arc(int param, byte* arc) +{ + int ok = 1; + + switch (param) { + case SLHDSA_SHAKE128S: *arc = 0x1a; break; + case SLHDSA_SHAKE128F: *arc = 0x1b; break; + case SLHDSA_SHAKE192S: *arc = 0x1c; break; + case SLHDSA_SHAKE192F: *arc = 0x1d; break; + case SLHDSA_SHAKE256S: *arc = 0x1e; break; + case SLHDSA_SHAKE256F: *arc = 0x1f; break; + #ifdef WOLFSSL_SLHDSA_SHA2 + case SLHDSA_SHA2_128S: *arc = 0x14; break; + case SLHDSA_SHA2_128F: *arc = 0x15; break; + case SLHDSA_SHA2_192S: *arc = 0x16; break; + case SLHDSA_SHA2_192F: *arc = 0x17; break; + case SLHDSA_SHA2_256S: *arc = 0x18; break; + case SLHDSA_SHA2_256F: *arc = 0x19; break; + #endif + default: + ok = 0; + break; + } + return ok; +} + +/* Set the X.509 signature AlgorithmIdentifier for the key's parameter set. */ +static int wp_slhdsa_get_alg_id(wp_SlhDsaSigCtx* ctx, OSSL_PARAM* p) +{ + int ok = 1; + byte aid[sizeof(wp_slhdsa_aid_prefix) + 1]; + byte arc = 0; + + if (!wp_slhdsa_aid_arc(wp_slhdsa_get_param(ctx->slhdsa), &arc)) { + ok = 0; + } + if (ok) { + XMEMCPY(aid, wp_slhdsa_aid_prefix, sizeof(wp_slhdsa_aid_prefix)); + aid[sizeof(wp_slhdsa_aid_prefix)] = arc; + if (!OSSL_PARAM_set_octet_string(p, aid, sizeof(aid))) { + ok = 0; + } + } + return ok; +} + +/* Provides the X.509 signature AlgorithmIdentifier so certificate and other + * structure signing (ASN1_item_sign_ctx) can build the signatureAlgorithm. */ +static int wp_slhdsa_get_ctx_params(wp_SlhDsaSigCtx* ctx, OSSL_PARAM* params) +{ + int ok = 1; + OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_get_ctx_params"); + + if (ctx == NULL) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_SIGNATURE_PARAM_ALGORITHM_ID); + if (p != NULL) { + ok = wp_slhdsa_get_alg_id(ctx, p); + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +static const OSSL_PARAM* wp_slhdsa_gettable_ctx_params(wp_SlhDsaSigCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_gettable[] = { + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_ALGORITHM_ID, NULL, 0), + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_slhdsa_gettable; +} + +/* Honor the FIPS 205 signature params OpenSSL drives SLH-DSA with: context + * string, deterministic/hedged selection, and a test-only randomizer. + * message-encoding selects pure (1) or the pre-built FIPS 205 M' (0). */ +static int wp_slhdsa_set_ctx_params(wp_SlhDsaSigCtx* ctx, + const OSSL_PARAM params[]) +{ + int ok = 1; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_set_ctx_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_CONTEXT_STRING); + if (p != NULL) { + void* vp = ctx->context; + size_t len = 0; + + /* Changing the context mid-stream would sign a different M' than the + * accumulated message was started under. */ + if (ctx->mdLen > 0) { + ok = 0; + } + if (ok && !OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->context), + &len)) { + ok = 0; + } + if (ok) { + ctx->contextLen = len; + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_TEST_ENTROPY); + if (p != NULL) { + void* vp = ctx->testEntropy; + size_t len = 0; + + ctx->testEntropyLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, + sizeof(ctx->testEntropy), &len)) { + ok = 0; + } + /* addrnd is exactly n bytes. A short value would silently fall + * back to hedged signing, breaking the caller's reproducibility. */ + if (ok && (len != (size_t)wp_slhdsa_get_n(ctx->slhdsa))) { + ok = 0; + } + if (ok) { + ctx->testEntropyLen = len; + } + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_DETERMINISTIC); + if (p != NULL) { + int det = 0; + if (!OSSL_PARAM_get_int(p, &det)) { + ok = 0; + } + if (ok) { + ctx->deterministic = (det != 0); + } + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, + OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING); + if (p != NULL) { + int enc = 0; + /* 1 builds M' from the message here; 0 means the caller already + * did, per the FIPS 205 internal interface. Changing it mid-stream + * would reinterpret the accumulated bytes under a different domain + * separation, so refuse once input has been consumed. */ + if (ctx->mdLen > 0) { + ok = 0; + } + if (ok && (!OSSL_PARAM_get_int(p, &enc) || + ((enc != 0) && (enc != 1)))) { + ok = 0; + } + if (ok) { + ctx->rawMsg = (enc == 0); + } + } + } +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_SIGNATURE); + if (p != NULL) { + unsigned char* sig = NULL; + size_t sigLen = 0; + + if (!OSSL_PARAM_get_octet_string(p, (void**)&sig, 0, &sigLen)) { + ok = 0; + } + if (ok) { + OPENSSL_free(ctx->verifySig); + ctx->verifySig = sig; + ctx->verifySigLen = sigLen; + } + } + } +#endif + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +static const OSSL_PARAM* wp_slhdsa_settable_ctx_params(wp_SlhDsaSigCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_slhdsa_settable[] = { + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_CONTEXT_STRING, NULL, 0), + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_TEST_ENTROPY, NULL, 0), + OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_DETERMINISTIC, NULL), + OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING, NULL), +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_SIGNATURE, NULL, 0), +#endif + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_slhdsa_settable; +} + +/* Verify-only builds omit the sign entry points entirely rather than stubbing + * them: OpenSSL then reports "operation not supported for this key type". */ +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define WP_SLHDSA_SIGN_DISPATCH \ + { OSSL_FUNC_SIGNATURE_SIGN_INIT, \ + (DFUNC)wp_slhdsa_sign_init }, \ + { OSSL_FUNC_SIGNATURE_SIGN, \ + (DFUNC)wp_slhdsa_sign }, \ + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_INIT, \ + (DFUNC)wp_slhdsa_digest_sign_init }, \ + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_UPDATE, \ + (DFUNC)wp_slhdsa_digest_signverify_update }, \ + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_FINAL, \ + (DFUNC)wp_slhdsa_digest_sign_final }, +#else +#define WP_SLHDSA_SIGN_DISPATCH +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x30500000L +#ifdef WP_HAVE_SLHDSA_PRIVATE +#define WP_SLHDSA_SIGN_MESSAGE_DISPATCH \ + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_INIT, \ + (DFUNC)wp_slhdsa_message_init }, \ + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_UPDATE, \ + (DFUNC)wp_slhdsa_digest_signverify_update }, \ + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_FINAL, \ + (DFUNC)wp_slhdsa_sign_message_final }, +#else +#define WP_SLHDSA_SIGN_MESSAGE_DISPATCH +#endif +#define WP_SLHDSA_VERIFY_MESSAGE_DISPATCH \ + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_INIT, \ + (DFUNC)wp_slhdsa_verify_message_init }, \ + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_UPDATE, \ + (DFUNC)wp_slhdsa_digest_signverify_update }, \ + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_FINAL, \ + (DFUNC)wp_slhdsa_verify_message_final }, +#else +#define WP_SLHDSA_SIGN_MESSAGE_DISPATCH +#define WP_SLHDSA_VERIFY_MESSAGE_DISPATCH +#endif + +/* Dispatch table shared by all SLH-DSA parameter sets. */ +const OSSL_DISPATCH wp_slhdsa_signature_functions[] = { + { OSSL_FUNC_SIGNATURE_NEWCTX, + (DFUNC)wp_slhdsa_newctx }, + { OSSL_FUNC_SIGNATURE_FREECTX, + (DFUNC)wp_slhdsa_freectx }, + { OSSL_FUNC_SIGNATURE_DUPCTX, + (DFUNC)wp_slhdsa_dupctx }, + WP_SLHDSA_SIGN_DISPATCH + { OSSL_FUNC_SIGNATURE_VERIFY_INIT, + (DFUNC)wp_slhdsa_verify_init }, + { OSSL_FUNC_SIGNATURE_VERIFY, + (DFUNC)wp_slhdsa_verify }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_INIT, + (DFUNC)wp_slhdsa_digest_verify_init }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_UPDATE, + (DFUNC)wp_slhdsa_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_FINAL, + (DFUNC)wp_slhdsa_digest_verify_final }, + WP_SLHDSA_SIGN_MESSAGE_DISPATCH + WP_SLHDSA_VERIFY_MESSAGE_DISPATCH + { OSSL_FUNC_SIGNATURE_GET_CTX_PARAMS, + (DFUNC)wp_slhdsa_get_ctx_params }, + { OSSL_FUNC_SIGNATURE_GETTABLE_CTX_PARAMS, + (DFUNC)wp_slhdsa_gettable_ctx_params }, + { OSSL_FUNC_SIGNATURE_SET_CTX_PARAMS, + (DFUNC)wp_slhdsa_set_ctx_params }, + { OSSL_FUNC_SIGNATURE_SETTABLE_CTX_PARAMS, + (DFUNC)wp_slhdsa_settable_ctx_params }, + { 0, NULL } +}; + +#endif /* WP_HAVE_SLHDSA */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index f09c5607..3f70948e 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -701,6 +701,54 @@ static const OSSL_ALGORITHM wolfprov_keymgmt[] = { { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, wp_mldsa87_keymgmt_functions, "" }, #endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S + { WP_NAMES_SLH_DSA_SHA2_128S, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_128s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F + { WP_NAMES_SLH_DSA_SHA2_128F, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_128f_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S + { WP_NAMES_SLH_DSA_SHA2_192S, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_192s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F + { WP_NAMES_SLH_DSA_SHA2_192F, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_192f_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S + { WP_NAMES_SLH_DSA_SHA2_256S, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_256s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F + { WP_NAMES_SLH_DSA_SHA2_256F, WOLFPROV_PROPERTIES, + wp_slhdsa_sha2_256f_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S + { WP_NAMES_SLH_DSA_SHAKE_128S, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_128s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F + { WP_NAMES_SLH_DSA_SHAKE_128F, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_128f_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S + { WP_NAMES_SLH_DSA_SHAKE_192S, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_192s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F + { WP_NAMES_SLH_DSA_SHAKE_192F, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_192f_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S + { WP_NAMES_SLH_DSA_SHAKE_256S, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_256s_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F + { WP_NAMES_SLH_DSA_SHAKE_256F, WOLFPROV_PROPERTIES, + wp_slhdsa_shake_256f_keymgmt_functions, "" }, +#endif { NULL, NULL, NULL, NULL } }; @@ -769,6 +817,54 @@ static const OSSL_ALGORITHM wolfprov_signature[] = { { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, wp_mldsa_signature_functions, "" }, #endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S + { WP_NAMES_SLH_DSA_SHA2_128S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F + { WP_NAMES_SLH_DSA_SHA2_128F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S + { WP_NAMES_SLH_DSA_SHA2_192S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F + { WP_NAMES_SLH_DSA_SHA2_192F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S + { WP_NAMES_SLH_DSA_SHA2_256S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F + { WP_NAMES_SLH_DSA_SHA2_256F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S + { WP_NAMES_SLH_DSA_SHAKE_128S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F + { WP_NAMES_SLH_DSA_SHAKE_128F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S + { WP_NAMES_SLH_DSA_SHAKE_192S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F + { WP_NAMES_SLH_DSA_SHAKE_192F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S + { WP_NAMES_SLH_DSA_SHAKE_256S, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F + { WP_NAMES_SLH_DSA_SHAKE_256F, WOLFPROV_PROPERTIES, + wp_slhdsa_signature_functions, "" }, +#endif { NULL, NULL, NULL, NULL } }; @@ -1062,6 +1158,198 @@ static const OSSL_ALGORITHM wolfprov_encoder[] = { { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), wp_mldsa87_epki_pem_encoder_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_128s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_128s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_128s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_128s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_128s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_128s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_128f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_128f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_128f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_128f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_128f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_128f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_192s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_192s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_192s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_192s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_192s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_192s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_192f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_192f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_192f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_192f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_192f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_192f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_256s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_256s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_256s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_256s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_256s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_256s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_shake_256f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_shake_256f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_shake_256f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_shake_256f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_shake_256f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_shake_256f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_128s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_128s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_128s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_128s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_128s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_128s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_128f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_128f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_128f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_128f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_128f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_128F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_128f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_192s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_192s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_192s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_192s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_192s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_192s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_192f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_192f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_192f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_192f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_192f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_192F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_192f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_256s_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_256s_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_256s_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_256s_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_256s_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256S, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_256s_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_slhdsa_sha2_256f_spki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_slhdsa_sha2_256f_spki_pem_encoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_slhdsa_sha2_256f_pki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_slhdsa_sha2_256f_pki_pem_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_slhdsa_sha2_256f_epki_der_encoder_functions, "" }, + { WP_NAMES_SLH_DSA_SHA2_256F, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_slhdsa_sha2_256f_epki_pem_encoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ #endif { NULL, NULL, NULL, NULL } @@ -1243,6 +1531,102 @@ static const OSSL_ALGORITHM wolfprov_decoder[] = { { WP_NAMES_ML_DSA_87, WP_DECODER_PROPERTIES(PrivateKeyInfo), wp_mldsa87_pki_decoder_functions, "" }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128S + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_128s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_128S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_128s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_128f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_128F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_128f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192S + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_192s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_192S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_192s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_192f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_192F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_192f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256S + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_256s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_256S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_256s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_shake_256f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHAKE_256F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_shake_256f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128S + { WP_NAMES_SLH_DSA_SHA2_128S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_128s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_128S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_128s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_128F + { WP_NAMES_SLH_DSA_SHA2_128F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_128f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_128F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_128f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192S + { WP_NAMES_SLH_DSA_SHA2_192S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_192s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_192S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_192s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_192F + { WP_NAMES_SLH_DSA_SHA2_192F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_192f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_192F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_192f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256S + { WP_NAMES_SLH_DSA_SHA2_256S, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_256s_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_256S, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_256s_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ +#endif +#ifdef WP_HAVE_SLH_DSA_SHA2_256F + { WP_NAMES_SLH_DSA_SHA2_256F, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_slhdsa_sha2_256f_spki_decoder_functions, "" }, +#ifdef WP_HAVE_SLHDSA_PRIVATE + { WP_NAMES_SLH_DSA_SHA2_256F, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_slhdsa_sha2_256f_pki_decoder_functions, "" }, +#endif /* WP_HAVE_SLHDSA_PRIVATE */ #endif /* Dummy decoder added to match PKI bit not match EPKI from context. From 5f21caec58c44bc7920f2615db654a245f0ab674 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 10:52:53 -0700 Subject: [PATCH 03/15] Add SLH-DSA unit coverage --- test/include.am | 1 + test/test_slhdsa.c | 1430 ++++++++++++++++++++++++++++++++++++++++++++ test/unit.c | 26 + test/unit.h | 40 ++ 4 files changed, 1497 insertions(+) create mode 100644 test/test_slhdsa.c diff --git a/test/include.am b/test/include.am index 8a0e9528..30cb3b6d 100644 --- a/test/include.am +++ b/test/include.am @@ -34,6 +34,7 @@ test_unit_test_SOURCES = \ test/test_pkcs7_x509.c \ test/test_mlkem.c \ test/test_mldsa.c \ + test/test_slhdsa.c \ test/test_rand.c \ test/test_rsa.c \ test/test_seccomp_sandbox.c \ diff --git a/test/test_slhdsa.c b/test/test_slhdsa.c new file mode 100644 index 00000000..15c0fff8 --- /dev/null +++ b/test/test_slhdsa.c @@ -0,0 +1,1430 @@ +/* test_slhdsa.c + * + * Copyright (C) 2006-2026 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 "unit.h" + +#include +#include +#include +#include +#include + +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) && \ + defined(WP_SLHDSA_TEST_SETS) + +#include + +/* Per-parameter-set metadata. */ +typedef struct slhdsa_test_set { + const char* name; + size_t pubKeySize; + size_t privKeySize; + size_t sigSize; + int securityBits; + int securityCategory; +} slhdsa_test_set; + +/* A representative spread rather than all 12: both hash families, both the + * small and fast tradeoffs, and all three security parameters. The 's' sets + * sign roughly ten times slower, so only one is in the default matrix. */ +static const slhdsa_test_set slhdsa_sets[] = { +#ifdef WP_HAVE_SLH_DSA_SHA2_128F + { "SLH-DSA-SHA2-128f", WC_SLHDSA_SHA2_128F_PUB_LEN, + WC_SLHDSA_SHA2_128F_PRIV_LEN, WC_SLHDSA_SHA2_128F_SIG_LEN, 128, 1 }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_128F + { "SLH-DSA-SHAKE-128f", WC_SLHDSA_SHAKE128F_PUB_LEN, + WC_SLHDSA_SHAKE128F_PRIV_LEN, WC_SLHDSA_SHAKE128F_SIG_LEN, 128, 1 }, +#endif +#ifndef WOLFPROV_QUICKTEST +#ifdef WP_HAVE_SLH_DSA_SHA2_128S + { "SLH-DSA-SHA2-128s", WC_SLHDSA_SHA2_128S_PUB_LEN, + WC_SLHDSA_SHA2_128S_PRIV_LEN, WC_SLHDSA_SHA2_128S_SIG_LEN, 128, 1 }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_192F + { "SLH-DSA-SHAKE-192f", WC_SLHDSA_SHAKE192F_PUB_LEN, + WC_SLHDSA_SHAKE192F_PRIV_LEN, WC_SLHDSA_SHAKE192F_SIG_LEN, 192, 3 }, +#endif +#ifdef WP_HAVE_SLH_DSA_SHAKE_256F + { "SLH-DSA-SHAKE-256f", WC_SLHDSA_SHAKE256F_PUB_LEN, + WC_SLHDSA_SHAKE256F_PRIV_LEN, WC_SLHDSA_SHAKE256F_SIG_LEN, 256, 5 }, +#endif +#endif /* !WOLFPROV_QUICKTEST */ +}; +#define SLHDSA_SET_COUNT (sizeof(slhdsa_sets) / sizeof(slhdsa_sets[0])) + + + +static const unsigned char slhdsa_test_msg[] = + "wolfProvider SLH-DSA test message bytes for FIPS 205 sign/verify"; +#define SLHDSA_TEST_MSG_LEN (sizeof(slhdsa_test_msg) - 1) + + +/** + * Generate an SLH-DSA key pair via wolfProvider. + * + * @param [in] name Algorithm name (e.g. "SLH-DSA-SHA2-128f"). + * @param [out] pkey Generated EVP_PKEY (caller frees). + * @return 0 on success, non-zero on failure. + */ +static int slhdsa_keygen(const char* name, EVP_PKEY** pkey) +{ + int err = 0; + EVP_PKEY_CTX* ctx = NULL; + + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, name, NULL); + err = (ctx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_keygen(ctx, pkey) != 1; + } + EVP_PKEY_CTX_free(ctx); + return err; +} + +/** + * Extract a raw octet-string key parameter from an SLH-DSA EVP_PKEY. + */ +static int slhdsa_get_raw(EVP_PKEY* pkey, const char* param, + unsigned char** out, size_t* len) +{ + int err = 0; + size_t need = 0; + + err = EVP_PKEY_get_octet_string_param(pkey, param, NULL, 0, &need) != 1; + if (err == 0) { + *out = (unsigned char*)OPENSSL_malloc(need); + err = (*out == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(pkey, param, *out, need, + len) != 1; + } + if (err && (*out != NULL)) { + OPENSSL_free(*out); + *out = NULL; + } + return err; +} + +static int slhdsa_get_pub(EVP_PKEY* pkey, unsigned char** out, size_t* len) +{ + return slhdsa_get_raw(pkey, OSSL_PKEY_PARAM_PUB_KEY, out, len); +} + +/** + * Sign a message with the given SLH-DSA EVP_PKEY using the digest-sign API + * (which for SLH-DSA passes the whole message to the one-shot signer). + */ +static int slhdsa_sign_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, unsigned char** sigOut, size_t* sigLenOut) +{ + int err = 0; + EVP_MD_CTX* mdctx = NULL; + size_t sigLen = 0; + unsigned char* sig = NULL; + + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sigLen, msg, msgLen) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig, &sigLen, msg, msgLen) != 1; + } + if (err == 0) { + *sigOut = sig; + *sigLenOut = sigLen; + } + else { + OPENSSL_free(sig); + } + EVP_MD_CTX_free(mdctx); + return err; +} + +/** + * Verify a signature on a message with the given SLH-DSA EVP_PKEY. + * + * @return 1 if verified, 0 if not (does not set err on bad sig). + */ +static int slhdsa_verify_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, const unsigned char* sig, size_t sigLen) +{ + int ok = 0; + int rc; + EVP_MD_CTX* mdctx = NULL; + + mdctx = EVP_MD_CTX_new(); + if (mdctx == NULL) { + return 0; + } + rc = EVP_DigestVerifyInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, NULL); + if (rc == 1) { + rc = EVP_DigestVerify(mdctx, sig, sigLen, msg, msgLen); + if (rc == 1) { + ok = 1; + } + } + EVP_MD_CTX_free(mdctx); + return ok; +} + +/** + * Test SLH-DSA key generation; verify key sizes and that two keys differ. + */ +int test_slhdsa_keygen(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + unsigned char* p1 = NULL; + unsigned char* p2 = NULL; + size_t p1Len = 0; + size_t p2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Keygen %s", set->name); + + err = slhdsa_keygen(set->name, &k1); + if (err == 0) { + err = slhdsa_keygen(set->name, &k2); + } + if (err == 0) { + err = slhdsa_get_pub(k1, &p1, &p1Len); + } + if (err == 0) { + err = slhdsa_get_pub(k2, &p2, &p2Len); + } + if (err == 0) { + err = (p1Len != set->pubKeySize); + if (err) { + PRINT_ERR_MSG("Unexpected pub key size %zu vs %zu", + p1Len, set->pubKeySize); + } + } + if (err == 0) { + err = (memcmp(p1, p2, p1Len) == 0); + } + + OPENSSL_free(p1); p1 = NULL; + OPENSSL_free(p2); p2 = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/** + * Test SLH-DSA raw key import/export round-trip. + */ +int test_slhdsa_import_export_roundtrip(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + OSSL_PARAM_BLD* bld = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* pub2 = NULL; + unsigned char* priv2 = NULL; + size_t pubLen = 0, privLen = 0, pub2Len = 0, priv2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Import/export round-trip %s", set->name); + + err = slhdsa_keygen(set->name, &k1); + if (err == 0) { + err = slhdsa_get_pub(k1, &pub, &pubLen); + } + if (err == 0) { + err = slhdsa_get_raw(k1, OSSL_PKEY_PARAM_PRIV_KEY, &priv, + &privLen); + } + if (err == 0) { + err = (privLen != set->privKeySize); + } + if (err == 0) { + bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL); + } + if (err == 0) { + err = OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub, pubLen) != 1; + } + if (err == 0) { + err = OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + } + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, set->name, NULL); + err = (ctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_fromdata_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &k2, EVP_PKEY_KEYPAIR, params) != 1; + } + if (err == 0) { + err = slhdsa_get_pub(k2, &pub2, &pub2Len); + } + if (err == 0) { + err = slhdsa_get_raw(k2, OSSL_PKEY_PARAM_PRIV_KEY, &priv2, + &priv2Len); + } + if (err == 0) { + err = (pubLen != pub2Len) || (memcmp(pub, pub2, pubLen) != 0); + } + if (err == 0) { + err = (privLen != priv2Len) || (memcmp(priv, priv2, privLen) != 0); + } + + OPENSSL_free(pub); pub = NULL; + OPENSSL_free(priv); priv = NULL; + OPENSSL_free(pub2); pub2 = NULL; + OPENSSL_free(priv2); priv2 = NULL; + OSSL_PARAM_free(params); params = NULL; + OSSL_PARAM_BLD_free(bld); bld = NULL; + EVP_PKEY_CTX_free(ctx); ctx = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/** + * Test SLH-DSA sign then verify succeeds, and the signature is the expected + * FIPS 205 size. + */ +int test_slhdsa_sign_verify(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* key = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Sign/verify %s", set->name); + + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + err = slhdsa_sign_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + err = (sigLen != set->sigSize); + if (err) { + PRINT_ERR_MSG("Unexpected sig size %zu vs %zu", sigLen, + set->sigSize); + } + } + if (err == 0) { + err = !slhdsa_verify_msg(key, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen); + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(key); key = NULL; + } + return err; +} + +/** + * Test that a tampered signature fails verification. + */ +int test_slhdsa_verify_tampered_sig(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* key = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Tampered sig %s", set->name); + + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + err = slhdsa_sign_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + /* Flip a bit in the randomizer at the front of the signature. */ + if (err == 0) { + sig[0] ^= 0x01; + err = slhdsa_verify_msg(key, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen); + } + /* And one deep in the hypertree authentication path. */ + if (err == 0) { + sig[0] ^= 0x01; + sig[sigLen - 1] ^= 0x80; + err = slhdsa_verify_msg(key, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen); + } + /* A truncated signature must be rejected, not read out of bounds. */ + if (err == 0) { + sig[sigLen - 1] ^= 0x80; + err = slhdsa_verify_msg(key, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen - 1); + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(key); key = NULL; + } + return err; +} + +/** + * Test that a modified message fails verification. + */ +int test_slhdsa_verify_tampered_msg(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* key = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + unsigned char msg[SLHDSA_TEST_MSG_LEN]; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Tampered msg %s", set->name); + + memcpy(msg, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN); + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + err = slhdsa_sign_msg(key, msg, SLHDSA_TEST_MSG_LEN, &sig, + &sigLen); + } + if (err == 0) { + msg[0] ^= 0x01; + err = slhdsa_verify_msg(key, msg, SLHDSA_TEST_MSG_LEN, sig, + sigLen); + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(key); key = NULL; + } + return err; +} + +/** + * Test that a signature does not verify under a different key. + */ +int test_slhdsa_verify_wrong_key(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Wrong key %s", set->name); + + err = slhdsa_keygen(set->name, &k1); + if (err == 0) { + err = slhdsa_keygen(set->name, &k2); + } + if (err == 0) { + err = slhdsa_sign_msg(k1, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + err = slhdsa_verify_msg(k2, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + sig, sigLen); + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/** + * Test EVP_PKEY_dup of an SLH-DSA key: the copy must sign and verify. + */ +int test_slhdsa_dup(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* key = NULL; + EVP_PKEY* dup = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Dup %s", set->name); + + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + dup = EVP_PKEY_dup(key); + err = (dup == NULL); + } + /* Sign with the duplicate, verify with the original. */ + if (err == 0) { + err = slhdsa_sign_msg(dup, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + err = !slhdsa_verify_msg(key, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen); + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(dup); dup = NULL; + EVP_PKEY_free(key); key = NULL; + } + return err; +} + +/** + * Test EVP_PKEY_eq: a key matches itself and its dup, not an unrelated key. + */ +int test_slhdsa_match(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY* dup = NULL; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + PRINT_MSG("Match %s", set->name); + + err = slhdsa_keygen(set->name, &k1); + if (err == 0) { + err = slhdsa_keygen(set->name, &k2); + } + if (err == 0) { + dup = EVP_PKEY_dup(k1); + err = (dup == NULL); + } + if (err == 0) { + err = EVP_PKEY_eq(k1, dup) != 1; + } + if (err == 0) { + err = EVP_PKEY_eq(k1, k2) == 1; + } + + EVP_PKEY_free(dup); dup = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/** + * Test EVP_MD_CTX_copy_ex of an in-progress SLH-DSA signing context. + */ +int test_slhdsa_dupctx(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + EVP_MD_CTX* mdctx = NULL; + EVP_MD_CTX* copy = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + PRINT_MSG("Dup signing context %s", slhdsa_sets[0].name); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, key, + NULL) != 1; + } + /* Feed part of the message, copy, then finish on the copy. */ + if (err == 0) { + err = EVP_DigestSignUpdate(mdctx, slhdsa_test_msg, 8) != 1; + } + if (err == 0) { + copy = EVP_MD_CTX_new(); + err = (copy == NULL); + } + if (err == 0) { + err = EVP_MD_CTX_copy_ex(copy, mdctx) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(copy, slhdsa_test_msg + 8, + SLHDSA_TEST_MSG_LEN - 8) != 1; + } + if (err == 0) { + err = EVP_DigestSignFinal(copy, NULL, &sigLen) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSignFinal(copy, sig, &sigLen) != 1; + } + if (err == 0) { + err = !slhdsa_verify_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + sig, sigLen); + } + + OPENSSL_free(sig); + EVP_MD_CTX_free(copy); + EVP_MD_CTX_free(mdctx); + EVP_PKEY_free(key); + return err; +} + +/** + * Test the one-shot EVP_PKEY_sign/verify API (no digest context). + */ +int test_slhdsa_oneshot_sign_verify(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + EVP_PKEY_CTX* ctx = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + PRINT_MSG("One-shot sign/verify %s", slhdsa_sets[0].name); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, key, NULL); + err = (ctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_sign_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_sign(ctx, NULL, &sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_PKEY_sign(ctx, sig, &sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + EVP_PKEY_CTX_free(ctx); ctx = NULL; + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, key, NULL); + err = (ctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_verify_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_verify(ctx, sig, sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + + OPENSSL_free(sig); + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(key); + return err; +} + +/** + * Test the SLH-DSA key parameters reported to OpenSSL. + */ +int test_slhdsa_get_params(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* key = NULL; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + int bits = 0; + int secBits = 0; + int cat = 0; + int maxSize = 0; + + PRINT_MSG("Get params %s", set->name); + + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + err = EVP_PKEY_get_int_param(key, OSSL_PKEY_PARAM_BITS, + &bits) != 1; + } + if (err == 0) { + err = (bits != (int)set->pubKeySize * 8); + } + if (err == 0) { + err = EVP_PKEY_get_int_param(key, OSSL_PKEY_PARAM_SECURITY_BITS, + &secBits) != 1; + } + if (err == 0) { + err = (secBits != set->securityBits); + } + if (err == 0) { + err = EVP_PKEY_get_int_param(key, + OSSL_PKEY_PARAM_SECURITY_CATEGORY, &cat) != 1; + } + if (err == 0) { + err = (cat != set->securityCategory); + } + if (err == 0) { + err = EVP_PKEY_get_int_param(key, OSSL_PKEY_PARAM_MAX_SIZE, + &maxSize) != 1; + } + if (err == 0) { + err = (maxSize != (int)set->sigSize); + } + if (err == 0) { + err = EVP_PKEY_get_size(key) != (int)set->sigSize; + } + + EVP_PKEY_free(key); key = NULL; + } + return err; +} + +/** + * Test that a public-only key cannot sign. + */ +int test_slhdsa_pubonly_sign_fails(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + EVP_PKEY* pubOnly = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + OSSL_PARAM_BLD* bld = NULL; + EVP_MD_CTX* mdctx = NULL; + unsigned char* pub = NULL; + size_t pubLen = 0; + + (void)data; + + PRINT_MSG("Public-only key cannot sign"); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + err = slhdsa_get_pub(key, &pub, &pubLen); + } + if (err == 0) { + bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL); + } + if (err == 0) { + err = OSSL_PARAM_BLD_push_octet_string(bld, OSSL_PKEY_PARAM_PUB_KEY, + pub, pubLen) != 1; + } + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, slhdsa_sets[0].name, NULL); + err = (ctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_fromdata_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &pubOnly, EVP_PKEY_PUBLIC_KEY, + params) != 1; + } + /* Signing must fail: either at init or at the sign call. */ + if (err == 0) { + size_t sigLen = 0; + unsigned char* sig = NULL; + + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + if (err == 0) { + if (EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, + pubOnly, NULL) == 1) { + /* A real buffer: if the sign ever stops failing, it must not + * write a multi-kilobyte signature over the message array. */ + err = EVP_DigestSign(mdctx, NULL, &sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) == 1 && + (sig = OPENSSL_malloc(sigLen)) != NULL && + EVP_DigestSign(mdctx, sig, + &sigLen, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN) == 1; + OPENSSL_free(sig); + } + } + } + + EVP_MD_CTX_free(mdctx); + OPENSSL_free(pub); + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(bld); + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(pubOnly); + EVP_PKEY_free(key); + return err; +} + +/** + * Test that importing a mismatched public/private pair is rejected. + */ +int test_slhdsa_import_mismatched_pubpriv(void* data) +{ + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY* bad = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + OSSL_PARAM_BLD* bld = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + size_t pubLen = 0, privLen = 0; + + PRINT_MSG("Mismatched pub/priv %s", set->name); + + err = slhdsa_keygen(set->name, &k1); + if (err == 0) { + err = slhdsa_keygen(set->name, &k2); + } + /* Public from one key, private from another. */ + if (err == 0) { + err = slhdsa_get_pub(k1, &pub, &pubLen); + } + if (err == 0) { + err = slhdsa_get_raw(k2, OSSL_PKEY_PARAM_PRIV_KEY, &priv, + &privLen); + } + if (err == 0) { + bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL); + } + if (err == 0) { + err = OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub, pubLen) != 1; + } + if (err == 0) { + err = OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + } + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, set->name, NULL); + err = (ctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_fromdata_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &bad, EVP_PKEY_KEYPAIR, + params) == 1; + if (err) { + PRINT_ERR_MSG("Mismatched pub/priv was accepted"); + } + } + + OPENSSL_free(pub); + OPENSSL_free(priv); + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(bld); + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(bad); + EVP_PKEY_free(k1); + EVP_PKEY_free(k2); + } + return err; +} + +/** + * Test signing an empty message, which FIPS 205 permits. + */ +int test_slhdsa_empty_message(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + PRINT_MSG("Empty message %s", slhdsa_sets[0].name); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + err = slhdsa_sign_msg(key, (const unsigned char*)"", 0, &sig, &sigLen); + } + if (err == 0) { + err = !slhdsa_verify_msg(key, (const unsigned char*)"", 0, sig, + sigLen); + } + /* A non-empty message must not verify under the empty-message signature. */ + if (err == 0) { + err = slhdsa_verify_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + sig, sigLen); + } + + OPENSSL_free(sig); + EVP_PKEY_free(key); + return err; +} + +/** + * Test that re-init with a NULL key reuses the key already on the context. + */ +int test_slhdsa_reinit_null_key(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + EVP_MD_CTX* mdctx = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + PRINT_MSG("Reinit with NULL key reuses context key"); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, key, + NULL) != 1; + } + /* Second init with a NULL key must reuse the first key. */ + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, NULL, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig, &sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + err = !slhdsa_verify_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, + sig, sigLen); + } + + OPENSSL_free(sig); + EVP_MD_CTX_free(mdctx); + EVP_PKEY_free(key); + return err; +} + +/** + * Test PEM encode/decode round-trip for both SPKI and PKCS#8, and that a + * key reloaded from PEM still signs and verifies. + */ +int test_slhdsa_encode_decode(void* data) +{ + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + EVP_PKEY* key = NULL; + EVP_PKEY* privKey = NULL; + EVP_PKEY* pubKey = NULL; + BIO* bio = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + PRINT_MSG("Encode/decode %s", set->name); + + err = slhdsa_keygen(set->name, &key); + + /* PKCS#8 private key round-trip. */ + if (err == 0) { + bio = BIO_new(BIO_s_mem()); + err = (bio == NULL); + } + if (err == 0) { + err = PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, + NULL) != 1; + } + if (err == 0) { + privKey = PEM_read_bio_PrivateKey_ex(bio, NULL, NULL, NULL, + wpLibCtx, NULL); + err = (privKey == NULL); + } + BIO_free(bio); bio = NULL; + + /* SubjectPublicKeyInfo round-trip. */ + if (err == 0) { + bio = BIO_new(BIO_s_mem()); + err = (bio == NULL); + } + if (err == 0) { + err = PEM_write_bio_PUBKEY(bio, key) != 1; + } + if (err == 0) { + pubKey = PEM_read_bio_PUBKEY_ex(bio, NULL, NULL, NULL, wpLibCtx, + NULL); + err = (pubKey == NULL); + } + BIO_free(bio); bio = NULL; + + /* The reloaded private signs; the reloaded public verifies. */ + if (err == 0) { + err = slhdsa_sign_msg(privKey, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, &sig, &sigLen); + } + if (err == 0) { + err = !slhdsa_verify_msg(pubKey, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN, sig, sigLen); + } + if (err == 0) { + err = EVP_PKEY_eq(key, pubKey) != 1; + } + + OPENSSL_free(sig); + EVP_PKEY_free(pubKey); + EVP_PKEY_free(privKey); + EVP_PKEY_free(key); + } + return err; +} + +/** + * Test signing and verifying an X.509 certificate with an SLH-DSA key. + */ +/* Sign the standard message with the supplied signature params. */ +static int slhdsa_sign_with(EVP_PKEY* key, const OSSL_PARAM* params, + unsigned char** sig, size_t* sigLen) +{ + int err; + EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); + + *sig = NULL; + *sigLen = 0; + err = (mdctx == NULL); + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, key, + params) != 1; + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + *sig = (unsigned char*)OPENSSL_malloc(*sigLen); + err = (*sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, *sig, sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err != 0) { + OPENSSL_free(*sig); + *sig = NULL; + } + EVP_MD_CTX_free(mdctx); + return err; +} + +/* Verify the standard message with the supplied signature params. */ +static int slhdsa_verify_with(EVP_PKEY* key, const OSSL_PARAM* params, + const unsigned char* sig, size_t sigLen) +{ + int rc = 0; + EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); + + if (mdctx != NULL) { + if (EVP_DigestVerifyInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, key, + params) == 1) { + rc = EVP_DigestVerify(mdctx, sig, sigLen, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN); + } + } + EVP_MD_CTX_free(mdctx); + return rc; +} + +int test_slhdsa_sig_params(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + unsigned char* sigA = NULL; + unsigned char* sigB = NULL; + size_t lenA = 0; + size_t lenB = 0; + unsigned char entropy[32]; + OSSL_PARAM ctxParams[2]; + OSSL_PARAM detParams[2]; + OSSL_PARAM entParams[2]; + OSSL_PARAM rawParams[2]; + unsigned int detOn = 1; + int encRaw = 0; + size_t n; + + (void)data; + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + + /* Context string: a signature made under one context must not verify + * under another. */ + if (err == 0) { + PRINT_MSG("Context string affects the signature"); + ctxParams[0] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_CONTEXT_STRING, (void*)"ctx-A", 5); + ctxParams[1] = OSSL_PARAM_construct_end(); + err = slhdsa_sign_with(key, ctxParams, &sigA, &lenA); + } + if (err == 0) { + err = slhdsa_verify_with(key, ctxParams, sigA, lenA) != 1; + } + if (err == 0) { + OSSL_PARAM other[2]; + other[0] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_CONTEXT_STRING, (void*)"ctx-B", 5); + other[1] = OSSL_PARAM_construct_end(); + err = slhdsa_verify_with(key, other, sigA, lenA) == 1; + if (err) { + PRINT_ERR_MSG("Signature verified under the wrong context"); + } + } + OPENSSL_free(sigA); sigA = NULL; + + /* Deterministic: same key and message must give byte-identical output. */ + if (err == 0) { + PRINT_MSG("Deterministic signing is reproducible"); + detParams[0] = OSSL_PARAM_construct_uint( + OSSL_SIGNATURE_PARAM_DETERMINISTIC, &detOn); + detParams[1] = OSSL_PARAM_construct_end(); + err = slhdsa_sign_with(key, detParams, &sigA, &lenA); + } + if (err == 0) { + err = slhdsa_sign_with(key, detParams, &sigB, &lenB); + } + if (err == 0) { + err = (lenA != lenB) || (XMEMCMP(sigA, sigB, lenA) != 0); + if (err) { + PRINT_ERR_MSG("Deterministic signatures differ"); + } + } + if (err == 0) { + err = slhdsa_verify_with(key, NULL, sigA, lenA) != 1; + } + OPENSSL_free(sigA); sigA = NULL; + OPENSSL_free(sigB); sigB = NULL; + + /* Test entropy: fixing the randomizer must also be reproducible, and the + * length is tied to the parameter set's n. */ + if (err == 0) { + PRINT_MSG("Test entropy is reproducible"); + n = (size_t)(slhdsa_sets[0].pubKeySize / 2); + XMEMSET(entropy, 0xA5, sizeof(entropy)); + entParams[0] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_TEST_ENTROPY, entropy, n); + entParams[1] = OSSL_PARAM_construct_end(); + err = slhdsa_sign_with(key, entParams, &sigA, &lenA); + } + if (err == 0) { + err = slhdsa_sign_with(key, entParams, &sigB, &lenB); + } + if (err == 0) { + err = (lenA != lenB) || (XMEMCMP(sigA, sigB, lenA) != 0); + if (err) { + PRINT_ERR_MSG("Fixed-entropy signatures differ"); + } + } + if (err == 0) { + err = slhdsa_verify_with(key, NULL, sigA, lenA) != 1; + } + OPENSSL_free(sigA); sigA = NULL; + OPENSSL_free(sigB); sigB = NULL; + + /* A wrong-length randomizer must be refused, not silently padded. */ + if (err == 0) { + OSSL_PARAM bad[2]; + bad[0] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_TEST_ENTROPY, entropy, n - 1); + bad[1] = OSSL_PARAM_construct_end(); + err = slhdsa_sign_with(key, bad, &sigA, &lenA) == 0; + if (err) { + PRINT_ERR_MSG("Undersized test entropy was accepted"); + } + OPENSSL_free(sigA); sigA = NULL; + } + + /* Raw message encoding: the caller supplies M' itself. Sign and verify + * must agree with each other under the same encoding. */ + if (err == 0) { + PRINT_MSG("Raw message encoding round-trips"); + rawParams[0] = OSSL_PARAM_construct_int( + OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING, &encRaw); + rawParams[1] = OSSL_PARAM_construct_end(); + err = slhdsa_sign_with(key, rawParams, &sigA, &lenA); + } + if (err == 0) { + err = slhdsa_verify_with(key, rawParams, sigA, lenA) != 1; + } + /* Pure mode must not accept a raw-mode signature: different domain. */ + if (err == 0) { + err = slhdsa_verify_with(key, NULL, sigA, lenA) == 1; + if (err) { + PRINT_ERR_MSG("Raw signature verified in pure mode"); + } + } + OPENSSL_free(sigA); sigA = NULL; + + EVP_PKEY_free(key); + return err; +} + +int test_slhdsa_keygen_seed(void* data) +{ + int err = 0; + EVP_PKEY* keyA = NULL; + EVP_PKEY* keyB = NULL; + EVP_PKEY_CTX* ctx = NULL; + unsigned char seed[96]; + unsigned char pubA[128]; + unsigned char pubB[128]; + size_t pubALen = 0; + size_t pubBLen = 0; + OSSL_PARAM params[2]; + size_t seedLen; + size_t i; + + (void)data; + + PRINT_MSG("Seeded keygen is reproducible %s", slhdsa_sets[0].name); + + /* FIPS 205 seeds SK.seed || SK.prf || PK.seed, each n bytes. */ + seedLen = (slhdsa_sets[0].pubKeySize / 2) * 3; + for (i = 0; i < seedLen; i++) { + seed[i] = (unsigned char)i; + } + params[0] = OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_SLH_DSA_SEED, seed, seedLen); + params[1] = OSSL_PARAM_construct_end(); + + for (i = 0; (err == 0) && (i < 2); i++) { + EVP_PKEY** out = (i == 0) ? &keyA : &keyB; + + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, slhdsa_sets[0].name, NULL); + err = (ctx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_CTX_set_params(ctx, params) != 1; + } + if (err == 0) { + err = EVP_PKEY_keygen(ctx, out) != 1; + } + EVP_PKEY_CTX_free(ctx); ctx = NULL; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(keyA, OSSL_PKEY_PARAM_PUB_KEY, + pubA, sizeof(pubA), &pubALen) != 1; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(keyB, OSSL_PKEY_PARAM_PUB_KEY, + pubB, sizeof(pubB), &pubBLen) != 1; + } + if (err == 0) { + err = (pubALen != pubBLen) || (XMEMCMP(pubA, pubB, pubALen) != 0); + if (err) { + PRINT_ERR_MSG("Same seed produced different keys"); + } + } + + EVP_PKEY_free(keyA); + EVP_PKEY_free(keyB); + return err; +} + +int test_slhdsa_x509_sign_verify(void* data) +{ + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < SLHDSA_SET_COUNT); i++) { + const slhdsa_test_set* set = &slhdsa_sets[i]; + EVP_PKEY* key = NULL; + X509* cert = NULL; + X509_NAME* name = NULL; + + PRINT_MSG("X509 sign/verify %s", set->name); + + err = slhdsa_keygen(set->name, &key); + if (err == 0) { + cert = X509_new_ex(wpLibCtx, NULL); + err = (cert == NULL); + } + if (err == 0) { + err = X509_set_version(cert, X509_VERSION_3) != 1; + } + if (err == 0) { + err = ASN1_INTEGER_set(X509_get_serialNumber(cert), 1) != 1; + } + if (err == 0) { + err = X509_gmtime_adj(X509_getm_notBefore(cert), 0) == NULL; + } + if (err == 0) { + err = X509_gmtime_adj(X509_getm_notAfter(cert), 31536000L) == NULL; + } + if (err == 0) { + err = X509_set_pubkey(cert, key) != 1; + } + if (err == 0) { + name = X509_get_subject_name(cert); + err = X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char*)"wolfProvider SLH-DSA", -1, -1, 0) != 1; + } + if (err == 0) { + err = X509_set_issuer_name(cert, name) != 1; + } + if (err == 0) { + err = X509_sign(cert, key, NULL) == 0; + } + if (err == 0) { + err = X509_verify(cert, key) != 1; + } + + X509_free(cert); + EVP_PKEY_free(key); + } + return err; +} + +#ifdef WP_HAVE_EPKI_TEST +/** + * Test encrypted PKCS#8 encode/decode round-trip. + */ +int test_slhdsa_encode_epki(void* data) +{ + int err = 0; + EVP_PKEY* key = NULL; + EVP_PKEY* dec = NULL; + BIO* bio = NULL; + const char* pw = "wolfprov-slhdsa"; + + (void)data; + + PRINT_MSG("Encode EPKI %s", slhdsa_sets[0].name); + + err = slhdsa_keygen(slhdsa_sets[0].name, &key); + if (err == 0) { + bio = BIO_new(BIO_s_mem()); + err = (bio == NULL); + } + if (err == 0) { + err = PEM_write_bio_PrivateKey(bio, key, + EVP_aes_256_cbc(), (unsigned char*)pw, (int)strlen(pw), NULL, + NULL) != 1; + } + if (err == 0) { + dec = PEM_read_bio_PrivateKey_ex(bio, NULL, NULL, (void*)pw, wpLibCtx, + NULL); + err = (dec == NULL); + } + if (err == 0) { + err = EVP_PKEY_eq(key, dec) != 1; + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo DER with cipher set must encrypt"); + err = test_pki_cipher_encrypts(key, "DER", "provider=libwolfprov", + wpLibCtx, 1); + } + if (err == 0) { + PRINT_MSG("PrivateKeyInfo PEM with cipher set must encrypt"); + err = test_pki_cipher_encrypts(key, "PEM", "provider=libwolfprov", + wpLibCtx, 1); + } + + BIO_free(bio); + EVP_PKEY_free(dec); + EVP_PKEY_free(key); + return err; +} +#endif /* WP_HAVE_EPKI_TEST */ + +#endif /* WP_HAVE_SLHDSA && WP_HAVE_SLHDSA_PRIVATE */ diff --git a/test/unit.c b/test/unit.c index dad5eca1..f479f3a4 100644 --- a/test/unit.c +++ b/test/unit.c @@ -592,6 +592,32 @@ TEST_CASE test_case[] = { TEST_DECL(test_mldsa_encode_decode, NULL), TEST_DECL(test_mldsa_x509_sign_verify, NULL), #endif + +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) && \ + defined(WP_SLHDSA_TEST_SETS) + #ifdef WP_HAVE_EPKI_TEST + TEST_DECL(test_slhdsa_encode_epki, NULL), + #endif + TEST_DECL(test_slhdsa_keygen, NULL), + TEST_DECL(test_slhdsa_import_export_roundtrip, NULL), + TEST_DECL(test_slhdsa_sign_verify, NULL), + TEST_DECL(test_slhdsa_verify_tampered_sig, NULL), + TEST_DECL(test_slhdsa_verify_tampered_msg, NULL), + TEST_DECL(test_slhdsa_verify_wrong_key, NULL), + TEST_DECL(test_slhdsa_dup, NULL), + TEST_DECL(test_slhdsa_match, NULL), + TEST_DECL(test_slhdsa_dupctx, NULL), + TEST_DECL(test_slhdsa_oneshot_sign_verify, NULL), + TEST_DECL(test_slhdsa_get_params, NULL), + TEST_DECL(test_slhdsa_pubonly_sign_fails, NULL), + TEST_DECL(test_slhdsa_import_mismatched_pubpriv, NULL), + TEST_DECL(test_slhdsa_empty_message, NULL), + TEST_DECL(test_slhdsa_reinit_null_key, NULL), + TEST_DECL(test_slhdsa_encode_decode, NULL), + TEST_DECL(test_slhdsa_sig_params, NULL), + TEST_DECL(test_slhdsa_keygen_seed, NULL), + TEST_DECL(test_slhdsa_x509_sign_verify, NULL), +#endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index 7e8ef69d..8fe4956d 100644 --- a/test/unit.h +++ b/test/unit.h @@ -57,6 +57,20 @@ #define WP_HAVE_EPKI_TEST #endif +/* slhdsa_sets[] in test_slhdsa.c carries a curated spread of parameter sets and + * every test indexes element 0. A build enabling only a set outside that spread + * would leave the array empty, so the tests are skipped instead. */ +#ifdef WOLFPROV_QUICKTEST + #if defined(WP_HAVE_SLH_DSA_SHA2_128F) || defined(WP_HAVE_SLH_DSA_SHAKE_128F) + #define WP_SLHDSA_TEST_SETS + #endif +#elif defined(WP_HAVE_SLH_DSA_SHA2_128F) || defined(WP_HAVE_SLH_DSA_SHAKE_128F) || \ + defined(WP_HAVE_SLH_DSA_SHA2_128S) || defined(WP_HAVE_SLH_DSA_SHAKE_192F) || \ + defined(WP_HAVE_SLH_DSA_SHAKE_256F) + #define WP_SLHDSA_TEST_SETS +#endif + + #ifdef TEST_MULTITHREADED #define PRINT_MSG(str) #define PRINT_ERR_MSG(str) @@ -615,4 +629,30 @@ int test_mldsa_encode_decode(void *data); int test_mldsa_x509_sign_verify(void *data); #endif +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) && \ + defined(WP_SLHDSA_TEST_SETS) +#ifdef WP_HAVE_EPKI_TEST +int test_slhdsa_encode_epki(void *data); +#endif +int test_slhdsa_keygen(void *data); +int test_slhdsa_import_export_roundtrip(void *data); +int test_slhdsa_sign_verify(void *data); +int test_slhdsa_verify_tampered_sig(void *data); +int test_slhdsa_verify_tampered_msg(void *data); +int test_slhdsa_verify_wrong_key(void *data); +int test_slhdsa_dup(void *data); +int test_slhdsa_match(void *data); +int test_slhdsa_dupctx(void *data); +int test_slhdsa_oneshot_sign_verify(void *data); +int test_slhdsa_get_params(void *data); +int test_slhdsa_pubonly_sign_fails(void *data); +int test_slhdsa_import_mismatched_pubpriv(void *data); +int test_slhdsa_empty_message(void *data); +int test_slhdsa_reinit_null_key(void *data); +int test_slhdsa_encode_decode(void *data); +int test_slhdsa_sig_params(void *data); +int test_slhdsa_keygen_seed(void *data); +int test_slhdsa_x509_sign_verify(void *data); +#endif + #endif /* UNIT_H */ From dfc26e1be148620a3ea4f62837b6ba1004b8e482 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 10:52:53 -0700 Subject: [PATCH 04/15] Run the OpenSSL SLH-DSA KAT vectors --- .github/workflows/wolfssl-pqc-kat.yml | 4 ++-- scripts/test-pqc-kat.sh | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wolfssl-pqc-kat.yml b/.github/workflows/wolfssl-pqc-kat.yml index 444e6882..8e384e72 100644 --- a/.github/workflows/wolfssl-pqc-kat.yml +++ b/.github/workflows/wolfssl-pqc-kat.yml @@ -60,9 +60,9 @@ jobs: echo "Latest stable wolfSSL: $LATEST" echo "Latest OpenSSL: $OSSL" echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" - PQC_FLOOR="v5.9.1-stable" + PQC_FLOOR="v5.9.2-stable" if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \ - | sort -V | tail -n1)" != "$PQC_FLOOR" ]; then + | sort -V | tail -n1)" = "$LATEST" ]; then LATEST_PQC=true else LATEST_PQC=false diff --git a/scripts/test-pqc-kat.sh b/scripts/test-pqc-kat.sh index f0383dd3..35c057a4 100755 --- a/scripts/test-pqc-kat.sh +++ b/scripts/test-pqc-kat.sh @@ -17,13 +17,13 @@ # You should have received a copy of the GNU General Public License # along with wolfProvider. If not, see . -# Run OpenSSL's own ML-KEM/ML-DSA EVP KAT vectors (NIST ACVP + Wycheproof, -# 2602 sub-tests) through wolfProvider using OpenSSL's own evp_test harness. -# This proves wolfcrypt serves the FIPS 203 / FIPS 204 reference vectors -# unmodified via the OpenSSL provider interface. +# Run OpenSSL's own ML-KEM/ML-DSA/SLH-DSA EVP KAT vectors (NIST ACVP + +# Wycheproof, 3099 sub-tests) through wolfProvider using OpenSSL's own evp_test harness. +# This proves wolfcrypt serves the FIPS 203 / FIPS 204 / FIPS 205 reference +# vectors unmodified via the OpenSSL provider interface. # # The script reports a raw result: exit 0 only when every vector file passes -# and all 2602 sub-tests ran. The caller owns force-fail interpretation: under +# and all 3099 sub-tests ran. The caller owns force-fail interpretation: under # WOLFPROV_FORCE_FAIL=1 every operation fails, so this exits non-zero, and the # CI job inverts that via check-workflow-result.sh. wolfProvider (replace-default # or not) must already be built by a prior build-wolfprovider.sh step; this @@ -32,12 +32,12 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" source ${SCRIPT_DIR}/utils-wolfprovider.sh -# OpenSSL's own KAT data files, run unmodified: the wolfcrypt FIPS 203/204 -# decode fix makes every ML-KEM/ML-DSA vector pass as-is, so nothing is -# staged or edited here. +# OpenSSL's own KAT data files, run unmodified. Stanzas marked Extended-Test +# are skipped unless EVP_TEST_EXTENDED is set, matching how OpenSSL runs them; +# the expected count below is the default-gated total. VECTOR_DIR=${OPENSSL_SOURCE_DIR}/test/recipes/30-test_evp_data EVP_TEST=${OPENSSL_TEST}/evp_test -EXPECTED_TESTS=2602 +EXPECTED_TESTS=3099 require_evp_test() { if [ -x "${EVP_TEST}" ]; then @@ -70,7 +70,8 @@ run_pqc_kat() { "${EXPECTED_TESTS}" for f in ${VECTOR_DIR}/evppkey_ml_kem_*.txt \ - ${VECTOR_DIR}/evppkey_ml_dsa_*.txt; do + ${VECTOR_DIR}/evppkey_ml_dsa_*.txt \ + ${VECTOR_DIR}/evppkey_slh_dsa_*.txt; do files=$((files + 1)) printf "\t%-42s ... " "$(basename ${f})" out=$(${EVP_TEST} -config ${WOLFPROV_CONFIG} "${f}" 2>&1) From 2442961e3651cc29aba182c981ab77859f2a9553 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 10:52:53 -0700 Subject: [PATCH 05/15] Add SLH-DSA interoperability coverage --- .../tests/pqc_interop/test_pqc_interop.c | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index 72fa246e..98b8b038 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -70,6 +70,9 @@ #include #include +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) +#include +#endif #include #define WP_NAME "libwolfprov" @@ -641,6 +644,177 @@ static int test_mldsa_pair_to_wp(const char* alg, const char* partner) } +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) + +static const char* slhdsa_msg = + "SLH-DSA three-way interop message (FIPS 205)."; + +/* Map a registered SLH-DSA name to its wolfCrypt parameter set. */ +static int slhdsa_name_to_param(const char* alg, enum SlhDsaParam* param) +{ + static const struct { + const char* name; + enum SlhDsaParam param; + } map[] = { +#ifdef WOLFSSL_SLHDSA_SHA2 + { "SLH-DSA-SHA2-128f", SLHDSA_SHA2_128F }, + { "SLH-DSA-SHA2-192f", SLHDSA_SHA2_192F }, +#endif + { "SLH-DSA-SHAKE-128f", SLHDSA_SHAKE128F }, + }; + size_t i; + + for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + if (strcmp(alg, map[i].name) == 0) { + *param = map[i].param; + return 1; + } + } + return 0; +} + +static EVP_PKEY* slhdsa_wp_keygen(const char* alg) +{ + EVP_PKEY* k = NULL; + EVP_PKEY_CTX* g = EVP_PKEY_CTX_new_from_name(wp_ctx, alg, NULL); + if (g && EVP_PKEY_keygen_init(g) == 1) EVP_PKEY_keygen(g, &k); + EVP_PKEY_CTX_free(g); + return k; +} + +/* wolfSSL-direct sign with an empty context string (FIPS 205 pure SLH-DSA). */ +static int wc_slhdsa_sign_direct(const char* alg, const unsigned char* priv, + size_t privLen, const unsigned char* msg, size_t msgLen, + unsigned char** sig, size_t* sigLen) +{ + SlhDsaKey key; + enum SlhDsaParam param; + int rc; + int sigSz; + word32 outLen; + + if (!slhdsa_name_to_param(alg, ¶m)) return 0; + if (wc_SlhDsaKey_Init(&key, param, NULL, INVALID_DEVID) != 0) return 0; + rc = wc_SlhDsaKey_ImportPrivate(&key, priv, (word32)privLen); + if (rc != 0) { wc_SlhDsaKey_Free(&key); return 0; } + sigSz = wc_SlhDsaKey_SigSize(&key); + if (sigSz <= 0) { wc_SlhDsaKey_Free(&key); return 0; } + *sig = OPENSSL_malloc(sigSz); + if (*sig == NULL) { wc_SlhDsaKey_Free(&key); return 0; } + outLen = (word32)sigSz; + rc = wc_SlhDsaKey_Sign(&key, NULL, 0, msg, (word32)msgLen, *sig, &outLen, + &g_rng); + wc_SlhDsaKey_Free(&key); + if (rc != 0) { OPENSSL_free(*sig); *sig = NULL; return 0; } + *sigLen = outLen; + return 1; +} + +/* wolfSSL-direct verify. */ +static int wc_slhdsa_verify_direct(const char* alg, const unsigned char* pub, + size_t pubLen, const unsigned char* msg, size_t msgLen, + const unsigned char* sig, size_t sigLen) +{ + SlhDsaKey key; + enum SlhDsaParam param; + int rc; + + if (!slhdsa_name_to_param(alg, ¶m)) return 0; + if (wc_SlhDsaKey_Init(&key, param, NULL, INVALID_DEVID) != 0) return 0; + rc = wc_SlhDsaKey_ImportPublic(&key, pub, (word32)pubLen); + if (rc != 0) { wc_SlhDsaKey_Free(&key); return 0; } + rc = wc_SlhDsaKey_Verify(&key, NULL, 0, msg, (word32)msgLen, sig, + (word32)sigLen); + wc_SlhDsaKey_Free(&key); + return rc == 0; +} + +/* wolfProvider sign -> partner verify (partner=default OR direct). */ +static int test_slhdsa_pair_wp_to(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = slhdsa_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* sig = NULL; + size_t pubLen = 0, privLen = 0, sigLen = 0; + size_t msgLen = strlen(slhdsa_msg); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + if (!evp_sign(wp_ctx, wp_key, (const unsigned char*)slhdsa_msg, msgLen, + &sig, &sigLen)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, NULL, 0); + if (!part_key) goto end; + ok = evp_verify(oss_ctx, part_key, (const unsigned char*)slhdsa_msg, + msgLen, sig, sigLen); + } + else { /* direct */ + ok = wc_slhdsa_verify_direct(alg, pub, pubLen, + (const unsigned char*)slhdsa_msg, msgLen, sig, sigLen); + } + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-18s wolfProv sign -> %-7s vrfy: %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(sig); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + +/* partner sign -> wolfProvider verify. */ +static int test_slhdsa_pair_to_wp(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = slhdsa_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* sig = NULL; + size_t pubLen = 0, privLen = 0, sigLen = 0; + size_t msgLen = strlen(slhdsa_msg); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, priv, + privLen); + if (!part_key) goto end; + if (!evp_sign(oss_ctx, part_key, (const unsigned char*)slhdsa_msg, + msgLen, &sig, &sigLen)) goto end; + } + else { /* direct */ + if (!wc_slhdsa_sign_direct(alg, priv, privLen, + (const unsigned char*)slhdsa_msg, msgLen, &sig, &sigLen)) + goto end; + } + + ok = evp_verify(wp_ctx, wp_key, (const unsigned char*)slhdsa_msg, msgLen, + sig, sigLen); + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-18s %-7s sign -> wolfProv vrfy: %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(sig); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + +#endif /* WP_HAVE_SLHDSA && WP_HAVE_SLHDSA_PRIVATE */ + /* * TLS 1.3 group interop - drives a real handshake over an in-memory BIO pair * with one peer on wolfProvider's library context and the other on OpenSSL's @@ -834,6 +1008,14 @@ int main(int argc, char* argv[]) }; const char* mlkem[] = { "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024" }; const char* mldsa[] = { "ML-DSA-44", "ML-DSA-65", "ML-DSA-87" }; +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) + const char* slhdsa[] = { +#ifdef WOLFSSL_SLHDSA_SHA2 + "SLH-DSA-SHA2-128f", "SLH-DSA-SHA2-192f", +#endif + "SLH-DSA-SHAKE-128f" + }; +#endif const char* wp_path = ".libs"; const char* env_path; size_t i; @@ -868,6 +1050,19 @@ int main(int argc, char* argv[]) if (!test_mldsa_pair_to_wp(mldsa[i], "direct")) fail++; } +#if defined(WP_HAVE_SLHDSA) && defined(WP_HAVE_SLHDSA_PRIVATE) + /* Only the fast ('f') parameter sets: the small ('s') variants sign + * orders of magnitude slower and would dominate CI runtime. */ + printf("\nSLH-DSA three-way interop:\n"); + printf(" (wolfProvider) <-> (OpenSSL default) and <-> (wolfSSL direct)\n"); + for (i = 0; i < sizeof(slhdsa) / sizeof(slhdsa[0]); i++) { + if (!test_slhdsa_pair_wp_to(slhdsa[i], "default")) fail++; + if (!test_slhdsa_pair_to_wp(slhdsa[i], "default")) fail++; + if (!test_slhdsa_pair_wp_to(slhdsa[i], "direct")) fail++; + if (!test_slhdsa_pair_to_wp(slhdsa[i], "direct")) fail++; + } +#endif + printf("\nTLS 1.3 group interop (handshake over BIO pair):\n"); printf(" (wolfProvider) <-> (OpenSSL default), both directions\n"); for (i = 0; i < sizeof(groups) / sizeof(groups[0]); i++) { From c20363e735b9ad6782b06c868c626e6a1e1885ec Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 10:52:53 -0700 Subject: [PATCH 06/15] Integrate SLH-DSA into builds and CI --- .github/workflows/wolfssl-versions-pqc.yml | 55 +++++++++++++--------- README.md | 8 ++-- docs/INTEGRATION_GUIDE.md | 9 ++-- scripts/build-wolfprovider.sh | 20 +++++--- scripts/utils-wolfprovider.sh | 3 ++ scripts/utils-wolfssl.sh | 4 ++ 6 files changed, 64 insertions(+), 35 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index c213adf8..878583a8 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -6,9 +6,8 @@ name: wolfSSL Versions (PQC) # floor, then the build job runs three rows: pre-PQC floor, dynamically # resolved latest -stable, and master. # -# PQC is opt-in (--enable-pqc). PQC_FLOOR is v5.9.1-stable: the wc_MlDsaKey_* -# seed/message API wolfProvider's PQC code depends on lands post-v5.9.1-stable -# (wolfSSL PR #10436), so v5.9.2-stable+ is the first PQC-eligible release. +# PQC is opt-in (--enable-pqc). v5.9.2-stable is the first release with the +# wc_MlDsaKey_* seed/message API required by wolfProvider. # PQC rows build with --enable-pqc against the latest OpenSSL (>= 3.6 required); # older/no-flag rows build without it and verify PQC is absent (opt-in). @@ -58,20 +57,17 @@ jobs: echo "Latest OpenSSL: $OSSL" echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT" echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" - # Enable PQC when $LATEST is strictly newer than v5.9.1-stable - # (i.e. v5.9.2-stable, v5.10+, v6+, ...). Anything at or before - # the floor lacks the wc_MlDsaKey_* / wc_dilithium_sign_ctx_msg - # API and stays on the no-symbol path. - PQC_FLOOR="v5.9.1-stable" + # Enable PQC for v5.9.2-stable or later. + PQC_FLOOR="v5.9.2-stable" if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \ - | sort -V | tail -n1)" != "$PQC_FLOOR" ]; then + | sort -V | tail -n1)" = "$LATEST" ]; then LATEST_PQC_ELIGIBLE=true else LATEST_PQC_ELIGIBLE=false fi echo "latest-stable PQC eligible: $LATEST_PQC_ELIGIBLE" # Each row carries the build flag (enable) and which PQC test families - # must result (expect: both | mlkem | mldsa | none). This exercises the + # must result (expect: all | mlkem | mldsa | slhdsa | none). This exercises # combined, per-algorithm, and opt-in-absent paths. MATRIX=$(jq -nc \ --arg latest "$LATEST" \ @@ -81,15 +77,17 @@ jobs: "wolfssl-ref":"v5.8.0-stable","enable":"","expect":"none"}, {"name":("latest stable (" + $latest + ")"),"wolfssl-ref":$latest, "enable":(if $latest_pqc then "--enable-pqc" else "" end), - "expect":(if $latest_pqc then "both" else "none" end)}, + "expect":(if $latest_pqc then "all" else "none" end)}, {"name":"master (--enable-pqc)", - "wolfssl-ref":"master","enable":"--enable-pqc","expect":"both"}, + "wolfssl-ref":"master","enable":"--enable-pqc","expect":"all"}, {"name":"master (no flag, opt-in check)", "wolfssl-ref":"master","enable":"","expect":"none"}, {"name":"master (--enable-mlkem only)", "wolfssl-ref":"master","enable":"--enable-mlkem","expect":"mlkem"}, {"name":"master (--enable-mldsa only)", - "wolfssl-ref":"master","enable":"--enable-mldsa","expect":"mldsa"} + "wolfssl-ref":"master","enable":"--enable-mldsa","expect":"mldsa"}, + {"name":"master (--enable-slhdsa only)", + "wolfssl-ref":"master","enable":"--enable-slhdsa","expect":"slhdsa"} ] }') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" @@ -130,6 +128,17 @@ jobs: WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ ./scripts/build-wolfprovider.sh ${{ matrix.enable }} + - name: Run OpenSSL provider example + run: | + export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" + ./examples/openssl_example + + - name: Run PQC OpenSSL example (${{ matrix.expect }}) + if: matrix.expect != 'none' + run: | + export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" + ./examples/pqc_openssl_example + - name: Push build dependencies uses: ./.github/actions/oras-build-deps-push with: @@ -140,20 +149,22 @@ jobs: wolfssl_hit: ${{ steps.deps.outputs.wolfssl_hit }} # Opt-in is per-algorithm: assert exactly the expected PQC test families - # are present (both / mlkem / mldsa / none). This catches a leaked + # are present (all / mlkem / mldsa / slhdsa / none). Catches a leaked # algorithm, a missing one, or PQC dragged in without a flag. - name: Verify PQC test presence matches opt-in (${{ matrix.expect }}) run: | tests=$(./test/unit.test --list) || exit 1 - kem=0; dsa=0 + kem=0; dsa=0; slh=0 printf '%s\n' "$tests" | grep -q 'test_mlkem_keygen' && kem=1 printf '%s\n' "$tests" | grep -q 'test_mldsa_sign_verify' && dsa=1 - echo "expect=${{ matrix.expect }} mlkem=$kem mldsa=$dsa" + printf '%s\n' "$tests" | grep -q 'test_slhdsa_sign_verify' && slh=1 + echo "expect=${{ matrix.expect }} mlkem=$kem mldsa=$dsa slhdsa=$slh" case "${{ matrix.expect }}" in - both) [ "$kem" = 1 ] && [ "$dsa" = 1 ] ;; - mlkem) [ "$kem" = 1 ] && [ "$dsa" = 0 ] ;; - mldsa) [ "$kem" = 0 ] && [ "$dsa" = 1 ] ;; - none) [ "$kem" = 0 ] && [ "$dsa" = 0 ] ;; + all) [ "$kem" = 1 ] && [ "$dsa" = 1 ] && [ "$slh" = 1 ] ;; + mlkem) [ "$kem" = 1 ] && [ "$dsa" = 0 ] && [ "$slh" = 0 ] ;; + mldsa) [ "$kem" = 0 ] && [ "$dsa" = 1 ] && [ "$slh" = 0 ] ;; + slhdsa) [ "$kem" = 0 ] && [ "$dsa" = 0 ] && [ "$slh" = 1 ] ;; + none) [ "$kem" = 0 ] && [ "$dsa" = 0 ] && [ "$slh" = 0 ] ;; *) false ;; esac || { echo "ERROR: PQC test families do not match expect=${{ matrix.expect }}"; exit 1; } @@ -166,7 +177,7 @@ jobs: if: matrix.expect != 'none' run: | export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" - idxs=$(./test/unit.test --list | grep -iE 'mlkem|mldsa|mlx' \ + idxs=$(./test/unit.test --list | grep -iE 'mlkem|mldsa|mlx|slhdsa' \ | grep -oE '^[0-9]+') if [ -z "$idxs" ]; then echo "::error::No PQC unit tests found" @@ -187,7 +198,7 @@ jobs: # to the system libcrypto/libssl (Ubuntu 22.04 ships 3.0.2, which has # no ML-KEM/ML-DSA in the default provider). - name: Three-way PQC interop validation - if: matrix.expect == 'both' + if: matrix.expect == 'all' run: | LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" \ ./test/pqc_interop.test diff --git a/README.md b/README.md index ea423d25..ca7415c8 100644 --- a/README.md +++ b/README.md @@ -78,15 +78,17 @@ Information on how to configure, build, and test wolfProvider can be found here: PQC is opt-in and requires wolfSSL master/v5.9.2-stable+ and OpenSSL 3.6+. * With the script: `./scripts/build-wolfprovider.sh --enable-pqc` - (or `--enable-mlkem` / `--enable-mldsa` for one only) + (or `--enable-mlkem` / `--enable-mldsa` / `--enable-slhdsa` for one only) * Building wolfProvider directly: `./configure --enable-pqc` - (or `--enable-mlkem` / `--enable-mldsa`); build wolfSSL with the matching - `--enable-mlkem` / `--enable-mldsa` and link an OpenSSL 3.6+ + (or `--enable-mlkem` / `--enable-mldsa` / `--enable-slhdsa`); build wolfSSL + with the matching `--enable-mlkem` / `--enable-mldsa` / `--enable-slhdsa` + and link an OpenSSL 3.6+ Without an enable flag no PQC code is compiled, regardless of what wolfSSL enables. * ML-KEM (FIPS 203): ML-KEM-512, ML-KEM-768, ML-KEM-1024 (key encapsulation) * ML-DSA (FIPS 204): ML-DSA-44, ML-DSA-65, ML-DSA-87 (signatures, pure mode with empty context per FIPS 204 sec 5.2) +* SLH-DSA (FIPS 205): all 12 parameter sets, SLH-DSA-SHA2-{128,192,256}{s,f} and SLH-DSA-SHAKE-{128,192,256}{s,f} (signatures, pure mode) ## Support diff --git a/docs/INTEGRATION_GUIDE.md b/docs/INTEGRATION_GUIDE.md index 074bdaf8..04e31aa1 100644 --- a/docs/INTEGRATION_GUIDE.md +++ b/docs/INTEGRATION_GUIDE.md @@ -30,7 +30,8 @@ This retrieves dependencies (OpenSSL and wolfSSL) and compiles them as necessary | `--openssl-dir=/path` | Use existing OpenSSL installation | | `--replace-default` | Make wolfProvider the default provider | | `--enable-replace-default-testing` | Enable unit testing with replace-default | -| `--enable-pqc` | Enable ML-KEM and ML-DSA post-quantum algorithms (adds `--enable-mlkem --enable-mldsa` to wolfSSL). Requires wolfSSL post-v5.9.1-stable. | +| `--enable-pqc` | Enable ML-KEM, ML-DSA and SLH-DSA post-quantum algorithms (adds `--enable-mlkem --enable-mldsa --enable-slhdsa` to wolfSSL). Requires wolfSSL v5.9.2-stable or later. | +| `--enable-slhdsa` | Enable SLH-DSA (FIPS 205) only. | **Examples:** @@ -83,7 +84,7 @@ sudo make install | `--enable-pwdbased` | PKCS#12 support | | `--enable-hmac-copy` | Faster repeated HMAC with same key (wolfSSL 5.7.8+) | | `--enable-sp=yes,asm --enable-sp-math-all` | SP Integer maths | -| `--enable-mlkem --enable-mldsa` | ML-KEM and ML-DSA post-quantum algorithms (wolfSSL post-v5.9.1-stable). The `build-wolfprovider.sh --enable-pqc` flag sets these automatically. | +| `--enable-mlkem --enable-mldsa --enable-slhdsa` | ML-KEM, ML-DSA and SLH-DSA post-quantum algorithms (wolfSSL v5.9.2-stable or later). The `build-wolfprovider.sh --enable-pqc` flag sets these automatically. | **Optional CPPFLAGS:** @@ -166,7 +167,7 @@ ML-DSA uses pure mode with an empty context string (FIPS 204 sec 5.2, Algorithm ### Requirements -- **wolfSSL**: post-v5.9.1-stable (i.e. v5.9.2-stable or master). v5.9.1-stable defines `HAVE_DILITHIUM` and exposes `wc_dilithium_sign_ctx_msg` (the older name for the FIPS 204 pure-mode signer) but does not yet ship the canonical `WOLFSSL_HAVE_MLDSA` macro, `` header, or `wc_MlDsaKey_SignCtx` alias that wolfProvider gates on. +- **wolfSSL**: v5.9.2-stable or later. v5.9.1-stable defines `HAVE_DILITHIUM` and exposes `wc_dilithium_sign_ctx_msg` (the older name for the FIPS 204 pure-mode signer) but does not yet ship the canonical `WOLFSSL_HAVE_MLDSA` macro, `` header, or `wc_MlDsaKey_SignCtx` alias that wolfProvider gates on. - **OpenSSL**: any 3.x. OpenSSL 3.5+ is required only for cross-provider interop against its native ML-KEM/ML-DSA implementations. ### Building with PQC @@ -175,7 +176,7 @@ ML-DSA uses pure mode with an empty context string (FIPS 204 sec 5.2, Algorithm ./scripts/build-wolfprovider.sh --enable-pqc ``` -This adds `--enable-mlkem --enable-mldsa` to the wolfSSL configure step. wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `WOLFSSL_HAVE_MLDSA` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of `` / ``) and registers the six PQC algorithms. +This adds `--enable-mlkem --enable-mldsa --enable-slhdsa` to the wolfSSL configure step. wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `WOLFSSL_HAVE_MLDSA` / `WOLFSSL_HAVE_SLHDSA` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of `` / `` / ``) and registers the PQC algorithms. ### Usage Example diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index 41236bc9..09a570f1 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -32,9 +32,10 @@ show_help() { echo " --debug-silent Debug logging compiled in but silent by default. Use WOLFPROV_LOG_LEVEL and WOLFPROV_LOG_COMPONENTS env vars to enable at runtime. Requires --debug." echo " --enable-seed-src Enable SEED-SRC entropy source with /dev/urandom caching for fork-safe entropy." echo " Note: This also enables WC_RNG_SEED_CB in wolfSSL." - echo " --enable-pqc Enable both ML-KEM and ML-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)." + echo " --enable-pqc Enable ML-KEM, ML-DSA and SLH-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)." echo " --enable-mlkem Enable ML-KEM only." echo " --enable-mldsa Enable ML-DSA only." + echo " --enable-slhdsa Enable SLH-DSA only." echo " --enable-openssl-test Build OpenSSL with its test suite (e.g. evp_test). For CI that runs OpenSSL's own tests." echo "" echo "Environment Variables:" @@ -55,9 +56,10 @@ show_help() { echo " WOLFPROV_FIPS_BASELINE If set to 1, applies FIPS baseline patch to OpenSSL (mutually exclusive with WOLFPROV_REPLACE_DEFAULT)" echo " WOLFPROV_LEAVE_SILENT If set to 1, suppress logging of return 0 in functions where return 0 is expected behavior sometimes." echo " WOLFPROV_SEED_SRC If set to 1, enables SEED-SRC with /dev/urandom caching (also enables WC_RNG_SEED_CB in wolfSSL)" - echo " WOLFPROV_PQC If set to 1, enables both ML-KEM and ML-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)" + echo " WOLFPROV_PQC If set to 1, enables ML-KEM, ML-DSA and SLH-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)" echo " WOLFPROV_MLKEM If set to 1, enables ML-KEM only" echo " WOLFPROV_MLDSA If set to 1, enables ML-DSA only" + echo " WOLFPROV_SLHDSA If set to 1, enables SLH-DSA only" echo "" } @@ -156,6 +158,7 @@ for arg in "$@"; do --enable-pqc) WOLFPROV_MLKEM=1 WOLFPROV_MLDSA=1 + WOLFPROV_SLHDSA=1 ;; --enable-mlkem) WOLFPROV_MLKEM=1 @@ -163,6 +166,9 @@ for arg in "$@"; do --enable-mldsa) WOLFPROV_MLDSA=1 ;; + --enable-slhdsa) + WOLFPROV_SLHDSA=1 + ;; --enable-openssl-test) WOLFPROV_OPENSSL_TEST=1 ;; @@ -216,8 +222,10 @@ fi if [ "$WOLFPROV_PQC" = "1" ]; then WOLFPROV_MLKEM=1 WOLFPROV_MLDSA=1 + WOLFPROV_SLHDSA=1 fi -if [ "$WOLFPROV_MLKEM" = "1" ] || [ "$WOLFPROV_MLDSA" = "1" ]; then +if [ "$WOLFPROV_MLKEM" = "1" ] || [ "$WOLFPROV_MLDSA" = "1" ] || \ + [ "$WOLFPROV_SLHDSA" = "1" ]; then WOLFPROV_PQC=1 fi @@ -228,7 +236,7 @@ fi # addition: forward the per-algorithm flags through install-wolfprov.sh and # debian/rules (mirroring the --debug/--fips flags). if [ -n "$build_debian" ] && [ "$WOLFPROV_PQC" = "1" ]; then - echo "ERROR: PQC (--enable-pqc/--enable-mlkem/--enable-mldsa) is not supported with --debian; the distro OpenSSL is older than the required 3.6+." + echo "ERROR: PQC (--enable-pqc/--enable-mlkem/--enable-mldsa/--enable-slhdsa) is not supported with --debian; the distro OpenSSL is older than the required 3.6+." exit 1 fi @@ -332,7 +340,7 @@ elif [ "$WOLFPROV_PQC" = "1" ]; then v*-stable) if [ "$(printf '%s\n%s\n' "$PQC_MIN_WOLFSSL" "$WOLFSSL_TAG" \ | sort -V | head -n1)" != "$PQC_MIN_WOLFSSL" ]; then - echo "ERROR: ML-KEM/ML-DSA require wolfSSL master or ${PQC_MIN_WOLFSSL} or higher (got ${WOLFSSL_TAG})." + echo "ERROR: PQC requires wolfSSL master or ${PQC_MIN_WOLFSSL} or higher (got ${WOLFSSL_TAG})." exit 1 fi ;; @@ -342,7 +350,7 @@ elif [ "$WOLFPROV_PQC" = "1" ]; then openssl-3.*) if [ "$(printf '%s\n%s\n' "$PQC_MIN_OPENSSL" "$OPENSSL_TAG" \ | sort -V | head -n1)" != "$PQC_MIN_OPENSSL" ]; then - echo "ERROR: ML-KEM/ML-DSA require ${PQC_MIN_OPENSSL} or higher (got ${OPENSSL_TAG})." + echo "ERROR: PQC requires ${PQC_MIN_OPENSSL} or higher (got ${OPENSSL_TAG})." exit 1 fi ;; diff --git a/scripts/utils-wolfprovider.sh b/scripts/utils-wolfprovider.sh index 92661f00..1c7f6793 100644 --- a/scripts/utils-wolfprovider.sh +++ b/scripts/utils-wolfprovider.sh @@ -115,6 +115,9 @@ install_wolfprov() { if [ "$WOLFPROV_MLDSA" = "1" ]; then WOLFPROV_CONFIG_OPTS+=" --enable-mldsa" fi + if [ "$WOLFPROV_SLHDSA" = "1" ]; then + WOLFPROV_CONFIG_OPTS+=" --enable-slhdsa" + fi if [ "$WOLFPROV_SEED_SRC" = "1" ]; then WOLFPROV_CONFIG_OPTS+=" --enable-seed-src" diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 48b5c204..74290570 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -61,6 +61,10 @@ fi if [ "$WOLFPROV_MLDSA" = "1" ]; then WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mldsa" fi +# 'yes,sha2' selects all 12 FIPS 205 parameter sets; plain 'yes' is SHAKE only. +if [ "$WOLFPROV_SLHDSA" = "1" ]; then + WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-slhdsa=yes,sha2" +fi WOLFSSL_DEBUG_ASN_TEMPLATE=${DWOLFSSL_DEBUG_ASN_TEMPLATE:-0} WOLFPROV_DISABLE_ERR_TRACE=${WOLFPROV_DISABLE_ERR_TRACE:-0} From bc7a9de923627660d46c64243e9db69b3da1e3d2 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 15:19:39 -0700 Subject: [PATCH 07/15] Run nginx PQC tests in the shared CI container --- .github/nginx/Dockerfile | 93 -------------------------------- .github/nginx/test.sh | 5 +- .github/workflows/nginx-pqc.yml | 95 +++++++++++++++++++++++++-------- 3 files changed, 75 insertions(+), 118 deletions(-) delete mode 100644 .github/nginx/Dockerfile diff --git a/.github/nginx/Dockerfile b/.github/nginx/Dockerfile deleted file mode 100644 index e5e2a6cc..00000000 --- a/.github/nginx/Dockerfile +++ /dev/null @@ -1,93 +0,0 @@ -# oqs-demos nginx, but with wolfProvider in place of liboqs + oqs-provider. -# -# Mirrors https://github.com/open-quantum-safe/oqs-demos/tree/main/nginx: build -# a quantum-safe OpenSSL provider, build nginx against that OpenSSL, generate an -# ML-DSA CA + server certificate, and serve TLS 1.3 with PQC groups. The only -# swap is the crypto provider: wolfProvider (backed by wolfSSL) instead of -# oqs-provider, restricted to the FIPS 203 / FIPS 204 algorithms wolfProvider -# implements. The default groups and SIG_ALG below are the wolfProvider- -# supported subset of the oqs-demos defaults. -# -# Build context is the wolfProvider repository root: -# docker build -f .github/nginx/Dockerfile -t wolfprov-nginx . - -ARG WOLFSSL_REF=master -ARG OPENSSL_REF=openssl-3.6.0 -ARG NGINX_VERSION=1.28.0 -ARG SIG_ALG=ML-DSA-65 -ARG DEFAULT_GROUPS=MLKEM512:MLKEM768:MLKEM1024:X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024 -ARG REPLACE_DEFAULT_FLAG=--replace-default -ARG WP_OPENSSL_CONF=/opt/wolfProvider/openssl-install/ssl/openssl.cnf - -FROM ubuntu:22.04 AS build -ARG WOLFSSL_REF -ARG OPENSSL_REF -ARG NGINX_VERSION -ARG SIG_ALG -ARG DEFAULT_GROUPS -ARG REPLACE_DEFAULT_FLAG -ARG WP_OPENSSL_CONF -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential git cmake autoconf automake libtool pkg-config \ - libpcre3-dev zlib1g-dev wget ca-certificates perl python3 \ - && rm -rf /var/lib/apt/lists/* - -# Build OpenSSL 3.6+, wolfSSL, and wolfProvider. REPLACE_DEFAULT_FLAG is -# --replace-default (wolfProvider is the compile-time default) or empty -# (wolfProvider is a loadable module selected at runtime via provider.conf). -# The matrix passes both REPLACE_DEFAULT_FLAG and the matching WP_OPENSSL_CONF. -COPY . /opt/wolfProvider -WORKDIR /opt/wolfProvider -RUN OPENSSL_TAG=${OPENSSL_REF} WOLFSSL_TAG=${WOLFSSL_REF} \ - ./scripts/build-wolfprovider.sh --enable-pqc ${REPLACE_DEFAULT_FLAG} - -ENV WOLFPROV_ROOT=/opt/wolfProvider -ENV O=/opt/wolfProvider/openssl-install - -# Build nginx against the wolfProvider-backed OpenSSL. The download and build -# run BEFORE LD_LIBRARY_PATH points at the wolfProvider OpenSSL: otherwise -# wget's own HTTPS to nginx.org would route through wolfProvider and fail -# certificate verification. nginx finds the libs at runtime via -rpath. -WORKDIR /opt/wolfProvider -RUN wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \ - && tar -zxf "nginx-${NGINX_VERSION}.tar.gz" && rm "nginx-${NGINX_VERSION}.tar.gz" -WORKDIR /opt/wolfProvider/nginx-${NGINX_VERSION} -RUN ./configure --prefix=/opt/nginx --with-http_ssl_module \ - --with-cc-opt="-I${O}/include" \ - --with-ld-opt="-L${O}/lib -L${O}/lib64 -Wl,-rpath,${O}/lib -Wl,-rpath,${O}/lib64 -Wl,-rpath,/opt/wolfProvider/wolfprov-install/lib -Wl,-rpath,/opt/wolfProvider/wolfssl-install/lib" \ - && make -j"$(nproc)" && make install - -# From here on cert generation and the runtime server go through wolfProvider. -# In replace-default builds WP_OPENSSL_CONF is the stock openssl.cnf (a no-op); -# in non-replace builds it is provider.conf, which activates only wolfProvider -# so the PQC crypto is genuinely served by wolfSSL, not OpenSSL's native PQC. -ENV LD_LIBRARY_PATH=/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:/opt/wolfProvider/openssl-install/lib:/opt/wolfProvider/openssl-install/lib64 -ENV OPENSSL_CONF=${WP_OPENSSL_CONF} -ENV OPENSSL_MODULES=/opt/wolfProvider/wolfprov-install/lib - -# Generate the ML-DSA CA + server certificate through wolfProvider (the oqs-demos -# SIG_ALG=mldsa65 arrangement) and lay down the PQC nginx config + test page. -WORKDIR /opt/nginx -COPY .github/nginx/nginx.conf /opt/nginx/conf/nginx.conf -RUN mkdir -p pki cacert logs html \ - && echo "wolfProvider quantum-safe nginx" > html/index.html \ - && "${O}/bin/openssl" req -x509 -new -newkey "${SIG_ALG}" -nodes \ - -keyout cacert/CA.key -out cacert/CA.crt -subj "/CN=wolfProvider PQC CA" \ - -days 365 -addext basicConstraints=critical,CA:TRUE \ - -addext keyUsage=critical,keyCertSign \ - && "${O}/bin/openssl" genpkey -algorithm "${SIG_ALG}" -out pki/server.key \ - && "${O}/bin/openssl" req -new -key pki/server.key -subj "/CN=oqs-nginx" \ - -addext subjectAltName=DNS:localhost -out server.csr \ - && "${O}/bin/openssl" x509 -req -in server.csr -CA cacert/CA.crt -CAkey cacert/CA.key \ - -CAcreateserial -days 365 -copy_extensions copy -out pki/server.crt - -COPY .github/nginx/test.sh /opt/nginx/test.sh -RUN chmod +x /opt/nginx/test.sh - -ENV DEFAULT_GROUPS=${DEFAULT_GROUPS} -EXPOSE 4433 - -# Default: serve. The CI test step overrides this with test.sh. -CMD ["/opt/nginx/sbin/nginx", "-c", "/opt/nginx/conf/nginx.conf", "-g", "daemon off;"] diff --git a/.github/nginx/test.sh b/.github/nginx/test.sh index bddbe406..715a7adb 100644 --- a/.github/nginx/test.sh +++ b/.github/nginx/test.sh @@ -7,13 +7,14 @@ # chain, and that the page is served. Exits non-zero if any group fails; the CI # step inverts that under WOLFPROV_FORCE_FAIL=1. -O=/opt/wolfProvider/openssl-install +WOLFPROV_ROOT=${WOLFPROV_ROOT:-/opt/wolfProvider} +O=${WOLFPROV_ROOT}/openssl-install CA=/opt/nginx/cacert/CA.crt PORT=4433 # NB: not "GROUPS" -- that is a bash special array (the user's group IDs). KEX_GROUPS="X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM512 MLKEM768 MLKEM1024" -export LD_LIBRARY_PATH="/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:${O}/lib:${O}/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +export LD_LIBRARY_PATH="${WOLFPROV_ROOT}/wolfprov-install/lib:${WOLFPROV_ROOT}/wolfssl-install/lib:${O}/lib:${O}/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" echo "Quantum-safe groups under test: ${KEX_GROUPS}" /opt/nginx/sbin/nginx -c /opt/nginx/conf/nginx.conf diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml index e75cf558..3a34d3d0 100644 --- a/.github/workflows/nginx-pqc.yml +++ b/.github/workflows/nginx-pqc.yml @@ -1,8 +1,8 @@ name: Nginx PQC Tests # Runs the open-quantum-safe oqs-demos nginx demo with wolfProvider swapped in -# for liboqs + oqs-provider (.github/nginx/Dockerfile), then runs oqs-demos' -# own connection test (.github/nginx/test.sh): connect once per quantum-safe +# for liboqs + oqs-provider, then runs the connection test +# (.github/nginx/test.sh): connect once per quantum-safe # group and assert ML-DSA (FIPS 204) certificate authentication plus an # ML-KEM / hybrid (FIPS 203) key exchange. # @@ -10,8 +10,8 @@ name: Nginx PQC Tests # replace-default and non-replace (wolfProvider loaded via provider.conf), each # in normal and force-fail. Versioning matches the other PQC workflows: wolfSSL # latest -stable (PQC floor v5.9.2-stable) and master, against the latest -# OpenSSL 3.x release. The OSP source (nginx) is pinned to a fixed release in -# the Dockerfile -- upstream master is unstable and non-reproducible. +# OpenSSL 3.x release. nginx is pinned to a fixed release because upstream +# master is unstable and non-reproducible. on: workflow_call: @@ -63,22 +63,21 @@ jobs: else REFS=$(printf '%s\n%s\n' "master" "$LATEST") fi - # PQC needs the wc_MlDsaKey_* seed/message API that lands after - # v5.9.1-stable, so the floor is v5.9.2-stable. master is always - # eligible; a -stable ref only runs once it is past the floor. A + # PQC needs the wc_MlDsaKey_* seed/message API available starting at + # v5.9.2-stable. master is always eligible; a stable ref runs at or + # above the floor. A # pre-floor ref (e.g. v5.8.4-stable from Wave 2) is dropped -- it # would just fail the --enable-pqc gate. - PQC_FLOOR="v5.9.1-stable" + PQC_FLOOR="v5.9.2-stable" ELIGIBLE=() for ref in $REFS; do if [ "$ref" = "master" ]; then ELIGIBLE+=("$ref") - elif [ "$ref" != "$PQC_FLOOR" ] && \ - [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$ref" \ + elif [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$ref" \ | sort -V | tail -n1)" = "$ref" ]; then ELIGIBLE+=("$ref") else - echo "::notice::Skipping pre-PQC-floor wolfSSL ref $ref (floor past $PQC_FLOOR)" + echo "::notice::Skipping pre-PQC-floor wolfSSL ref $ref (floor $PQC_FLOOR)" fi done if [ ${#ELIGIBLE[@]} -eq 0 ]; then @@ -113,6 +112,10 @@ jobs: needs: discover-versions runs-on: ubuntu-22.04 timeout-minutes: 60 + container: + image: ghcr.io/wolfssl/wolfprovider-test-deps:bookworm + env: + DEBIAN_FRONTEND: noninteractive strategy: fail-fast: false matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} @@ -122,28 +125,74 @@ jobs: with: fetch-depth: 1 - # Build the oqs-demos nginx image with wolfProvider in place of - # oqs-provider. nginx is pinned to release-1.28.0 in the Dockerfile. - - name: Build quantum-safe nginx image + - name: Install nginx build dependencies run: | - docker build \ - --build-arg WOLFSSL_REF=${{ matrix.wolfssl-ref }} \ - --build-arg OPENSSL_REF=${{ needs.discover-versions.outputs.openssl-tag }} \ - --build-arg REPLACE_DEFAULT_FLAG=${{ matrix.replace && '--replace-default' || '' }} \ - --build-arg WP_OPENSSL_CONF=${{ matrix.replace && '/opt/wolfProvider/openssl-install/ssl/openssl.cnf' || '/opt/wolfProvider/provider.conf' }} \ - -f .github/nginx/Dockerfile -t wolfprov-nginx . + apt-get update + apt-get install -y --no-install-recommends libpcre3-dev zlib1g-dev + rm -rf /var/lib/apt/lists/* - # Run oqs-demos' connection test against the running server. In normal + - name: Build wolfProvider + run: | + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/build-wolfprovider.sh --enable-pqc \ + ${{ matrix.replace && '--replace-default' || '' }} + + - name: Build nginx 1.28.0 + run: | + wget -q https://nginx.org/download/nginx-1.28.0.tar.gz + tar -zxf nginx-1.28.0.tar.gz + cd nginx-1.28.0 + ./configure --prefix=/opt/nginx --with-http_ssl_module \ + --with-cc-opt="-I$GITHUB_WORKSPACE/openssl-install/include" \ + --with-ld-opt="-L$GITHUB_WORKSPACE/openssl-install/lib -L$GITHUB_WORKSPACE/openssl-install/lib64 -Wl,-rpath,$GITHUB_WORKSPACE/openssl-install/lib -Wl,-rpath,$GITHUB_WORKSPACE/openssl-install/lib64 -Wl,-rpath,$GITHUB_WORKSPACE/wolfprov-install/lib -Wl,-rpath,$GITHUB_WORKSPACE/wolfssl-install/lib" + make -j"$(nproc)" + make install + + - name: Configure quantum-safe nginx + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/wolfprov-install/lib:${{ github.workspace }}/wolfssl-install/lib:${{ github.workspace }}/openssl-install/lib:${{ github.workspace }}/openssl-install/lib64 + OPENSSL_CONF: ${{ matrix.replace && format('{0}/openssl-install/ssl/openssl.cnf', github.workspace) || format('{0}/provider.conf', github.workspace) }} + OPENSSL_MODULES: ${{ github.workspace }}/wolfprov-install/lib + run: | + cp .github/nginx/nginx.conf /opt/nginx/conf/nginx.conf + mkdir -p /opt/nginx/pki /opt/nginx/cacert /opt/nginx/logs /opt/nginx/html + echo "wolfProvider quantum-safe nginx" > /opt/nginx/html/index.html + O="$GITHUB_WORKSPACE/openssl-install/bin/openssl" + "$O" req -x509 -new -newkey ML-DSA-65 -nodes \ + -keyout /opt/nginx/cacert/CA.key \ + -out /opt/nginx/cacert/CA.crt \ + -subj "/CN=wolfProvider PQC CA" -days 365 \ + -addext basicConstraints=critical,CA:TRUE \ + -addext keyUsage=critical,keyCertSign + "$O" genpkey -algorithm ML-DSA-65 \ + -out /opt/nginx/pki/server.key + "$O" req -new -key /opt/nginx/pki/server.key \ + -subj "/CN=oqs-nginx" \ + -addext subjectAltName=DNS:localhost \ + -out /opt/nginx/server.csr + "$O" x509 -req -in /opt/nginx/server.csr \ + -CA /opt/nginx/cacert/CA.crt \ + -CAkey /opt/nginx/cacert/CA.key -CAcreateserial \ + -days 365 -copy_extensions copy \ + -out /opt/nginx/pki/server.crt + + # Run the connection test against the server. In normal # mode every quantum-safe group must negotiate with an ML-DSA verified # chain; in force-fail mode wolfProvider fails every operation so the # handshakes break and check-workflow-result.sh inverts that to a pass, # proving wolfProvider genuinely served the PQC crypto. - name: Run oqs-demos nginx PQC connection test shell: bash + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/wolfprov-install/lib:${{ github.workspace }}/wolfssl-install/lib:${{ github.workspace }}/openssl-install/lib:${{ github.workspace }}/openssl-install/lib64 + OPENSSL_CONF: ${{ matrix.replace && format('{0}/openssl-install/ssl/openssl.cnf', github.workspace) || format('{0}/provider.conf', github.workspace) }} + OPENSSL_MODULES: ${{ github.workspace }}/wolfprov-install/lib + WOLFPROV_ROOT: ${{ github.workspace }} run: | set +e - docker run --rm -e ${{ matrix.force_fail || 'WOLFPROV_NOOP=1' }} \ - wolfprov-nginx /opt/nginx/test.sh 2>&1 | tee nginx-pqc.log + export ${{ matrix.force_fail || 'WOLFPROV_NOOP=1' }} + bash .github/nginx/test.sh 2>&1 | tee nginx-pqc.log TEST_RESULT=${PIPESTATUS[0]} $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ $TEST_RESULT "${{ matrix.force_fail }}" nginx-pqc From 8d48f10235387e30d9c44a9c46c4b411462d2fa3 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 16:10:02 -0700 Subject: [PATCH 08/15] Add libacvp PQC integration coverage --- .github/workflows/libacvp-pqc.yml | 196 ++++++++++++++++++++++++++++ .github/workflows/nginx-pqc.yml | 51 ++++++-- .github/workflows/nightly-osp.yml | 178 ++++++++++++------------- .github/workflows/pr-osp-select.yml | 4 + 4 files changed, 332 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/libacvp-pqc.yml diff --git a/.github/workflows/libacvp-pqc.yml b/.github/workflows/libacvp-pqc.yml new file mode 100644 index 00000000..f96511ff --- /dev/null +++ b/.github/workflows/libacvp-pqc.yml @@ -0,0 +1,196 @@ +name: libacvp PQC Tests + +on: + workflow_call: + inputs: + wolfssl_refs_json: + description: "JSON array of wolfssl refs to test; empty = master + latest -stable (pre-PQC-floor refs are dropped)" + required: false + type: string + default: "" + workflow_dispatch: {} + +permissions: + contents: read + packages: write + +jobs: + discover-versions: + name: Resolve wolfSSL/OpenSSL versions + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + openssl-tag: ${{ steps.set-matrix.outputs.openssl-tag }} + steps: + - name: Resolve wolfSSL refs and latest OpenSSL release + id: set-matrix + env: + INPUT_REFS: ${{ inputs.wolfssl_refs_json }} + run: | + set -euo pipefail + LATEST=$(git ls-remote --tags --refs \ + https://github.com/wolfSSL/wolfssl.git 'v*-stable' \ + | awk -F/ '{print $NF}' | sort -V | tail -n 1) + if [ -z "${LATEST:-}" ]; then + echo "::error::Could not resolve latest wolfSSL -stable tag" + exit 1 + fi + # PQC needs OpenSSL 3.6+, so always build against the latest release. + OSSL=$(git ls-remote --tags --refs \ + https://github.com/openssl/openssl.git 'openssl-3.*' \ + | awk -F/ '{print $NF}' | grep -E '^openssl-3\.[0-9.]+$' \ + | sort -V | tail -n 1) + if [ -z "${OSSL:-}" ]; then + echo "::error::Could not resolve latest OpenSSL release tag" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "Latest OpenSSL: $OSSL" + echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" + # Caller can override the ref set (e.g. nightly Wave 2); default is + # master + latest -stable. + if [ -n "${INPUT_REFS:-}" ]; then + REFS=$(printf '%s' "$INPUT_REFS" | jq -r '.[]') + else + REFS=$(printf '%s\n%s\n' "master" "$LATEST") + fi + # PQC needs the wc_MlDsaKey_* seed/message API in v5.9.2-stable. + # master is always eligible; a stable ref must be at the floor or + # newer. A + # pre-floor ref (e.g. v5.8.4-stable from Wave 2) is dropped, since it + # would fail the --enable-pqc gate. + PQC_FLOOR="v5.9.2-stable" + ELIGIBLE=() + for ref in $REFS; do + if [ "$ref" = "master" ]; then + ELIGIBLE+=("$ref") + elif [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$ref" \ + | sort -V | tail -n1)" = "$ref" ]; then + ELIGIBLE+=("$ref") + else + echo "::notice::Skipping pre-PQC-floor wolfSSL ref $ref (floor $PQC_FLOOR)" + fi + done + if [ ${#ELIGIBLE[@]} -eq 0 ]; then + REFS_JSON='[]' + else + REFS_JSON=$(printf '%s\n' "${ELIGIBLE[@]}" | jq -R . | jq -sc .) + fi + echo "Eligible wolfSSL refs: $REFS_JSON" + # Each eligible wolfSSL ref expands to 4 rows: replace-default and + # non-replace, each in normal and force-fail anti-test mode. + MATRIX=$(jq -nc --argjson refs "$REFS_JSON" ' + def rows($ref; $lbl): + [ {"replace":true, "ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [replace-default] [force-fail]"}, + {"replace":true, "ff":"","sfx":" [replace-default]"}, + {"replace":false, "ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [non-replace] [force-fail]"}, + {"replace":false, "ff":"","sfx":" [non-replace]"} ] + | map({"name":($lbl+.sfx), "wolfssl-ref":$ref, + "replace":.replace, "force_fail":.ff}); + { include: ( $refs + | map(rows(.; (if . == "master" then "master" + else "stable (" + . + ")" end))) + | add // [] ) }') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + libacvp-pqc: + name: ${{ matrix.name }} + needs: discover-versions + runs-on: ubuntu-22.04 + timeout-minutes: 60 + container: + image: ghcr.io/wolfssl/wolfprovider-test-deps:bookworm + env: + DEBIAN_FRONTEND: noninteractive + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Cache build dependencies + id: deps + uses: ./.github/actions/oras-build-deps + with: + variant: libacvp-pqc-bookworm${{ matrix.replace && '-rd' || '' }} + openssl_ref: ${{ needs.discover-versions.outputs.openssl-tag }} + wolfssl_ref: ${{ matrix.wolfssl-ref }} + extra_key: --enable-pqc + cache_openssl_source: ${{ matrix.replace }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build wolfProvider + run: | + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/build-wolfprovider.sh --enable-pqc \ + ${{ matrix.replace && '--replace-default' || '' }} + + - name: Push build dependencies + uses: ./.github/actions/oras-build-deps-push + with: + registry: ${{ steps.deps.outputs.registry }} + openssl_install_tag: ${{ steps.deps.outputs.openssl_install_tag }} + wolfssl_install_tag: ${{ steps.deps.outputs.wolfssl_install_tag }} + openssl_source_tag: ${{ steps.deps.outputs.openssl_source_tag }} + openssl_hit: ${{ steps.deps.outputs.openssl_hit }} + wolfssl_hit: ${{ steps.deps.outputs.wolfssl_hit }} + openssl_source_hit: ${{ steps.deps.outputs.openssl_source_hit }} + cache_openssl_source: ${{ steps.deps.outputs.cache_openssl_source }} + + - name: Checkout libacvp v2.3.1 + uses: actions/checkout@v4 + with: + repository: cisco/libacvp + ref: v2.3.1 + path: libacvp + fetch-depth: 1 + + - name: Checkout OSP + uses: actions/checkout@v4 + with: + repository: aidangarske/osp + ref: add-libacvp-pqc-tests + path: osp + fetch-depth: 1 + + - name: Build libacvp test suite + working-directory: libacvp + run: | + PATCH=$($GITHUB_WORKSPACE/scripts/resolve-osp-patch.sh \ + "$GITHUB_WORKSPACE/osp" libacvp v2.3.1 \ + "${{ matrix.wolfssl-ref }}") + patch -p1 < "$PATCH" + autoreconf -fiv + ./configure --enable-unit-tests \ + --with-ssl-dir="$GITHUB_WORKSPACE/openssl-install" \ + --with-libcurl-dir=/usr + make -j"$(nproc)" + + - name: Run full libacvp suite with PQC coverage + working-directory: libacvp/test + shell: bash + run: | + export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/wolfprov-install/lib:$GITHUB_WORKSPACE/wolfssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib64" + export OPENSSL_MODULES="$GITHUB_WORKSPACE/wolfprov-install/lib" + if [ "${{ matrix.replace }}" = "true" ]; then + export OPENSSL_CONF="$GITHUB_WORKSPACE/openssl-install/ssl/openssl.cnf" + else + export OPENSSL_CONF="$GITHUB_WORKSPACE/provider.conf" + fi + set +e + export ${{ matrix.force_fail || 'WOLFPROV_NOOP=1' }} + if [ -n "${{ matrix.force_fail }}" ]; then + ./runtest -g APP_PQC_HANDLER -v 2>&1 | tee libacvp-pqc.log + TEST_RESULT=${PIPESTATUS[0]} + else + ./runtest -v 2>&1 | tee libacvp-pqc.log + TEST_RESULT=${PIPESTATUS[0]} + fi + $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ + $TEST_RESULT "${{ matrix.force_fail }}" libacvp-pqc diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml index 3a34d3d0..37928ee1 100644 --- a/.github/workflows/nginx-pqc.yml +++ b/.github/workflows/nginx-pqc.yml @@ -23,6 +23,10 @@ on: default: "" workflow_dispatch: {} +permissions: + contents: read + packages: write + jobs: discover-versions: name: Resolve wolfSSL/OpenSSL versions @@ -131,6 +135,17 @@ jobs: apt-get install -y --no-install-recommends libpcre3-dev zlib1g-dev rm -rf /var/lib/apt/lists/* + - name: Cache build dependencies + id: deps + uses: ./.github/actions/oras-build-deps + with: + variant: nginx-pqc-bookworm${{ matrix.replace && '-rd' || '' }} + openssl_ref: ${{ needs.discover-versions.outputs.openssl-tag }} + wolfssl_ref: ${{ matrix.wolfssl-ref }} + extra_key: --enable-pqc + cache_openssl_source: ${{ matrix.replace }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Build wolfProvider run: | OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ @@ -138,6 +153,18 @@ jobs: ./scripts/build-wolfprovider.sh --enable-pqc \ ${{ matrix.replace && '--replace-default' || '' }} + - name: Push build dependencies + uses: ./.github/actions/oras-build-deps-push + with: + registry: ${{ steps.deps.outputs.registry }} + openssl_install_tag: ${{ steps.deps.outputs.openssl_install_tag }} + wolfssl_install_tag: ${{ steps.deps.outputs.wolfssl_install_tag }} + openssl_source_tag: ${{ steps.deps.outputs.openssl_source_tag }} + openssl_hit: ${{ steps.deps.outputs.openssl_hit }} + wolfssl_hit: ${{ steps.deps.outputs.wolfssl_hit }} + openssl_source_hit: ${{ steps.deps.outputs.openssl_source_hit }} + cache_openssl_source: ${{ steps.deps.outputs.cache_openssl_source }} + - name: Build nginx 1.28.0 run: | wget -q https://nginx.org/download/nginx-1.28.0.tar.gz @@ -150,11 +177,14 @@ jobs: make install - name: Configure quantum-safe nginx - env: - LD_LIBRARY_PATH: ${{ github.workspace }}/wolfprov-install/lib:${{ github.workspace }}/wolfssl-install/lib:${{ github.workspace }}/openssl-install/lib:${{ github.workspace }}/openssl-install/lib64 - OPENSSL_CONF: ${{ matrix.replace && format('{0}/openssl-install/ssl/openssl.cnf', github.workspace) || format('{0}/provider.conf', github.workspace) }} - OPENSSL_MODULES: ${{ github.workspace }}/wolfprov-install/lib run: | + export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/wolfprov-install/lib:$GITHUB_WORKSPACE/wolfssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib64" + export OPENSSL_MODULES="$GITHUB_WORKSPACE/wolfprov-install/lib" + if [ "${{ matrix.replace }}" = "true" ]; then + export OPENSSL_CONF="$GITHUB_WORKSPACE/openssl-install/ssl/openssl.cnf" + else + export OPENSSL_CONF="$GITHUB_WORKSPACE/provider.conf" + fi cp .github/nginx/nginx.conf /opt/nginx/conf/nginx.conf mkdir -p /opt/nginx/pki /opt/nginx/cacert /opt/nginx/logs /opt/nginx/html echo "wolfProvider quantum-safe nginx" > /opt/nginx/html/index.html @@ -184,12 +214,15 @@ jobs: # proving wolfProvider genuinely served the PQC crypto. - name: Run oqs-demos nginx PQC connection test shell: bash - env: - LD_LIBRARY_PATH: ${{ github.workspace }}/wolfprov-install/lib:${{ github.workspace }}/wolfssl-install/lib:${{ github.workspace }}/openssl-install/lib:${{ github.workspace }}/openssl-install/lib64 - OPENSSL_CONF: ${{ matrix.replace && format('{0}/openssl-install/ssl/openssl.cnf', github.workspace) || format('{0}/provider.conf', github.workspace) }} - OPENSSL_MODULES: ${{ github.workspace }}/wolfprov-install/lib - WOLFPROV_ROOT: ${{ github.workspace }} run: | + export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/wolfprov-install/lib:$GITHUB_WORKSPACE/wolfssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib:$GITHUB_WORKSPACE/openssl-install/lib64" + export OPENSSL_MODULES="$GITHUB_WORKSPACE/wolfprov-install/lib" + export WOLFPROV_ROOT="$GITHUB_WORKSPACE" + if [ "${{ matrix.replace }}" = "true" ]; then + export OPENSSL_CONF="$GITHUB_WORKSPACE/openssl-install/ssl/openssl.cnf" + else + export OPENSSL_CONF="$GITHUB_WORKSPACE/provider.conf" + fi set +e export ${{ matrix.force_fail || 'WOLFPROV_NOOP=1' }} bash .github/nginx/test.sh 2>&1 | tee nginx-pqc.log diff --git a/.github/workflows/nightly-osp.yml b/.github/workflows/nightly-osp.yml index 32a423b9..bfb90cb5 100644 --- a/.github/workflows/nightly-osp.yml +++ b/.github/workflows/nightly-osp.yml @@ -6,8 +6,8 @@ name: Nightly OSP Suite # with if: always() so a single Wave 1 failure doesn't skip the # older-line coverage. multi-compiler and static-analysis run once # outside the waves. -# nginx-pqc is Wave 1 only: PQC has a v5.9.2-stable floor, so the -# v5.8.4-stable Wave 2 line has no eligible ref to run. +# libacvp-pqc and nginx-pqc are Wave 1 only. PQC has a v5.9.2-stable +# floor, so the v5.8.4-stable Wave 2 line has no eligible ref to run. on: schedule: @@ -29,99 +29,101 @@ concurrency: cancel-in-progress: false jobs: - # === Wave 1: v5.9.1-stable === - bind9-591: { uses: ./.github/workflows/bind9.yml, } - cjose-591: { uses: ./.github/workflows/cjose.yml, } - curl-591: { uses: ./.github/workflows/curl.yml, } - debian-package-591: { uses: ./.github/workflows/debian-package.yml, } - git-ssh-dr-591: { uses: ./.github/workflows/git-ssh-dr.yml, } - grpc-591: { uses: ./.github/workflows/grpc.yml, } - hostap-591: { uses: ./.github/workflows/hostap.yml, } - iperf-591: { uses: ./.github/workflows/iperf.yml, } - krb5-591: { uses: ./.github/workflows/krb5.yml, } - libcryptsetup-591: { uses: ./.github/workflows/libcryptsetup.yml, } - libeac3-591: { uses: ./.github/workflows/libeac3.yml, } - libfido2-591: { uses: ./.github/workflows/libfido2.yml, } - libhashkit2-591: { uses: ./.github/workflows/libhashkit2.yml, } - libnice-591: { uses: ./.github/workflows/libnice.yml, } - liboauth2-591: { uses: ./.github/workflows/liboauth2.yml, } - librelp-591: { uses: ./.github/workflows/librelp.yml, } - libssh2-591: { uses: ./.github/workflows/libssh2.yml, } - libtss2-591: { uses: ./.github/workflows/libtss2.yml, } - libwebsockets-591: { uses: ./.github/workflows/libwebsockets.yml, } - net-snmp-591: { uses: ./.github/workflows/net-snmp.yml, } - nginx-591: { uses: ./.github/workflows/nginx.yml, } - nginx-pqc-591: { uses: ./.github/workflows/nginx-pqc.yml, } - openldap-591: { uses: ./.github/workflows/openldap.yml, } - opensc-591: { uses: ./.github/workflows/opensc.yml, } - openssh-591: { uses: ./.github/workflows/openssh.yml, } - openssl-version-591: { uses: ./.github/workflows/openssl-version.yml, } - openvpn-591: { uses: ./.github/workflows/openvpn.yml, } - pam-pkcs11-591: { uses: ./.github/workflows/pam-pkcs11.yml, } - ppp-591: { uses: ./.github/workflows/ppp.yml, } - python3-ntp-591: { uses: ./.github/workflows/python3-ntp.yml, } - qt5network5-591: { uses: ./.github/workflows/qt5network5.yml, } - rsync-591: { uses: ./.github/workflows/rsync.yml, } - socat-591: { uses: ./.github/workflows/socat.yml, } - sscep-591: { uses: ./.github/workflows/sscep.yml, } - sssd-591: { uses: ./.github/workflows/sssd.yml, } - stunnel-591: { uses: ./.github/workflows/stunnel.yml, } - systemd-591: { uses: ./.github/workflows/systemd.yml, } - tcpdump-591: { uses: ./.github/workflows/tcpdump.yml, } - tnftp-591: { uses: ./.github/workflows/tnftp.yml, } - tpm2-tools-591: { uses: ./.github/workflows/tpm2-tools.yml, } - x11vnc-591: { uses: ./.github/workflows/x11vnc.yml, } - xmlsec-591: { uses: ./.github/workflows/xmlsec.yml, } + # === Wave 1: master + latest stable === + bind9-current: { uses: ./.github/workflows/bind9.yml, } + cjose-current: { uses: ./.github/workflows/cjose.yml, } + curl-current: { uses: ./.github/workflows/curl.yml, } + debian-package-current: { uses: ./.github/workflows/debian-package.yml, } + git-ssh-dr-current: { uses: ./.github/workflows/git-ssh-dr.yml, } + grpc-current: { uses: ./.github/workflows/grpc.yml, } + hostap-current: { uses: ./.github/workflows/hostap.yml, } + iperf-current: { uses: ./.github/workflows/iperf.yml, } + krb5-current: { uses: ./.github/workflows/krb5.yml, } + libacvp-pqc-current: { uses: ./.github/workflows/libacvp-pqc.yml, } + libcryptsetup-current: { uses: ./.github/workflows/libcryptsetup.yml, } + libeac3-current: { uses: ./.github/workflows/libeac3.yml, } + libfido2-current: { uses: ./.github/workflows/libfido2.yml, } + libhashkit2-current: { uses: ./.github/workflows/libhashkit2.yml, } + libnice-current: { uses: ./.github/workflows/libnice.yml, } + liboauth2-current: { uses: ./.github/workflows/liboauth2.yml, } + librelp-current: { uses: ./.github/workflows/librelp.yml, } + libssh2-current: { uses: ./.github/workflows/libssh2.yml, } + libtss2-current: { uses: ./.github/workflows/libtss2.yml, } + libwebsockets-current: { uses: ./.github/workflows/libwebsockets.yml, } + net-snmp-current: { uses: ./.github/workflows/net-snmp.yml, } + nginx-current: { uses: ./.github/workflows/nginx.yml, } + nginx-pqc-current: { uses: ./.github/workflows/nginx-pqc.yml, } + openldap-current: { uses: ./.github/workflows/openldap.yml, } + opensc-current: { uses: ./.github/workflows/opensc.yml, } + openssh-current: { uses: ./.github/workflows/openssh.yml, } + openssl-version-current: { uses: ./.github/workflows/openssl-version.yml, } + openvpn-current: { uses: ./.github/workflows/openvpn.yml, } + pam-pkcs11-current: { uses: ./.github/workflows/pam-pkcs11.yml, } + ppp-current: { uses: ./.github/workflows/ppp.yml, } + python3-ntp-current: { uses: ./.github/workflows/python3-ntp.yml, } + qt5network5-current: { uses: ./.github/workflows/qt5network5.yml, } + rsync-current: { uses: ./.github/workflows/rsync.yml, } + socat-current: { uses: ./.github/workflows/socat.yml, } + sscep-current: { uses: ./.github/workflows/sscep.yml, } + sssd-current: { uses: ./.github/workflows/sssd.yml, } + stunnel-current: { uses: ./.github/workflows/stunnel.yml, } + systemd-current: { uses: ./.github/workflows/systemd.yml, } + tcpdump-current: { uses: ./.github/workflows/tcpdump.yml, } + tnftp-current: { uses: ./.github/workflows/tnftp.yml, } + tpm2-tools-current: { uses: ./.github/workflows/tpm2-tools.yml, } + x11vnc-current: { uses: ./.github/workflows/x11vnc.yml, } + xmlsec-current: { uses: ./.github/workflows/xmlsec.yml, } # Fan-in between waves. if: always() so Wave 1 failures don't skip Wave 2. wave1-done: needs: - - bind9-591 - - cjose-591 - - curl-591 - - debian-package-591 - - git-ssh-dr-591 - - grpc-591 - - hostap-591 - - iperf-591 - - krb5-591 - - libcryptsetup-591 - - libeac3-591 - - libfido2-591 - - libhashkit2-591 - - libnice-591 - - liboauth2-591 - - librelp-591 - - libssh2-591 - - libtss2-591 - - libwebsockets-591 - - net-snmp-591 - - nginx-591 - - nginx-pqc-591 - - openldap-591 - - opensc-591 - - openssh-591 - - openssl-version-591 - - openvpn-591 - - pam-pkcs11-591 - - ppp-591 - - python3-ntp-591 - - qt5network5-591 - - rsync-591 - - socat-591 - - sscep-591 - - sssd-591 - - stunnel-591 - - systemd-591 - - tcpdump-591 - - tnftp-591 - - tpm2-tools-591 - - x11vnc-591 - - xmlsec-591 + - bind9-current + - cjose-current + - curl-current + - debian-package-current + - git-ssh-dr-current + - grpc-current + - hostap-current + - iperf-current + - krb5-current + - libacvp-pqc-current + - libcryptsetup-current + - libeac3-current + - libfido2-current + - libhashkit2-current + - libnice-current + - liboauth2-current + - librelp-current + - libssh2-current + - libtss2-current + - libwebsockets-current + - net-snmp-current + - nginx-current + - nginx-pqc-current + - openldap-current + - opensc-current + - openssh-current + - openssl-version-current + - openvpn-current + - pam-pkcs11-current + - ppp-current + - python3-ntp-current + - qt5network5-current + - rsync-current + - socat-current + - sscep-current + - sssd-current + - stunnel-current + - systemd-current + - tcpdump-current + - tnftp-current + - tpm2-tools-current + - x11vnc-current + - xmlsec-current if: always() runs-on: ubuntu-latest steps: - - run: echo "Wave 1 (v5.9.1-stable) complete" + - run: echo "Wave 1 (master + latest stable) complete" # === Wave 2: v5.8.4-stable, gated on wave1-done === bind9-584: { needs: wave1-done, if: always(), uses: ./.github/workflows/bind9.yml, with: { wolfssl_refs_json: '["v5.8.4-stable"]' } } diff --git a/.github/workflows/pr-osp-select.yml b/.github/workflows/pr-osp-select.yml index c98eb7c8..9d2d1db4 100644 --- a/.github/workflows/pr-osp-select.yml +++ b/.github/workflows/pr-osp-select.yml @@ -83,6 +83,10 @@ jobs: needs: select if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' krb5 ') uses: ./.github/workflows/krb5.yml + libacvp-pqc: + needs: select + if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' libacvp-pqc ') + uses: ./.github/workflows/libacvp-pqc.yml libcryptsetup: needs: select if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' libcryptsetup ') From c78c2b2ab6d544af15ad513085db5237ff8e83be Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 30 Jul 2026 17:31:09 -0700 Subject: [PATCH 09/15] Add maintained OpenSSL provider examples --- configure.ac | 5 + examples/include.am | 14 +- examples/openssl_example.c | 103 +++++++++------ examples/pqc_openssl_example.c | 227 +++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 43 deletions(-) create mode 100644 examples/pqc_openssl_example.c diff --git a/configure.ac b/configure.ac index ebf00198..52972076 100644 --- a/configure.ac +++ b/configure.ac @@ -204,6 +204,11 @@ if test "x$ENABLED_SLHDSA" = "xyes"; then AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_SLHDSA" fi +AM_CONDITIONAL([BUILD_PQC_EXAMPLE], + [test "x$ENABLED_MLKEM" = "xyes" || + test "x$ENABLED_MLDSA" = "xyes" || + test "x$ENABLED_SLHDSA" = "xyes"]) + # Set OpenSSL lib directory for installing libdefault.so if test "x$ENABLED_REPLACE_DEFAULT" = "xyes"; then if test -d "$OPENSSL_INSTALL_DIR/lib64"; then diff --git a/examples/include.am b/examples/include.am index 0928733d..35c5682e 100644 --- a/examples/include.am +++ b/examples/include.am @@ -17,4 +17,16 @@ # You should have received a copy of the GNU General Public License # along with wolfProvider. If not, see . -EXTRA_DIST += examples/openssl_example.c +EXTRA_DIST += examples/openssl_example.c examples/pqc_openssl_example.c + +check_PROGRAMS += examples/openssl_example +examples_openssl_example_SOURCES = examples/openssl_example.c +examples_openssl_example_LDADD = libwolfprov.la +DISTCLEANFILES += examples/.libs/openssl_example + +if BUILD_PQC_EXAMPLE +check_PROGRAMS += examples/pqc_openssl_example +examples_pqc_openssl_example_SOURCES = examples/pqc_openssl_example.c +examples_pqc_openssl_example_LDADD = libwolfprov.la +DISTCLEANFILES += examples/.libs/pqc_openssl_example +endif diff --git a/examples/openssl_example.c b/examples/openssl_example.c index 4b0751c2..4c31f69a 100644 --- a/examples/openssl_example.c +++ b/examples/openssl_example.c @@ -1,52 +1,71 @@ +/* openssl_example.c + * + * Copyright (C) 2006-2026 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 + +#define WOLFPROV_NAME "libwolfprov" int main(void) { - /* - OSSL_PROVIDER *legacy; - OSSL_PROVIDER *deflt; - - // Load Multiple providers into the default (NULL) library context - legacy = OSSL_PROVIDER_load(NULL, "legacy"); - if (legacy == NULL) { - printf("Failed to load Legacy provider\n"); - exit(EXIT_FAILURE); + int rc = 0; + const char* buildInfo = NULL; + OSSL_LIB_CTX* libCtx = NULL; + OSSL_PROVIDER* wolfProv = NULL; + OSSL_PARAM request[] = { + OSSL_PARAM_utf8_ptr("buildinfo", (char**)&buildInfo, 0), + OSSL_PARAM_END + }; + + libCtx = OSSL_LIB_CTX_new(); + if (libCtx == NULL) { + rc = 1; } - deflt = OSSL_PROVIDER_load(NULL, "default"); - if (deflt == NULL) { - printf("Failed to load Default provider\n"); - OSSL_PROVIDER_unload(legacy); - exit(EXIT_FAILURE); + + if (rc == 0) { + OSSL_PROVIDER_set_default_search_path(libCtx, ".libs"); + wolfProv = OSSL_PROVIDER_load(libCtx, WOLFPROV_NAME); + if ((wolfProv == NULL) || + (OSSL_PROVIDER_get_params(wolfProv, request) != 1)) { + rc = 1; + } + } + + if (rc == 0) { + printf("Provider '%s' buildinfo: %s\n", WOLFPROV_NAME, buildInfo); + if (OSSL_PROVIDER_self_test(wolfProv) != 1) { + fprintf(stderr, "Provider self-test failed\n"); + rc = 1; + } + } + if (rc == 0) { + printf("Provider self-test passed\n"); + } + + if (rc != 0) { + ERR_print_errors_fp(stderr); } - OSSL_PROVIDER_unload(legacy); - OSSL_PROVIDER_unload(deflt); - */ - - // Rest of application - - OSSL_PROVIDER *prov = NULL; - const char *build = NULL; - OSSL_PARAM request[] = { - { "buildinfo", OSSL_PARAM_UTF8_PTR, &build, 0, 0 }, - { NULL, 0, NULL, 0, 0 } - }; - - if ((prov = OSSL_PROVIDER_load(NULL, "libwolfprov")) != NULL - && OSSL_PROVIDER_get_params(prov, request)) - printf("Provider 'libwolfprov' buildinfo: %s\n", build); - else - ERR_print_errors_fp(stderr); - - if (OSSL_PROVIDER_self_test(prov) == 0) - printf("Provider selftest failed\n"); - else - printf("Provider selftest passed\n"); - - OSSL_PROVIDER_unload(prov); - exit(EXIT_SUCCESS); + OSSL_PROVIDER_unload(wolfProv); + OSSL_LIB_CTX_free(libCtx); + return rc; } diff --git a/examples/pqc_openssl_example.c b/examples/pqc_openssl_example.c new file mode 100644 index 00000000..b49a4b88 --- /dev/null +++ b/examples/pqc_openssl_example.c @@ -0,0 +1,227 @@ +/* pqc_openssl_example.c + * + * Copyright (C) 2006-2026 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 + +#define WOLFPROV_NAME "libwolfprov" + +static EVP_PKEY* generate_key(OSSL_LIB_CTX* libCtx, const char* algorithm) +{ + EVP_PKEY* key = NULL; + EVP_PKEY_CTX* keyCtx = NULL; + + keyCtx = EVP_PKEY_CTX_new_from_name(libCtx, algorithm, NULL); + if ((keyCtx == NULL) || (EVP_PKEY_keygen_init(keyCtx) != 1) || + (EVP_PKEY_keygen(keyCtx, &key) != 1)) { + EVP_PKEY_free(key); + key = NULL; + } + + EVP_PKEY_CTX_free(keyCtx); + return key; +} + +#ifdef WOLFPROV_HAVE_MLKEM +static int run_mlkem(OSSL_LIB_CTX* libCtx) +{ + int rc = 0; + EVP_PKEY* key = NULL; + EVP_PKEY_CTX* encapCtx = NULL; + EVP_PKEY_CTX* decapCtx = NULL; + unsigned char* ciphertext = NULL; + unsigned char* encapSecret = NULL; + unsigned char* decapSecret = NULL; + size_t ciphertextLen = 0; + size_t encapSecretLen = 0; + size_t decapSecretLen = 0; + + key = generate_key(libCtx, "ML-KEM-768"); + if (key == NULL) { + rc = 1; + } + + if (rc == 0) { + encapCtx = EVP_PKEY_CTX_new_from_pkey(libCtx, key, NULL); + if ((encapCtx == NULL) || + (EVP_PKEY_encapsulate_init(encapCtx, NULL) != 1) || + (EVP_PKEY_encapsulate(encapCtx, NULL, &ciphertextLen, NULL, + &encapSecretLen) != 1)) { + rc = 1; + } + } + + if (rc == 0) { + decapSecretLen = encapSecretLen; + ciphertext = OPENSSL_malloc(ciphertextLen); + encapSecret = OPENSSL_malloc(encapSecretLen); + decapSecret = OPENSSL_malloc(decapSecretLen); + if ((ciphertext == NULL) || (encapSecret == NULL) || + (decapSecret == NULL)) { + rc = 1; + } + } + + if (rc == 0) { + if (EVP_PKEY_encapsulate(encapCtx, ciphertext, &ciphertextLen, + encapSecret, &encapSecretLen) != 1) { + rc = 1; + } + } + + if (rc == 0) { + decapCtx = EVP_PKEY_CTX_new_from_pkey(libCtx, key, NULL); + if ((decapCtx == NULL) || + (EVP_PKEY_decapsulate_init(decapCtx, NULL) != 1) || + (EVP_PKEY_decapsulate(decapCtx, decapSecret, &decapSecretLen, + ciphertext, ciphertextLen) != 1)) { + rc = 1; + } + } + + if (rc == 0) { + rc = (encapSecretLen != decapSecretLen) || + (CRYPTO_memcmp(encapSecret, decapSecret, encapSecretLen) != 0); + } + + OPENSSL_clear_free(decapSecret, decapSecretLen); + OPENSSL_clear_free(encapSecret, encapSecretLen); + OPENSSL_free(ciphertext); + EVP_PKEY_CTX_free(decapCtx); + EVP_PKEY_CTX_free(encapCtx); + EVP_PKEY_free(key); + return rc; +} +#endif + +#if defined(WOLFPROV_HAVE_MLDSA) || defined(WOLFPROV_HAVE_SLHDSA) +static int run_signature(OSSL_LIB_CTX* libCtx, const char* algorithm) +{ + static const unsigned char message[] = + "wolfProvider post-quantum example"; + int rc = 0; + EVP_PKEY* key = NULL; + EVP_MD_CTX* signCtx = NULL; + EVP_MD_CTX* verifyCtx = NULL; + unsigned char* signature = NULL; + size_t signatureLen = 0; + + key = generate_key(libCtx, algorithm); + signCtx = EVP_MD_CTX_new(); + if ((key == NULL) || (signCtx == NULL) || + (EVP_DigestSignInit_ex(signCtx, NULL, NULL, libCtx, NULL, key, + NULL) != 1) || + (EVP_DigestSign(signCtx, NULL, &signatureLen, message, + sizeof(message) - 1) != 1)) { + rc = 1; + } + + if (rc == 0) { + signature = OPENSSL_malloc(signatureLen); + if ((signature == NULL) || + (EVP_DigestSign(signCtx, signature, &signatureLen, message, + sizeof(message) - 1) != 1)) { + rc = 1; + } + } + + if (rc == 0) { + verifyCtx = EVP_MD_CTX_new(); + if ((verifyCtx == NULL) || + (EVP_DigestVerifyInit_ex(verifyCtx, NULL, NULL, libCtx, NULL, + key, NULL) != 1)) { + rc = 1; + } + } + if (rc == 0) { + rc = EVP_DigestVerify(verifyCtx, signature, signatureLen, message, + sizeof(message) - 1) != 1; + } + + OPENSSL_free(signature); + EVP_MD_CTX_free(verifyCtx); + EVP_MD_CTX_free(signCtx); + EVP_PKEY_free(key); + return rc; +} +#endif + +int main(void) +{ + int rc = 0; + OSSL_LIB_CTX* libCtx = NULL; + OSSL_PROVIDER* wolfProv = NULL; + + libCtx = OSSL_LIB_CTX_new(); + if (libCtx == NULL) { + rc = 1; + } + + if (rc == 0) { + OSSL_PROVIDER_set_default_search_path(libCtx, ".libs"); + wolfProv = OSSL_PROVIDER_load(libCtx, WOLFPROV_NAME); + if (wolfProv == NULL) { + rc = 1; + } + } + +#ifdef WOLFPROV_HAVE_MLKEM + if ((rc == 0) && (run_mlkem(libCtx) != 0)) { + fprintf(stderr, "ML-KEM-768 encapsulation failed\n"); + rc = 1; + } + if (rc == 0) { + printf("ML-KEM-768 encapsulation passed\n"); + } +#endif + +#ifdef WOLFPROV_HAVE_MLDSA + if ((rc == 0) && (run_signature(libCtx, "ML-DSA-65") != 0)) { + fprintf(stderr, "ML-DSA-65 signature failed\n"); + rc = 1; + } + if (rc == 0) { + printf("ML-DSA-65 signature passed\n"); + } +#endif + +#ifdef WOLFPROV_HAVE_SLHDSA + if ((rc == 0) && + (run_signature(libCtx, "SLH-DSA-SHA2-128f") != 0)) { + fprintf(stderr, "SLH-DSA-SHA2-128f signature failed\n"); + rc = 1; + } + if (rc == 0) { + printf("SLH-DSA-SHA2-128f signature passed\n"); + } +#endif + + if (rc != 0) { + ERR_print_errors_fp(stderr); + } + OSSL_PROVIDER_unload(wolfProv); + OSSL_LIB_CTX_free(libCtx); + return rc; +} From ee6d7fe2701498bdd5ba6d9e725ec34d7706b6ae Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 31 Jul 2026 12:33:26 -0700 Subject: [PATCH 10/15] Synchronize DH private key encoding --- src/wp_dh_kmgmt.c | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/wp_dh_kmgmt.c b/src/wp_dh_kmgmt.c index f681cb55..43a8b890 100644 --- a/src/wp_dh_kmgmt.c +++ b/src/wp_dh_kmgmt.c @@ -2636,6 +2636,16 @@ static int wp_dh_encode_spki(const wp_Dh *dh, unsigned char* keyData, return ok; } +static wolfSSL_Mutex* wp_dh_get_mutex(const wp_Dh* dh) +{ +#ifndef WP_SINGLE_THREADED + return (wolfSSL_Mutex*)&dh->mutex; +#else + (void)dh; + return NULL; +#endif +} + /** * Copy a generated private key into the inner wolfSSL key if not already set. * @@ -2643,22 +2653,34 @@ static int wp_dh_encode_spki(const wp_Dh *dh, unsigned char* keyData, * @return 1 on success. * @return 0 on failure. */ -static int wp_dh_sync_priv_to_key(const wp_Dh *dh) +static int wp_dh_sync_priv_to_key(const wp_Dh* dh) { int ok = 1; int ret; + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_sync_priv_to_key"); + if (wp_lock(wp_dh_get_mutex(dh)) != 1) { + ok = 0; + } + else { + locked = 1; + } + /* If we have a generated private key that is not set in the inner key, * set it now */ - if (mp_bitsused(&dh->key.priv) == 0 && dh->priv != NULL && dh->privSz > 0) { + if (ok && (mp_bitsused(&dh->key.priv) == 0) && (dh->priv != NULL) && + (dh->privSz > 0)) { ret = wc_DhImportKeyPair((DhKey*)&dh->key, dh->priv, (word32)dh->privSz, dh->pub, (word32)dh->pubSz); if (ret != 0) { ok = 0; } } + if (locked) { + wp_unlock(wp_dh_get_mutex(dh)); + } WOLFPROV_LEAVE(WP_LOG_COMP_DH, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -2797,10 +2819,14 @@ static int wp_dh_encode_epki(const wp_DhEncDecCtx* ctx, const wp_Dh *dh, WOLFPROV_ENTER(WP_LOG_COMP_DH, "wp_dh_encode_epki"); + ok = wp_dh_sync_priv_to_key(dh); + /* Determine the plaintext PKCS #8 length. */ - rc = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &pkcs8Len); - if (rc != LENGTH_ONLY_E) { - ok = 0; + if (ok) { + rc = wc_DhPrivKeyToDer((DhKey*)&dh->key, NULL, &pkcs8Len); + if (rc != LENGTH_ONLY_E) { + ok = 0; + } } if (ok) { /* Allocate the plaintext buffer - must differ from the output. */ From 75cffb950d95d521153ac7c83a200d7fed50f5cf Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 31 Jul 2026 12:33:26 -0700 Subject: [PATCH 11/15] Address SLH-DSA review findings --- examples/include.am | 4 +- examples/pqc_openssl_example.c | 39 ++++++- src/wp_slhdsa_kmgmt.c | 201 +++++++++++++++++++++++++++------ src/wp_slhdsa_sig.c | 33 +++--- test/test_slhdsa.c | 91 ++++++++++++++- 5 files changed, 303 insertions(+), 65 deletions(-) diff --git a/examples/include.am b/examples/include.am index 35c5682e..9cbda6de 100644 --- a/examples/include.am +++ b/examples/include.am @@ -19,13 +19,13 @@ EXTRA_DIST += examples/openssl_example.c examples/pqc_openssl_example.c -check_PROGRAMS += examples/openssl_example +noinst_PROGRAMS += examples/openssl_example examples_openssl_example_SOURCES = examples/openssl_example.c examples_openssl_example_LDADD = libwolfprov.la DISTCLEANFILES += examples/.libs/openssl_example if BUILD_PQC_EXAMPLE -check_PROGRAMS += examples/pqc_openssl_example +noinst_PROGRAMS += examples/pqc_openssl_example examples_pqc_openssl_example_SOURCES = examples/pqc_openssl_example.c examples_pqc_openssl_example_LDADD = libwolfprov.la DISTCLEANFILES += examples/.libs/pqc_openssl_example diff --git a/examples/pqc_openssl_example.c b/examples/pqc_openssl_example.c index b49a4b88..28805280 100644 --- a/examples/pqc_openssl_example.c +++ b/examples/pqc_openssl_example.c @@ -26,8 +26,36 @@ #include #include +#include + #define WOLFPROV_NAME "libwolfprov" +#if defined(WP_HAVE_SLH_DSA_SHAKE_128F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-128f" +#elif defined(WP_HAVE_SLH_DSA_SHA2_128F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-128f" +#elif defined(WP_HAVE_SLH_DSA_SHAKE_128S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-128s" +#elif defined(WP_HAVE_SLH_DSA_SHA2_128S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-128s" +#elif defined(WP_HAVE_SLH_DSA_SHAKE_192F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-192f" +#elif defined(WP_HAVE_SLH_DSA_SHA2_192F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-192f" +#elif defined(WP_HAVE_SLH_DSA_SHAKE_192S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-192s" +#elif defined(WP_HAVE_SLH_DSA_SHA2_192S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-192s" +#elif defined(WP_HAVE_SLH_DSA_SHAKE_256F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-256f" +#elif defined(WP_HAVE_SLH_DSA_SHA2_256F) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-256f" +#elif defined(WP_HAVE_SLH_DSA_SHAKE_256S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHAKE-256s" +#elif defined(WP_HAVE_SLH_DSA_SHA2_256S) + #define EXAMPLE_SLH_DSA_NAME "SLH-DSA-SHA2-256s" +#endif + static EVP_PKEY* generate_key(OSSL_LIB_CTX* libCtx, const char* algorithm) { EVP_PKEY* key = NULL; @@ -116,7 +144,7 @@ static int run_mlkem(OSSL_LIB_CTX* libCtx) } #endif -#if defined(WOLFPROV_HAVE_MLDSA) || defined(WOLFPROV_HAVE_SLHDSA) +#if defined(WOLFPROV_HAVE_MLDSA) || defined(EXAMPLE_SLH_DSA_NAME) static int run_signature(OSSL_LIB_CTX* libCtx, const char* algorithm) { static const unsigned char message[] = @@ -207,14 +235,13 @@ int main(void) } #endif -#ifdef WOLFPROV_HAVE_SLHDSA - if ((rc == 0) && - (run_signature(libCtx, "SLH-DSA-SHA2-128f") != 0)) { - fprintf(stderr, "SLH-DSA-SHA2-128f signature failed\n"); +#ifdef EXAMPLE_SLH_DSA_NAME + if ((rc == 0) && (run_signature(libCtx, EXAMPLE_SLH_DSA_NAME) != 0)) { + fprintf(stderr, "%s signature failed\n", EXAMPLE_SLH_DSA_NAME); rc = 1; } if (rc == 0) { - printf("SLH-DSA-SHA2-128f signature passed\n"); + printf("%s signature passed\n", EXAMPLE_SLH_DSA_NAME); } #endif diff --git a/src/wp_slhdsa_kmgmt.c b/src/wp_slhdsa_kmgmt.c index 6e82890d..c508da19 100644 --- a/src/wp_slhdsa_kmgmt.c +++ b/src/wp_slhdsa_kmgmt.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -57,7 +58,8 @@ struct wp_SlhDsa { const wp_SlhDsaData* data; #ifndef WP_SINGLE_THREADED - wolfSSL_Mutex mutex; + wolfSSL_Mutex refMutex; + wolfSSL_Mutex keyMutex; #endif int refCnt; @@ -219,13 +221,13 @@ int wp_slhdsa_up_ref(wp_SlhDsa* slhdsa) WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_up_ref"); - rc = wc_LockMutex(&slhdsa->mutex); - if (rc < 0) { + rc = wc_LockMutex(&slhdsa->refMutex); + if (rc != 0) { ok = 0; } if (ok) { slhdsa->refCnt++; - wc_UnLockMutex(&slhdsa->mutex); + wc_UnLockMutex(&slhdsa->refMutex); } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -257,7 +259,7 @@ void* wp_slhdsa_get_key(wp_SlhDsa* slhdsa) wolfSSL_Mutex* wp_slhdsa_get_mutex(wp_SlhDsa* slhdsa) { #ifndef WP_SINGLE_THREADED - return &slhdsa->mutex; + return &slhdsa->keyMutex; #else (void)slhdsa; return NULL; @@ -335,12 +337,20 @@ static wp_SlhDsa* wp_slhdsa_new(WOLFPROV_CTX* provCtx, } #ifndef WP_SINGLE_THREADED if (ok) { - rc = wc_InitMutex(&slhdsa->mutex); + rc = wc_InitMutex(&slhdsa->refMutex); if (rc != 0) { wc_SlhDsaKey_Free(&slhdsa->key); ok = 0; } } + if (ok) { + rc = wc_InitMutex(&slhdsa->keyMutex); + if (rc != 0) { + wc_FreeMutex(&slhdsa->refMutex); + wc_SlhDsaKey_Free(&slhdsa->key); + ok = 0; + } + } #endif if (ok) { slhdsa->provCtx = provCtx; @@ -367,14 +377,14 @@ void wp_slhdsa_free(wp_SlhDsa* slhdsa) #ifndef WP_SINGLE_THREADED int rc; - rc = wc_LockMutex(&slhdsa->mutex); + rc = wc_LockMutex(&slhdsa->refMutex); if (rc == 0) { cnt = --slhdsa->refCnt; - wc_UnLockMutex(&slhdsa->mutex); + wc_UnLockMutex(&slhdsa->refMutex); } else { /* Cannot safely decrement without the lock; keep the object. */ - cnt = slhdsa->refCnt; + cnt = 1; } #else cnt = --slhdsa->refCnt; @@ -382,7 +392,8 @@ void wp_slhdsa_free(wp_SlhDsa* slhdsa) if (cnt == 0) { #ifndef WP_SINGLE_THREADED - wc_FreeMutex(&slhdsa->mutex); + wc_FreeMutex(&slhdsa->keyMutex); + wc_FreeMutex(&slhdsa->refMutex); #endif wc_SlhDsaKey_Free(&slhdsa->key); OPENSSL_free(slhdsa); @@ -407,18 +418,24 @@ static wp_SlhDsa* wp_slhdsa_dup(const wp_SlhDsa* src, int selection) word32 privAllocLen = 0; int rc; int ok = 1; + int locked = 0; int dupPub; int dupPriv; if (!wolfssl_prov_is_running() || (src == NULL)) { return NULL; } + if (wp_lock(wp_slhdsa_get_mutex((wp_SlhDsa*)src)) != 1) { + return NULL; + } + locked = 1; dupPub = ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && src->hasPub; dupPriv = ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) && src->hasPriv; dst = wp_slhdsa_new(src->provCtx, src->data); if (dst == NULL) { + wp_unlock(wp_slhdsa_get_mutex((wp_SlhDsa*)src)); return NULL; } @@ -482,6 +499,10 @@ static wp_SlhDsa* wp_slhdsa_dup(const wp_SlhDsa* src, int selection) (void)dupPriv; #endif + if (locked) { + wp_unlock(wp_slhdsa_get_mutex((wp_SlhDsa*)src)); + } + if (!ok) { wp_slhdsa_free(dst); return NULL; @@ -514,6 +535,7 @@ static const wp_SlhDsa* wp_slhdsa_load(const wp_SlhDsa** pSlhDsa, size_t size) static int wp_slhdsa_has(const wp_SlhDsa* slhdsa, int selection) { int ok = 1; + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_has"); @@ -523,12 +545,21 @@ static int wp_slhdsa_has(const wp_SlhDsa* slhdsa, int selection) if (ok && (slhdsa == NULL)) { ok = 0; } + if (ok && (wp_lock(wp_slhdsa_get_mutex((wp_SlhDsa*)slhdsa)) != 1)) { + ok = 0; + } + else if (ok) { + locked = 1; + } if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { ok &= slhdsa->hasPub; } if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { ok &= slhdsa->hasPriv; } + if (locked) { + wp_unlock(wp_slhdsa_get_mutex((wp_SlhDsa*)slhdsa)); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -553,6 +584,10 @@ static int wp_slhdsa_match(const wp_SlhDsa* a, const wp_SlhDsa* b, word32 allocA = 0; word32 allocB = 0; int checked = 0; + wp_SlhDsa* first; + wp_SlhDsa* second; + int firstLocked = 0; + int secondLocked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_match"); @@ -560,12 +595,33 @@ static int wp_slhdsa_match(const wp_SlhDsa* a, const wp_SlhDsa* b, WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - if (a->data->param != b->data->param) { - WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); - return 0; + if ((uintptr_t)a <= (uintptr_t)b) { + first = (wp_SlhDsa*)a; + second = (wp_SlhDsa*)b; + } + else { + first = (wp_SlhDsa*)b; + second = (wp_SlhDsa*)a; + } + if (wp_lock(wp_slhdsa_get_mutex(first)) != 1) { + ok = 0; + } + else { + firstLocked = 1; + } + if (ok && (second != first)) { + if (wp_lock(wp_slhdsa_get_mutex(second)) != 1) { + ok = 0; + } + else { + secondLocked = 1; + } + } + if (ok && (a->data->param != b->data->param)) { + ok = 0; } /* Presence mismatch fails; both-present compares; neither-present skips. */ - if (((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && (a->hasPub != b->hasPub)) { ok = 0; } @@ -645,6 +701,12 @@ static int wp_slhdsa_match(const wp_SlhDsa* a, const wp_SlhDsa* b, OSSL_KEYMGMT_SELECT_PRIVATE_KEY)) != 0)) { ok = 0; } + if (secondLocked) { + wp_unlock(wp_slhdsa_get_mutex(second)); + } + if (firstLocked) { + wp_unlock(wp_slhdsa_get_mutex(first)); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -666,6 +728,7 @@ static int wp_slhdsa_import(wp_SlhDsa* slhdsa, int selection, unsigned char* pubData = NULL; size_t privLen = 0; size_t pubLen = 0; + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_import"); @@ -675,6 +738,12 @@ static int wp_slhdsa_import(wp_SlhDsa* slhdsa, int selection, if (ok && ((selection & WP_SLHDSA_POSSIBLE_SELECTIONS) == 0)) { ok = 0; } + if (ok && (wp_lock(wp_slhdsa_get_mutex(slhdsa)) != 1)) { + ok = 0; + } + else if (ok) { + locked = 1; + } #ifdef WP_HAVE_SLHDSA_PRIVATE if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PRIV_KEY, @@ -694,10 +763,10 @@ static int wp_slhdsa_import(wp_SlhDsa* slhdsa, int selection, ok = 0; } if (ok) { - /* Unlike ML-DSA, the 4n FIPS 205 private key ends with the - * public key, so importing it yields both halves. */ - slhdsa->hasPriv = 1; - slhdsa->hasPub = 1; + slhdsa->hasPriv = + ((slhdsa->key.flags & WC_SLHDSA_FLAG_PRIVATE) != 0) ? 1 : 0; + slhdsa->hasPub = + ((slhdsa->key.flags & WC_SLHDSA_FLAG_PUBLIC) != 0) ? 1 : 0; } } } @@ -741,6 +810,9 @@ static int wp_slhdsa_import(wp_SlhDsa* slhdsa, int selection, slhdsa->hasPriv = 0; slhdsa->hasPub = 0; } + if (locked) { + wp_unlock(wp_slhdsa_get_mutex(slhdsa)); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -816,12 +888,19 @@ static int wp_slhdsa_export(wp_SlhDsa* slhdsa, int selection, word32 privAllocLen = 0; int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_export"); if (!wolfssl_prov_is_running() || (slhdsa == NULL)) { ok = 0; } + if (ok && (wp_lock(wp_slhdsa_get_mutex(slhdsa)) != 1)) { + ok = 0; + } + else if (ok) { + locked = 1; + } XMEMSET(params, 0, sizeof(params)); if (ok && expPub && slhdsa->hasPub) { @@ -864,12 +943,18 @@ static int wp_slhdsa_export(wp_SlhDsa* slhdsa, int selection, (void)expPriv; (void)privLen; #endif + if (ok && (paramsSz == 0) && (expPub || expPriv)) { + ok = 0; + } if (ok) { ok = paramCb(params, cbArg); } OPENSSL_free(pubBuf); /* Zero full allocation in case ExportPrivate truncated privLen. */ OPENSSL_clear_free(privBuf, privAllocLen); + if (locked) { + wp_unlock(wp_slhdsa_get_mutex(slhdsa)); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -909,6 +994,7 @@ static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) int ok = 1; int rc; OSSL_PARAM* p; + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_get_params"); @@ -916,6 +1002,12 @@ static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } + if (wp_lock(wp_slhdsa_get_mutex(slhdsa)) != 1) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + locked = 1; p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); if ((p != NULL) && @@ -948,7 +1040,7 @@ static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) if (p != NULL) { word32 outLen = slhdsa->data->pubKeySize; if (!slhdsa->hasPub) { - ok = 0; + p->return_size = 0; } else if (p->data == NULL) { /* Size query. */ @@ -979,7 +1071,7 @@ static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) if (p != NULL) { word32 outLen = slhdsa->data->privKeySize; if (!slhdsa->hasPriv) { - ok = 0; + p->return_size = 0; } else if (p->data == NULL) { p->return_size = outLen; @@ -1002,6 +1094,9 @@ static int wp_slhdsa_get_params(wp_SlhDsa* slhdsa, OSSL_PARAM params[]) } } #endif + if (locked) { + wp_unlock(wp_slhdsa_get_mutex(slhdsa)); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -1046,6 +1141,11 @@ static int wp_slhdsa_set_params(wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) static int wp_slhdsa_gen_set_params(wp_SlhDsaGenCtx* ctx, const OSSL_PARAM params[]); +static size_t wp_slhdsa_seed_size(const wp_SlhDsaData* data) +{ + return (size_t)(3 * (data->pubKeySize / 2)); +} + /** * Create SLH-DSA generation context object. * @@ -1067,11 +1167,15 @@ static wp_SlhDsaGenCtx* wp_slhdsa_gen_init_base(WOLFPROV_CTX* provCtx, if (ctx != NULL) { int rc; int ok = 1; + int rngInit = 0; rc = wc_InitRng(&ctx->rng); if (rc != 0) { ok = 0; } + else { + rngInit = 1; + } if (ok) { ctx->provCtx = provCtx; ctx->data = data; @@ -1084,7 +1188,9 @@ static wp_SlhDsaGenCtx* wp_slhdsa_gen_init_base(WOLFPROV_CTX* provCtx, } } if (!ok) { - wc_FreeRng(&ctx->rng); + if (rngInit) { + wc_FreeRng(&ctx->rng); + } OPENSSL_clear_free(ctx, sizeof(*ctx)); ctx = NULL; } @@ -1114,7 +1220,7 @@ static wp_SlhDsa* wp_slhdsa_gen(wp_SlhDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, int rc; word32 n = ctx->data->pubKeySize / 2; - if (ctx->seedLen == (size_t)(3 * n)) { + if (ctx->seedLen == wp_slhdsa_seed_size(ctx->data)) { /* Seed is SK.seed || SK.prf || PK.seed, n bytes each. */ rc = wc_SlhDsaKey_MakeKeyWithRandom(&slhdsa->key, ctx->seed, n, ctx->seed + n, n, ctx->seed + (2 * n), n); @@ -1161,13 +1267,10 @@ static int wp_slhdsa_gen_set_params(wp_SlhDsaGenCtx* ctx, void* vp = ctx->seed; ctx->seedLen = 0; if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->seed), - &ctx->seedLen)) { - WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); - return 0; - } - /* A short seed would silently fall back to RNG keygen, breaking the - * caller's reproducibility contract. Require exactly 3n. */ - if (ctx->seedLen != (size_t)((3 * ctx->data->pubKeySize) / 2)) { + &ctx->seedLen) || + (ctx->seedLen != wp_slhdsa_seed_size(ctx->data))) { + OPENSSL_cleanse(ctx->seed, sizeof(ctx->seed)); + ctx->seedLen = 0; WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -1660,6 +1763,7 @@ static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, PKCS8_PRIVATEKEY_TYPE; int private = (ctx->format == WP_ENC_FORMAT_PKI) || (ctx->format == WP_ENC_FORMAT_EPKI); + int locked = 0; WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_slhdsa_encode"); @@ -1672,6 +1776,12 @@ static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, if (ok && (out == NULL)) { ok = 0; } + if (ok && (wp_lock(wp_slhdsa_get_mutex((wp_SlhDsa*)slhdsa)) != 1)) { + ok = 0; + } + else if (ok) { + locked = 1; + } if (ok) { rc = ctx->encode((void*)&slhdsa->key, NULL, 0); @@ -1699,13 +1809,20 @@ static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, derLen = (size_t)rc; } } + if (locked) { + wp_unlock(wp_slhdsa_get_mutex((wp_SlhDsa*)slhdsa)); + locked = 0; + } /* By default the plaintext DER is the source for the output encoding. */ srcData = derData; srcLen = derLen; /* A cipher on a PrivateKeyInfo encoder selects the encrypted form. */ - if (ok && ((ctx->format == WP_ENC_FORMAT_EPKI) || - ((ctx->format == WP_ENC_FORMAT_PKI) && - (ctx->cipherName != NULL)))) { + 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. */ @@ -1775,6 +1892,9 @@ static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, OPENSSL_free(pemData); } OPENSSL_clear_free(encData, encLen); + if (locked) { + wp_unlock(wp_slhdsa_get_mutex((wp_SlhDsa*)slhdsa)); + } BIO_free(out); WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -1917,6 +2037,18 @@ static int wp_slhdsa_priv_encode(void* key, byte* output, word32 inLen) } #endif /* WP_HAVE_SLHDSA_PRIVATE */ +static void* wp_slhdsa_enc_import_object(wp_SlhDsaEncDecCtx* ctx, + int selection, const OSSL_PARAM params[]) +{ + wp_SlhDsa* slhdsa = ctx->newKey(ctx->provCtx); + + if ((slhdsa != NULL) && !wp_slhdsa_import(slhdsa, selection, params)) { + wp_slhdsa_free(slhdsa); + slhdsa = NULL; + } + return slhdsa; +} + /* * Per-parameter-set encoder/decoder context constructors and dispatch tables. */ @@ -1975,20 +2107,19 @@ const OSSL_DISPATCH wp_##alg##_##fmt##_##enc##_encoder_functions[] = { \ (DFUNC)wp_slhdsa_enc_dec_set_ctx_params }, \ { OSSL_FUNC_ENCODER_DOES_SELECTION, (DFUNC)dsel }, \ { OSSL_FUNC_ENCODER_ENCODE, (DFUNC)wp_slhdsa_encode }, \ - { OSSL_FUNC_ENCODER_IMPORT_OBJECT, (DFUNC)wp_slhdsa_import }, \ + { OSSL_FUNC_ENCODER_IMPORT_OBJECT, \ + (DFUNC)wp_slhdsa_enc_import_object }, \ { OSSL_FUNC_ENCODER_FREE_OBJECT, (DFUNC)wp_slhdsa_free }, \ { 0, NULL } \ }; /* Encode-function selectors for the table macro, mapping the lower-case token * pasted into each table name onto its format/encoding enum value. */ -#ifndef WP_ENC_FORMAT_spki_VAL #define WP_ENC_FORMAT_spki_VAL WP_ENC_FORMAT_SPKI #define WP_ENC_FORMAT_pki_VAL WP_ENC_FORMAT_PKI #define WP_ENC_FORMAT_epki_VAL WP_ENC_FORMAT_EPKI #define WP_FORMAT_der_VAL WP_FORMAT_DER #define WP_FORMAT_pem_VAL WP_FORMAT_PEM -#endif #define WP_SLHDSA_ENC_spki_ENCODE wp_slhdsa_pub_encode #ifdef WP_HAVE_SLHDSA_PRIVATE #define WP_SLHDSA_ENC_pki_ENCODE wp_slhdsa_priv_encode diff --git a/src/wp_slhdsa_sig.c b/src/wp_slhdsa_sig.c index 25e35dd3..9bc70738 100644 --- a/src/wp_slhdsa_sig.c +++ b/src/wp_slhdsa_sig.c @@ -282,9 +282,12 @@ static int wp_slhdsa_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, ctx->slhdsa = slhdsa; } wp_slhdsa_buf_reset(ctx); - /* Pure mode is the newctx default and persists across a re-init, as do the - * context string, deterministic flag and test entropy. Resetting the - * encoding here would silently change what a raw-mode caller signs. */ + wc_ForceZero(ctx->context, sizeof(ctx->context)); + ctx->contextLen = 0; + wc_ForceZero(ctx->testEntropy, sizeof(ctx->testEntropy)); + ctx->testEntropyLen = 0; + ctx->deterministic = 0; + ctx->rawMsg = 0; if (!wp_slhdsa_set_ctx_params(ctx, params)) { WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; @@ -336,6 +339,7 @@ static int wp_slhdsa_sign(wp_SlhDsaSigCtx* ctx, unsigned char* sig, { int ok = 1; int rc; + int locked = 0; word32 sigSz; /* FIPS 205 permits an empty message; give wolfSSL a valid pointer so a * NULL+0 message does not become a backend-dependent NULL deref. */ @@ -391,6 +395,9 @@ static int wp_slhdsa_sign(wp_SlhDsaSigCtx* ctx, unsigned char* sig, if (wp_lock(wp_slhdsa_get_mutex(ctx->slhdsa)) != 1) { ok = 0; } + else { + locked = 1; + } if (ok && ctx->rawMsg) { /* FIPS 205 internal interface: the message already is M', so the * context string is part of it and must not be applied again. */ @@ -427,7 +434,7 @@ static int wp_slhdsa_sign(wp_SlhDsaSigCtx* ctx, unsigned char* sig, rc = wc_SlhDsaKey_Sign(key, ctx->context, (byte)ctx->contextLen, m, (word32)msgLen, sig, &outLen, &ctx->rng); } - if (ok) { + if (locked) { wp_unlock(wp_slhdsa_get_mutex(ctx->slhdsa)); } if (ok && (rc != 0)) { @@ -457,6 +464,7 @@ static int wp_slhdsa_verify(wp_SlhDsaSigCtx* ctx, const unsigned char* sig, { int ok = 1; int rc; + int locked = 0; /* FIPS 205 permits an empty message; give wolfSSL a valid pointer. */ unsigned char dummy = 0; const unsigned char* m = msg; @@ -487,6 +495,9 @@ static int wp_slhdsa_verify(wp_SlhDsaSigCtx* ctx, const unsigned char* sig, if (wp_lock(wp_slhdsa_get_mutex(ctx->slhdsa)) != 1) { ok = 0; } + else { + locked = 1; + } if (ok && ctx->rawMsg) { rc = wc_SlhDsaKey_VerifyMsg((SlhDsaKey*)wp_slhdsa_get_key(ctx->slhdsa), m, (word32)msgLen, sig, (word32)sigLen); @@ -496,7 +507,7 @@ static int wp_slhdsa_verify(wp_SlhDsaSigCtx* ctx, const unsigned char* sig, ctx->context, (byte)ctx->contextLen, m, (word32)msgLen, sig, (word32)sigLen); } - if (ok) { + if (locked) { wp_unlock(wp_slhdsa_get_mutex(ctx->slhdsa)); } if (ok && (rc != 0)) { @@ -597,8 +608,6 @@ static int wp_slhdsa_digest_verify_final(wp_SlhDsaSigCtx* ctx, return ok; } -#if defined(OPENSSL_VERSION_NUMBER) && \ - (OPENSSL_VERSION_NUMBER >= 0x30500000L) /* OpenSSL 3.5+ signature message API. */ static int wp_slhdsa_message_init(wp_SlhDsaSigCtx* ctx, wp_SlhDsa* slhdsa, const OSSL_PARAM params[]) @@ -638,7 +647,6 @@ static int wp_slhdsa_verify_message_final(wp_SlhDsaSigCtx* ctx) return wp_slhdsa_digest_verify_final(ctx, ctx->verifySig, ctx->verifySigLen); } -#endif /* OPENSSL_VERSION_NUMBER >= 0x30500000L */ /* DER AlgorithmIdentifier (SEQUENCE { OID }) for each SLH-DSA parameter set. * FIPS 205 signature algorithms carry no parameters, so the encoding differs @@ -821,7 +829,6 @@ static int wp_slhdsa_set_ctx_params(wp_SlhDsaSigCtx* ctx, } } } -#if OPENSSL_VERSION_NUMBER >= 0x30500000L if (ok) { p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_SIGNATURE); if (p != NULL) { @@ -838,7 +845,6 @@ static int wp_slhdsa_set_ctx_params(wp_SlhDsaSigCtx* ctx, } } } -#endif WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -851,9 +857,7 @@ static const OSSL_PARAM* wp_slhdsa_settable_ctx_params(wp_SlhDsaSigCtx* ctx, OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_TEST_ENTROPY, NULL, 0), OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_DETERMINISTIC, NULL), OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING, NULL), -#if OPENSSL_VERSION_NUMBER >= 0x30500000L OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_SIGNATURE, NULL, 0), -#endif OSSL_PARAM_END }; (void)ctx; @@ -879,7 +883,6 @@ static const OSSL_PARAM* wp_slhdsa_settable_ctx_params(wp_SlhDsaSigCtx* ctx, #define WP_SLHDSA_SIGN_DISPATCH #endif -#if OPENSSL_VERSION_NUMBER >= 0x30500000L #ifdef WP_HAVE_SLHDSA_PRIVATE #define WP_SLHDSA_SIGN_MESSAGE_DISPATCH \ { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_INIT, \ @@ -898,10 +901,6 @@ static const OSSL_PARAM* wp_slhdsa_settable_ctx_params(wp_SlhDsaSigCtx* ctx, (DFUNC)wp_slhdsa_digest_signverify_update }, \ { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_FINAL, \ (DFUNC)wp_slhdsa_verify_message_final }, -#else -#define WP_SLHDSA_SIGN_MESSAGE_DISPATCH -#define WP_SLHDSA_VERIFY_MESSAGE_DISPATCH -#endif /* Dispatch table shared by all SLH-DSA parameter sets. */ const OSSL_DISPATCH wp_slhdsa_signature_functions[] = { diff --git a/test/test_slhdsa.c b/test/test_slhdsa.c index 15c0fff8..72fee442 100644 --- a/test/test_slhdsa.c +++ b/test/test_slhdsa.c @@ -949,7 +949,14 @@ int test_slhdsa_reinit_null_key(void* data) EVP_PKEY* key = NULL; EVP_MD_CTX* mdctx = NULL; unsigned char* sig = NULL; + unsigned char* sig2 = NULL; size_t sigLen = 0; + size_t sig2Len = 0; + unsigned char entropy[32]; + OSSL_PARAM params[5]; + int detOn = 1; + int encRaw = 0; + size_t n; (void)data; @@ -961,10 +968,21 @@ int test_slhdsa_reinit_null_key(void* data) err = (mdctx == NULL); } if (err == 0) { + n = (size_t)(slhdsa_sets[0].pubKeySize / 2); + XMEMSET(entropy, 0xA5, sizeof(entropy)); + params[0] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_CONTEXT_STRING, (void*)"ctx-A", 5); + params[1] = OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_TEST_ENTROPY, entropy, n); + params[2] = OSSL_PARAM_construct_int( + OSSL_SIGNATURE_PARAM_DETERMINISTIC, &detOn); + params[3] = OSSL_PARAM_construct_int( + OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING, &encRaw); + params[4] = OSSL_PARAM_construct_end(); err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, key, - NULL) != 1; + params) != 1; } - /* Second init with a NULL key must reuse the first key. */ + /* Re-init must reuse the key and reset operation-specific parameters. */ if (err == 0) { err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, NULL, NULL) != 1; @@ -985,8 +1003,31 @@ int test_slhdsa_reinit_null_key(void* data) err = !slhdsa_verify_msg(key, slhdsa_test_msg, SLHDSA_TEST_MSG_LEN, sig, sigLen); } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, NULL, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sig2Len, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + sig2 = (unsigned char*)OPENSSL_malloc(sig2Len); + err = (sig2 == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig2, &sig2Len, slhdsa_test_msg, + SLHDSA_TEST_MSG_LEN) != 1; + } + if (err == 0) { + err = (sigLen == sig2Len) && (XMEMCMP(sig, sig2, sigLen) == 0); + if (err) { + PRINT_ERR_MSG("Reinit retained deterministic signature state"); + } + } OPENSSL_free(sig); + OPENSSL_free(sig2); EVP_MD_CTX_free(mdctx); EVP_PKEY_free(key); return err; @@ -1136,7 +1177,7 @@ int test_slhdsa_sig_params(void* data) OSSL_PARAM detParams[2]; OSSL_PARAM entParams[2]; OSSL_PARAM rawParams[2]; - unsigned int detOn = 1; + int detOn = 1; int encRaw = 0; size_t n; @@ -1171,7 +1212,7 @@ int test_slhdsa_sig_params(void* data) /* Deterministic: same key and message must give byte-identical output. */ if (err == 0) { PRINT_MSG("Deterministic signing is reproducible"); - detParams[0] = OSSL_PARAM_construct_uint( + detParams[0] = OSSL_PARAM_construct_int( OSSL_SIGNATURE_PARAM_DETERMINISTIC, &detOn); detParams[1] = OSSL_PARAM_construct_end(); err = slhdsa_sign_with(key, detParams, &sigA, &lenA); @@ -1314,6 +1355,46 @@ int test_slhdsa_keygen_seed(void* data) } } + EVP_PKEY_free(keyA); keyA = NULL; + EVP_PKEY_free(keyB); keyB = NULL; + + if (err == 0) { + PRINT_MSG("Rejected seed does not persist in keygen context"); + params[0] = OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_SLH_DSA_SEED, NULL, seedLen); + } + for (i = 0; (err == 0) && (i < 2); i++) { + EVP_PKEY** out = (i == 0) ? &keyA : &keyB; + + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, slhdsa_sets[0].name, NULL); + err = (ctx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_CTX_set_params(ctx, params) == 1; + } + if (err == 0) { + err = EVP_PKEY_keygen(ctx, out) != 1; + } + EVP_PKEY_CTX_free(ctx); ctx = NULL; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(keyA, OSSL_PKEY_PARAM_PUB_KEY, + pubA, sizeof(pubA), &pubALen) != 1; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(keyB, OSSL_PKEY_PARAM_PUB_KEY, + pubB, sizeof(pubB), &pubBLen) != 1; + } + if (err == 0) { + err = (pubALen == pubBLen) && + (XMEMCMP(pubA, pubB, pubALen) == 0); + if (err) { + PRINT_ERR_MSG("Rejected seed produced deterministic keys"); + } + } + EVP_PKEY_free(keyA); EVP_PKEY_free(keyB); return err; @@ -1427,4 +1508,4 @@ int test_slhdsa_encode_epki(void* data) } #endif /* WP_HAVE_EPKI_TEST */ -#endif /* WP_HAVE_SLHDSA && WP_HAVE_SLHDSA_PRIVATE */ +#endif /* WP_HAVE_SLHDSA && WP_HAVE_SLHDSA_PRIVATE && WP_SLHDSA_TEST_SETS */ From 1bb69c24ade7c0909765fb2bbf318e009a66bb91 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 31 Jul 2026 17:31:02 -0700 Subject: [PATCH 12/15] Tighten SLH-DSA decode and signature validation --- src/wp_slhdsa_kmgmt.c | 5 +++-- src/wp_slhdsa_sig.c | 4 +++- test/test_pkey.c | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wp_slhdsa_kmgmt.c b/src/wp_slhdsa_kmgmt.c index c508da19..22077741 100644 --- a/src/wp_slhdsa_kmgmt.c +++ b/src/wp_slhdsa_kmgmt.c @@ -1613,7 +1613,8 @@ static int wp_slhdsa_decode_enc_pki(wp_SlhDsaEncDecCtx* ctx, wp_SlhDsa* slhdsa, ok = 0; } /* Decode the recovered plaintext private key. */ - if (ok && (ctx->decode(data, &idx, (void*)&slhdsa->key, len) != 0)) { + if (ok && ((ctx->decode(data, &idx, (void*)&slhdsa->key, len) != 0) || + (idx != len))) { ok = 0; } @@ -1673,7 +1674,7 @@ static int wp_slhdsa_decode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } if (ok) { rc = ctx->decode(data, &idx, (void*)&slhdsa->key, len); - if (rc != 0) { + if ((rc != 0) || (idx != len)) { #if defined(WP_HAVE_PKCS8_ENC) && defined(WP_HAVE_SLHDSA_PRIVATE) /* May be an encrypted PKCS#8 key - decrypt and retry. */ if ((ctx->format != WP_ENC_FORMAT_PKI) || diff --git a/src/wp_slhdsa_sig.c b/src/wp_slhdsa_sig.c index 9bc70738..cfae6371 100644 --- a/src/wp_slhdsa_sig.c +++ b/src/wp_slhdsa_sig.c @@ -835,7 +835,9 @@ static int wp_slhdsa_set_ctx_params(wp_SlhDsaSigCtx* ctx, unsigned char* sig = NULL; size_t sigLen = 0; - if (!OSSL_PARAM_get_octet_string(p, (void**)&sig, 0, &sigLen)) { + /* Reject malformed signatures before allocating a copy. */ + if ((p->data_size != (size_t)wp_slhdsa_get_sig_size(ctx->slhdsa)) || + !OSSL_PARAM_get_octet_string(p, (void**)&sig, 0, &sigLen)) { ok = 0; } if (ok) { diff --git a/test/test_pkey.c b/test/test_pkey.c index fb18dd65..66ea661b 100644 --- a/test/test_pkey.c +++ b/test/test_pkey.c @@ -59,6 +59,7 @@ int test_digest_sign(EVP_PKEY *pkey, OSSL_LIB_CTX* libCtx, unsigned char *data, return err; } + int test_digest_verify(EVP_PKEY *pkey, OSSL_LIB_CTX* libCtx, unsigned char *data, size_t len, const char *md, const EVP_MD *mgf1Md, unsigned char *sig, size_t sigLen, int padMode, int saltlen) @@ -401,8 +402,7 @@ int test_pki_cipher_encrypts(EVP_PKEY* pkey, const char* fmt, (const unsigned char*)badPass, strlen(badPass)) != 1; } if (err == 0) { - if ((OSSL_DECODER_from_data(bctx, &pp, &dataLen) == 1) && - (badKey != NULL)) { + if (OSSL_DECODER_from_data(bctx, &pp, &dataLen) == 1) { PRINT_ERR_MSG("Wrong passphrase recovered the key"); err = 1; } @@ -510,4 +510,3 @@ int test_epki_encode_decode(EVP_PKEY* pkey, const char* fmt, return err; } - From de5ab7bdf42cbdbaca78c5f34c2ad52d97577808 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 1 Aug 2026 00:54:47 -0700 Subject: [PATCH 13/15] Address SLH-DSA Skoll findings --- examples/openssl_example.c | 15 +++++++++++++-- examples/pqc_openssl_example.c | 5 +++-- src/wp_slhdsa_kmgmt.c | 8 +++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/openssl_example.c b/examples/openssl_example.c index 4c31f69a..b53ba18b 100644 --- a/examples/openssl_example.c +++ b/examples/openssl_example.c @@ -19,6 +19,8 @@ */ #include +#include +#include #include #include @@ -26,7 +28,7 @@ #define WOLFPROV_NAME "libwolfprov" -int main(void) +int main(int argc, char** argv) { int rc = 0; const char* buildInfo = NULL; @@ -36,6 +38,8 @@ int main(void) OSSL_PARAM_utf8_ptr("buildinfo", (char**)&buildInfo, 0), OSSL_PARAM_END }; + char executable[PATH_MAX]; + char providerPath[PATH_MAX]; libCtx = OSSL_LIB_CTX_new(); if (libCtx == NULL) { @@ -43,7 +47,14 @@ int main(void) } if (rc == 0) { - OSSL_PROVIDER_set_default_search_path(libCtx, ".libs"); + if ((argc < 1) || (realpath(argv[0], executable) == NULL) || + (snprintf(providerPath, sizeof(providerPath), "%s/../.libs", + executable) >= (int)sizeof(providerPath))) { + rc = 1; + } + } + if (rc == 0) { + OSSL_PROVIDER_set_default_search_path(libCtx, providerPath); wolfProv = OSSL_PROVIDER_load(libCtx, WOLFPROV_NAME); if ((wolfProv == NULL) || (OSSL_PROVIDER_get_params(wolfProv, request) != 1)) { diff --git a/examples/pqc_openssl_example.c b/examples/pqc_openssl_example.c index 28805280..0bf890e1 100644 --- a/examples/pqc_openssl_example.c +++ b/examples/pqc_openssl_example.c @@ -144,7 +144,8 @@ static int run_mlkem(OSSL_LIB_CTX* libCtx) } #endif -#if defined(WOLFPROV_HAVE_MLDSA) || defined(EXAMPLE_SLH_DSA_NAME) +#if defined(WOLFPROV_HAVE_MLDSA) || \ + (defined(WOLFPROV_HAVE_SLHDSA) && defined(EXAMPLE_SLH_DSA_NAME)) static int run_signature(OSSL_LIB_CTX* libCtx, const char* algorithm) { static const unsigned char message[] = @@ -235,7 +236,7 @@ int main(void) } #endif -#ifdef EXAMPLE_SLH_DSA_NAME +#if defined(WOLFPROV_HAVE_SLHDSA) && defined(EXAMPLE_SLH_DSA_NAME) if ((rc == 0) && (run_signature(libCtx, EXAMPLE_SLH_DSA_NAME) != 0)) { fprintf(stderr, "%s signature failed\n", EXAMPLE_SLH_DSA_NAME); rc = 1; diff --git a/src/wp_slhdsa_kmgmt.c b/src/wp_slhdsa_kmgmt.c index 22077741..d73588b3 100644 --- a/src/wp_slhdsa_kmgmt.c +++ b/src/wp_slhdsa_kmgmt.c @@ -1609,7 +1609,8 @@ static int wp_slhdsa_decode_enc_pki(wp_SlhDsaEncDecCtx* ctx, wp_SlhDsa* slhdsa, ok = 0; } /* Decrypt the PBES2 EncryptedPrivateKeyInfo in place. */ - if (ok && (!wp_decrypt_key_pkcs8(data, &len, pwCb, pwCbArg))) { + if (ok && (pwCb == NULL || + !wp_decrypt_key_pkcs8(data, &len, pwCb, pwCbArg))) { ok = 0; } /* Decode the recovered plaintext private key. */ @@ -1837,8 +1838,9 @@ static int wp_slhdsa_encode(wp_SlhDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, ok = 0; } } - if (ok && (!wp_encrypt_key_pkcs8(ctx->provCtx, ctx->cipher, derData, - (word32)derLen, encData, &encLen, pwCb, pwCbArg))) { + if (ok && ((pwCb == NULL) || + !wp_encrypt_key_pkcs8(ctx->provCtx, ctx->cipher, derData, + (word32)derLen, encData, &encLen, pwCb, pwCbArg))) { ok = 0; } if (ok) { From bf66a73625fcf7087babcd5849afb0786c155f8c Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 1 Aug 2026 01:21:26 -0700 Subject: [PATCH 14/15] Fix provider example libtool path resolution --- examples/openssl_example.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/openssl_example.c b/examples/openssl_example.c index b53ba18b..4bf17ce5 100644 --- a/examples/openssl_example.c +++ b/examples/openssl_example.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -48,10 +49,18 @@ int main(int argc, char** argv) if (rc == 0) { if ((argc < 1) || (realpath(argv[0], executable) == NULL) || - (snprintf(providerPath, sizeof(providerPath), "%s/../.libs", - executable) >= (int)sizeof(providerPath))) { + (snprintf(providerPath, sizeof(providerPath), + "%s/../../.libs", executable) >= + (int)sizeof(providerPath))) { rc = 1; } + if ((rc == 0) && (access(providerPath, R_OK | X_OK) != 0)) { + if (snprintf(providerPath, sizeof(providerPath), "%s/../.libs", + executable) >= (int)sizeof(providerPath) || + access(providerPath, R_OK | X_OK) != 0) { + rc = 1; + } + } } if (rc == 0) { OSSL_PROVIDER_set_default_search_path(libCtx, providerPath); From 1bd7734796c05cbadfc93e678e097abc8aff7d9f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 1 Aug 2026 01:23:52 -0700 Subject: [PATCH 15/15] Resolve provider path from example directory --- examples/openssl_example.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/openssl_example.c b/examples/openssl_example.c index 4bf17ce5..1f9738cc 100644 --- a/examples/openssl_example.c +++ b/examples/openssl_example.c @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -41,6 +42,7 @@ int main(int argc, char** argv) }; char executable[PATH_MAX]; char providerPath[PATH_MAX]; + char* executableDir; libCtx = OSSL_LIB_CTX_new(); if (libCtx == NULL) { @@ -49,14 +51,15 @@ int main(int argc, char** argv) if (rc == 0) { if ((argc < 1) || (realpath(argv[0], executable) == NULL) || + ((executableDir = dirname(executable)) == NULL) || (snprintf(providerPath, sizeof(providerPath), - "%s/../../.libs", executable) >= + "%s/../../.libs", executableDir) >= (int)sizeof(providerPath))) { rc = 1; } if ((rc == 0) && (access(providerPath, R_OK | X_OK) != 0)) { if (snprintf(providerPath, sizeof(providerPath), "%s/../.libs", - executable) >= (int)sizeof(providerPath) || + executableDir) >= (int)sizeof(providerPath) || access(providerPath, R_OK | X_OK) != 0) { rc = 1; }