From 800657a3ae69572dc145bcf6826e2571040024ad Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Fri, 25 Sep 2026 14:11:54 -0700 Subject: [PATCH] Add EdDSA streaming support --- src/wp_ecx_sig.c | 154 ++++++++++++++++++++++++++++++++++ test/test_ecx.c | 212 +++++++++++++++++++++++++++++++++++++++++++++++ test/unit.c | 1 + test/unit.h | 1 + 4 files changed, 368 insertions(+) diff --git a/src/wp_ecx_sig.c b/src/wp_ecx_sig.c index 80ed0d35..9ea1165f 100644 --- a/src/wp_ecx_sig.c +++ b/src/wp_ecx_sig.c @@ -59,6 +59,14 @@ typedef struct wp_EcxSigCtx { char* propQuery; /** Name of hash algorithm. */ char mdName[WP_MAX_MD_NAME_SIZE]; + + /** Message cached by the streaming interface. EdDSA is one-shot, so the + * data is gathered here and signed or verified in final. */ + unsigned char* msg; + /** Length of cached message in bytes. */ + size_t msgLen; + /** Size of the cached message buffer in bytes. */ + size_t msgSz; } wp_EcxSigCtx; @@ -114,6 +122,7 @@ static void wp_ecx_freectx(wp_EcxSigCtx* ctx) if (ctx != NULL) { wp_ecx_free(ctx->ecx); OPENSSL_free(ctx->propQuery); + OPENSSL_clear_free(ctx->msg, ctx->msgSz); OPENSSL_free(ctx); } } @@ -148,6 +157,17 @@ static wp_EcxSigCtx* wp_ecx_dupctx(wp_EcxSigCtx* srcCtx) if (ok && (!wp_ecx_up_ref(srcCtx->ecx))) { ok = 0; } + if (ok && (srcCtx->msgLen > 0)) { + dstCtx->msg = OPENSSL_malloc(srcCtx->msgLen); + if (dstCtx->msg == NULL) { + ok = 0; + } + else { + XMEMCPY(dstCtx->msg, srcCtx->msg, srcCtx->msgLen); + dstCtx->msgLen = srcCtx->msgLen; + dstCtx->msgSz = srcCtx->msgLen; + } + } if (ok) { dstCtx->ecx = srcCtx->ecx; #if LIBWOLFSSL_VERSION_HEX < 0x05007004 @@ -204,6 +224,7 @@ static int wp_ecx_digest_signverify_init(wp_EcxSigCtx *ctx, } if (ok) { ctx->op = op; + ctx->msgLen = 0; } WOLFPROV_LEAVE(WP_LOG_COMP_KE, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -357,6 +378,59 @@ static int wp_ed25519_get_ctx_params(wp_EcxSigCtx *ctx, OSSL_PARAM *params) * @return 1 on success. * @return 0 on failure. */ +/** + * Append data to the cached message. + * + * EdDSA signs the whole message at once, so the streaming interface + * gathers the data here and defers the operation to final. + * + * @param [in, out] ctx ECX signature context object. + * @param [in] data Data to append. + * @param [in] dataLen Length of data in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ecx_digest_signverify_update(wp_EcxSigCtx *ctx, + const unsigned char *data, size_t dataLen) +{ + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_KE, "wp_ecx_digest_signverify_update"); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (dataLen > 0)) { + if (ctx->msgLen + dataLen > ctx->msgSz) { + size_t newSz = ctx->msgSz * 2; + unsigned char* newMsg; + + if (newSz < ctx->msgLen + dataLen) { + newSz = ctx->msgLen + dataLen; + } + newMsg = OPENSSL_malloc(newSz); + if (newMsg == NULL) { + ok = 0; + } + else { + if (ctx->msgLen > 0) { + XMEMCPY(newMsg, ctx->msg, ctx->msgLen); + } + OPENSSL_clear_free(ctx->msg, ctx->msgSz); + ctx->msg = newMsg; + ctx->msgSz = newSz; + } + } + if (ok) { + XMEMCPY(ctx->msg + ctx->msgLen, data, dataLen); + ctx->msgLen += dataLen; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_KE, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + static int wp_ed25519_digest_sign(wp_EcxSigCtx *ctx, unsigned char *sig, size_t *sigLen, size_t sigSize, const unsigned char *tbs, size_t tbsLen) { @@ -486,15 +560,55 @@ static int wp_ed25519_digest_verify(wp_EcxSigCtx *ctx, unsigned char *sig, } /** Dspatch table for Ed25519 signing and verification. */ +/** + * Sign the cached message. + * + * @param [in, out] ctx ECX signature context object. + * @param [out] sig Buffer to hold signature. May be NULL. + * @param [out] sigLen Length of signature in bytes. + * @param [in] sigSize Size of signature buffer in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ed25519_digest_sign_final(wp_EcxSigCtx *ctx, unsigned char *sig, + size_t *sigLen, size_t sigSize) +{ + return wp_ed25519_digest_sign(ctx, sig, sigLen, sigSize, ctx->msg, + ctx->msgLen); +} + +/** + * Verify a signature over the cached message. + * + * @param [in, out] ctx ECX signature context object. + * @param [in] sig Signature to verify. + * @param [in] sigLen Length of signature in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ed25519_digest_verify_final(wp_EcxSigCtx *ctx, unsigned char *sig, + size_t sigLen) +{ + return wp_ed25519_digest_verify(ctx, sig, sigLen, ctx->msg, ctx->msgLen); +} + const OSSL_DISPATCH wp_ed25519_signature_functions[] = { { OSSL_FUNC_SIGNATURE_NEWCTX, (DFUNC)wp_ecx_newctx }, { OSSL_FUNC_SIGNATURE_FREECTX, (DFUNC)wp_ecx_freectx }, { OSSL_FUNC_SIGNATURE_DUPCTX, (DFUNC)wp_ecx_dupctx }, { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_INIT, (DFUNC)wp_ecx_digest_sign_init }, { OSSL_FUNC_SIGNATURE_DIGEST_SIGN, (DFUNC)wp_ed25519_digest_sign }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_UPDATE, + (DFUNC)wp_ecx_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_FINAL, + (DFUNC)wp_ed25519_digest_sign_final }, { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_INIT, (DFUNC)wp_ecx_digest_verify_init }, { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY, (DFUNC)wp_ed25519_digest_verify }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_UPDATE, + (DFUNC)wp_ecx_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_FINAL, + (DFUNC)wp_ed25519_digest_verify_final }, { OSSL_FUNC_SIGNATURE_GET_CTX_PARAMS, (DFUNC)wp_ed25519_get_ctx_params }, { OSSL_FUNC_SIGNATURE_GETTABLE_CTX_PARAMS, (DFUNC)wp_ecx_gettable_ctx_params }, @@ -704,15 +818,55 @@ static int wp_ed448_digest_verify(wp_EcxSigCtx *ctx, unsigned char *sig, } /** Dspatch table for Ed448 signing and verification. */ +/** + * Sign the cached message. + * + * @param [in, out] ctx ECX signature context object. + * @param [out] sig Buffer to hold signature. May be NULL. + * @param [out] sigLen Length of signature in bytes. + * @param [in] sigSize Size of signature buffer in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ed448_digest_sign_final(wp_EcxSigCtx *ctx, unsigned char *sig, + size_t *sigLen, size_t sigSize) +{ + return wp_ed448_digest_sign(ctx, sig, sigLen, sigSize, ctx->msg, + ctx->msgLen); +} + +/** + * Verify a signature over the cached message. + * + * @param [in, out] ctx ECX signature context object. + * @param [in] sig Signature to verify. + * @param [in] sigLen Length of signature in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ed448_digest_verify_final(wp_EcxSigCtx *ctx, unsigned char *sig, + size_t sigLen) +{ + return wp_ed448_digest_verify(ctx, sig, sigLen, ctx->msg, ctx->msgLen); +} + const OSSL_DISPATCH wp_ed448_signature_functions[] = { { OSSL_FUNC_SIGNATURE_NEWCTX, (DFUNC)wp_ecx_newctx }, { OSSL_FUNC_SIGNATURE_FREECTX, (DFUNC)wp_ecx_freectx }, { OSSL_FUNC_SIGNATURE_DUPCTX, (DFUNC)wp_ecx_dupctx }, { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_INIT, (DFUNC)wp_ecx_digest_sign_init }, { OSSL_FUNC_SIGNATURE_DIGEST_SIGN, (DFUNC)wp_ed448_digest_sign }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_UPDATE, + (DFUNC)wp_ecx_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_FINAL, + (DFUNC)wp_ed448_digest_sign_final }, { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_INIT, (DFUNC)wp_ecx_digest_verify_init }, { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY, (DFUNC)wp_ed448_digest_verify }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_UPDATE, + (DFUNC)wp_ecx_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_FINAL, + (DFUNC)wp_ed448_digest_verify_final }, { OSSL_FUNC_SIGNATURE_GET_CTX_PARAMS, (DFUNC)wp_ed448_get_ctx_params }, { OSSL_FUNC_SIGNATURE_GETTABLE_CTX_PARAMS, (DFUNC)wp_ecx_gettable_ctx_params }, diff --git a/test/test_ecx.c b/test/test_ecx.c index faa83124..d843a1ab 100644 --- a/test/test_ecx.c +++ b/test/test_ecx.c @@ -1643,6 +1643,218 @@ int test_ecx_shared_key_first_use(void *data) #endif /* WP_HAVE_ECX_SHARED_KEY_TEST */ } +/* Size the shared buffers from the EdDSA types that are built so an + * Ed25519-only build compiles. */ +#if defined(WP_HAVE_ED25519) && defined(WP_HAVE_ED448) +#define WP_ECX_STREAM_SIG_SIZE MAX(ED25519_SIG_SIZE, ED448_SIG_SIZE) +#elif defined(WP_HAVE_ED448) +#define WP_ECX_STREAM_SIG_SIZE ED448_SIG_SIZE +#else +#define WP_ECX_STREAM_SIG_SIZE ED25519_SIG_SIZE +#endif + +#define WP_ECX_STREAM_SPLIT 7 + +/* + * Sign the message in two update calls and return the signature. + */ +static int wp_ecx_stream_sign(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, unsigned char* sig, size_t* sigLen) +{ + int err; + size_t queryLen = 0; + EVP_MD_CTX* 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_DigestSignUpdate(mdCtx, msg, WP_ECX_STREAM_SPLIT) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(mdCtx, msg + WP_ECX_STREAM_SPLIT, + msgLen - WP_ECX_STREAM_SPLIT) != 1; + } + /* Query the length first, the way an application sizes its buffer. */ + if (err == 0) { + err = EVP_DigestSignFinal(mdCtx, NULL, &queryLen) != 1; + } + if (err == 0) { + err = queryLen > *sigLen; + } + if (err == 0) { + *sigLen = queryLen; + err = EVP_DigestSignFinal(mdCtx, sig, sigLen) != 1; + } + + EVP_MD_CTX_free(mdCtx); + return err; +} + +/* + * Verify the signature over the message in two update calls. + */ +static int wp_ecx_stream_verify(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, const unsigned char* sig, size_t sigLen) +{ + int err; + EVP_MD_CTX* mdCtx = EVP_MD_CTX_new(); + + err = mdCtx == NULL; + if (err == 0) { + err = EVP_DigestVerifyInit_ex(mdCtx, NULL, NULL, wpLibCtx, NULL, pkey, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestVerifyUpdate(mdCtx, msg, WP_ECX_STREAM_SPLIT) != 1; + } + if (err == 0) { + err = EVP_DigestVerifyUpdate(mdCtx, msg + WP_ECX_STREAM_SPLIT, + msgLen - WP_ECX_STREAM_SPLIT) != 1; + } + if (err == 0) { + err = EVP_DigestVerifyFinal(mdCtx, sig, sigLen) != 1; + } + + EVP_MD_CTX_free(mdCtx); + return err; +} + +/* + * Sign in one call, for comparison with the streamed signature. + */ +static int wp_ecx_oneshot_sign(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, unsigned char* sig, size_t* sigLen) +{ + int err; + EVP_MD_CTX* 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) { + err = EVP_DigestSign(mdCtx, sig, sigLen, msg, msgLen) != 1; + } + + EVP_MD_CTX_free(mdCtx); + return err; +} + +static int wp_ecx_stream_sign_verify(int type, const unsigned char* keyDer, + size_t keyDerLen, size_t sigSize, const char* name) +{ + int err = 0; + const unsigned char* p = keyDer; + EVP_PKEY* pkey = NULL; + EVP_PKEY* pkeyOssl = NULL; + EVP_MD_CTX* mdCtx = NULL; + unsigned char oneShot[WP_ECX_STREAM_SIG_SIZE]; + unsigned char streamed[WP_ECX_STREAM_SIG_SIZE]; + size_t oneShotLen = sizeof(oneShot); + size_t streamedLen = sizeof(streamed); + static const unsigned char msg[] = + "ECX message split over more than one update call"; + const size_t msgLen = sizeof(msg) - 1; + + PRINT_MSG("Testing ECX streaming sign/verify (%s)", name); + + pkey = d2i_PrivateKey_ex(type, NULL, &p, (long)keyDerLen, wpLibCtx, NULL); + err = pkey == NULL; + if (err) { + PRINT_ERR_MSG("could not create key"); + } + + if (err == 0) { + err = wp_ecx_oneshot_sign(pkey, msg, msgLen, oneShot, &oneShotLen); + if (err) { + PRINT_ERR_MSG("one-shot sign failed"); + } + } + if (err == 0) { + err = wp_ecx_stream_sign(pkey, msg, msgLen, streamed, &streamedLen); + if (err) { + PRINT_ERR_MSG("streaming sign failed"); + } + } + if (err == 0) { + err = (oneShotLen != sigSize) || (streamedLen != sigSize); + if (err) { + PRINT_ERR_MSG("unexpected signature length"); + } + } + /* EdDSA is deterministic, so both paths must produce the same bytes. */ + if (err == 0) { + err = memcmp(oneShot, streamed, sigSize) != 0; + if (err) { + PRINT_ERR_MSG("streamed signature differs from one-shot"); + } + } + if (err == 0) { + err = wp_ecx_stream_verify(pkey, msg, msgLen, streamed, streamedLen); + if (err) { + PRINT_ERR_MSG("streaming verify rejected a good signature"); + } + } + + if (err == 0) { + PRINT_MSG("Verify streamed signature with OpenSSL (%s)", name); + p = keyDer; + pkeyOssl = d2i_PrivateKey_ex(type, NULL, &p, (long)keyDerLen, + osslLibCtx, NULL); + err = pkeyOssl == NULL; + } + if (err == 0) { + mdCtx = EVP_MD_CTX_new(); + err = mdCtx == NULL; + } + if (err == 0) { + err = EVP_DigestVerifyInit_ex(mdCtx, NULL, NULL, osslLibCtx, NULL, + pkeyOssl, NULL) != 1; + } + if (err == 0) { + err = EVP_DigestVerify(mdCtx, streamed, streamedLen, msg, msgLen) != 1; + if (err) { + PRINT_ERR_MSG("OpenSSL rejected the streamed signature"); + } + } + + EVP_MD_CTX_free(mdCtx); + EVP_PKEY_free(pkeyOssl); + EVP_PKEY_free(pkey); + return err; +} + +/* + * EdDSA has no digest of its own, so the message is gathered by the update + * calls and signed in final. Check that path against the one-shot one. + */ +int test_ecx_stream_sign_verify(void *data) +{ + int err = 0; + + (void)data; + +#ifdef WP_HAVE_ED25519 + err = wp_ecx_stream_sign_verify(EVP_PKEY_ED25519, ed25519_key_der, + sizeof(ed25519_key_der), ED25519_SIG_SIZE, "ed25519"); +#endif +#ifdef WP_HAVE_ED448 + if (err == 0) { + err = wp_ecx_stream_sign_verify(EVP_PKEY_ED448, ed448_key_der, + sizeof(ed448_key_der), ED448_SIG_SIZE, "ed448"); + } +#endif + + return err; +} + #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 be7f72a2..662f68da 100644 --- a/test/unit.c +++ b/test/unit.c @@ -596,6 +596,7 @@ TEST_CASE test_case[] = { #endif TEST_DECL(test_ecx_dup, NULL), TEST_DECL(test_ecx_shared_key_first_use, NULL), + TEST_DECL(test_ecx_stream_sign_verify, 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 35bc245b..55bed678 100644 --- a/test/unit.h +++ b/test/unit.h @@ -631,6 +631,7 @@ 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); +int test_ecx_stream_sign_verify(void *data); #endif #if defined(WP_HAVE_X25519) || defined(WP_HAVE_X448)