From afb09869f79a451d6b3119c35c9a9702c44d27e6 Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Mon, 25 May 2026 13:56:48 +0200 Subject: [PATCH 1/3] failing test for issue #764 - small-subgroup attack regression test --- tests/ecc_test.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/ecc_test.c b/tests/ecc_test.c index 8e2e8243a..1bcdff0ca 100644 --- a/tests/ecc_test.c +++ b/tests/ecc_test.c @@ -2549,6 +2549,33 @@ static int s_ecc_test_recovery(void) } #endif +#ifdef LTC_ECC_SECP112R2 +/* https://github.com/libtom/libtomcrypt/issues/764 - small-subgroup attack regression test + SECP112R2 has cofactor 4, so the curve contains a subgroup of order 4 outside the prime-order generator subgroup + A point in that small subgroup is on the curve so Test 2 in ltc_ecc_verify_key passes. However n * P != O, so Test 3 must reject it + The point below has order 4: n * P gives -P, not the point at infinity. Therefore, importing it as a public key must fail +*/ +static int s_ecc_issue764(void) +{ + const ltc_ecc_curve *cu; + ecc_key key; + int err; + 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)); + 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; + } + return CRYPT_OK; +} +#endif + int ecc_test(void) { if (ltc_mp.name == NULL) return CRYPT_NOP; @@ -2574,6 +2601,9 @@ int ecc_test(void) DO(s_ecc_issue443_447()); DO(s_ecc_issue630()); DO(s_ecc_issue116()); +#ifdef LTC_ECC_SECP112R2 + DO(s_ecc_issue764()); +#endif #ifdef LTC_ECC_SHAMIR DO(s_ecc_test_shamir()); DO(s_ecc_test_recovery()); From ec7c05f94ea4c7a7e1e4eb79a9eddcc6d7f6be17 Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Mon, 25 May 2026 14:00:31 +0200 Subject: [PATCH 2/3] fix issue #764 as suggested by @yaotushaozhu --- src/pk/ecc/ltc_ecc_verify_key.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pk/ecc/ltc_ecc_verify_key.c b/src/pk/ecc/ltc_ecc_verify_key.c index 7c83d6a85..d196a1dfa 100644 --- a/src/pk/ecc/ltc_ecc_verify_key.c +++ b/src/pk/ecc/ltc_ecc_verify_key.c @@ -40,10 +40,10 @@ int ltc_ecc_verify_key(const ecc_key *key) /* Test 3: does nG = O? (n = order, O = point at infinity, G = public key) */ point = ltc_ecc_new_point(); - if ((err = ltc_ecc_mulmod(order, &(key->pubkey), point, a, prime, 1)) != CRYPT_OK) { goto done1; } + if ((err = ltc_ecc_mulmod(order, &(key->pubkey), point, a, prime, 0)) != CRYPT_OK) { goto done1; } err = ltc_ecc_is_point_at_infinity(point, prime, &inf); - if (err != CRYPT_OK || inf) { + if (err != CRYPT_OK || !inf) { err = CRYPT_ERROR; } else { From bc95a1fd2d9ebd7905d3b8fcb0e7485b43dd968d Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Mon, 25 May 2026 16:18:14 +0200 Subject: [PATCH 3/3] 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 9da8937e6..56fb2342b 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 1bcdff0ca..32f012848 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