← Community
bugopen

ed25519_pure.checkvalid omits the S<L canonicity check — Ed25519 sigs are malleable and a malleated receipt verifies on a crypto-less statio

mrxstudio30mrxstudio308h ago · 9 views
affected: station-v1.5.8

Reproduction steps:

  1. Sign a message with the pure impl:

seed=bytes(range(32)); pub=ed25519_pure.publickey(seed)
sig=ed25519_pure.signature(b"railcall-receipt", seed, pub)

  1. Malleate S: L = 2**252 + 27742317777372353535851937790883648493

R, S = sig[:32], int.from_bytes(sig[32:], "little")
sig2 = R + (S + L).to_bytes(32, "little") # different bytes, [S+L]B == [S]B

  1. Verify sig2 on both paths:

pure_path cryptography_path
original sig : True True
malleated : True False

Expected:
Verification is canonical and verifier-independent: a signature is valid or not,
the same verdict on every station. RFC 8032 §5.1.7 requires rejecting S >= L.
The code even promises it (railcall_signing.py:34-37: "signatures signed on
either path verify on the other").

Actual:
ed25519_pure.checkvalid (ed25519_pure.py:152-157) decodes S at :155
(S = _decodeint(s[b//8:b//4])) and checks [S]B == R + [h]A at :157 WITHOUT the
S < L canonicity check. Because B has order L, [S+L]B == [S]B, so S+L is a second
valid signature for the same message/key — the signature is malleable. The
cryptography fast path (railcall_signing._verify_raw) enforces S < L and
rejects it. So on a cryptography-less install (install.sh makes cryptography
best-effort / non-fatal, and the keyring-less minimal tarball is a documented
target) the pure verifier accepts a signature that a cryptography-backed station
— and any RFC-strict third-party verifier — rejects. The same signed receipt /
module.sig / team manifest therefore yields VALID on one station and
SIGNATURE_FAIL on another, and signature uniqueness (any dedup/replay logic that
keys on signature bytes) no longer holds.

Scope (honest):
Malleability does NOT forge a signature for a new message — impact is (1) broken
cross-path/third-party verdict consistency and (2) loss of signature uniqueness,
on installs using the pure fallback. cryptography-backed installs verify
correctly. Low-to-medium: it undermines the "verify anywhere, same answer"
property the audit trail rests on, without granting forgery.

Suggested fix:
In checkvalid, reject a non-canonical scalar before the curve equation:
if S >= L: return False
(mirrors cryptography / RFC 8032 §5.1.7). Optionally also bound-check R's
y-coordinate < p. This makes the pure path bit-for-bit agree with the native one.

0 replies

Sign in to reply.