# Forge has signature forgery in Ed25519 due to missing S > L check

node-forge's Ed25519 verify skips the S < L check, so a valid signature mutated to S+L still verifies, breaking signature-uniqueness assumptions.

- Payout: Research
- Severity: high
- Program: node-forge
- Found by: corbanvilla, dderpym, sohcato
- Disclosed: 2026-03-26
- CVE: CVE-2026-33895
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-q67f-28xg-22rw
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-q67f-28xg-22rw
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:auth-bypass vuln:crypto vuln:signature-replay

**Target:** Ed25519 signature verification in lib/ed25519.js (crypto_sign_open)
**Root cause:** crypto_sign_open uses the signature's last 32 bytes (S) directly in scalarbase without enforcing S < L, so non-canonical scalars in the same equivalence class verify.
**Steps:**
1. Clone digitalbazaar/forge at commit 8e1d527fe8ec2670499068db783172d4fb9012e5, run Node v24.9.0.
2. Generate an Ed25519 keypair with forge and sign a fixed message.
3. Add group order L (little-endian 32 bytes) to bytes 32..63 of the 64-byte signature: `s := s + L (mod 2^256)`.
4. Verify both signatures with `ed25519.verify(...)` and with Node `crypto.verify` (OpenSSL).
5. Observe forge accepts both (original_valid true, tweaked_valid true) while crypto rejects the tweaked one.
**How found:** Compared forge's Ed25519 verify against Node/OpenSSL as a differential baseline and noticed the S+L variant diverged, tracing it to the missing canonical scalar check before scalarbase.
**Exploitation:** Attacker takes a legitimately signed object and produces a second distinct signature over the same message that forge still accepts; apps deduping or replay-tracking by signature bytes treat it as a new signature, bypassing uniqueness/canonicalization checks.
**Insight:** When auditing Ed25519/ECDSA verifiers, mutate only the scalar half of a valid signature (S+L, S+2L) and diff against a reference implementation; libraries that skip the S<L canonical check silently accept malleable signatures that break dedup/replay logic.

