Lack of data validation In node-forge
Description
Forge has signature forgery in Ed25519 due to missing S > L check
Summary
Ed25519 signature verification accepts forged non-canonical signatures where the scalar S is not reduced modulo the group order (S >= L). A valid signature and its S + L variant both verify in forge, while Node.js crypto.verify (OpenSSL-backed) rejects the S + L variant, as defined by the specification. This class of signature malleability has been exploited in practice to bypass authentication and authorization logic (see CVE-2026-25793, CVE-2022-35961). Applications relying on signature uniqueness (i.e., dedup by signature bytes, replay tracking, signed-object canonicalization checks) may be bypassed.
Impacted Deployments
Tested commit: 8e1d527fe8ec2670499068db783172d4fb9012e5
Affected versions: tested on v1.3.3 (latest release) and all versions since Ed25519 was implemented.
Configuration assumptions:
Default forge Ed25519 verify API path (ed25519.verify(...)).
Root Cause
In lib/ed25519.js, crypto_sign_open(...) uses the signature's last 32 bytes (S) directly in scalar multiplication:
scalarbase(q, sm.subarray(32));
There is no prior check enforcing S < L (Ed25519 group order). As a result, equivalent scalar classes can pass verification, including a modified signature where S := S + L (mod 2^256) when that value remains non-canonical. The PoC demonstrates this by mutating only the S half of a valid 64-byte signature.
Reproduction Steps
Use Node.js (tested with v24.9.0) and clone digitalbazaar/forge at commit 8e1d527fe8ec2670499068db783172d4fb9012e5.
Place and run the PoC script (poc.js) with node poc.js in the same level as the forge folder.
The script generates an Ed25519 keypair via forge, signs a fixed message, mutates the signature by adding Ed25519 order L to S (bytes 32..63), and verifies both original and tweaked signatures with forge and Node/OpenSSL (crypto.verify).
Confirm output includes:
{ "forge": { "original_valid": true, "tweaked_valid": true }, "crypto": { "original_valid": true, "tweaked_valid": false...
Proof of Concept
Overview:
Demonstrates a valid control signature and a forged (S + L) signature in one run.
Uses Node/OpenSSL as a differential verification baseline.
Observed output on tested commit:
{ "forge": { "original_valid": true, "tweaked_valid": true }, "crypto": { "original_valid": true, "tweaked_valid": false...
poc.js
#!/usr/bin/env node 'use strict'; const path = require('path'); const crypto = require('crypto'); const forge = require('./forge'); const ed = forge.ed25519; ...
Suggested Patch
Add strict canonical scalar validation in Ed25519 verify path before scalar multiplication. (Parse S as little-endian 32-byte integer and reject if S >= L).
Here is a patch we tested on our end to resolve the issue, though please verify it on your end:
index f3e6faa..87eb709 100644 --- a/lib/ed25519.js +++ b/lib/ed25519.js @@ -380,6 +380,10 @@ function crypto_sign_open(m, sm, n, pk) { return -1; } + if(!_isCanonicalSignatureScalar(sm, 32)) {...
Resources
RFC 8032 (Ed25519): https://datatracker.ietf.org/doc/html/rfc8032#section-8.4
Ed25519 and Ed448 signatures are not malleable due to the verification check that decoded S is smaller than l
Credit
This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
npm | 1.4.0 |
Aliases
References