Skip to content

Commit 929dd99

Browse files
committed
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)
1 parent f207e18 commit 929dd99

2 files changed

Lines changed: 156 additions & 9 deletions

File tree

wolfcrypt/src/ecc.c

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4761,6 +4761,78 @@ int wc_ecc_shared_secret(ecc_key* private_key, ecc_key* public_key, byte* out,
47614761
return ECC_BAD_ARG_E;
47624762
}
47634763

4764+
#if !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \
4765+
!defined(WOLFSSL_CRYPTOCELL) && !defined(WOLFSSL_SILABS_SE_ACCEL) && \
4766+
!defined(WOLFSSL_KCAPI_ECC) && !defined(WOLFSSL_SE050) && \
4767+
!defined(WOLF_CRYPTO_CB_ONLY_ECC)
4768+
/* Defense-in-depth: verify peer public key lies on the intended curve
4769+
* regardless of how the key was imported. Catches any import path that
4770+
* skipped the on-curve check (e.g. trusted-flag set,
4771+
* WOLFSSL_VALIDATE_ECC_IMPORT not defined, or future callers using
4772+
* wc_ecc_import_x963_ex2 directly). */
4773+
if (public_key->idx != ECC_CUSTOM_IDX) {
4774+
#ifdef WOLFSSL_HAVE_SP_ECC
4775+
/* Use compact SP check functions for supported curves to avoid the
4776+
* mp_int stack cost of wc_ecc_point_is_on_curve on embedded targets. */
4777+
#ifndef WOLFSSL_SP_NO_256
4778+
if (ecc_sets[public_key->idx].id == ECC_SECP256R1) {
4779+
err = sp_ecc_check_key_256(public_key->pubkey.x,
4780+
public_key->pubkey.y,
4781+
NULL, public_key->heap);
4782+
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
4783+
if (err == MP_VAL) {
4784+
/* Retry with SM2 check when SP-256 returns invalid.
4785+
* This is required as in some cases, the SM2 curve is
4786+
* not recognized correctly while parsing the encoded
4787+
* input. In this case, SM2 keys are invalidly identified
4788+
* as SECP256R1 keys. */
4789+
err = sp_ecc_check_key_sm2_256(public_key->pubkey.x,
4790+
public_key->pubkey.y,
4791+
NULL, public_key->heap);
4792+
}
4793+
#endif
4794+
}
4795+
else
4796+
#endif
4797+
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
4798+
if (ecc_sets[public_key->idx].id == ECC_SM2P256V1) {
4799+
err = sp_ecc_check_key_sm2_256(public_key->pubkey.x,
4800+
public_key->pubkey.y,
4801+
NULL, public_key->heap);
4802+
}
4803+
else
4804+
#endif
4805+
#ifdef WOLFSSL_SP_384
4806+
if (ecc_sets[public_key->idx].id == ECC_SECP384R1) {
4807+
err = sp_ecc_check_key_384(public_key->pubkey.x,
4808+
public_key->pubkey.y,
4809+
NULL, public_key->heap);
4810+
}
4811+
else
4812+
#endif
4813+
#ifdef WOLFSSL_SP_521
4814+
if (ecc_sets[public_key->idx].id == ECC_SECP521R1) {
4815+
err = sp_ecc_check_key_521(public_key->pubkey.x,
4816+
public_key->pubkey.y,
4817+
NULL, public_key->heap);
4818+
}
4819+
else
4820+
#endif
4821+
{
4822+
err = wc_ecc_point_is_on_curve(&public_key->pubkey,
4823+
public_key->idx);
4824+
}
4825+
#else
4826+
err = wc_ecc_point_is_on_curve(&public_key->pubkey, public_key->idx);
4827+
#endif /* WOLFSSL_HAVE_SP_ECC */
4828+
4829+
if (err != MP_OKAY) {
4830+
WOLFSSL_MSG("wc_ecc_shared_secret: peer public key not on curve");
4831+
return ECC_BAD_ARG_E;
4832+
}
4833+
}
4834+
#endif
4835+
47644836
#if defined(WOLFSSL_ATECC508A) || defined(WOLFSSL_ATECC608A)
47654837
/* For SECP256R1 use hardware */
47664838
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,
1101111083
!defined(WOLFSSL_CRYPTOCELL) && \
1101211084
(!defined(WOLF_CRYPTO_CB_ONLY_ECC) || defined(WOLFSSL_QNX_CAAM) || \
1101311085
defined(WOLFSSL_IMXRT1170_CAAM))
11014-
if (untrusted) {
11015-
/* Only do quick checks. */
11086+
if ((err == MP_OKAY) && untrusted) {
11087+
#ifdef WOLFSSL_HAVE_SP_ECC
11088+
/* For SP-supported curves sp_ecc_check_key_NNN validates infinity,
11089+
* coordinate range, on-curve equation, and point*order=infinity using
11090+
* compact sp_digit arrays - no mp_int stack cost. */
11091+
if (key->idx != ECC_CUSTOM_IDX) {
11092+
#ifndef WOLFSSL_SP_NO_256
11093+
if (ecc_sets[key->idx].id == ECC_SECP256R1) {
11094+
err = sp_ecc_check_key_256(key->pubkey.x, key->pubkey.y,
11095+
NULL, key->heap);
11096+
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
11097+
if (err == MP_VAL) {
11098+
/* Retry with SM2 check when SP-256 returns invalid.
11099+
* This is required as in some cases, the SM2 curve is
11100+
* not recognized correctly while parsing the encoded
11101+
* input. In this case, SM2 keys are invalidly identified
11102+
* as SECP256R1 keys. */
11103+
err = sp_ecc_check_key_sm2_256(key->pubkey.x,
11104+
key->pubkey.y, NULL,
11105+
key->heap);
11106+
}
11107+
#endif
11108+
}
11109+
else
11110+
#endif
11111+
#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SP_SM2)
11112+
if (ecc_sets[key->idx].id == ECC_SM2P256V1) {
11113+
/* Native SM2 curve: always use SM2 check. */
11114+
err = sp_ecc_check_key_sm2_256(key->pubkey.x, key->pubkey.y,
11115+
NULL, key->heap);
11116+
}
11117+
else
11118+
#endif
11119+
#ifdef WOLFSSL_SP_384
11120+
if (ecc_sets[key->idx].id == ECC_SECP384R1) {
11121+
err = sp_ecc_check_key_384(key->pubkey.x, key->pubkey.y,
11122+
NULL, key->heap);
11123+
}
11124+
else
11125+
#endif
11126+
#ifdef WOLFSSL_SP_521
11127+
if (ecc_sets[key->idx].id == ECC_SECP521R1) {
11128+
err = sp_ecc_check_key_521(key->pubkey.x, key->pubkey.y,
11129+
NULL, key->heap);
11130+
}
11131+
else
11132+
#endif
11133+
{
11134+
/* Non-SP curve: fall back to generic checks */
11135+
if ((err == MP_OKAY) &&
11136+
wc_ecc_point_is_at_infinity(&key->pubkey)) {
11137+
err = ECC_INF_E;
11138+
}
11139+
if (err == MP_OKAY) {
11140+
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
11141+
}
11142+
}
11143+
}
11144+
#else
11145+
/* Generic checks for all curves */
1101611146
if ((err == MP_OKAY) && wc_ecc_point_is_at_infinity(&key->pubkey)) {
1101711147
err = ECC_INF_E;
1101811148
}
1101911149
#ifdef USE_ECC_B_PARAM
11020-
if ((err == MP_OKAY) && (key->idx != ECC_CUSTOM_IDX)) {
11150+
if ((err == MP_OKAY) && (key->idx != ECC_CUSTOM_IDX)) {
1102111151
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
11152+
#if defined(WOLFSSL_SM2)
11153+
if (err != MP_OKAY && curve_id < 0) {
11154+
/* Retry with SM2 check when default identified curve returns
11155+
* invalid. This is required as in some cases, the SM2 curve is
11156+
* not recognized correctly while parsing the encoded
11157+
* input. In this case, SM2 keys are invalidly identified
11158+
* as SECP256R1 keys. */
11159+
err = wc_ecc_set_curve(key, WOLFSSL_SM2_KEY_BITS / 8,
11160+
ECC_SM2P256V1);
11161+
if (err == MP_OKAY) {
11162+
err = wc_ecc_point_is_on_curve(&key->pubkey, key->idx);
11163+
}
11164+
}
11165+
#endif
1102211166
}
1102311167
#endif /* USE_ECC_B_PARAM */
11168+
#endif /* WOLFSSL_HAVE_SP_ECC */
1102411169
}
1102511170
#endif
1102611171
(void)untrusted;
@@ -11047,7 +11192,8 @@ int wc_ecc_import_x963_ex2(const byte* in, word32 inLen, ecc_key* key,
1104711192
int wc_ecc_import_x963_ex(const byte* in, word32 inLen, ecc_key* key,
1104811193
int curve_id)
1104911194
{
11050-
return wc_ecc_import_x963_ex2(in, inLen, key, curve_id, 0);
11195+
/* treat as untrusted: validate the point is on the curve */
11196+
return wc_ecc_import_x963_ex2(in, inLen, key, curve_id, 1);
1105111197
}
1105211198

1105311199
WOLFSSL_ABI

wolfssl/wolfcrypt/ecc.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@
8484
WOLFSSL_LOCAL int wolfCrypt_FIPS_ECC_sanity(void);
8585
#endif
8686

87-
/* Enable curve B parameter if needed */
88-
#if defined(HAVE_COMP_KEY) || defined(ECC_CACHE_CURVE)
89-
#ifndef USE_ECC_B_PARAM /* Allow someone to force enable */
90-
#define USE_ECC_B_PARAM
91-
#endif
87+
/* Enable curve B parameter for on-curve point validation.
88+
* The b coefficient is present in every compiled-in ecc_set_type entry;
89+
* making it always available lets wc_ecc_point_is_on_curve() run in all
90+
* builds and closes the invalid-curve attack surface. */
91+
#ifndef USE_ECC_B_PARAM
92+
#define USE_ECC_B_PARAM
9293
#endif
9394

9495

0 commit comments

Comments
 (0)