Skip to content

Commit 93ff85c

Browse files
committed
Reject small-order public keys for Edwards and Montgomery curves
A public key that is the identity or another small-order point makes h*A vanish during EdDSA verification, so a forged (R = [S]B, S) pair verifies for any message; for X25519/X448 ECDH the same input yields a low-entropy shared secret the peer fully controls. Honest key generation never produces such keys, but we did not reject them on import or verification. wc_ed{25519,448}_check_key() and ed{25519,448}_verify_msg_final_with_sha() now reject every small-order encoding; wc_curve25519_check_public() rejects the two intermediate u-coordinates the range checks missed; and wc_curve{25519,448}_ import_public_ex() now invoke check_public() on peer-supplied input. New tests are gated on !HAVE_FIPS || FIPS_VERSION3_GE(7,0,0).
1 parent 7cf84dd commit 93ff85c

11 files changed

Lines changed: 623 additions & 37 deletions

File tree

tests/api/test_curve25519.c

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,32 @@ int test_wc_curve25519_import_private_raw_ex(void)
517517
&key, endian), WC_NO_ERR_TRACE(ECC_BAD_ARG_E));
518518
ExpectIntEQ(wc_curve25519_import_private_raw_ex(priv, privSz, pub, 0,
519519
&key, endian), WC_NO_ERR_TRACE(ECC_BAD_ARG_E));
520+
#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)
521+
{
522+
byte privLE[CURVE25519_KEYSIZE];
523+
byte pubLE[CURVE25519_KEYSIZE];
524+
word32 privLESz = sizeof(privLE);
525+
word32 pubLESz = sizeof(pubLE);
526+
527+
/* wc_curve25519_import_public_ex() now calls check_public() on
528+
* the input, so we must export the public half in little-endian
529+
* to round-trip through an LE import. On older FIPS modules
530+
* import_public_ex skipped validation and accepted the
531+
* BE-encoded bytes labelled as LE; we keep that behaviour
532+
* exercised below for those builds. */
533+
ExpectIntEQ(wc_curve25519_export_private_raw_ex(&key, privLE,
534+
&privLESz, EC25519_LITTLE_ENDIAN), 0);
535+
ExpectIntEQ(wc_curve25519_export_public_ex(&key, pubLE, &pubLESz,
536+
EC25519_LITTLE_ENDIAN), 0);
537+
ExpectIntEQ(wc_curve25519_import_private_raw_ex(privLE, privLESz,
538+
pubLE, pubLESz, &key, EC25519_LITTLE_ENDIAN), 0);
539+
}
540+
#else
541+
/* Older FIPS behaviour: BE-encoded pub accepted under LE flag
542+
* because import_public_ex did not run check_public(). */
520543
ExpectIntEQ(wc_curve25519_import_private_raw_ex(priv, privSz, pub, pubSz,
521544
&key, EC25519_LITTLE_ENDIAN), 0);
545+
#endif
522546

523547
DoExpectIntEQ(wc_FreeRng(&rng), 0);
524548
wc_curve25519_free(&key);
@@ -666,3 +690,88 @@ int test_wc_Curve25519KeyToDer_oneasymkey_version(void)
666690
return EXPECT_RESULT();
667691
}
668692

