fix(tests): restore the line continuation that unterminates TEST() in test_image_verify - #77
Closed
Kartikey1306 wants to merge 1 commit into
Closed
Conversation
… test_image_verify
master does not build. The TEST() macro in tests/unit/test_image_verify.c lost
the backslash on one line:
#define TEST(name) \
static void name(void); \
static void run_##name(void) { \
memset(sim_flash, 0xFF, sizeof(sim_flash)); \
sim_unreadable_from = UINT32_MAX; <-- no continuation
sim_tick = 0; \
...
The macro therefore ends at that line, and every line after it -- the tick
reset, eos_hal_init(), the printf, the call, the closing brace -- is parsed as
file-scope code instead of macro body. That produces 24 errors that all look
unrelated to the cause:
error: redefinition of 'sim_tick' with a different type: 'int' vs 'uint32_t'
error: conflicting types for 'eos_hal_init'
error: conflicting types for 'printf'
error: extraneous closing brace ('}')
Restoring the backslash is the whole fix.
before: 24 errors, ctest 13 of 19 failing
after: 0 errors, ctest 19/19 pass
Introduced by embeddedos-org#70 (881f00c). It also means every TEST() in that file was
running without its per-test reset of sim_unreadable_from and sim_tick, so the
fixture state leaked between cases -- the file has not compiled since, so this
never showed up as a wrong result, only as a build failure.
`CI — eBoot` was already red on master for exactly this before I started.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 1, 2026
…ing GCM
core/fw_decrypt.c is a hand-written AES-256-GCM sitting in the secure boot
path with no tests. It has an opt-in fast path:
if (ops && ops->hw_aes_decrypt) {
int rc = ops->hw_aes_decrypt(ctx->key, EOS_AES_KEY_SIZE,
ctx->iv, data, data, len);
if (rc == EOS_OK) { ctx->bytes_processed += len; return EOS_OK; }
}
The hook is (key, key_len, iv, in, out, len). That signature cannot carry
streaming GCM, and taking the path broke decryption two ways:
1. No counter position. ctx->iv is passed unchanged on every call, so a second
chunk restarts the CTR keystream at block 0 and is decrypted against the
same keystream as the first. Reusing a CTR keystream across two plaintexts
is the one thing the mode must never do.
2. No GHASH. The hook returns plaintext only, so ctx->ghash_acc is never fed.
eos_fw_decrypt_final() computes the tag over an empty accumulator and
rejects the image -- a board with an AES engine could not install a
correctly encrypted firmware update at all.
(2) is why this was never noticed: it fails closed, and no board in-tree
implements the hook yet. (1) is why it cannot be patched by also feeding
GHASH: the plaintext would still be wrong past the first chunk. Re-enabling
needs a hook that takes a block offset and either exposes GHASH state or does
the whole GCM operation including the tag. Removed, with that written down
where the next person will look.
Also adds tests/unit/test_fw_decrypt.c -- the first tests this file has had.
Vectors come from an independent implementation (Python cryptography, i.e.
OpenSSL) rather than from this code, so they pin behaviour rather than
recording it:
- whole blocks, and a 20-byte payload with a partial trailing block
- the same ciphertext split [32] / [16,16] / [10,22] / [1,31] / [5,15]:
GCM is a stream, so the result must not depend on how the caller sliced it
- a board advertising a working AES engine must reach the same answer as one
without -- this is the case that fails on master
- every single-bit flip in the tag (128 of them), and in each ciphertext
byte, must be rejected
- unprovisioned/unreadable OTP keys and uninitialised contexts are refused
Verified: 8/8 pass with this change; against the unmodified fw_decrypt.c the
suite fails on
test_board_with_aes_engine_still_accepts_a_genuine_image, the assertion it
exists to make. The software GCM itself is correct -- I checked it against the
reference vectors before changing anything, including every chunk split above.
Note: the full test build on master is currently broken by test_image_verify.c
(fixed in embeddedos-org#77), so this target was built directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 1, 2026
Contributor
Author
|
Closing — this landed. sim_unreadable_from = UINT32_MAX; \Merging current master into this branch leaves an empty diff, so there is nothing here to review. Glad it is in either way: it was blocking every PR that builds the test suite. #80, #81 and #82 were stacked on this only because master would not compile; I have rebased them onto master directly. |
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 1, 2026
…ing GCM
core/fw_decrypt.c is a hand-written AES-256-GCM sitting in the secure boot
path with no tests. It has an opt-in fast path:
if (ops && ops->hw_aes_decrypt) {
int rc = ops->hw_aes_decrypt(ctx->key, EOS_AES_KEY_SIZE,
ctx->iv, data, data, len);
if (rc == EOS_OK) { ctx->bytes_processed += len; return EOS_OK; }
}
The hook is (key, key_len, iv, in, out, len). That signature cannot carry
streaming GCM, and taking the path broke decryption two ways:
1. No counter position. ctx->iv is passed unchanged on every call, so a second
chunk restarts the CTR keystream at block 0 and is decrypted against the
same keystream as the first. Reusing a CTR keystream across two plaintexts
is the one thing the mode must never do.
2. No GHASH. The hook returns plaintext only, so ctx->ghash_acc is never fed.
eos_fw_decrypt_final() computes the tag over an empty accumulator and
rejects the image -- a board with an AES engine could not install a
correctly encrypted firmware update at all.
(2) is why this was never noticed: it fails closed, and no board in-tree
implements the hook yet. (1) is why it cannot be patched by also feeding
GHASH: the plaintext would still be wrong past the first chunk. Re-enabling
needs a hook that takes a block offset and either exposes GHASH state or does
the whole GCM operation including the tag. Removed, with that written down
where the next person will look.
Also adds tests/unit/test_fw_decrypt.c -- the first tests this file has had.
Vectors come from an independent implementation (Python cryptography, i.e.
OpenSSL) rather than from this code, so they pin behaviour rather than
recording it:
- whole blocks, and a 20-byte payload with a partial trailing block
- the same ciphertext split [32] / [16,16] / [10,22] / [1,31] / [5,15]:
GCM is a stream, so the result must not depend on how the caller sliced it
- a board advertising a working AES engine must reach the same answer as one
without -- this is the case that fails on master
- every single-bit flip in the tag (128 of them), and in each ciphertext
byte, must be rejected
- unprovisioned/unreadable OTP keys and uninitialised contexts are refused
Verified: 8/8 pass with this change; against the unmodified fw_decrypt.c the
suite fails on
test_board_with_aes_engine_still_accepts_a_genuine_image, the assertion it
exists to make. The software GCM itself is correct -- I checked it against the
reference vectors before changing anything, including every chunk split above.
Note: the full test build on master is currently broken by test_image_verify.c
(fixed in embeddedos-org#77), so this target was built directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 1, 2026
…ul boot
cfg.lock_debug asks for SWD/JTAG to be closed before the verified image runs.
Step 7 of eos_secure_boot() honoured that request like this:
if (cfg->lock_debug) {
eos_secure_boot_lock_debug();
}
/* ---- Step 8: Record successful attestation ---- */
attest_record(2, hdr.image_version, hdr.hash, NULL, EOS_SBOOT_OK);
return EOS_SBOOT_OK;
eos_secure_boot_lock_debug() returned void and discarded the result of the OTP
write that actually blows the fuse. So when the write failed, boot continued,
attestation recorded EOS_SBOOT_OK, and the device ran the image with its debug
port open -- the exact condition the policy existed to prevent, reported as a
clean secure boot.
The interesting case is not a flaky fuse. eos_hal_otp_write() returns
EOS_ERR_NOT_SUPPORTED when the board provides no otp_write hook at all, so on
every such board `lock_debug: true` was silently a no-op. That is the default
configuration, not an edge case.
eos_secure_boot_lock_debug() now returns int, and a caller that asked for the
lock and did not get it fails with EOS_SBOOT_ERR_POLICY -- a code that already
existed for exactly this ("Boot policy violation") -- with the failure recorded
in the attestation log rather than a success.
Why this was never observable: core/secure_boot.c is not in CMakeLists.txt.
The module has never been compiled, so this path could not run and could not be
tested. Added the one line that builds it -- the same line embeddedos-org#72 adds, written
identically so whichever lands first leaves the other a trivial rebase.
tests/unit/test_secure_boot_policy.c covers the three outcomes: the fuse
written, the write failing, and a board with no otp_write. Kept in its own file
so it does not collide with the test_secure_boot.c embeddedos-org#72 introduces.
Against master the new test does not compile -- `invalid operands to binary
expression ('void' and 'int')` -- because there is no result to check. That is
the defect stated as a compile error.
Verified on this branch: build clean, ctest 20/20, pytest 30 passed.
Stacked on embeddedos-org#77 (master's test suite does not compile without it).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 1, 2026
…ul boot
cfg.lock_debug asks for SWD/JTAG to be closed before the verified image runs.
Step 7 of eos_secure_boot() honoured that request like this:
if (cfg->lock_debug) {
eos_secure_boot_lock_debug();
}
/* ---- Step 8: Record successful attestation ---- */
attest_record(2, hdr.image_version, hdr.hash, NULL, EOS_SBOOT_OK);
return EOS_SBOOT_OK;
eos_secure_boot_lock_debug() returned void and discarded the result of the OTP
write that actually blows the fuse. So when the write failed, boot continued,
attestation recorded EOS_SBOOT_OK, and the device ran the image with its debug
port open -- the exact condition the policy existed to prevent, reported as a
clean secure boot.
The interesting case is not a flaky fuse. eos_hal_otp_write() returns
EOS_ERR_NOT_SUPPORTED when the board provides no otp_write hook at all, so on
every such board `lock_debug: true` was silently a no-op. That is the default
configuration, not an edge case.
eos_secure_boot_lock_debug() now returns int, and a caller that asked for the
lock and did not get it fails with EOS_SBOOT_ERR_POLICY -- a code that already
existed for exactly this ("Boot policy violation") -- with the failure recorded
in the attestation log rather than a success.
Why this was never observable: core/secure_boot.c is not in CMakeLists.txt.
The module has never been compiled, so this path could not run and could not be
tested. Added the one line that builds it -- the same line embeddedos-org#72 adds, written
identically so whichever lands first leaves the other a trivial rebase.
tests/unit/test_secure_boot_policy.c covers the three outcomes: the fuse
written, the write failing, and a board with no otp_write. Kept in its own file
so it does not collide with the test_secure_boot.c embeddedos-org#72 introduces.
Against master the new test does not compile -- `invalid operands to binary
expression ('void' and 'int')` -- because there is no result to check. That is
the defect stated as a compile error.
Verified on this branch: build clean, ctest 20/20, pytest 30 passed.
Stacked on embeddedos-org#77 (master's test suite does not compile without it).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 1, 2026
…ing GCM
core/fw_decrypt.c is a hand-written AES-256-GCM sitting in the secure boot
path with no tests. It has an opt-in fast path:
if (ops && ops->hw_aes_decrypt) {
int rc = ops->hw_aes_decrypt(ctx->key, EOS_AES_KEY_SIZE,
ctx->iv, data, data, len);
if (rc == EOS_OK) { ctx->bytes_processed += len; return EOS_OK; }
}
The hook is (key, key_len, iv, in, out, len). That signature cannot carry
streaming GCM, and taking the path broke decryption two ways:
1. No counter position. ctx->iv is passed unchanged on every call, so a second
chunk restarts the CTR keystream at block 0 and is decrypted against the
same keystream as the first. Reusing a CTR keystream across two plaintexts
is the one thing the mode must never do.
2. No GHASH. The hook returns plaintext only, so ctx->ghash_acc is never fed.
eos_fw_decrypt_final() computes the tag over an empty accumulator and
rejects the image -- a board with an AES engine could not install a
correctly encrypted firmware update at all.
(2) is why this was never noticed: it fails closed, and no board in-tree
implements the hook yet. (1) is why it cannot be patched by also feeding
GHASH: the plaintext would still be wrong past the first chunk. Re-enabling
needs a hook that takes a block offset and either exposes GHASH state or does
the whole GCM operation including the tag. Removed, with that written down
where the next person will look.
Also adds tests/unit/test_fw_decrypt.c -- the first tests this file has had.
Vectors come from an independent implementation (Python cryptography, i.e.
OpenSSL) rather than from this code, so they pin behaviour rather than
recording it:
- whole blocks, and a 20-byte payload with a partial trailing block
- the same ciphertext split [32] / [16,16] / [10,22] / [1,31] / [5,15]:
GCM is a stream, so the result must not depend on how the caller sliced it
- a board advertising a working AES engine must reach the same answer as one
without -- this is the case that fails on master
- every single-bit flip in the tag (128 of them), and in each ciphertext
byte, must be rejected
- unprovisioned/unreadable OTP keys and uninitialised contexts are refused
Verified: 8/8 pass with this change; against the unmodified fw_decrypt.c the
suite fails on
test_board_with_aes_engine_still_accepts_a_genuine_image, the assertion it
exists to make. The software GCM itself is correct -- I checked it against the
reference vectors before changing anything, including every chunk split above.
Note: the full test build on master is currently broken by test_image_verify.c
(fixed in embeddedos-org#77), so this target was built directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 3, 2026
…ing GCM
core/fw_decrypt.c is a hand-written AES-256-GCM sitting in the secure boot
path with no tests. It has an opt-in fast path:
if (ops && ops->hw_aes_decrypt) {
int rc = ops->hw_aes_decrypt(ctx->key, EOS_AES_KEY_SIZE,
ctx->iv, data, data, len);
if (rc == EOS_OK) { ctx->bytes_processed += len; return EOS_OK; }
}
The hook is (key, key_len, iv, in, out, len). That signature cannot carry
streaming GCM, and taking the path broke decryption two ways:
1. No counter position. ctx->iv is passed unchanged on every call, so a second
chunk restarts the CTR keystream at block 0 and is decrypted against the
same keystream as the first. Reusing a CTR keystream across two plaintexts
is the one thing the mode must never do.
2. No GHASH. The hook returns plaintext only, so ctx->ghash_acc is never fed.
eos_fw_decrypt_final() computes the tag over an empty accumulator and
rejects the image -- a board with an AES engine could not install a
correctly encrypted firmware update at all.
(2) is why this was never noticed: it fails closed, and no board in-tree
implements the hook yet. (1) is why it cannot be patched by also feeding
GHASH: the plaintext would still be wrong past the first chunk. Re-enabling
needs a hook that takes a block offset and either exposes GHASH state or does
the whole GCM operation including the tag. Removed, with that written down
where the next person will look.
Also adds tests/unit/test_fw_decrypt.c -- the first tests this file has had.
Vectors come from an independent implementation (Python cryptography, i.e.
OpenSSL) rather than from this code, so they pin behaviour rather than
recording it:
- whole blocks, and a 20-byte payload with a partial trailing block
- the same ciphertext split [32] / [16,16] / [10,22] / [1,31] / [5,15]:
GCM is a stream, so the result must not depend on how the caller sliced it
- a board advertising a working AES engine must reach the same answer as one
without -- this is the case that fails on master
- every single-bit flip in the tag (128 of them), and in each ciphertext
byte, must be rejected
- unprovisioned/unreadable OTP keys and uninitialised contexts are refused
Verified: 8/8 pass with this change; against the unmodified fw_decrypt.c the
suite fails on
test_board_with_aes_engine_still_accepts_a_genuine_image, the assertion it
exists to make. The software GCM itself is correct -- I checked it against the
reference vectors before changing anything, including every chunk split above.
Note: the full test build on master is currently broken by test_image_verify.c
(fixed in embeddedos-org#77), so this target was built directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kartikey1306
added a commit
to Kartikey1306/eBoot
that referenced
this pull request
Sep 3, 2026
…ul boot
cfg.lock_debug asks for SWD/JTAG to be closed before the verified image runs.
Step 7 of eos_secure_boot() honoured that request like this:
if (cfg->lock_debug) {
eos_secure_boot_lock_debug();
}
/* ---- Step 8: Record successful attestation ---- */
attest_record(2, hdr.image_version, hdr.hash, NULL, EOS_SBOOT_OK);
return EOS_SBOOT_OK;
eos_secure_boot_lock_debug() returned void and discarded the result of the OTP
write that actually blows the fuse. So when the write failed, boot continued,
attestation recorded EOS_SBOOT_OK, and the device ran the image with its debug
port open -- the exact condition the policy existed to prevent, reported as a
clean secure boot.
The interesting case is not a flaky fuse. eos_hal_otp_write() returns
EOS_ERR_NOT_SUPPORTED when the board provides no otp_write hook at all, so on
every such board `lock_debug: true` was silently a no-op. That is the default
configuration, not an edge case.
eos_secure_boot_lock_debug() now returns int, and a caller that asked for the
lock and did not get it fails with EOS_SBOOT_ERR_POLICY -- a code that already
existed for exactly this ("Boot policy violation") -- with the failure recorded
in the attestation log rather than a success.
Why this was never observable: core/secure_boot.c is not in CMakeLists.txt.
The module has never been compiled, so this path could not run and could not be
tested. Added the one line that builds it -- the same line embeddedos-org#72 adds, written
identically so whichever lands first leaves the other a trivial rebase.
tests/unit/test_secure_boot_policy.c covers the three outcomes: the fuse
written, the write failing, and a board with no otp_write. Kept in its own file
so it does not collide with the test_secure_boot.c embeddedos-org#72 introduces.
Against master the new test does not compile -- `invalid operands to binary
expression ('void' and 'int')` -- because there is no result to check. That is
the defect stated as a compile error.
Verified on this branch: build clean, ctest 20/20, pytest 30 passed.
Stacked on embeddedos-org#77 (master's test suite does not compile without it).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
srpatcha
pushed a commit
that referenced
this pull request
Sep 8, 2026
…ul boot (#82) * fix: repair master — the ABI asserts and the Ed25519 verifier both merged broken master (22d8f8b) does not compile. Two independent double-merges, both the same shape: two PRs fixing adjacent things landed on stale bases, each was green on its own branch, and the result was never rebuilt. 1. include/eos_image.h — #93 replaced reserved[30] with tlv_len (2) + tlv_hash[28], preserving every offset. #87 merged afterwards carrying asserts written against the older struct: error: no member named 'reserved' in 'eos_image_header_t' (x2) #93 already asserts tlv_len at 62 and tlv_hash at 64, so the offset assert was a duplicate; the width assert had no replacement and is restored as two asserts covering both halves of the same 30-byte span. No offset moves and the wire format is unchanged. 2. core/ed25519_verify.c — #86 and #57 both landed a subgroup guard, so the file carried two byte-identical point_is_identity() definitions: error: redefinition of 'point_is_identity' Only #57's public_key_is_valid_subgroup() is wired to the call site, so #86's key_has_prime_order() was dead. Kept the live function, folded #86's fuller rationale onto it, deleted the duplicate. 3. tests/unit/test_ed25519.c — collateral from the same merge. Two copies of test_ed25519_identity_key_forgery_rejected, main() calling it twice and two tests not at all, and test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery referencing k_low_order[] and messages[] that the merge had dropped. While restoring the corpus, corrected it (review finding on #86): the array claimed to hold "the eight low-order point encodings" and held five. Every order here was computed rather than copied — decode y, recover x, add the point to itself until it reaches the identity — giving 1, 2, 4, 4, 8, 8, 8, 8. Missing before: y=0 with the sign bit set, and both sign-flipped order-8 encodings. D9FF..FF was in the array and is not a low-order point at all — no x satisfies the curve equation for that y — so it moves to a separate k_non_canonical[], with EDFF..FF7F (y=p) and EEFF..FF7F (y=p+1). tests_run was assigned a literal (11) in main() and never incremented, which is how the duplicate call and the two unregistered tests went unnoticed. The TEST macro now increments it, so the total cannot drift. Verified: cmake -DEBLDR_BUILD_TESTS=ON on master FAILS to build, 3 errors same with this commit builds clean ctest 21/21 PASS ctest -DEBLDR_SANITIZE=ON (ASan+UBSan) 21/21 PASS pytest tests/ 24 passed, 1 skipped test_ed25519 14/14 PASS (was 11 claimed, 12 run) discrimination, with `public_key_is_valid_subgroup` disabled: test_ed25519_low_order_keys_rejected FAILS, as it must test_ed25519_non_canonical_... still PASSES — those are refused by unpackneg() on canonicality, a different mechanism, which is the reason they are held in a separate array rather than counted among the eight. * fix(secure-boot): a debug lock that failed must not report a successful boot cfg.lock_debug asks for SWD/JTAG to be closed before the verified image runs. Step 7 of eos_secure_boot() honoured that request like this: if (cfg->lock_debug) { eos_secure_boot_lock_debug(); } /* ---- Step 8: Record successful attestation ---- */ attest_record(2, hdr.image_version, hdr.hash, NULL, EOS_SBOOT_OK); return EOS_SBOOT_OK; eos_secure_boot_lock_debug() returned void and discarded the result of the OTP write that actually blows the fuse. So when the write failed, boot continued, attestation recorded EOS_SBOOT_OK, and the device ran the image with its debug port open -- the exact condition the policy existed to prevent, reported as a clean secure boot. The interesting case is not a flaky fuse. eos_hal_otp_write() returns EOS_ERR_NOT_SUPPORTED when the board provides no otp_write hook at all, so on every such board `lock_debug: true` was silently a no-op. That is the default configuration, not an edge case. eos_secure_boot_lock_debug() now returns int, and a caller that asked for the lock and did not get it fails with EOS_SBOOT_ERR_POLICY -- a code that already existed for exactly this ("Boot policy violation") -- with the failure recorded in the attestation log rather than a success. Why this was never observable: core/secure_boot.c is not in CMakeLists.txt. The module has never been compiled, so this path could not run and could not be tested. Added the one line that builds it -- the same line #72 adds, written identically so whichever lands first leaves the other a trivial rebase. tests/unit/test_secure_boot_policy.c covers the three outcomes: the fuse written, the write failing, and a board with no otp_write. Kept in its own file so it does not collide with the test_secure_boot.c #72 introduces. Against master the new test does not compile -- `invalid operands to binary expression ('void' and 'int')` -- because there is no result to check. That is the defect stated as a compile error. Verified on this branch: build clean, ctest 20/20, pytest 30 passed. Stacked on #77 (master's test suite does not compile without it). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(secure-boot): test the policy the PR changed, not only the helper it calls Answers finding 1 (Medium) from the review on #82. All three tests called eos_secure_boot_lock_debug() directly and asserted its return value. None drove eos_secure_boot() with cfg.lock_debug = true, so the step-7 early return this PR adds -- the attest_record + return EOS_SBOOT_ERR_POLICY -- never executed under test. The file's docblock says "the debug-lock policy must be enforced, not merely attempted"; what it covered was the attempted half. Adds three end-to-end cases through eos_secure_boot(): - lock_debug: true on a board with no otp_write -> EOS_SBOOT_ERR_POLICY - the same image with lock_debug: false -> EOS_SBOOT_OK, and the recorded entry point is the header's, which is the counter-check: without it the first test would also pass if steps 1-6 were failing for an unrelated reason and never reaching step 7 - lock_debug: true with a working fuse -> EOS_SBOOT_OK, one write Reaching step 7 needs an image that clears steps 1, 2 and 5, so the fixture gains a simulated flash and stages an unsigned, unencrypted image whose SHA-256 matches. EOS_IMG_FLAG_HASH_SHA256 is load-bearing there: without it verify_integrity takes the CRC32 branch, reads a CRC out of hash[], and step 2 fails before the policy step is ever reached. Finding 4 (Low), the tests/CMakeLists.txt anchor shared with #80, is resolved on #80's side -- its block moved to the end of the registrations, so this one keeps its position and the two no longer collide. Verified: ctest 22/22 PASS test_secure_boot_policy 6/6 PASS discrimination: with step 7 reverted to `(void)eos_secure_boot_lock_debug();` test_secure_boot_refuses_when_the_debug_lock_cannot_be_taken FAILS the three original helper tests still PASS which is the gap the finding described, reproduced. Refs #82 * fix(secure-boot): step 4 must not report success for a check it never makes Answers the second review on #82. Finding 1 (High) -- step 4, "Verify signing key against OTP root-of-trust", was the same fail-open this PR removes from step 7, two steps earlier. An `eos_hal_otp_read()` failure was discarded and boot continued; and when the read succeeded and the anchor was provisioned, the body of the `if` was `/* In a full implementation, extract key hash from TLV and compare */`. So a device whose root of trust *is* provisioned booted an image signed by any key the image carried, and step 8 recorded EOS_SBOOT_OK. Implementing the TLV comparison is out of scope, as the review says. The two things that are in scope are done: a non-EOS_OK otp_read now fails EOS_SBOOT_ERR_SIGNATURE, and a provisioned anchor that nothing compares against refuses the boot rather than proceeding. An unprovisioned board (all-zero anchor) is deliberately unchanged -- there is nothing to check against, and refusing would brick every board that has not been provisioned. The comment now states plainly that the step is planned rather than implemented, which §8.1 asks for and a comment inside an `if` was not. No test for those two refusals, deliberately, and the file says why rather than leaving it to be discovered. Reaching step 4 requires passing step 3 -- a real Ed25519 signature checked against the keystore. I wrote the obvious test first and it was worthless: with require_signature = true and an unsigned fixture the boot fails at step 3 and returns the same EOS_SBOOT_ERR_SIGNATURE step 4 returns, so it passed against the unfixed code too. I confirmed that by reverting step 4 and watching it still pass. A test that cannot fail is worse than none. What is there instead is the counter-check that the change does not refuse a boot it should allow. The fixture is buildable -- the keystore ships RFC 8032 TEST 1's public key and the matching private key is in the RFC -- but that machinery is #88's (tools/gen_signed_image_fixture.py). Worth doing once #88 lands. Finding 3 (Low) -- `TEST()` did not increment `tests_run` and `main()` hardcoded `tests_run = 6`, so a test added to the file but not wired into `main()` would have been skipped with a zero exit. #94 removed exactly this from tests/unit/test_ed25519.c, where a hardcoded 11 was masking two uncalled tests. Same fix here. Finding 4 (Low) -- the Valgrind `foreach` is hand-maintained and missed 6 of 22 registered tests. Added test_secure_boot_policy, test_fdt_loader and test_fw_decrypt. The list being hand-maintained at all is the real defect and is not fixed here -- it is the same class as the hardcoded count, one level up. Finding 2 (Medium), that eos_secure_boot() has no production caller, stands and is not addressed here; finding 1 makes it sharper rather than resolving it. Finding 5 (Low) is a PR-body correction. Verified: ctest 22/22 PASS test_secure_boot_policy 7/7 PASS step 7 discrimination, still: reverting the step-7 branch fails test_secure_boot_refuses_when_the_debug_lock_cannot_be_taken Refs #82 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
srpatcha
pushed a commit
that referenced
this pull request
Sep 8, 2026
…ing GCM (#80) * fix: repair master — the ABI asserts and the Ed25519 verifier both merged broken master (22d8f8b) does not compile. Two independent double-merges, both the same shape: two PRs fixing adjacent things landed on stale bases, each was green on its own branch, and the result was never rebuilt. 1. include/eos_image.h — #93 replaced reserved[30] with tlv_len (2) + tlv_hash[28], preserving every offset. #87 merged afterwards carrying asserts written against the older struct: error: no member named 'reserved' in 'eos_image_header_t' (x2) #93 already asserts tlv_len at 62 and tlv_hash at 64, so the offset assert was a duplicate; the width assert had no replacement and is restored as two asserts covering both halves of the same 30-byte span. No offset moves and the wire format is unchanged. 2. core/ed25519_verify.c — #86 and #57 both landed a subgroup guard, so the file carried two byte-identical point_is_identity() definitions: error: redefinition of 'point_is_identity' Only #57's public_key_is_valid_subgroup() is wired to the call site, so #86's key_has_prime_order() was dead. Kept the live function, folded #86's fuller rationale onto it, deleted the duplicate. 3. tests/unit/test_ed25519.c — collateral from the same merge. Two copies of test_ed25519_identity_key_forgery_rejected, main() calling it twice and two tests not at all, and test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery referencing k_low_order[] and messages[] that the merge had dropped. While restoring the corpus, corrected it (review finding on #86): the array claimed to hold "the eight low-order point encodings" and held five. Every order here was computed rather than copied — decode y, recover x, add the point to itself until it reaches the identity — giving 1, 2, 4, 4, 8, 8, 8, 8. Missing before: y=0 with the sign bit set, and both sign-flipped order-8 encodings. D9FF..FF was in the array and is not a low-order point at all — no x satisfies the curve equation for that y — so it moves to a separate k_non_canonical[], with EDFF..FF7F (y=p) and EEFF..FF7F (y=p+1). tests_run was assigned a literal (11) in main() and never incremented, which is how the duplicate call and the two unregistered tests went unnoticed. The TEST macro now increments it, so the total cannot drift. Verified: cmake -DEBLDR_BUILD_TESTS=ON on master FAILS to build, 3 errors same with this commit builds clean ctest 21/21 PASS ctest -DEBLDR_SANITIZE=ON (ASan+UBSan) 21/21 PASS pytest tests/ 24 passed, 1 skipped test_ed25519 14/14 PASS (was 11 claimed, 12 run) discrimination, with `public_key_is_valid_subgroup` disabled: test_ed25519_low_order_keys_rejected FAILS, as it must test_ed25519_non_canonical_... still PASSES — those are refused by unpackneg() on canonicality, a different mechanism, which is the reason they are held in a separate array rather than counted among the eight. * fix(fw_decrypt): drop a HW-crypto shortcut that cannot express streaming GCM core/fw_decrypt.c is a hand-written AES-256-GCM sitting in the secure boot path with no tests. It has an opt-in fast path: if (ops && ops->hw_aes_decrypt) { int rc = ops->hw_aes_decrypt(ctx->key, EOS_AES_KEY_SIZE, ctx->iv, data, data, len); if (rc == EOS_OK) { ctx->bytes_processed += len; return EOS_OK; } } The hook is (key, key_len, iv, in, out, len). That signature cannot carry streaming GCM, and taking the path broke decryption two ways: 1. No counter position. ctx->iv is passed unchanged on every call, so a second chunk restarts the CTR keystream at block 0 and is decrypted against the same keystream as the first. Reusing a CTR keystream across two plaintexts is the one thing the mode must never do. 2. No GHASH. The hook returns plaintext only, so ctx->ghash_acc is never fed. eos_fw_decrypt_final() computes the tag over an empty accumulator and rejects the image -- a board with an AES engine could not install a correctly encrypted firmware update at all. (2) is why this was never noticed: it fails closed, and no board in-tree implements the hook yet. (1) is why it cannot be patched by also feeding GHASH: the plaintext would still be wrong past the first chunk. Re-enabling needs a hook that takes a block offset and either exposes GHASH state or does the whole GCM operation including the tag. Removed, with that written down where the next person will look. Also adds tests/unit/test_fw_decrypt.c -- the first tests this file has had. Vectors come from an independent implementation (Python cryptography, i.e. OpenSSL) rather than from this code, so they pin behaviour rather than recording it: - whole blocks, and a 20-byte payload with a partial trailing block - the same ciphertext split [32] / [16,16] / [10,22] / [1,31] / [5,15]: GCM is a stream, so the result must not depend on how the caller sliced it - a board advertising a working AES engine must reach the same answer as one without -- this is the case that fails on master - every single-bit flip in the tag (128 of them), and in each ciphertext byte, must be rejected - unprovisioned/unreadable OTP keys and uninitialised contexts are refused Verified: 8/8 pass with this change; against the unmodified fw_decrypt.c the suite fails on test_board_with_aes_engine_still_accepts_a_genuine_image, the assertion it exists to make. The software GCM itself is correct -- I checked it against the reference vectors before changing anything, including every chunk split above. Note: the full test build on master is currently broken by test_image_verify.c (fixed in #77), so this target was built directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(fw_decrypt): answer the review — stale doc, orphaned hook, anchor clash Three findings from the review on #80, all in files this branch already owns. Finding 2 (Medium) -- core/fw_decrypt.c's file header still read "Falls back to HAL hw_aes_decrypt if available." The diff started at line 192, so that line survived and directly contradicted the twenty-line rationale this PR installs below it. A self-contradictory file is worse than the original. Finding 1 (Medium) -- after the removal, hw_aes_decrypt has zero call sites while eos_hal.h still advertises it under "software fallback used if NULL", which is now false in the other direction: a board author who implements the hook gets nothing, silently, with no diagnostic. That is the same class of failure this PR is fixing. Kept the member rather than deleting it -- removing it from a public struct is an ABI change for out-of-tree boards, and the hook is worth having once it can express the operation -- and documented it as reserved and currently unconsumed, with the reason and with what a streaming-capable replacement would need. Finding 3 (Low) -- this PR and #82 both inserted their add_executable/add_test triple immediately after add_test(NAME test_keystore ...), so whichever landed second would have conflicted for no reason but placement. Re-anchored this one to the end of the registrations, with a comment saying why. Also rebased: this branch was 7 commits behind and its diff against current master would have reverted the test_recovery link-line fix. It is now stacked on #94, which repairs master -- without that, every PR that builds the test suite is red on include/eos_image.h and core/ed25519_verify.c. Verified: cmake --build (EBLDR_BUILD_TESTS=ON) clean ctest 22/22 PASS test_fw_decrypt 8/8 PASS git grep hw_aes_decrypt header + this file's comment only Refs #80 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
masterdoes not build. One backslash.The macro ends at that line, so everything after it — the tick reset,
eos_hal_init(), theprintf, the call, the closing brace — is parsed as file-scope code rather than macro body. The 24 errors that fall out all point somewhere other than the cause:ctestpytest tests/Worth knowing beyond the fix
Introduced by #70 (
881f00c). Because the macro was unterminated, everyTEST()in that file was defined without its per-test reset ofsim_unreadable_fromandsim_tick— fixture state would have leaked between cases. That never produced a wrong result only because the file has not compiled since, so the tests have not run at all.CI — eBootwas already red onmasterfor this before I opened anything, so this is not a regression from another in-flight PR.🤖 Generated with Claude Code