From 25a1a204447338a4296a2fce11f46606c3a2fc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 27 May 2026 16:37:11 +0200 Subject: [PATCH] Reject small-order public keys for Ed25519 and Ed448 Add defense-in-depth checks to wc_ed{25519,448}_check_key() and ed{25519,448}_verify_msg_final_with_sha() that reject the identity point and other small-order public keys. Honest EdDSA key generation never produces such keys, but wolfSSL previously accepted them on import and verification. The guard runs at both entry points so it holds even when a key is imported with trusted=1. New tests are gated on !HAVE_FIPS || FIPS_VERSION3_GE(7,0,0). --- tests/api/test_ed25519.c | 181 ++++++++++++++++++++++++++++++++ tests/api/test_ed25519.h | 4 +- tests/api/test_ed448.c | 221 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ed448.h | 4 +- wolfcrypt/src/ed25519.c | 75 ++++++++++++- wolfcrypt/src/ed448.c | 110 ++++++++++++++----- wolfcrypt/test/test.c | 67 +++++++++++- 7 files changed, 626 insertions(+), 36 deletions(-) diff --git a/tests/api/test_ed25519.c b/tests/api/test_ed25519.c index 2df8d41270..e68505b862 100644 --- a/tests/api/test_ed25519.c +++ b/tests/api/test_ed25519.c @@ -725,3 +725,184 @@ int test_wc_Ed25519KeyToDer_oneasymkey_version(void) return EXPECT_RESULT(); } +/* Ed25519 identity and small-order public keys must be rejected. When + * the public key is the identity point (or any small-order point), any + * signature of the form (R = [S]B, S) verifies for arbitrary messages + * because h*A is the neutral element. Gated on FIPS_VERSION3_GE(7,0,0) + * because older FIPS-certified modules do not have this check in their + * frozen copy of ed25519.c and would fail this test. */ +int test_wc_ed25519_reject_small_order_keys(void) +{ + EXPECT_DECLS; +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT) + /* Each entry holds an encoded small-order Ed25519 public key. The + * sign-bit variants of each y-coordinate are listed explicitly so + * the test catches both possible encodings of each y. */ + static const byte small_order_keys[][ED25519_PUB_KEY_SIZE] = { + /* identity (y = 1) */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* identity with x-sign bit set */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80}, + /* order 2: y = p - 1 */ + {0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + /* order 2: y = p - 1 with x-sign bit set */ + {0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff}, + /* non-canonical y = p (decodes to y = 0) */ + {0xed,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + /* non-canonical y = p with x-sign bit set */ + {0xed,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff}, + /* non-canonical y = p + 1 (decodes to y = 1) */ + {0xee,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + /* non-canonical y = p + 1 with x-sign bit set */ + {0xee,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff}, + /* order 4: y = 0 */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* order 4 with x-sign bit set */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80}, + /* order 8 */ + {0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0, + 0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0, + 0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39, + 0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x05}, + /* order 8 with x-sign bit set */ + {0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0, + 0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0, + 0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39, + 0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x85}, + /* order 8 (other y) */ + {0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f, + 0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f, + 0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6, + 0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0x7a}, + /* order 8 (other y) with x-sign bit set */ + {0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f, + 0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f, + 0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6, + 0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0xfa}, + }; + /* Forged signature: R = B (base point), S = 1. + * With public key A = identity, S*B - h*A = B = R for any message. */ + static const byte forged_sig[ED25519_SIG_SIZE] = { + 0x58,0x66,0x66,0x66,0x66,0x66,0x66,0x66, + 0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, + 0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, + 0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + }; + ed25519_key key; + word32 i; + word32 num_keys = (word32)(sizeof(small_order_keys) / ED25519_PUB_KEY_SIZE); + + /* (1) Untrusted wc_ed25519_import_public must reject every small-order + * encoding (it runs wc_ed25519_check_key as part of the import). */ + for (i = 0; i < num_keys; i++) { + int rc; + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed25519_init(&key), 0); + rc = wc_ed25519_import_public(small_order_keys[i], + ED25519_PUB_KEY_SIZE, &key); + if (rc != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + fprintf(stderr, "small_order_keys[%u]: import_public returned %d, " + "expected PUBLIC_KEY_E\n", (unsigned)i, rc); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + wc_ed25519_free(&key); + } + + /* (2) wc_ed25519_check_key called directly must also reject. Guards + * against a refactor that moves the small-order check out of + * check_key and into the import path: (1) would still pass, but the + * documented check_key contract would silently regress. */ + for (i = 0; i < num_keys; i++) { + int rc; + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed25519_init(&key), 0); + /* trusted = 1 bypasses the import-time check_key call so the + * direct check_key below is what's under test. */ + ExpectIntEQ(wc_ed25519_import_public_ex(small_order_keys[i], + ED25519_PUB_KEY_SIZE, &key, 1), 0); + rc = wc_ed25519_check_key(&key); + if (rc != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + fprintf(stderr, "small_order_keys[%u]: check_key returned %d, " + "expected PUBLIC_KEY_E\n", (unsigned)i, rc); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + wc_ed25519_free(&key); + } + + /* (3) Even a "trusted" import (which bypasses wc_ed25519_check_key) + * must not let wc_ed25519_verify_msg accept a forged signature against + * an identity public key. Test both the canonical encoding (y = 1, + * small_order_keys[0]) and the non-canonical encoding (y = p + 1, + * small_order_keys[6]) so the verify-side check is exercised against + * the canonical-form bypass route, not just the byte-for-byte + * identity. The forged sig (R = B, S = 1) verifies for an identity + * public key only - other small-order points would reject it on the + * math alone, so they aren't useful here. */ + { + static const word32 identity_indices[] = { 0, 6 }; + const char* msg = "forged message"; + word32 j; + + for (j = 0; + j < sizeof(identity_indices)/sizeof(identity_indices[0]); + j++) { + word32 idx = identity_indices[j]; + int verify_result = 1; + int rc; + + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed25519_init(&key), 0); + ExpectIntEQ(wc_ed25519_import_public_ex(small_order_keys[idx], + ED25519_PUB_KEY_SIZE, &key, 1), 0); + rc = wc_ed25519_verify_msg(forged_sig, sizeof(forged_sig), + (const byte*)msg, (word32)XSTRLEN(msg), &verify_result, &key); + if (rc != WC_NO_ERR_TRACE(BAD_FUNC_ARG) || verify_result != 0) { + fprintf(stderr, "verify_msg with identity-equiv " + "small_order_keys[%u]: rc=%d verify_result=%d " + "(expected BAD_FUNC_ARG and 0)\n", + (unsigned)idx, rc, verify_result); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_result, 0); + wc_ed25519_free(&key); + } + } +#endif + return EXPECT_RESULT(); +} + diff --git a/tests/api/test_ed25519.h b/tests/api/test_ed25519.h index 4ac3693f66..a14b2a4496 100644 --- a/tests/api/test_ed25519.h +++ b/tests/api/test_ed25519.h @@ -37,6 +37,7 @@ int test_wc_Ed25519PublicKeyToDer(void); int test_wc_Ed25519KeyToDer(void); int test_wc_Ed25519PrivateKeyToDer(void); int test_wc_Ed25519KeyToDer_oneasymkey_version(void); +int test_wc_ed25519_reject_small_order_keys(void); #define TEST_ED25519_DECLS \ TEST_DECL_GROUP("ed25519", test_wc_ed25519_make_key), \ @@ -51,6 +52,7 @@ int test_wc_Ed25519KeyToDer_oneasymkey_version(void); TEST_DECL_GROUP("ed25519", test_wc_Ed25519PublicKeyToDer), \ TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer), \ TEST_DECL_GROUP("ed25519", test_wc_Ed25519PrivateKeyToDer), \ - TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer_oneasymkey_version) + TEST_DECL_GROUP("ed25519", test_wc_Ed25519KeyToDer_oneasymkey_version), \ + TEST_DECL_GROUP("ed25519", test_wc_ed25519_reject_small_order_keys) #endif /* WOLFCRYPT_TEST_ED25519_H */ diff --git a/tests/api/test_ed448.c b/tests/api/test_ed448.c index f6b7552302..6bb7934615 100644 --- a/tests/api/test_ed448.c +++ b/tests/api/test_ed448.c @@ -649,3 +649,224 @@ int test_wc_Ed448KeyToDer_oneasymkey_version(void) return EXPECT_RESULT(); } +/* Ed448 identity and small-order public keys must be rejected. + * Edwards448 has cofactor 4, so the small-order subgroup contains the + * identity, an order-2 point, and two order-4 points. With any of these + * as the public key, h*A is the neutral element and forged signatures + * verify for arbitrary messages. Gated on FIPS_VERSION3_GE(7,0,0) + * because older FIPS-certified modules do not have this check in their + * frozen copy of ed448.c. */ +int test_wc_ed448_reject_small_order_keys(void) +{ + EXPECT_DECLS; +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(HAVE_ED448) && defined(HAVE_ED448_KEY_IMPORT) + /* Two regressions are guarded here. Both sign-bit variants of each + * y are listed so weakening the "clear all of byte 56" mask in + * ed448_is_small_order() would be caught. The non-canonical rows + * (y = p, y = p + 1) guard against dropping the canonical-form + * coverage: fe448_from_bytes reads bytes 0-55 modulo p with no + * canonical-form check, so y = p decodes to 0 and y = p + 1 + * decodes to 1, both of which are small order. */ + static const byte small_order_keys[][ED448_PUB_KEY_SIZE] = { + /* identity (y = 1), sign 0 */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00}, + /* identity (y = 1), sign bit set */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80}, + /* order 4: y = 0, x-sign 0 */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00}, + /* order 4: y = 0, x-sign 1 */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80}, + /* order 2: y = p - 1, x = 0, sign 0 */ + {0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + /* order 2: y = p - 1, sign bit set */ + {0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x80}, + /* non-canonical y = p (decodes to y = 0), sign 0 */ + {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + /* non-canonical y = p, sign bit set */ + {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x80}, + /* non-canonical y = p + 1 (decodes to y = 1), sign 0 */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + /* non-canonical y = p + 1, sign bit set */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x80}, + }; + /* Arbitrary signature bytes: S = 1 (must be below the Ed448 group + * order or wc_ed448_verify_msg() returns BAD_FUNC_ARG before the + * small-order check has a chance to fire). The R bytes do not need + * to encode a valid curve point for this test - the small-order + * defence in ed448_verify_msg_final_with_sha() rejects the public + * key before the R/S verification math runs. */ + static const byte forged_sig[ED448_SIG_SIZE] = { + /* R: 57 bytes of arbitrary data (last byte 0 to satisfy the + * spec-mandated zero of byte 56 bits 0-6; sign bit doesn't + * matter here). */ + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00, + /* S = 1 */ + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00 + }; + ed448_key key; + word32 i; + word32 num_keys = (word32)(sizeof(small_order_keys) / ED448_PUB_KEY_SIZE); + + /* (1) Untrusted wc_ed448_import_public must reject every small-order + * encoding (it runs wc_ed448_check_key as part of the import). */ + for (i = 0; i < num_keys; i++) { + int rc; + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed448_init(&key), 0); + rc = wc_ed448_import_public(small_order_keys[i], + ED448_PUB_KEY_SIZE, &key); + if (rc != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + fprintf(stderr, "small_order_keys[%u]: import_public returned %d, " + "expected PUBLIC_KEY_E\n", (unsigned)i, rc); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + wc_ed448_free(&key); + } + + /* (2) wc_ed448_check_key called directly must also reject. Guards + * against a refactor that moves the small-order check out of + * check_key and into the import path: (1) would still pass, but the + * documented check_key contract would silently regress. */ + for (i = 0; i < num_keys; i++) { + int rc; + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed448_init(&key), 0); + /* trusted = 1 bypasses the import-time check_key call so the + * direct check_key below is what's under test. */ + ExpectIntEQ(wc_ed448_import_public_ex(small_order_keys[i], + ED448_PUB_KEY_SIZE, &key, 1), 0); + rc = wc_ed448_check_key(&key); + if (rc != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + fprintf(stderr, "small_order_keys[%u]: check_key returned %d, " + "expected PUBLIC_KEY_E\n", (unsigned)i, rc); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + wc_ed448_free(&key); + } + + /* (3) Even a "trusted" import (which bypasses wc_ed448_check_key) + * must not let wc_ed448_verify_msg accept a forged signature against + * an identity public key. Test both the canonical encoding (y = 1, + * small_order_keys[0]) and the non-canonical encoding (y = p + 1, + * small_order_keys[8]) so the verify-side check is exercised against + * the canonical-form bypass route, not just the byte-for-byte + * identity. */ + { + static const word32 identity_indices[] = { 0, 8 }; + const char* msg = "forged message"; + word32 j; + + for (j = 0; + j < sizeof(identity_indices)/sizeof(identity_indices[0]); + j++) { + word32 idx = identity_indices[j]; + int verify_result = 1; + int rc; + + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_ed448_import_public_ex(small_order_keys[idx], + ED448_PUB_KEY_SIZE, &key, 1), 0); + rc = wc_ed448_verify_msg(forged_sig, sizeof(forged_sig), + (const byte*)msg, (word32)XSTRLEN(msg), &verify_result, + &key, NULL, 0); + if (rc != WC_NO_ERR_TRACE(BAD_FUNC_ARG) || verify_result != 0) { + fprintf(stderr, "verify_msg with identity-equiv " + "small_order_keys[%u]: rc=%d verify_result=%d " + "(expected BAD_FUNC_ARG and 0)\n", + (unsigned)idx, rc, verify_result); + } + ExpectIntEQ(rc, WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_result, 0); + wc_ed448_free(&key); + } + } +#endif + return EXPECT_RESULT(); +} + diff --git a/tests/api/test_ed448.h b/tests/api/test_ed448.h index 68d85a6338..a40edd17bb 100644 --- a/tests/api/test_ed448.h +++ b/tests/api/test_ed448.h @@ -37,6 +37,7 @@ int test_wc_Ed448PublicKeyToDer(void); int test_wc_Ed448KeyToDer(void); int test_wc_Ed448PrivateKeyToDer(void); int test_wc_Ed448KeyToDer_oneasymkey_version(void); +int test_wc_ed448_reject_small_order_keys(void); #define TEST_ED448_DECLS \ TEST_DECL_GROUP("ed448", test_wc_ed448_make_key), \ @@ -51,6 +52,7 @@ int test_wc_Ed448KeyToDer_oneasymkey_version(void); TEST_DECL_GROUP("ed448", test_wc_Ed448PublicKeyToDer), \ TEST_DECL_GROUP("ed448", test_wc_Ed448KeyToDer), \ TEST_DECL_GROUP("ed448", test_wc_Ed448PrivateKeyToDer), \ - TEST_DECL_GROUP("ed448", test_wc_Ed448KeyToDer_oneasymkey_version) + TEST_DECL_GROUP("ed448", test_wc_Ed448KeyToDer_oneasymkey_version), \ + TEST_DECL_GROUP("ed448", test_wc_ed448_reject_small_order_keys) #endif /* WOLFCRYPT_TEST_ED448_H */ diff --git a/wolfcrypt/src/ed25519.c b/wolfcrypt/src/ed25519.c index de12e87b83..b84d7e9b15 100644 --- a/wolfcrypt/src/ed25519.c +++ b/wolfcrypt/src/ed25519.c @@ -205,6 +205,64 @@ static int ed25519_hash(ed25519_key* key, const byte* in, word32 inLen, return ret; } +/* Reject small-order Ed25519 public keys: h*A vanishes during verification + * so any (R = [S]B, S) verifies for an arbitrary message. */ +static int ed25519_is_small_order(const byte p[ED25519_PUB_KEY_SIZE]) +{ + /* y-coordinates of every order-1/2/4/8 point plus the two non-canonical + * encodings y = p / y = p+1. Sign bit masked before compare. Only + * {y, y + p} fits in 32 bytes (2p overflows the 255-bit y field), so + * listing y and y + p exhausts the reachable encodings for each + * small-order y. */ + static const byte small_order_y[][ED25519_PUB_KEY_SIZE] = { + /* order 4: y = 0 */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* order 1: y = 1 (identity) */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* order 8 */ + {0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0, + 0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0, + 0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39, + 0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x05}, + /* order 8 */ + {0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f, + 0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f, + 0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6, + 0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0x7a}, + /* order 2: y = p - 1 */ + {0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + /* non-canonical y = p (decodes to y = 0) */ + {0xed,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + /* non-canonical y = p + 1 (decodes to y = 1) */ + {0xee,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f}, + }; + byte y[ED25519_PUB_KEY_SIZE]; + word32 i; + + XMEMCPY(y, p, ED25519_PUB_KEY_SIZE); + y[ED25519_PUB_KEY_SIZE - 1] &= 0x7f; + for (i = 0; i < sizeof(small_order_y) / ED25519_PUB_KEY_SIZE; i++) { + if (XMEMCMP(y, small_order_y[i], ED25519_PUB_KEY_SIZE) == 0) + return 1; + } + return 0; +} + #ifdef HAVE_ED25519_MAKE_KEY #if FIPS_VERSION3_GE(6,0,0) /* Performs a Pairwise Consistency Test on an Ed25519 key pair. @@ -808,6 +866,13 @@ static int ed25519_verify_msg_final_with_sha(const byte* sig, word32 sigLen, if (i == -1) return BAD_FUNC_ARG; + /* Defence in depth: also catch small-order keys imported with trusted=1. */ + if (ed25519_is_small_order(key->p)) { + WOLFSSL_MSG("Ed25519 small-order public key rejected during " + "signature verification"); + return BAD_FUNC_ARG; + } + /* uncompress A (public key), test if valid, and negate it */ #ifndef FREESCALE_LTC_ECC if (ge_frombytes_negate_vartime(&A, key->p) != 0) @@ -1502,6 +1567,13 @@ int wc_ed25519_check_key(ed25519_key* key) ret = PUBLIC_KEY_E; } + /* Reject small-order pub key before the priv-vs-pub compare so the + * diagnostic isn't masked by a "mismatch" error. */ + if ((ret == 0) && ed25519_is_small_order(key->p)) { + WOLFSSL_MSG("Ed25519 small-order public key rejected during key check"); + ret = PUBLIC_KEY_E; + } + #ifdef HAVE_ED25519_MAKE_KEY /* If we have a private key just make the public key and compare. */ if ((ret == 0) && (key->privKeySet)) { @@ -1521,9 +1593,6 @@ int wc_ed25519_check_key(ed25519_key* key) && (!key->privKeySet) #endif ) { - /* Verify that Q is not identity element 0. - * 0 has no representation for Ed25519. */ - /* Verify that xQ and yQ are integers in the interval [0, p - 1]. * Only have yQ so check that ordinate. p = 2^255 - 19 */ if ((key->p[ED25519_PUB_KEY_SIZE - 1] & 0x7f) == 0x7f) { diff --git a/wolfcrypt/src/ed448.c b/wolfcrypt/src/ed448.c index 72f1724856..d6c35c6245 100644 --- a/wolfcrypt/src/ed448.c +++ b/wolfcrypt/src/ed448.c @@ -238,6 +238,77 @@ static int ed448_pairwise_consistency_test(ed448_key* key, WC_RNG* rng) } #endif +/* Reject small-order Ed448 public keys: h*A vanishes during verification + * so any (R = [S]B, S) verifies for an arbitrary message. Cofactor is 4. */ +static int ed448_is_small_order(const byte p[ED448_PUB_KEY_SIZE]) +{ + /* y-coordinates of every order-1/2/4 point plus the non-canonical + * encodings y = p / y = p+1. Byte 56 is cleared in both table and + * input before compare, masking the x-sign bit and the + * spec-mandated-zero (but decoder-ignored) bits 0-6. The decoder + * (fe448_from_bytes) reads bytes 0-55 modulo p with no canonical-form + * check, so y = p decodes to 0 and y = p+1 decodes to 1; both must + * be rejected here. Only {y, y + p} fits in 56 bytes (2p overflows), + * so listing y and y + p exhausts the reachable encodings. */ + static const byte small_order_y[][ED448_PUB_KEY_SIZE] = { + /* order 1: identity y = 1, x = 0 */ + {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00}, + /* order 4: y = 0 (x = +/-1; sign bit covered by mask) */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00}, + /* order 2: y = p - 1, x = 0; p = 2^448 - 2^224 - 1 */ + {0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + /* non-canonical y = p (decodes to y = 0) */ + {0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xfe,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + /* non-canonical y = p + 1 (decodes to y = 1) */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00}, + }; + byte y[ED448_PUB_KEY_SIZE]; + word32 i; + + XMEMCPY(y, p, ED448_PUB_KEY_SIZE); + y[ED448_PUB_KEY_SIZE - 1] = 0; + for (i = 0; i < sizeof(small_order_y) / ED448_PUB_KEY_SIZE; i++) { + if (XMEMCMP(y, small_order_y[i], ED448_PUB_KEY_SIZE) == 0) + return 1; + } + return 0; +} + /* Derive the public key for the private key. * * key [in] Ed448 key object. @@ -731,16 +802,11 @@ static int ed448_verify_msg_final_with_sha(const byte* sig, word32 sigLen, if (i == -1) return BAD_FUNC_ARG; - /* Reject identity public key (0,1): 0x01 followed by 56 zero bytes. */ - { - int isIdentity = (key->p[0] == 0x01); - int j; - for (j = 1; j < ED448_PUB_KEY_SIZE && isIdentity; j++) { - if (key->p[j] != 0x00) - isIdentity = 0; - } - if (isIdentity) - return BAD_FUNC_ARG; + /* Defence in depth: also catch small-order keys imported with trusted=1. */ + if (ed448_is_small_order(key->p)) { + WOLFSSL_MSG("Ed448 small-order public key rejected during " + "signature verification"); + return BAD_FUNC_ARG; } /* uncompress A (public key), test if valid, and negate it */ @@ -1412,6 +1478,13 @@ int wc_ed448_check_key(ed448_key* key) ret = PUBLIC_KEY_E; } + /* Reject small-order pub key before the priv-vs-pub compare so the + * diagnostic isn't masked by a "mismatch" error. */ + if ((ret == 0) && ed448_is_small_order(key->p)) { + WOLFSSL_MSG("Ed448 small-order public key rejected during key check"); + ret = PUBLIC_KEY_E; + } + /* If we have a private key just make the public key and compare. */ if ((ret == 0) && key->privKeySet) { ret = wc_ed448_make_public(key, pubKey, sizeof(pubKey)); @@ -1421,23 +1494,6 @@ int wc_ed448_check_key(ed448_key* key) } /* No private key, check Y is valid. */ else if ((ret == 0) && (!key->privKeySet)) { - /* Reject the identity element (0, 1). - * Encoding: 0x01 followed by 56 zero bytes. */ - { - int isIdentity = 1; - int i; - if (key->p[0] != 0x01) - isIdentity = 0; - for (i = 1; i < ED448_PUB_KEY_SIZE && isIdentity; i++) { - if (key->p[i] != 0x00) - isIdentity = 0; - } - if (isIdentity) { - WOLFSSL_MSG("Ed448 public key is the identity element"); - ret = PUBLIC_KEY_E; - } - } - /* Verify that xQ and yQ are integers in the interval [0, p - 1]. * Only have yQ so check that ordinate. * p = 2^448-2^224-1 = 0xff..fe..ff diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 329c88cffd..116547b8dd 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -44767,7 +44767,9 @@ static wc_test_ret_t ed25519_test_check_key(void) 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f, }; - /* Y-ordinate value equal to prime - 1. */ + /* Y-ordinate value equal to prime - 1. Older FIPS modules accept + * this as a valid key; the current source rejects it as an order-2 + * point. */ WOLFSSL_SMALL_STACK_STATIC const byte key_y_is_p_minus_1[] = { 0x40, 0xec,0xff,0xff,0xff,0xff,0xff,0xff,0xff, @@ -44775,6 +44777,15 @@ static wc_test_ret_t ed25519_test_check_key(void) 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f, }; + /* RFC 8032 section 7.1 test-vector public key: a genuinely valid + * Ed25519 point used as a positive control. */ + WOLFSSL_SMALL_STACK_STATIC const byte key_good[] = { + 0x40, + 0xd7,0x5a,0x98,0x01,0x82,0xb1,0x0a,0xb7, + 0xd5,0x4b,0xfe,0xd3,0xc9,0x64,0x07,0x3a, + 0x0e,0xe1,0x72,0xf3,0xda,0xa6,0x23,0x25, + 0xaf,0x02,0x1a,0x68,0xf7,0x07,0x51,0x1a, + }; ed25519_key key; int ret; int res = 0; @@ -44807,9 +44818,26 @@ static wc_test_ret_t ed25519_test_check_key(void) } } if (res == 0) { - /* Load good public key only and perform checks. */ ret = wc_ed25519_import_public(key_y_is_p_minus_1, ED25519_PUB_KEY_SIZE + 1, &key); +#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0) + /* y = p - 1 is an order-2 point; check_key rejects it because + * h*A is the neutral element for small-order public keys and + * forged signatures would otherwise verify. */ + if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + res = WC_TEST_RET_ENC_NC; + } +#else + /* Older FIPS modules accept this order-2 point. */ + if (ret != 0) { + res = WC_TEST_RET_ENC_NC; + } +#endif + } + if (res == 0) { + /* Positive control: a real Ed25519 public key must be accepted. */ + ret = wc_ed25519_import_public(key_good, ED25519_PUB_KEY_SIZE + 1, + &key); if (ret != 0) { res = WC_TEST_RET_ENC_NC; } @@ -46484,7 +46512,9 @@ static wc_test_ret_t ed448_test_check_key(void) 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, 0xff }; - /* Y-ordinate value equal to prime - 1. */ + /* Y-ordinate value equal to prime - 1. Older FIPS modules accept + * this as a valid key; the current source rejects it as an order-2 + * point. */ WOLFSSL_SMALL_STACK_STATIC const byte key_y_is_p_minus_1[] = { 0x40, 0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff, @@ -46496,6 +46526,19 @@ static wc_test_ret_t ed448_test_check_key(void) 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, 0xff }; + /* RFC 8032 section 7.4 test-vector public key: a genuinely valid + * Ed448 point used as a positive control. */ + WOLFSSL_SMALL_STACK_STATIC const byte key_good[] = { + 0x40, + 0x5f,0xd7,0x44,0x9b,0x59,0xb4,0x61,0xfd, + 0x2c,0xe7,0x87,0xec,0x61,0x6a,0xd4,0x6a, + 0x1d,0xa1,0x34,0x24,0x85,0xa7,0x0e,0x1f, + 0x8a,0x0e,0xa7,0x5d,0x80,0xe9,0x67,0x78, + 0xed,0xf1,0x24,0x76,0x9b,0x46,0xc7,0x06, + 0x1b,0xd6,0x78,0x3d,0xf1,0xe5,0x0f,0x6c, + 0xd1,0xfa,0x1a,0xbe,0xaf,0xe8,0x25,0x61, + 0x80 + }; ed448_key key; int ret; int res = 0; @@ -46528,9 +46571,25 @@ static wc_test_ret_t ed448_test_check_key(void) } } if (res == 0) { - /* Load good public key only and perform checks. */ ret = wc_ed448_import_public(key_y_is_p_minus_1, ED448_PUB_KEY_SIZE + 1, &key); +#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0) + /* y = p - 1 is an order-2 point; check_key rejects it because + * h*A is the neutral element for small-order public keys and + * forged signatures would otherwise verify. */ + if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + res = WC_TEST_RET_ENC_NC; + } +#else + /* Older FIPS modules accept this order-2 point. */ + if (ret != 0) { + res = WC_TEST_RET_ENC_NC; + } +#endif + } + if (res == 0) { + /* Positive control: a real Ed448 public key must be accepted. */ + ret = wc_ed448_import_public(key_good, ED448_PUB_KEY_SIZE + 1, &key); if (ret != 0) { res = WC_TEST_RET_ENC_NC; }