693+
/* Defence-in-depth: wc_curve25519_check_public() must reject every known
694+
* small-order Curve25519 u-coordinate. A small-order public key during
695+
* ECDH leaks information about the private scalar; clamping bounds but
696+
* does not eliminate the leakage. Gated on FIPS_VERSION3_GE(7,0,0)
697+
* because older FIPS-certified modules do not have this check in their
698+
* frozen copy of curve25519.c and would fail this test. */
699+
int test_wc_curve25519_reject_small_order_keys(void)
700+
{
701+
EXPECT_DECLS;
702+
#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \
703+
defined(HAVE_CURVE25519)
704+
/* All seven small-order Curve25519 u-coordinates, little-endian. */
705+
static const byte small_order_u[][CURVE25519_KEYSIZE] = {
706+
/* u = 0 */
707+
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
708+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
709+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
710+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
711+
/* u = 1 */
712+
{0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
713+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
714+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
715+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
716+
/* u = 32558...104 (small order) */
717+
{0xe0,0xeb,0x7a,0x7c,0x3b,0x41,0xb8,0xae,
718+
0x16,0x56,0xe3,0xfa,0xf1,0x9f,0xc4,0x6a,
719+
0xda,0x09,0x8d,0xeb,0x9c,0x32,0xb1,0xfd,
720+
0x86,0x62,0x05,0x16,0x5f,0x49,0xb8,0x00},
721+
/* u = 39382...823 (small order) */
722+
{0x5f,0x9c,0x95,0xbc,0xa3,0x50,0x8c,0x24,
723+
0xb1,0xd0,0xb1,0x55,0x9c,0x83,0xef,0x5b,
724+
0x04,0x44,0x5c,0xc4,0x58,0x1c,0x8e,0x86,
725+
0xd8,0x22,0x4e,0xdd,0xd0,0x9f,0x11,0x57},
726+
/* u = p - 1 */
727+
{0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
728+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
729+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
730+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f},
731+
/* u = p */
732+
{0xed,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
733+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
734+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
735+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f},
736+
/* u = p + 1 */
737+
{0xee,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
738+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
739+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
740+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f},
741+
};
742+
byte be[CURVE25519_KEYSIZE];
743+
word32 i, j;
744+
745+
for (i = 0; i < sizeof(small_order_u) / CURVE25519_KEYSIZE; i++) {
746+
/* Reject in little-endian form. */
747+
ExpectIntEQ(wc_curve25519_check_public(small_order_u[i],
748+
CURVE25519_KEYSIZE, EC25519_LITTLE_ENDIAN),
749+
WC_NO_ERR_TRACE(ECC_BAD_ARG_E));
750+
751+
/* Reject in big-endian form (byte-reversed). */
752+
for (j = 0; j < CURVE25519_KEYSIZE; j++)
753+
be[j] = small_order_u[i][CURVE25519_KEYSIZE - 1 - j];
754+
ExpectIntEQ(wc_curve25519_check_public(be, CURVE25519_KEYSIZE,
755+
EC25519_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E));
756+
}
757+
758+
/* Higher-level entry point: wc_curve25519_import_public_ex() must
759+
* also fail because it calls check_public() during the import. This
760+
* pins that the unit-level guard is reachable through the API real
761+
* callers use. */
762+
#ifdef HAVE_CURVE25519_KEY_IMPORT
763+
{
764+
curve25519_key key;
765+
766+
XMEMSET(&key, 0, sizeof(key));
767+
ExpectIntEQ(wc_curve25519_init(&key), 0);
768+
ExpectIntEQ(wc_curve25519_import_public_ex(small_order_u[0],
769+
CURVE25519_KEYSIZE, &key, EC25519_LITTLE_ENDIAN),
770+
WC_NO_ERR_TRACE(ECC_BAD_ARG_E));
771+
wc_curve25519_free(&key);
772+
}
773+
#endif
774+
#endif
775+
return EXPECT_RESULT();
776+
}
777+

tests/api/test_curve25519.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ int test_wc_curve25519_import_private_raw_ex(void);
3737
int test_wc_curve25519_import_private(void);
3838
int test_wc_curve25519_priv_clamp_check(void);
3939
int test_wc_Curve25519KeyToDer_oneasymkey_version(void);
40+
int test_wc_curve25519_reject_small_order_keys(void);
4041

4142
#define TEST_CURVE25519_DECLS \
4243
TEST_DECL_GROUP("curve25519", test_wc_curve25519_init), \
@@ -51,6 +52,7 @@ int test_wc_Curve25519KeyToDer_oneasymkey_version(void);
5152
TEST_DECL_GROUP("curve25519", test_wc_curve25519_import_private_raw_ex), \
5253
TEST_DECL_GROUP("curve25519", test_wc_curve25519_import_private), \
5354
TEST_DECL_GROUP("curve25519", test_wc_curve25519_priv_clamp_check), \
54-
TEST_DECL_GROUP("curve25519", test_wc_Curve25519KeyToDer_oneasymkey_version)
55+
TEST_DECL_GROUP("curve25519", test_wc_Curve25519KeyToDer_oneasymkey_version), \
56+
TEST_DECL_GROUP("curve25519", test_wc_curve25519_reject_small_order_keys)
5557

5658
#endif /* WOLFCRYPT_TEST_CURVE25519_H */

tests/api/test_ed25519.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,109 @@ int test_wc_Ed25519KeyToDer_oneasymkey_version(void)
725725
return EXPECT_RESULT();
726726
}
727727

