Skip to content

fix: #927 — jwt.verify dispatches per algorithm (ES256/RS256), accepts private PEM - #1025

Merged
proggeramlug merged 1 commit into
mainfrom
fix/927-jwt-verify-algorithms
May 18, 2026
Merged

fix: #927 — jwt.verify dispatches per algorithm (ES256/RS256), accepts private PEM#1025
proggeramlug merged 1 commit into
mainfrom
fix/927-jwt-verify-algorithms

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

The previous #927 fix (#936) made jwt.verify return the parsed claims object instead of the JSON text, but ES256 / RS256 tokens were still failing verification silently — the generic NativeModSig table routed every algorithm through js_jwt_verify, which was hardcoded to HS256. The token was signed correctly with ES256, then Validation::new(Algorithm::HS256) rejected it with InvalidAlgorithm, the runtime returned a null pointer, and js_json_parse_or_null surfaced it as null — breaking shop-admin's auth middleware on the very first authenticated request after a successful signup.

Fix mirrors the sign side's algorithm-aware dispatch:

  • Add js_jwt_verify_es256 / js_jwt_verify_rs256 runtime functions in perry-stdlib/src/jsonwebtoken.rs. Both auto-derive the public key from a private PEM (PKCS#8 or SEC1 for EC, PKCS#8 or PKCS#1 for RSA) so callers can reuse the same PEM they passed to sign — matches Node jsonwebtoken's ergonomics. Direct public-key PEMs (SPKI) also accepted unchanged.
  • Replace the generic NativeModSig verify row with lower_jsonwebtoken_verify in crates/perry-codegen/src/lower_call/native.rs. Routes on the canonical algorithms: ['…'] array option as well as the singular algorithm: '…'. Falls back to HS256 when the option is absent or unparseable. Return path still pipes through js_json_parse_or_null so user code sees an object on success and null on failure (same contract as before).
  • Bonus: js_jwt_sign_es256 now also accepts SEC1 (-----BEGIN EC PRIVATE KEY-----) PEMs, which is what openssl ecparam -genkey -name prime256v1 emits by default. Symmetric with the new verify ergonomics.

New perry-stdlib deps gated on bundled-jsonwebtoken: p256 (EC public-key derivation), rsa (RSA public-key derivation), spki (PEM serialization helpers).

Test plan

Repro

import jwt from "jsonwebtoken";
import { readFileSync } from "fs";
const PEM = readFileSync("/tmp/skelpo-jwt.pem", "utf8");
const TOK = jwt.sign({ sub: "u-001", acc: "a-001" }, PEM, { algorithm: "ES256", issuer: "z" });
const decoded = jwt.verify(TOK, PEM, { algorithms: ["ES256"], issuer: "z" }) as any;
console.log("typeof:", typeof decoded, "sub:", decoded?.sub);

Before this PR: typeof: object sub: undefined (decoded was null).
After: typeof: object sub: u-001.

…s private PEM

The previous #927 fix (#936) made `jwt.verify` return the parsed
claims object instead of the JSON text, but ES256 / RS256 tokens
were still failing verification silently — the generic NativeModSig
table routed every algorithm through `js_jwt_verify`, which was
hardcoded to HS256. The token was signed correctly with ES256, then
`Validation::new(Algorithm::HS256)` rejected it with
`InvalidAlgorithm`, the runtime returned a null pointer, and
`js_json_parse_or_null` surfaced it as `null` — breaking the
shop-admin auth middleware on the very first authenticated request
after a successful signup.

Fix mirrors the `sign` side's algorithm-aware dispatch:

- Add `js_jwt_verify_es256` / `js_jwt_verify_rs256` runtime
  functions in `perry-stdlib`. Both auto-derive the public key from
  a private PEM (PKCS#8 *or* SEC1 for EC, PKCS#8 *or* PKCS#1 for
  RSA) so callers can reuse the same PEM they passed to `sign` —
  matches Node `jsonwebtoken`'s ergonomics. Direct public-key PEMs
  (SPKI) also accepted unchanged.

- Replace the generic NativeModSig `verify` row with
  `lower_jsonwebtoken_verify` in `lower_call/native.rs`. Routes on
  the canonical `algorithms: ['…']` array option as well as the
  singular `algorithm: '…'`. Falls back to HS256 when the option is
  absent or unparseable. Return path still pipes through
  `js_json_parse_or_null` so user code sees an object on success
  and `null` on failure (same contract as before).

- Bonus: `js_jwt_sign_es256` now accepts SEC1 (`BEGIN EC PRIVATE
  KEY`) PEMs in addition to PKCS#8, which is what `openssl ecparam
  -genkey` emits by default. Symmetric with the new verify
  ergonomics.

New `perry-stdlib` deps gated on `bundled-jsonwebtoken`: `p256`
(EC public-key derivation), `rsa` (RSA public-key derivation),
`spki` (PEM serialization).

Verified with a 9-line standalone repro (sign + verify a token
with `algorithms: ['ES256']`) — previously returned `null`, now
returns the parsed claims object with `decoded.sub` accessible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant