From bc95a1fd2d9ebd7905d3b8fcb0e7485b43dd968d Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Mon, 25 May 2026 16:18:14 +0200 Subject: [PATCH] hardening ecc_shared_secret against small-subgroup attacks --- src/pk/ecc/ecc_shared_secret.c | 15 ++++++++++++--- tests/ecc_test.c | 28 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/pk/ecc/ecc_shared_secret.c b/src/pk/ecc/ecc_shared_secret.c index 9da8937e..56fb2342 100644 --- a/src/pk/ecc/ecc_shared_secret.c +++ b/src/pk/ecc/ecc_shared_secret.c @@ -23,8 +23,8 @@ int ecc_shared_secret(const ecc_key *private_key, const ecc_key *public_key, { unsigned long x; ecc_point *result; - void *prime, *a; - int err; + void *prime, *a, *mp = NULL; + int err, inf; LTC_ARGCHK(private_key != NULL); LTC_ARGCHK(public_key != NULL); @@ -59,7 +59,15 @@ int ecc_shared_secret(const ecc_key *private_key, const ecc_key *public_key, prime = private_key->dp.prime; a = private_key->dp.A; - if ((err = ltc_mp.ecc_ptmul(private_key->k, &public_key->pubkey, result, a, prime, 1)) != CRYPT_OK) { goto done; } + if ((err = ltc_mp.ecc_ptmul(private_key->k, &public_key->pubkey, result, a, prime, 0)) != CRYPT_OK) { goto done; } + + /* reject small-subgroup / invalid-curve attacks: k*pubkey must not be O */ + if ((err = ltc_ecc_is_point_at_infinity(result, prime, &inf)) != CRYPT_OK) { goto done; } + if (inf) { err = CRYPT_ERROR; goto done; } + + /* map=0 above kept z meaningful for the infinity check; now finish the affine map */ + if ((err = ltc_mp_montgomery_setup(prime, &mp)) != CRYPT_OK) { goto done; } + if ((err = ltc_mp.ecc_map(result, prime, mp)) != CRYPT_OK) { goto done; } x = (unsigned long)ltc_mp_unsigned_bin_size(prime); if (*outlen < x) { @@ -73,6 +81,7 @@ int ecc_shared_secret(const ecc_key *private_key, const ecc_key *public_key, err = CRYPT_OK; *outlen = x; done: + if (mp != NULL) ltc_mp_montgomery_free(mp); ltc_ecc_del_point(result); return err; } diff --git a/tests/ecc_test.c b/tests/ecc_test.c index 1bcdff0c..32f01284 100644 --- a/tests/ecc_test.c +++ b/tests/ecc_test.c @@ -2558,20 +2558,46 @@ static int s_ecc_test_recovery(void) static int s_ecc_issue764(void) { const ltc_ecc_curve *cu; - ecc_key key; + ecc_key key, priv; int err; + unsigned char shared[14]; + unsigned long sharedlen; const unsigned char pub[] = { 0x04, /* uncompressed */ 0xB1,0xFD,0x8D,0xE1,0x27,0xD4,0x65,0x6B,0x57,0x3E,0xB5,0x13,0x98,0x4C, /* x = B1FD8DE127D4656B573EB513984C */ 0x2F,0x8C,0xD8,0x80,0x3D,0xB9,0x62,0x0F,0xA3,0xA6,0x0E,0x5B,0x31,0xE2 /* y = 2F8CD8803DB9620FA3A60E5B31E2 */ }; DO(ecc_find_curve("SECP112R2", &cu)); + + /* ecc_set_key must reject the malicious public key (verify_key Test 3) */ DO(ecc_set_curve(cu, &key)); err = ecc_set_key(pub, sizeof(pub), PK_PUBLIC, &key); /* must fail */ if (err == CRYPT_OK) { ecc_free(&key); return CRYPT_FAIL_TESTVECTOR; } + + /* ecc_shared_secret must reject the malicious peer key even when the caller bypassed ecc_set_key by directly loading the pubkey + priv.k = 4 (a multiple of the malicious point's order 4) so that k * pubkey is deterministically the point at infinity + */ + DO(ecc_set_curve(cu, &priv)); + DO(ltc_mp_set(priv.k, 4)); + priv.type = PK_PRIVATE; + + /* load the malicious point directly to simulate an attacker-supplied pubkey reaching ecc_shared_secret without import checks */ + DO(ecc_set_curve(cu, &key)); + DO(ltc_mp_read_unsigned_bin(key.pubkey.x, (unsigned char *)pub + 1, 14)); + DO(ltc_mp_read_unsigned_bin(key.pubkey.y, (unsigned char *)pub + 15, 14)); + DO(ltc_mp_set(key.pubkey.z, 1)); + key.type = PK_PUBLIC; + + sharedlen = sizeof(shared); + err = ecc_shared_secret(&priv, &key, shared, &sharedlen); /* must fail */ + ecc_free(&priv); + ecc_free(&key); + if (err == CRYPT_OK) { + return CRYPT_FAIL_TESTVECTOR; + } return CRYPT_OK; } #endif