eos_ed25519_verify() accepts a forged signature over any message when the
public key is a low-order point. This is a complete secure-boot bypass: an
attacker who can set the trusted key can boot arbitrary firmware.
Reproduction
Built against core/ed25519_verify.c as it stands on #58 — the approved build
repair, and the base of the current eBoot PR chain:
uint8_t identity_pub[32] = {1}; /* identity point, compressed: 01 00...00 */
uint8_t identity_sig[64] = {1}; /* R = identity, S = 0 */
const uint8_t msg[] = "untrusted firmware";
int rc = eos_ed25519_verify(identity_sig, identity_pub, msg, sizeof(msg) - 1);
identity-key verify: rc=0 -> ACCEPTED (forgery works)
rc == 0 is EOS_OK. The message is arbitrary; nothing about it is signed.
Why it verifies
The implementation is a real Ed25519 — canonical-S check, unpackneg, SHA-512,
scalarmult, constant-time compare. It is not a stub. What it lacks is any
check that the public key lies in the prime-order subgroup.
With A = identity:
s_is_canonical(0…0) passes, 0 < L
unpackneg(identity) succeeds, the identity is on the curve
k = SHA512(R ‖ A ‖ M) mod L
lhs = [k](-A) = [k](identity) = identity
rhs = [S]B = [0]B = identity
lhs + rhs = identity
point_pack(identity) = 01 00…00 = R ✓ equation holds
The verification equation is satisfied for every M, because every term
collapses to the identity regardless of the message.
What is and is not already covered
tests/unit/test_ed25519.c on #58 has ten tests, including
test_ed25519_zero_pubkey_rejected and test_ed25519_zero_signature_rejected.
Those catch an all-zero key, which is a different encoding — the identity is
01 00…00, not 00 00…00, and it decodes to a valid curve point where the
all-zero encoding does not. Ed25519 has eight low-order points; the existing
tests catch one edge case and miss the family.
RFC 8032 §5.1.7 permits but does not require cofactored verification, and
explicitly notes that implementations rejecting small-order keys are the ones
resistant to this. libsodium's crypto_sign_verify_detached rejects low-order
public keys for exactly this reason.
Fix
#57 already implements it: point_is_identity() plus a subgroup check that
multiplies the decoded key by L and requires the identity, rejecting both the
identity and torsion keys.
Verified against the same reproduction:
#58 (current base) rc=0 ACCEPTED
#57 rc=-4 rejected
and it does not break valid signatures — the RFC 8032 vectors still pass:
10/10 tests passed (test_ed25519)
16/16 tests passed (full ctest)
Priority
This should go in ahead of the rest of the eBoot queue. The bootloader's entire
trust model rests on this function, and every other PR in the chain is about
build repair or coverage. #57 is currently unmergeable for the same
signed-commit reason as the rest of the queue (#68), which is worth weighing when
deciding that policy.
eos_ed25519_verify()accepts a forged signature over any message when thepublic key is a low-order point. This is a complete secure-boot bypass: an
attacker who can set the trusted key can boot arbitrary firmware.
Reproduction
Built against
core/ed25519_verify.cas it stands on #58 — the approved buildrepair, and the base of the current eBoot PR chain:
rc == 0isEOS_OK. The message is arbitrary; nothing about it is signed.Why it verifies
The implementation is a real Ed25519 — canonical-S check,
unpackneg, SHA-512,scalarmult, constant-time compare. It is not a stub. What it lacks is anycheck that the public key lies in the prime-order subgroup.
With
A= identity:The verification equation is satisfied for every
M, because every termcollapses to the identity regardless of the message.
What is and is not already covered
tests/unit/test_ed25519.con #58 has ten tests, includingtest_ed25519_zero_pubkey_rejectedandtest_ed25519_zero_signature_rejected.Those catch an all-zero key, which is a different encoding — the identity is
01 00…00, not00 00…00, and it decodes to a valid curve point where theall-zero encoding does not. Ed25519 has eight low-order points; the existing
tests catch one edge case and miss the family.
RFC 8032 §5.1.7 permits but does not require cofactored verification, and
explicitly notes that implementations rejecting small-order keys are the ones
resistant to this. libsodium's
crypto_sign_verify_detachedrejects low-orderpublic keys for exactly this reason.
Fix
#57 already implements it:
point_is_identity()plus a subgroup check thatmultiplies the decoded key by
Land requires the identity, rejecting both theidentity and torsion keys.
Verified against the same reproduction:
and it does not break valid signatures — the RFC 8032 vectors still pass:
Priority
This should go in ahead of the rest of the eBoot queue. The bootloader's entire
trust model rests on this function, and every other PR in the chain is about
build repair or coverage. #57 is currently unmergeable for the same
signed-commit reason as the rest of the queue (#68), which is worth weighing when
deciding that policy.