ecc: fix invalid-curve attack via missing on-curve validation

wc_ecc_import_x963_ex2 only checked whether an imported public point
lies on the intended curve when both USE_ECC_B_PARAM was compiled in
and the caller passed untrusted=1. In a default ./configure build,
USE_ECC_B_PARAM is not defined, so the check was compiled out entirely.
Additionally, the legacy wrapper wc_ecc_import_x963_ex unconditionally
passed untrusted=0, meaning ECIES (wc_ecc_decrypt), PKCS#7 KARI, and
the EVP ECDH layer never triggered the check even when the macro was
present. wc_ecc_shared_secret performed no on-curve validation at all.

An attacker who can supply an EC public key (e.g. via an ECIES
ciphertext, PKCS#7 enveloped-data, or EVP_PKEY_derive) can choose a
point on a twist of the target curve with a smooth-order subgroup.
Each ECDH query leaks the victim's static private scalar modulo a small
prime; CRT reconstruction across enough queries recovers the full key
(Biehl-Meyer-Müller invalid-curve attack). Static-key ECIES and PKCS#7
KARI are directly affected; TLS is affected in default builds because
the USE_ECC_B_PARAM gate defeated the untrusted=1 flag that the
handshake does pass.

Three changes close the attack:

1. Define USE_ECC_B_PARAM unconditionally in ecc.h so that
   wc_ecc_point_is_on_curve() is compiled in all builds, not only
   those with HAVE_COMP_KEY or OPENSSL_EXTRA.

2. wc_ecc_import_x963_ex: pass untrusted=1 to wc_ecc_import_x963_ex2
   so that ECIES, PKCS#7 KARI, and EVP callers that go through the
   four-argument wrapper always validate the imported point.

3. wc_ecc_shared_secret: add defense-in-depth on-curve check before
   scalar multiplication, catching any import path that bypassed the
   import-time validation (e.g. direct wc_ecc_import_x963_ex2 with
   untrusted=0).

Both new validation sites dispatch to sp_ecc_check_key_NNN for
SP-supported curves (P-256/384/521, SM2) when WOLFSSL_HAVE_SP_ECC is
defined, keeping the mp_int stack cost off embedded targets. Non-SP
curves fall back to wc_ecc_point_is_on_curve.

Reported by: Nicholas Carlini (Anthropic) & Thai Duong (Calif.io)
This commit is contained in:
Tobias Frauenschläger
2026-03-30 15:19:35 +02:00
parent f207e18f7a
commit 929dd9913b
2 changed files with 156 additions and 9 deletions
+150 -4
View File
@@ -4761,6 +4761,78 @@ int wc_ecc_shared_secret(ecc_key* private_key, ecc_key* public_key, byte* out,
return ECC_BAD_ARG_E;
}
#if !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \
!defined(WOLFSSL_CRYPTOCELL) && !defined(WOLFSSL_SILABS_SE_ACCEL) && \
!defined(WOLFSSL_KCAPI_ECC) && !defined(WOLFSSL_SE050) && \
!defined(WOLF_CRYPTO_CB_ONLY_ECC)
/* Defense-in-depth: verify peer public key lies on the intended curve
* regardless of how the key was imported. Catches any import path that
* skipped the on-curve check (e.g. trusted-flag set,
* WOLFSSL_VALIDATE_ECC_IMPORT not defined, or future callers using
* wc_ecc_import_x963_ex2 directly). */
if (public_key->idx != ECC_CUSTOM_IDX) {
#ifdef WOLFSSL_HAVE_SP_ECC
/* Use compact SP check functions for supported curves to avoid the
* mp_int stack cost of wc_ecc_point_is_on_curve on embedded targets. */
#ifndef WOLFSSL_SP_NO_256
if (ecc_sets[public_key->idx].id == ECC_SECP256R1) {
err = sp_ecc_check_key_256(public_key->pubkey.x,
public_key->pubkey.y,
NULL, public_key->heap);
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
if (err == MP_VAL) {
/* Retry with SM2 check when SP-256 returns invalid.
* This is required as in some cases, the SM2 curve is
* not recognized correctly while parsing the encoded
* input. In this case, SM2 keys are invalidly identified
* as SECP256R1 keys. */
err = sp_ecc_check_key_sm2_256(public_key->pubkey.x,
public_key->pubkey.y,
NULL, public_key->heap);
}
#endif
}
else
#endif
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
if (ecc_sets[public_key->idx].id == ECC_SM2P256V1) {
err = sp_ecc_check_key_sm2_256(public_key->pubkey.x,
public_key->pubkey.y,
NULL, public_key->heap);
}
else
#endif
#ifdef WOLFSSL_SP_384
if (ecc_sets[public_key->idx].id == ECC_SECP384R1) {
err = sp_ecc_check_key_384(public_key->pubkey.x,
public_key->pubkey.y,
NULL, public_key->heap);
}
else
#endif
#ifdef WOLFSSL_SP_521
if (ecc_sets[public_key->idx].id == ECC_SECP521R1) {
err = sp_ecc_check_key_521(public_key->pubkey.x,
public_key->pubkey.y,
NULL, public_key->heap);
}
else
#endif
{
err = wc_ecc_point_is_on_curve(&public_key->pubkey,
public_key->idx);
}
#else
err = wc_ecc_point_is_on_curve(&public_key->pubkey, public_key->idx);
#endif /* WOLFSSL_HAVE_SP_ECC */
if (err != MP_OKAY) {
WOLFSSL_MSG("wc_ecc_shared_secret: peer public key not on curve");
return ECC_BAD_ARG_E;
}
}
#endif
#if defined(WOLFSSL_ATECC508A) || defined(WOLFSSL_ATECC608A)
/* For SECP256R1 use hardware */
if (private_key->dp->id == ECC_SECP256R1) {
@@ -11011,16 +11083,89 @@ int wc_ecc_import_x963_ex2(const byte* in, word32 inLen, ecc_key* key,
!defined(WOLFSSL_CRYPTOCELL) && \
(!defined(WOLF_CRYPTO_CB_ONLY_ECC) || defined(WOLFSSL_QNX_CAAM) || \
defined(WOLFSSL_IMXRT1170_CAAM))
if (untrusted) {
/* Only do quick checks. */
if ((err == MP_OKAY) && untrusted) {
#ifdef WOLFSSL_HAVE_SP_ECC
/* For SP-supported curves sp_ecc_check_key_NNN validates infinity,
* coordinate range, on-curve equation, and point*order=infinity using
* compact sp_digit arrays - no mp_int stack cost. */
if (key->idx != ECC_CUSTOM_IDX) {
#ifndef WOLFSSL_SP_NO_256
if (ecc_sets[key->idx].id == ECC_SECP256R1) {
err = sp_ecc_check_key_256(key->pubkey.x, key->pubkey.y,
NULL, key->heap);
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
if (err == MP_VAL) {
/* Retry with SM2 check when SP-256 returns invalid.
* This is required as in some cases, the SM2 curve is
* not recognized correctly while parsing the encoded
* input. In this case, SM2 keys are invalidly identified
* as SECP256R1 keys. */
err = sp_ecc_check_key_sm2_256(key->pubkey.x,
key->pubkey.y, NULL,
key->heap);
}
#endif
}
else
#endif
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
if (ecc_sets[key->idx].id == ECC_SM2P256V1) {
/* Native SM2 curve: always use SM2 check. */
err = sp_ecc_check_key_sm2_256(key->pubkey.x, key->pubkey.y,
NULL, key->heap);
}
else
#endif
#ifdef WOLFSSL_SP_384
if (ecc_sets[key->idx].id == ECC_SECP384R1) {
err = sp_ecc_check_key_384(key->pubkey.x, key->pubkey.y,
NULL, key->heap);
}
else
#endif
#ifdef WOLFSSL_SP_521
if (ecc_sets[key->idx].id == ECC_SECP521R1) {
err = sp_ecc_check_key_521(key->pubkey.x, key->pubkey.y,
NULL, key->heap);
}
else
#endif
{
/* Non-SP curve: fall back to generic checks */
if ((err == MP_OKAY) &&
wc_ecc_point_is_at_infinity(&key->pubkey)) {
err = ECC_INF_E;
}
if (err == MP_OKAY) {
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
}
}
}
#else
/* Generic checks for all curves */
if ((err == MP_OKAY) && wc_ecc_point_is_at_infinity(&key->pubkey)) {
err = ECC_INF_E;
}
#ifdef USE_ECC_B_PARAM
if ((err == MP_OKAY) && (key->idx != ECC_CUSTOM_IDX)) {
if ((err == MP_OKAY) && (key->idx != ECC_CUSTOM_IDX)) {
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
#if defined(WOLFSSL_SM2)
if (err != MP_OKAY && curve_id < 0) {
/* Retry with SM2 check when default identified curve returns
* invalid. This is required as in some cases, the SM2 curve is
* not recognized correctly while parsing the encoded
* input. In this case, SM2 keys are invalidly identified
* as SECP256R1 keys. */
err = wc_ecc_set_curve(key, WOLFSSL_SM2_KEY_BITS / 8,
ECC_SM2P256V1);
if (err == MP_OKAY) {
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
}
}
#endif
}
#endif /* USE_ECC_B_PARAM */
#endif /* WOLFSSL_HAVE_SP_ECC */
}
#endif
(void)untrusted;
@@ -11047,7 +11192,8 @@ int wc_ecc_import_x963_ex2(const byte* in, word32 inLen, ecc_key* key,
int wc_ecc_import_x963_ex(const byte* in, word32 inLen, ecc_key* key,
int curve_id)
{
return wc_ecc_import_x963_ex2(in, inLen, key, curve_id, 0);
/* treat as untrusted: validate the point is on the curve */
return wc_ecc_import_x963_ex2(in, inLen, key, curve_id, 1);
}
WOLFSSL_ABI
+6 -5
View File
@@ -84,11 +84,12 @@
WOLFSSL_LOCAL int wolfCrypt_FIPS_ECC_sanity(void);
#endif
/* Enable curve B parameter if needed */
#if defined(HAVE_COMP_KEY) || defined(ECC_CACHE_CURVE)
#ifndef USE_ECC_B_PARAM /* Allow someone to force enable */
#define USE_ECC_B_PARAM
#endif
/* Enable curve B parameter for on-curve point validation.
* The b coefficient is present in every compiled-in ecc_set_type entry;
* making it always available lets wc_ecc_point_is_on_curve() run in all
* builds and closes the invalid-curve attack surface. */
#ifndef USE_ECC_B_PARAM
#define USE_ECC_B_PARAM
#endif