GitHub Security Advisory: GHSA-q67f-28xg-22rw
Package: node-forge (NPM)
Severity: HIGH (CVSS 7.5)
CWE: CWE-347: Improper Verification of Cryptographic Signature
Vulnerable versions: < 1.4.0
Patched in: 1.4.0

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

Details:
## 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](https://datatracker.ietf.org/doc/html/rfc8032#section-8.4). This class of signature malleability has been exploited in practice to bypass authentication and authorization logic (see [CVE-2026-25793](https://nvd.nist.gov/vuln/detail/CVE-2026-25793), [CVE-2022-35961](https://nvd.nist.gov/vuln/detail/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:** `8e1d527fe8ec[PII_REDACTED]db783172d4fb9012e5`
**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:

```javascript
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 `8e1d527fe8ec[PII_REDACTED]db783172d4fb9012e5`.
- 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:

```json
{
	"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:

```text
{
    "forge": {
        "original_valid": true,
        "tweaked_valid": true
    },
    "crypto": {
        "original_valid": true,
        "tweaked_valid": false
    }
}
```

<details><summary>poc.js</summary>

```javascript
#!/usr/bin/env node
'use strict';

const path = require('path');
const crypto = require('crypto');
const forge = require('./forge');
const ed = forge.ed25519;

const MESSAGE = Buffer.from('dderpym is the coolest man alive!');

// Ed25519 group order L encoded as 32 bytes, little-endian (RFC 803