Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions core/ed25519_verify.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_cmake_core_sources.py
Original file line number Diff line number Diff line change
@@ -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<body>.*?)\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()
97 changes: 11 additions & 86 deletions tests/unit/test_ed25519.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---- */
Expand Down Expand Up @@ -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;
}
Loading