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; }