Skip to content

Ed25519 accepts a forged signature over any message when the key is a low-order point #73

Description

@srpatcha

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions