diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 053756d8..2ce49a79 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -236,6 +236,7 @@ int wp_ecx_up_ref(wp_Ecx* ecx); void wp_ecx_free(wp_Ecx* ecx); void* wp_ecx_get_key(wp_Ecx* ecx); wolfSSL_Mutex* wp_ecx_get_mutex(wp_Ecx* ecx); +int wp_ecx_ensure_pub(wp_Ecx* ecx); /* Internal ML-KEM types and functions. */ typedef struct wp_MlKem wp_MlKem; diff --git a/src/wp_ecx_kmgmt.c b/src/wp_ecx_kmgmt.c index f1993af1..e6b0a30a 100644 --- a/src/wp_ecx_kmgmt.c +++ b/src/wp_ecx_kmgmt.c @@ -85,6 +85,8 @@ typedef int (*WP_ECX_IMPORT_PRIV)(const byte* priv, word32 privLen, void* key, int endian); /** Type for function that exports a private key from a wolfSSL key. */ typedef int (*WP_ECX_EXPORT_PRIV)(void* key, const byte* out, word32* outLen); +/** Type for function that derives the public key from the private key. */ +typedef int (*WP_ECX_DERIVE_PUB)(void* key); /** Type for a wolfSSL function that checks a public key. */ typedef int (*WP_ECX_CHECK_PUB)(const byte* pub, word32 pubLen, int endian); /** Type for a wolfSSL function that checks a key. */ @@ -117,6 +119,8 @@ typedef struct wp_EcxData { WP_ECX_IMPORT_PRIV importPriv; /** Export private key from wolfSSL key object. */ WP_ECX_EXPORT_PRIV exportPriv; + /** Derive the public key from the private key. NULL when not needed. */ + WP_ECX_DERIVE_PUB derivePub; /** wolfSSL check of public key value. */ WP_ECX_CHECK_PUB checkPub; /** wolfSSL check of public/private key. */ @@ -258,6 +262,48 @@ wolfSSL_Mutex* wp_ecx_get_mutex(wp_Ecx* ecx) return &ecx->mutex; } +/** + * Make sure the public key is available in the wolfSSL key object. + * + * Ed25519 and Ed448 keys imported as a private key only have no public half + * until it is derived. Deriving writes into the wolfSSL key object, so the + * key mutex is held: threads that first use one shared key at the same time + * would otherwise sign or export with a partly written public key. + * + * Must not be called with the key mutex already held. + * + * @param [in, out] ecx ECX key object. + * @return 1 on success. + * @return 0 on failure. + */ +int wp_ecx_ensure_pub(wp_Ecx* ecx) +{ + int ok = 1; + + if ((ecx != NULL) && (ecx->data->derivePub != NULL)) { + int rc; + + #ifndef WP_SINGLE_THREADED + if (wp_lock(&ecx->mutex) != 1) { + ok = 0; + } + if (ok) { + #endif + rc = (*ecx->data->derivePub)((void*)&ecx->key); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "derivePub", + rc); + ok = 0; + } + #ifndef WP_SINGLE_THREADED + wp_unlock(&ecx->mutex); + } + #endif + } + + return ok; +} + /** * Create a new ECX key object. Base function. * @@ -364,6 +410,13 @@ static wp_Ecx* wp_ecx_dup(const wp_Ecx* src, int selection) /* Create a new ecx object. */ dst = wp_ecx_new(src->provCtx, src->data); } + /* Cache the public half before the copy below: deriving it later writes + * into the source key object that is being read here. */ + if ((dst != NULL) && src->hasPub && + (!wp_ecx_ensure_pub((wp_Ecx*)src))) { + wp_ecx_free(dst); + dst = NULL; + } if (dst != NULL) { dst->includePublic = src->includePublic; @@ -552,9 +605,18 @@ static int wp_ecx_get_params_enc_pub_key(wp_Ecx* ecx, OSSL_PARAM params[], if (p->data == NULL) { outLen = ecx->data->len; } + else if (!wp_ecx_ensure_pub(ecx)) { + ok = 0; + } + else if (wp_lock(wp_ecx_get_mutex(ecx)) != 1) { + ok = 0; + } else { + /* Signing mutates the key (persistent SHA), so hold the key mutex + * while reading it. */ int rc = (*ecx->data->exportPub)((void*)&ecx->key, p->data, &outLen, ECX_LITTLE_ENDIAN); + wp_unlock(wp_ecx_get_mutex(ecx)); if (rc != 0) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); ok = 0; @@ -775,22 +837,40 @@ static int wp_ecx_match_pub_key(const wp_Ecx* ecx1, const wp_Ecx* ecx2) XMEMSET(key2, 0, sizeof(key2)); ok &= ecx1->hasPub && ecx2->hasPub; if (ok) { + ok = wp_ecx_ensure_pub((wp_Ecx*)ecx1) && + wp_ecx_ensure_pub((wp_Ecx*)ecx2); + } + if (ok) { + /* Signing mutates the key (persistent SHA), so hold each key mutex + * while reading it. Lock one key at a time to avoid lock ordering. */ len1 = ecx1->data->len; - rc = (*ecx1->data->exportPub)((void*)&ecx1->key, key1, &len1, - ECX_LITTLE_ENDIAN); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); + if (wp_lock(wp_ecx_get_mutex((wp_Ecx*)ecx1)) != 1) { ok = 0; } + else { + rc = (*ecx1->data->exportPub)((void*)&ecx1->key, key1, &len1, + ECX_LITTLE_ENDIAN); + wp_unlock(wp_ecx_get_mutex((wp_Ecx*)ecx1)); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); + ok = 0; + } + } } if (ok) { len2 = ecx2->data->len; - rc = (*ecx2->data->exportPub)((void*)&ecx2->key, key2, &len2, - ECX_LITTLE_ENDIAN); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); + if (wp_lock(wp_ecx_get_mutex((wp_Ecx*)ecx2)) != 1) { ok = 0; } + else { + rc = (*ecx2->data->exportPub)((void*)&ecx2->key, key2, &len2, + ECX_LITTLE_ENDIAN); + wp_unlock(wp_ecx_get_mutex((wp_Ecx*)ecx2)); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); + ok = 0; + } + } } if (ok && (len1 != len2)) { ok = 0; @@ -1126,16 +1206,30 @@ static int wp_ecx_export_keypair(wp_Ecx* ecx, OSSL_PARAM* params, int* pIdx, int ok = 1; int rc; int i = *pIdx; + int locked = 0; word32 outLen; WOLFPROV_ENTER(WP_LOG_COMP_KE, "wp_ecx_export_keypair"); - outLen = ecx->data->len; - rc = (*ecx->data->exportPub)((void*)&ecx->key, data + *idx, &outLen, - ECX_LITTLE_ENDIAN); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); - ok = 0; + ok = wp_ecx_ensure_pub(ecx); + if (ok) { + /* Signing mutates the key (persistent SHA), so hold the key mutex + * while reading it. */ + if (wp_lock(wp_ecx_get_mutex(ecx)) != 1) { + ok = 0; + } + else { + locked = 1; + } + } + if (ok) { + outLen = ecx->data->len; + rc = (*ecx->data->exportPub)((void*)&ecx->key, data + *idx, &outLen, + ECX_LITTLE_ENDIAN); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "exportPub", rc); + ok = 0; + } } if (ok) { wp_param_set_octet_string_ptr(¶ms[i++], OSSL_PKEY_PARAM_PUB_KEY, @@ -1162,6 +1256,9 @@ static int wp_ecx_export_keypair(wp_Ecx* ecx, OSSL_PARAM* params, int* pIdx, *idx += outLen; } } + if (locked) { + wp_unlock(wp_ecx_get_mutex(ecx)); + } *pIdx = i; WOLFPROV_LEAVE(WP_LOG_COMP_KE, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -1483,6 +1580,7 @@ static const wp_EcxData x25519Data = { (WP_ECX_EXPORT_PUB)&wc_curve25519_export_public_ex, (WP_ECX_IMPORT_PRIV)&wc_curve25519_import_private_ex, (WP_ECX_EXPORT_PRIV)&wp_curve25519_export_private_raw, + NULL, (WP_ECX_CHECK_PUB)&wc_curve25519_check_public, NULL, }; @@ -1565,6 +1663,7 @@ static const wp_EcxData x448Data = { (WP_ECX_EXPORT_PUB)&wc_curve448_export_public_ex, (WP_ECX_IMPORT_PRIV)&wc_curve448_import_private_ex, (WP_ECX_EXPORT_PRIV)&wp_curve448_export_private_raw, + NULL, (WP_ECX_CHECK_PUB)&wc_curve448_check_public, NULL, }; @@ -1650,20 +1749,33 @@ static int wp_ed25519_import_public(const byte* in, word32 inLen, static int wp_ed25519_export_public(ed25519_key* key, const byte* out, word32* outLen, int endian) { - int ret; - (void)endian; + return wc_ed25519_export_public(key, (byte*)out, outLen); +} + +/** + * Derive the Ed25519 public key from the private key and cache it. + * + * A private-only key (a seed-only PKCS#8) has no public half until it is + * derived. Callers must hold the key mutex - see wp_ecx_ensure_pub(). + * + * @param [in, out] key wolfSSL Ed25519 key object. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp_ed25519_derive_public(ed25519_key* key) +{ + int ret = 0; + byte pub[ED25519_PUB_KEY_SIZE]; + if (!key->pubKeySet) { - ret = wc_ed25519_make_public(key, (byte*)out, *outLen); + ret = wc_ed25519_make_public(key, pub, sizeof(pub)); if (ret == 0) { - /* Store the generated public key in the key object for future use. */ - ret = wc_ed25519_import_public((byte*)out, *outLen, key); + /* Only import_public stores the value and sets pubKeySet. */ + ret = wc_ed25519_import_public(pub, sizeof(pub), key); } } - else { - ret = wc_ed25519_export_public(key, (byte*)out, outLen); - } return ret; } @@ -1698,6 +1810,7 @@ static const wp_EcxData ed25519Data = { (WP_ECX_EXPORT_PUB)&wp_ed25519_export_public, (WP_ECX_IMPORT_PRIV)&wp_ed25519_import_private, (WP_ECX_EXPORT_PRIV)&wc_ed25519_export_private_only, + (WP_ECX_DERIVE_PUB)&wp_ed25519_derive_public, NULL, (WP_ECX_CHECK_KEY)&wc_ed25519_check_key, }; @@ -1784,20 +1897,33 @@ static int wp_ed448_import_public(const byte* in, word32 inLen, ed448_key* key, static int wp_ed448_export_public(ed448_key* key, const byte* out, word32* outLen, int endian) { - int ret; - (void)endian; + return wc_ed448_export_public(key, (byte*)out, outLen); +} + +/** + * Derive the Ed448 public key from the private key and cache it. + * + * A private-only key (a seed-only PKCS#8) has no public half until it is + * derived. Callers must hold the key mutex - see wp_ecx_ensure_pub(). + * + * @param [in, out] key wolfSSL Ed448 key object. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp_ed448_derive_public(ed448_key* key) +{ + int ret = 0; + byte pub[ED448_PUB_KEY_SIZE]; + if (!key->pubKeySet) { - ret = wc_ed448_make_public(key, (byte*)out, *outLen); + ret = wc_ed448_make_public(key, pub, sizeof(pub)); if (ret == 0) { - /* Store the generated public key in the key object for future use. */ - ret = wc_ed448_import_public((byte*)out, *outLen, key); + /* Only import_public stores the value and sets pubKeySet. */ + ret = wc_ed448_import_public(pub, sizeof(pub), key); } } - else { - ret = wc_ed448_export_public(key, (byte*)out, outLen); - } return ret; } @@ -1832,6 +1958,7 @@ static const wp_EcxData ed448Data = { (WP_ECX_EXPORT_PUB)&wp_ed448_export_public, (WP_ECX_IMPORT_PRIV)&wp_ed448_import_private, (WP_ECX_EXPORT_PRIV)&wc_ed448_export_private_only, + (WP_ECX_DERIVE_PUB)&wp_ed448_derive_public, NULL, (WP_ECX_CHECK_KEY)&wc_ed448_check_key, }; @@ -2242,6 +2369,12 @@ static int wp_ecx_encode(wp_EcxEncDecCtx* ctx, OSSL_CORE_BIO *cBio, ok = 0; } + /* Only the public formats need the derived public half. Deriving for a + * private format would add the public key to the encoding. */ + if (ok && (ctx->format == WP_ENC_FORMAT_SPKI)) { + ok = wp_ecx_ensure_pub((wp_Ecx*)ecx); + } + if (ok) { rc = ctx->encode((void*)&ecx->key, derData, sizeof(derData)); if (rc <= 0) { @@ -2835,24 +2968,12 @@ const OSSL_DISPATCH wp_ed25519_spki_decoder_functions[] = { static int wp_Ed25519PublicKeyToDer(ed25519_key* key, byte* output, word32 inLen) { - int ok = 1; + int ok; WOLFPROV_ENTER(WP_LOG_COMP_KE, "wp_Ed25519PublicKeyToDer"); - /* Check if this is private key only. */ - if (!key->pubKeySet) { - int rc; - /* Make the public key to encode. */ - rc = wc_ed25519_make_public(key, key->p, ED25519_PUB_KEY_SIZE); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed25519_make_public", rc); - } - ok = key->pubKeySet = (rc == 0); - } - if (ok) { - /* Always include the algorithm. */ - ok = wc_Ed25519PublicKeyToDer(key, output, inLen, 1); - } + /* Always include the algorithm. */ + ok = wc_Ed25519PublicKeyToDer(key, output, inLen, 1); WOLFPROV_LEAVE(WP_LOG_COMP_KE, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -3485,24 +3606,12 @@ const OSSL_DISPATCH wp_ed448_spki_decoder_functions[] = { */ static int wp_Ed448PublicKeyToDer(ed448_key* key, byte* output, word32 inLen) { - int ok = 1; + int ok; WOLFPROV_ENTER(WP_LOG_COMP_KE, "wp_Ed448PublicKeyToDer"); - /* Check if this is private key only. */ - if (!key->pubKeySet) { - int rc; - /* Make the public key to encode. */ - rc = wc_ed448_make_public(key, key->p, ED448_PUB_KEY_SIZE); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed448_make_public", rc); - } - ok = key->pubKeySet = (rc == 0); - } - if (ok) { - /* Always include the algorithm. */ - ok = wc_Ed448PublicKeyToDer(key, output, inLen, 1); - } + /* Always include the algorithm. */ + ok = wc_Ed448PublicKeyToDer(key, output, inLen, 1); WOLFPROV_LEAVE(WP_LOG_COMP_KE, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; diff --git a/src/wp_ecx_sig.c b/src/wp_ecx_sig.c index dad4977f..04e6de13 100644 --- a/src/wp_ecx_sig.c +++ b/src/wp_ecx_sig.c @@ -383,22 +383,8 @@ static int wp_ed25519_digest_sign(wp_EcxSigCtx *ctx, unsigned char *sig, } len = (word32)sigSize; - if (!ed25519->pubKeySet) { - unsigned char pubKey[ED25519_PUB_KEY_SIZE]; - - rc = wc_ed25519_make_public(ed25519, pubKey, sizeof(pubKey)); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed25519_make_public", rc); - ok = 0; - } - if (ok) { - rc = wc_ed25519_import_public(pubKey, sizeof(pubKey), ed25519); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed25519_import_public", rc); - ok = 0; - } - } - } + /* EdDSA signs with the public half; make sure it is derived first. */ + ok = wp_ecx_ensure_pub(ctx->ecx); if (ok && (!WP_FITS_WORD32(tbsLen))) { ok = 0; } @@ -470,13 +456,22 @@ static int wp_ed25519_digest_verify(wp_EcxSigCtx *ctx, unsigned char *sig, } if (ok) { int res = 0; - int rc = wc_ed25519_verify_msg(sig, (word32)sigLen, tbs, (word32)tbsLen, - &res, wp_ecx_get_key(ctx->ecx)); - if (rc != 0) { + int rc = 0; + + /* Verify mutates the key (persistent SHA), so hold the key mutex. */ + if (wp_lock(wp_ecx_get_mutex(ctx->ecx)) != 1) { + ok = 0; + } + else { + rc = wc_ed25519_verify_msg(sig, (word32)sigLen, tbs, + (word32)tbsLen, &res, wp_ecx_get_key(ctx->ecx)); + wp_unlock(wp_ecx_get_mutex(ctx->ecx)); + } + if (ok && (rc != 0)) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed25519_verify_msg", rc); ok = 0; } - if (res == 0) { + if (ok && (res == 0)) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "Signature verification", rc); ok = 0; } @@ -596,22 +591,8 @@ static int wp_ed448_digest_sign(wp_EcxSigCtx *ctx, unsigned char *sig, } len = (word32)sigSize; - if (!ed448->pubKeySet) { - unsigned char pubKey[ED448_PUB_KEY_SIZE]; - - rc = wc_ed448_make_public(ed448, pubKey, sizeof(pubKey)); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed448_make_public", rc); - ok = 0; - } - if (ok) { - rc = wc_ed448_import_public(pubKey, sizeof(pubKey), ed448); - if (rc != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed448_import_public", rc); - ok = 0; - } - } - } + /* EdDSA signs with the public half; make sure it is derived first. */ + ok = wp_ecx_ensure_pub(ctx->ecx); if (ok && (!WP_FITS_WORD32(tbsLen))) { ok = 0; } @@ -620,8 +601,8 @@ static int wp_ed448_digest_sign(wp_EcxSigCtx *ctx, unsigned char *sig, ok = 0; } if (ok) { - rc = wc_ed448_sign_msg(tbs, (word32)tbsLen, sig, &len, - (ed448_key*)wp_ecx_get_key(ctx->ecx), NULL, 0); + rc = wc_ed448_sign_msg(tbs, (word32)tbsLen, sig, &len, ed448, + NULL, 0); wp_unlock(wp_ecx_get_mutex(ctx->ecx)); if (rc != 0) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed448_sign_msg", rc); @@ -689,13 +670,22 @@ static int wp_ed448_digest_verify(wp_EcxSigCtx *ctx, unsigned char *sig, } if (ok) { int res = 0; - int rc = wc_ed448_verify_msg(sig, (word32)sigLen, tbs, (word32)tbsLen, - &res, wp_ecx_get_key(ctx->ecx), NULL, 0); - if (rc != 0) { + int rc = 0; + + /* Verify mutates the key (persistent SHA), so hold the key mutex. */ + if (wp_lock(wp_ecx_get_mutex(ctx->ecx)) != 1) { + ok = 0; + } + else { + rc = wc_ed448_verify_msg(sig, (word32)sigLen, tbs, (word32)tbsLen, + &res, wp_ecx_get_key(ctx->ecx), NULL, 0); + wp_unlock(wp_ecx_get_mutex(ctx->ecx)); + } + if (ok && (rc != 0)) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "wc_ed448_verify_msg", rc); ok = 0; } - if (res == 0) { + if (ok && (res == 0)) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "Signature verification", rc); ok = 0; } diff --git a/test/test_ecx.c b/test/test_ecx.c index 50977f8b..5551b9c3 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -25,6 +25,10 @@ #include #include #include +#if defined(HAVE_PTHREAD) || defined(_POSIX_THREADS) +#include +#define WP_HAVE_ECX_SHARED_KEY_TEST +#endif #ifndef ARRAY_SIZE #define ARRAY_SIZE(a) ((sizeof(a)/sizeof(a[0]))) @@ -1174,6 +1178,326 @@ int test_ecx_dup(void *data) return err; } +#ifdef WP_HAVE_ECX_SHARED_KEY_TEST +/* Fixed, small workload: any failure is a defect, not bad timing. */ +#define WP_ECX_SHARED_THREADS 4 +#define WP_ECX_SHARED_TRIALS 2 +#define WP_ECX_SHARED_MSG "wp-ecx-shared-key" +/* Shared helpers run for whichever EdDSA types are built. Size the + * buffers from the types that exist so an Ed25519-only build compiles. */ +#if defined(WP_HAVE_ED25519) && defined(WP_HAVE_ED448) +#define WP_ECX_SHARED_SIG_SIZE MAX(ED25519_SIG_SIZE, ED448_SIG_SIZE) +#define WP_ECX_SHARED_PUB_SIZE MAX(ED25519_PUB_KEY_SIZE, ED448_PUB_KEY_SIZE) +#elif defined(WP_HAVE_ED448) +#define WP_ECX_SHARED_SIG_SIZE ED448_SIG_SIZE +#define WP_ECX_SHARED_PUB_SIZE ED448_PUB_KEY_SIZE +#else +#define WP_ECX_SHARED_SIG_SIZE ED25519_SIG_SIZE +#define WP_ECX_SHARED_PUB_SIZE ED25519_PUB_KEY_SIZE +#endif + +/* Worker modes: sign and export are first uses of the shared private-only + * key; verify runs on a shared wolfProvider public key to exercise the verify + * lock under contention. */ +#define WP_ECX_MODE_SIGN 0 +#define WP_ECX_MODE_EXPORT 1 +#define WP_ECX_MODE_VERIFY 2 + +typedef struct { + EVP_PKEY* key; + EVP_PKEY* verifyKey; + const unsigned char* pub; + size_t pubLen; + size_t sigLen; + unsigned char* vsig; + size_t vsigLen; + const char* algName; + /* Start gate: the parent releases every worker together, but only after + * each one has parked on startCond. A worker bumps ready and signals + * readyCond; the parent waits for all workers before it sets start. The + * mutex and condition variables give a real happens-before edge - a plain + * shared flag would itself be a data race. */ + pthread_mutex_t* lock; + pthread_cond_t* startCond; + pthread_cond_t* readyCond; + int* start; + int* ready; + int mode; + int err; +} wp_ecx_shared_args; + +/* + * Sign with the shared key and verify against a separate public-only key, so + * a bad public half on the shared key cannot hide a bad signature. + */ +static int wp_ecx_shared_sign(wp_ecx_shared_args* w) +{ + int err; + unsigned char sig[WP_ECX_SHARED_SIG_SIZE]; + size_t sigLen = w->sigLen; + unsigned char msg[] = WP_ECX_SHARED_MSG; + EVP_PKEY* pubKey = NULL; + + err = test_digest_sign(w->key, wpLibCtx, msg, sizeof(msg) - 1, NULL, NULL, + sig, &sigLen, 0, 0); + if (err == 0) { + pubKey = EVP_PKEY_new_raw_public_key_ex(osslLibCtx, w->algName, NULL, + w->pub, w->pubLen); + err = (pubKey == NULL); + } + if (err == 0) { + err = test_digest_verify(pubKey, osslLibCtx, msg, sizeof(msg) - 1, + NULL, NULL, sig, sigLen, 0, 0); + } + + EVP_PKEY_free(pubKey); + return err; +} + +/* Export the public key of the shared key and check the value. */ +static int wp_ecx_shared_get_pub(wp_ecx_shared_args* w) +{ + int err; + unsigned char pub[WP_ECX_SHARED_PUB_SIZE]; + size_t pubLen = sizeof(pub); + + err = EVP_PKEY_get_raw_public_key(w->key, pub, &pubLen) != 1; + if (err == 0) { + err = (pubLen != w->pubLen) || (memcmp(pub, w->pub, pubLen) != 0); + } + + return err; +} + +/* + * Verify a precomputed valid signature through wolfProvider on a public key + * shared with the other verify workers. Concurrent verify exercises the key + * mutex that protects the wolfProvider verification state. + */ +static int wp_ecx_shared_verify(wp_ecx_shared_args* w) +{ + unsigned char msg[] = WP_ECX_SHARED_MSG; + + return test_digest_verify(w->verifyKey, wpLibCtx, msg, sizeof(msg) - 1, + NULL, NULL, w->vsig, w->vsigLen, 0, 0); +} + +static void* wp_ecx_shared_key_thread(void* arg) +{ + wp_ecx_shared_args* w = (wp_ecx_shared_args*)arg; + + /* Tell the parent this worker is parked, then wait so that every worker + * reaches its first use of the key together. */ + pthread_mutex_lock(w->lock); + (*w->ready)++; + pthread_cond_broadcast(w->readyCond); + while (*w->start == 0) { + pthread_cond_wait(w->startCond, w->lock); + } + pthread_mutex_unlock(w->lock); + + if (w->mode == WP_ECX_MODE_EXPORT) { + w->err = wp_ecx_shared_get_pub(w); + } + else if (w->mode == WP_ECX_MODE_VERIFY) { + w->err = wp_ecx_shared_verify(w); + } + else { + w->err = wp_ecx_shared_sign(w); + } + + return NULL; +} + +/* + * Create the workers, hold them at the start gate until all have parked, then + * release them together and join. Returns non-zero if any worker failed or a + * thread could not be created. + */ +static int wp_ecx_run_workers(wp_ecx_shared_args* workers, int count) +{ + pthread_t tids[WP_ECX_SHARED_THREADS]; + pthread_mutex_t lock; + pthread_cond_t startCond; + pthread_cond_t readyCond; + int start = 0; + int ready = 0; + int created = 0; + int err = 0; + int i; + + if ((pthread_mutex_init(&lock, NULL) != 0) || + (pthread_cond_init(&startCond, NULL) != 0) || + (pthread_cond_init(&readyCond, NULL) != 0)) { + PRINT_ERR_MSG("Failed to init start primitives"); + return 1; + } + + for (i = 0; i < count; i++) { + workers[i].lock = &lock; + workers[i].startCond = &startCond; + workers[i].readyCond = &readyCond; + workers[i].start = &start; + workers[i].ready = &ready; + if (pthread_create(&tids[i], NULL, wp_ecx_shared_key_thread, + &workers[i]) != 0) { + PRINT_ERR_MSG("Failed to create thread %d", i); + err = 1; + break; + } + created++; + } + + /* Wait until every worker has parked, then release them together so they + * hit the first use at the same time. */ + pthread_mutex_lock(&lock); + while (ready < created) { + pthread_cond_wait(&readyCond, &lock); + } + start = 1; + pthread_cond_broadcast(&startCond); + pthread_mutex_unlock(&lock); + + for (i = 0; i < created; i++) { + pthread_join(tids[i], NULL); + if (workers[i].err != 0) { + err = 1; + } + } + + pthread_cond_destroy(&readyCond); + pthread_cond_destroy(&startCond); + pthread_mutex_destroy(&lock); + return err; +} + +static int wp_ecx_shared_key_first_use(int type, const unsigned char* der, + size_t derLen, const unsigned char* pub, size_t pubLen, + const char* algName, size_t sigSize) +{ + int err = 0; + int t; + + PRINT_MSG("First use of one shared private-only key from %d threads (%s)", + WP_ECX_SHARED_THREADS, algName); + + for (t = 0; (t < WP_ECX_SHARED_TRIALS) && (err == 0); t++) { + EVP_PKEY* key = NULL; + EVP_PKEY* verifyKey = NULL; + EVP_PKEY* signer = NULL; + const unsigned char* p = der; + const unsigned char* sp = der; + unsigned char msg[] = WP_ECX_SHARED_MSG; + unsigned char vsig[WP_ECX_SHARED_SIG_SIZE]; + size_t vsigLen = sizeof(vsig); + wp_ecx_shared_args workers[WP_ECX_SHARED_THREADS]; + int i; + + /* A new key each trial: the public half is only derived on first + * use, so a used key no longer exercises the derivation. */ + key = d2i_PrivateKey_ex(type, NULL, &p, (long)derLen, wpLibCtx, NULL); + if (key == NULL) { + PRINT_ERR_MSG("Failed to load %s private-only key", algName); + return 1; + } + + /* A shared wolfProvider public key for the concurrent verify wave, + * plus a valid signature from an independent signer so the shared key + * above keeps its public half underived for the first-use wave. */ + verifyKey = EVP_PKEY_new_raw_public_key_ex(wpLibCtx, algName, NULL, pub, + pubLen); + signer = d2i_PrivateKey_ex(type, NULL, &sp, (long)derLen, osslLibCtx, + NULL); + if ((verifyKey == NULL) || (signer == NULL) || + (test_digest_sign(signer, osslLibCtx, msg, sizeof(msg) - 1, + NULL, NULL, vsig, &vsigLen, 0, 0) != 0)) { + PRINT_ERR_MSG("Failed to prepare %s verify workload", algName); + EVP_PKEY_free(signer); + EVP_PKEY_free(verifyKey); + EVP_PKEY_free(key); + return 1; + } + EVP_PKEY_free(signer); + + /* Wave 1: every worker first-uses the one shared private-only key, so + * the lazy public-key derivation runs under full contention. */ + memset(workers, 0, sizeof(workers)); + for (i = 0; i < WP_ECX_SHARED_THREADS; i++) { + workers[i].key = key; + workers[i].pub = pub; + workers[i].pubLen = pubLen; + workers[i].sigLen = sigSize; + workers[i].algName = algName; + workers[i].mode = (i & 1) ? WP_ECX_MODE_EXPORT : WP_ECX_MODE_SIGN; + } + if (wp_ecx_run_workers(workers, WP_ECX_SHARED_THREADS) != 0) { + PRINT_ERR_MSG("%s first-use wave failed", algName); + err = 1; + } + + /* Wave 2: every worker verifies the same signature through the one + * shared wolfProvider public key, so the verify lock runs under + * contention. */ + if (err == 0) { + memset(workers, 0, sizeof(workers)); + for (i = 0; i < WP_ECX_SHARED_THREADS; i++) { + workers[i].verifyKey = verifyKey; + workers[i].vsig = vsig; + workers[i].vsigLen = vsigLen; + workers[i].algName = algName; + workers[i].mode = WP_ECX_MODE_VERIFY; + } + if (wp_ecx_run_workers(workers, WP_ECX_SHARED_THREADS) != 0) { + PRINT_ERR_MSG("%s concurrent verify wave failed", algName); + err = 1; + } + } + + EVP_PKEY_free(verifyKey); + EVP_PKEY_free(key); + } + + return err; +} +#endif /* WP_HAVE_ECX_SHARED_KEY_TEST */ + +/* + * Use one private-only EdDSA key from more than one thread at the same time. + * + * A seed-only PKCS#8 key has no public half until wolfProvider derives it. + * The derivation writes into the shared key object, so it must hold the key + * mutex. Without it, a thread signs or exports with a partly written public + * key. + */ +int test_ecx_shared_key_first_use(void *data) +{ + (void)data; + +#ifndef WP_HAVE_ECX_SHARED_KEY_TEST + PRINT_MSG("Skipping shared key first use test (no pthreads)"); + return 0; +#else + int err = 0; + +#ifdef WP_HAVE_ED25519 + if (wp_ecx_shared_key_first_use(EVP_PKEY_ED25519, ed25519_key_der, + sizeof(ed25519_key_der), ed25519_pub_key_raw, + sizeof(ed25519_pub_key_raw), "ED25519", + ED25519_SIG_SIZE) != 0) { + err = 1; + } +#endif +#ifdef WP_HAVE_ED448 + if (wp_ecx_shared_key_first_use(EVP_PKEY_ED448, ed448_key_der, + sizeof(ed448_key_der), ed448_pub_key_raw, + sizeof(ed448_pub_key_raw), "ED448", ED448_SIG_SIZE) != 0) { + err = 1; + } +#endif + return err; +#endif /* WP_HAVE_ECX_SHARED_KEY_TEST */ +} + #endif /* defined(WP_HAVE_ED25519) || defined(WP_HAVE_ED448) */ #if defined(WP_HAVE_X25519) || defined(WP_HAVE_X448) diff --git a/test/unit.c b/test/unit.c index cfb6e8c1..701b8311 100644 --- a/test/unit.c +++ b/test/unit.c @@ -546,6 +546,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_ecx_import_short_pub, NULL), #endif TEST_DECL(test_ecx_dup, NULL), + TEST_DECL(test_ecx_shared_key_first_use, NULL), #endif #if defined(WP_HAVE_X25519) || defined(WP_HAVE_X448) TEST_DECL(test_ecx_x_security_bits, NULL), diff --git a/test/unit.h b/test/unit.h index 5a956043..51c4f171 100644 --- a/test/unit.h +++ b/test/unit.h @@ -560,6 +560,7 @@ int test_ecx_import_zero_priv(void *data); int test_ecx_import_short_pub(void *data); #endif /* WP_HAVE_X25519 */ int test_ecx_dup(void *data); +int test_ecx_shared_key_first_use(void *data); #endif #if defined(WP_HAVE_X25519) || defined(WP_HAVE_X448)