diff --git a/core/ed25519_verify.c b/core/ed25519_verify.c index 5040122..6d06748 100644 --- a/core/ed25519_verify.c +++ b/core/ed25519_verify.c @@ -335,6 +335,34 @@ static void scalarbase(gf r[4], const uint8_t *s) scalarmult(r, q, s); } +static int point_is_identity(gf p[4]) +{ + uint8_t encoded[32]; + point_pack(encoded, p); + + uint8_t diff = (uint8_t)(encoded[0] ^ 1U); + for (int i = 1; i < 32; i++) + diff |= encoded[i]; + return diff == 0; +} + +/* Public keys must be non-identity points in Ed25519's prime-order subgroup. + * Merely decoding a point is insufficient: an identity or torsion key can + * make the verification equation true without knowledge of a private key. */ +static int public_key_is_valid_subgroup(gf public_key[4]) +{ + uint8_t order_l[32]; + gf q[4], multiple[4]; + + for (int i = 0; i < 32; i++) + order_l[i] = (uint8_t)ORDER_L[i]; + for (int i = 0; i < 4; i++) + fe_copy16(q[i], public_key[i]); + + scalarmult(multiple, q, order_l); + return point_is_identity(multiple) && !point_is_identity(public_key); +} + /* Decode a compressed point into -P (the negation is what verification wants). */ static int unpackneg(gf r[4], const uint8_t p[32]) { @@ -465,6 +493,8 @@ int eos_ed25519_verify(const uint8_t signature[64], gf A[4]; if (unpackneg(A, public_key) != EOS_OK) return EOS_ERR_SIGNATURE; + if (!public_key_is_valid_subgroup(A)) + return EOS_ERR_SIGNATURE; /* On the curve is not enough: a low-order key verifies anything. */ if (!key_has_prime_order(A)) diff --git a/tests/unit/test_ed25519.c b/tests/unit/test_ed25519.c index e2722a4..a816f92 100644 --- a/tests/unit/test_ed25519.c +++ b/tests/unit/test_ed25519.c @@ -197,6 +197,47 @@ TEST(test_ed25519_null_args) ASSERT(eos_ed25519_verify(sig, pk, NULL, sizeof(msg)) != EOS_OK); } +TEST(test_ed25519_identity_key_forgery_rejected) +{ + /* The identity point encodes as 01 00..00. With the public key and R both + * set to it and S zero, every term of the verification equation collapses + * to the identity, so the equation holds for any message at all. That is + * not a weakened signature; it is no signature. */ + uint8_t identity_pub[32] = {1}; + uint8_t identity_sig[64] = {1}; + const uint8_t msg[] = "untrusted firmware"; + + ASSERT(eos_ed25519_verify(identity_sig, identity_pub, + msg, sizeof(msg) - 1) != EOS_OK); +} + +TEST(test_ed25519_low_order_keys_rejected) +{ + /* zero_pubkey covers one encoding; Ed25519 has eight low-order points and + * the family is what matters. A subgroup test alone is not enough either: + * the identity has order 1, which divides L, so [L]identity = identity and + * it passes. Both checks are required. */ + static const uint8_t low_order[4][32] = { + {0}, + {1}, + {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}, + {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}, + }; + const uint8_t msg[] = "untrusted firmware"; + + for (int k = 0; k < 4; k++) { + for (int r = 0; r < 4; r++) { + uint8_t sig[64]; + memset(sig, 0, sizeof(sig)); + memcpy(sig, low_order[r], 32); + ASSERT(eos_ed25519_verify(sig, low_order[k], + msg, sizeof(msg) - 1) != EOS_OK); + } + } +} + TEST(test_ed25519_zero_pubkey_rejected) { uint8_t zero_pub[32], sig[64], msg[1]; @@ -397,6 +438,8 @@ int main(void) run_test_ed25519_wrong_public_key_rejected(); run_test_ed25519_malleated_signature_rejected(); run_test_ed25519_null_args(); + run_test_ed25519_identity_key_forgery_rejected(); + run_test_ed25519_low_order_keys_rejected(); run_test_ed25519_zero_pubkey_rejected(); run_test_ed25519_zero_signature_rejected(); run_test_ed25519_low_order_forgeries_rejected(); @@ -405,7 +448,7 @@ int main(void) run_test_sha512_known_answers(); run_test_sha512_streaming_matches_one_shot(); - tests_run = 13; + tests_run = 12; printf("\n%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; }