728+
/* Ed25519 identity and small-order public keys must be rejected. When
729+
* the public key is the identity point (or any small-order point), any
730+
* signature of the form (R = [S]B, S) verifies for arbitrary messages
731+
* because h*A is the neutral element. Gated on FIPS_VERSION3_GE(7,0,0)
732+
* because older FIPS-certified modules do not have this check in their
733+
* frozen copy of ed25519.c and would fail this test. */
734+
int test_wc_ed25519_reject_small_order_keys(void)
735+
{
736+
EXPECT_DECLS;
737+
#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \
738+
defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT)
739+
/* Each entry holds an encoded small-order Ed25519 public key. The
740+
* sign-bit variants of each y-coordinate are listed explicitly so
741+
* the test catches both possible encodings of each y. */
742+
static const byte small_order_keys[][ED25519_PUB_KEY_SIZE] = {
743+
/* identity (y = 1) */
744+
{0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
745+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
746+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
747+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
748+
/* identity with x-sign bit set */
749+
{0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
750+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
751+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
752+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80},
753+
/* order 2: y = p - 1 */
754+
{0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
755+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
756+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
757+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f},
758+
/* order 4: y = 0 */
759+
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
760+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
761+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
762+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
763+
/* order 4 with x-sign bit set */
764+
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
765+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
766+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
767+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80},
768+
/* order 8 */
769+
{0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0,
770+
0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0,
771+
0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39,
772+
0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x05},
773+
/* order 8 with x-sign bit set */
774+
{0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0,
775+
0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0,
776+
0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39,
777+
0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x85},
778+
/* order 8 (other y) */
779+
{0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f,
780+
0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f,
781+
0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6,
782+
0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0x7a},
783+
/* order 8 (other y) with x-sign bit set */
784+
{0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f,
785+
0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f,
786+
0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6,
787+
0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0xfa},
788+
};
789+
/* Forged signature: R = B (base point), S = 1.
790+
* With public key A = identity, S*B - h*A = B = R for any message. */
791+
static const byte forged_sig[ED25519_SIG_SIZE] = {
792+
0x58,0x66,0x66,0x66,0x66,0x66,0x66,0x66,
793+
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,
794+
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,
795+
0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,
796+
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
797+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
798+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
799+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
800+
};
801+
ed25519_key key;
802+
word32 i;
803+
804+
for (i = 0; i < sizeof(small_order_keys) / ED25519_PUB_KEY_SIZE; i++) {
805+
XMEMSET(&key, 0, sizeof(key));
806+
ExpectIntEQ(wc_ed25519_init(&key), 0);
807+
ExpectIntEQ(wc_ed25519_import_public(small_order_keys[i],
808+
ED25519_PUB_KEY_SIZE, &key),
809+
WC_NO_ERR_TRACE(PUBLIC_KEY_E));
810+
wc_ed25519_free(&key);
811+
}
812+
813+
/* Defence-in-depth: even a "trusted" import (which bypasses
814+
* wc_ed25519_check_key) must not let wc_ed25519_verify_msg accept a
815+
* forged signature against an identity public key. */
816+
{
817+
const char* msg = "forged message";
818+
int verify_result = 1;
819+
820+
XMEMSET(&key, 0, sizeof(key));
821+
ExpectIntEQ(wc_ed25519_init(&key), 0);
822+
ExpectIntEQ(wc_ed25519_import_public_ex(small_order_keys[0],
823+
ED25519_PUB_KEY_SIZE, &key, 1), 0);
824+
ExpectIntEQ(wc_ed25519_verify_msg(forged_sig, sizeof(forged_sig),
825+
(const byte*)msg, (word32)XSTRLEN(msg), &verify_result, &key),
826+
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
827+
ExpectIntEQ(verify_result, 0);
828+
wc_ed25519_free(&key);
829+
}
830+
#endif
831+
return EXPECT_RESULT();
832+
}
833+

tests/api/test_ed25519.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ int test_wc_Ed25519PublicKeyToDer(void);
3737
int test_wc_Ed25519KeyToDer(void);
3838
int test_wc_Ed25519PrivateKeyToDer(void);
3939
int test_wc_Ed25519KeyToDer_oneasymkey_version(void);
40+
int test_wc_ed25519_reject_small_order_keys(void);
4041

4142
#define TEST_ED25519_DECLS \
4243
TEST_DECL_GROUP("ed25519", test_wc_ed25519_make_key), \
@@ -51,6 +52,7 @@ int test_wc_Ed25519KeyToDer_oneasymkey_version(void);
5152
TEST_DECL_GROUP("ed25519", test_wc_Ed25519PublicKeyToDer), \
5253
TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer), \
5354
TEST_DECL_GROUP("ed25519", test_wc_Ed25519PrivateKeyToDer), \
54-
TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer_oneasymkey_version)
55+
TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer_oneasymkey_version), \
56+
TEST_DECL_GROUP("ed25519", test_wc_ed25519_reject_small_order_keys)
5557

5658
#endif /* WOLFCRYPT_TEST_ED25519_H */

0 commit comments

Comments
 (0)