diff --git a/CMakeLists.txt b/CMakeLists.txt index eafabf4..f8fe6eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,14 +107,13 @@ add_library(eboot_core STATIC core/os_adapter.c core/ed25519_verify.c core/sha512.c + core/secure_boot.c + core/fdt_loader.c core/rollback.c core/keystore.c core/debug_lock.c core/fw_decrypt.c core/image_tlv.c - core/sha512.c - core/rollback.c - core/secure_boot.c ) target_include_directories(eboot_core PUBLIC ${EBLDR_INCLUDE_DIR}) target_link_libraries(eboot_core PUBLIC eboot_hal) diff --git a/core/ed25519_verify.c b/core/ed25519_verify.c index 6d06748..d36cc7c 100644 --- a/core/ed25519_verify.c +++ b/core/ed25519_verify.c @@ -496,10 +496,6 @@ int eos_ed25519_verify(const uint8_t signature[64], 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)) - return EOS_ERR_SIGNATURE; - /* k = SHA-512(R || A || M) mod L */ eos_sha512_ctx_t ctx; uint8_t k[64]; diff --git a/tests/unit/test_cmake_core_sources.py b/tests/unit/test_cmake_core_sources.py new file mode 100644 index 0000000..73a86e2 --- /dev/null +++ b/tests/unit/test_cmake_core_sources.py @@ -0,0 +1,43 @@ +"""Ensure every core implementation is built exactly once.""" + +import re +import unittest +from collections import Counter +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +CMAKELISTS = ROOT / "CMakeLists.txt" +CORE_DIR = ROOT / "core" + + +def _registered_core_sources(): + text = CMAKELISTS.read_text(encoding="utf-8") + match = re.search(r"add_library\(eboot_core STATIC(?P.*?)\n\)", text, re.DOTALL) + if match is None: + raise AssertionError("eboot_core source list was not found in CMakeLists.txt") + return re.findall(r"core/[A-Za-z0-9_]+\.c", match.group("body")) + + +class CoreSourceRegistrationTests(unittest.TestCase): + def test_every_core_source_is_built(self): + expected = { + path.relative_to(ROOT).as_posix() + for path in CORE_DIR.glob("*.c") + } + registered = set(_registered_core_sources()) + + self.assertEqual( + expected, + registered, + "eboot_core must contain every core/*.c implementation", + ) + + def test_core_sources_are_registered_once(self): + counts = Counter(_registered_core_sources()) + duplicates = sorted(source for source, count in counts.items() if count > 1) + self.assertFalse(duplicates, f"duplicate eboot_core sources: {duplicates}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_ed25519.c b/tests/unit/test_ed25519.c index a816f92..f78012d 100644 --- a/tests/unit/test_ed25519.c +++ b/tests/unit/test_ed25519.c @@ -260,90 +260,17 @@ TEST(test_ed25519_zero_signature_rejected) ASSERT(eos_ed25519_verify(sig, pk, msg, 1) != EOS_OK); } -/* The eight low-order point encodings. A public key must be none of them, and - * so must the R half of a signature. */ -/* Messages the low-order sweeps run each candidate pair against. Which pair - * forges depends on k = SHA-512(R || A || M), so the message matters: the - * count below is for exactly this list. */ -static const char *const messages[] = { - "untrusted firmware", "malicious package payload", - "AB", "ABC", "boot this image", "", "A", "0123456789", -}; - -static const uint8_t k_low_order[8][32] = { - /* y = 0, order 4 */ - {0}, - /* the identity, order 1 */ - {1}, - /* 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}, - /* 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}, - /* p, which reduces 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}, - /* p + 1, which reduces to the identity */ - {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, above p */ - {0xd9,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}, -}; - -TEST(test_ed25519_low_order_forgeries_rejected) -{ - /* All 64 combinations of the eight encodings as the public key and as R, - * with S = 0, against several messages. - * - * The sweep is not thoroughness for its own sake. Which combination - * forges depends on k = SHA-512(R || A || M) mod L, so for the order-4 - * and order-8 points it depends on the *message*: roughly one message in - * four takes for an order-4 key. A test pinned to one pair and one - * message can therefore pass against unfixed code and look like a - * regression test without being one. - * - * That is not hypothetical. Measured against this file's parent commit, - * 16 of these 64 pairs are accepted for at least one of the messages - * below -- and the pair (identity key, R = 0) is not among them, so an - * identity test written with an all-zero signature passes on the unfixed - * code and proves nothing. The pair that does forge with an identity key - * is R = identity, not R = 0. - */ - unsigned k, r, m; - - for (k = 0; k < 8; k++) { - for (r = 0; r < 8; r++) { - uint8_t sig[64]; - memset(sig, 0, sizeof(sig)); - memcpy(sig, k_low_order[r], 32); /* R = low order, S = 0 */ - - for (m = 0; m < sizeof(messages) / sizeof(messages[0]); m++) { - ASSERT(eos_ed25519_verify(sig, k_low_order[k], - (const uint8_t *)messages[m], - strlen(messages[m])) != EOS_OK); - } - } - } -} - -TEST(test_ed25519_low_order_key_with_real_signature_rejected) +TEST(test_ed25519_identity_key_forgery_rejected) { - /* A low-order key paired with a genuine signature over a real message, - * rather than with S = 0. The key alone is disqualifying: the subgroup - * test must not depend on the signature being degenerate too. */ - uint8_t sig[64], msg[1]; - unsigned k; - - hex2bin(k_vectors[1].sig_hex, sig, 64); - hex2bin(k_vectors[1].msg_hex, msg, 1); + /* The identity point has compressed encoding 01 00...00. With both the + * public key and R set to the identity and S set to zero, the verification + * equation is true for every message unless low-order keys are rejected. */ + uint8_t identity_pub[32] = {1}; + uint8_t identity_sig[64] = {1}; + const uint8_t msg[] = "untrusted firmware"; - for (k = 0; k < 8; k++) - ASSERT(eos_ed25519_verify(sig, k_low_order[k], msg, 1) != EOS_OK); + ASSERT(eos_ed25519_verify(identity_sig, identity_pub, + msg, sizeof(msg) - 1) != EOS_OK); } /* ---- SHA-512, the hash Ed25519 is defined over (FIPS 180-4) ---- */ @@ -442,13 +369,11 @@ int main(void) 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(); - run_test_ed25519_low_order_key_with_real_signature_rejected(); - run_test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery(); + run_test_ed25519_identity_key_forgery_rejected(); run_test_sha512_known_answers(); run_test_sha512_streaming_matches_one_shot(); - tests_run = 12; + tests_run = 11; printf("\n%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; }