From 5999c7f3413514aef50e854c20f850fa1a635a7f Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Thu, 28 May 2026 05:14:21 +0000 Subject: [PATCH 1/5] Add JWT Sign browser test --- tests/browser/02_ops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 9d29f50dfe..fc06cc96b6 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -219,7 +219,7 @@ module.exports = { // testOp(browser, "JSON Minify", "test input", "test_output"); // testOp(browser, "JSON to CSV", "test input", "test_output"); // testOp(browser, "JWT Decode", "test input", "test_output"); - // testOp(browser, "JWT Sign", "test input", "test_output"); + testOp(browser, "JWT Sign", '{"a":{"b":1}}', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjp7ImIiOjF9fQ.5PSBsZ9_B-qZa8H3l9tRAEV6qt8NgEHNJaoyjVcnTsU", ["A-key-of-256-bits-or-larger-as-per-RFC7518", "HS256", "{}"]); // testOp(browser, "JWT Verify", "test input", "test_output"); // testOp(browser, "JavaScript Beautify", "test input", "test_output"); // testOp(browser, "JavaScript Minify", "test input", "test_output"); From 01ab7855fadf8afb702dab7131dc29b74dfaa2b0 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Fri, 29 May 2026 09:26:27 +0000 Subject: [PATCH 2/5] Instal jose for use in JWT operations --- package-lock.json | 10 ++++++++++ package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3c08fc83a0..6fecfda4be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "highlight.js": "^11.11.1", "ieee754": "^1.2.1", "jimp": "1.6.0", + "jose": "^6.2.3", "jq-web": "^0.5.1", "jquery": "3.7.1", "js-sha3": "^0.9.3", @@ -12443,6 +12444,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", diff --git a/package.json b/package.json index d7e2f23c5b..6f2daa1cbd 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "highlight.js": "^11.11.1", "ieee754": "^1.2.1", "jimp": "1.6.0", + "jose": "^6.2.3", "jq-web": "^0.5.1", "jquery": "3.7.1", "js-sha3": "^0.9.3", From 3b9914f8e21614de9ebff6c7a0898c919fba4064 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Fri, 29 May 2026 14:50:03 +0000 Subject: [PATCH 3/5] Produce bug-compatible version of JWT Sign using jose instead of jsonwebtoken --- src/core/operations/JWTSign.mjs | 41 +++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/core/operations/JWTSign.mjs b/src/core/operations/JWTSign.mjs index 66831efad1..bd0ac6627c 100644 --- a/src/core/operations/JWTSign.mjs +++ b/src/core/operations/JWTSign.mjs @@ -4,7 +4,8 @@ * @license Apache-2.0 */ import Operation from "../Operation.mjs"; -import jwt from "jsonwebtoken"; +import { SignJWT, importPKCS8 } from "jose"; +import { createPrivateKey } from "crypto"; import OperationError from "../errors/OperationError.mjs"; import {JWT_ALGORITHMS} from "../lib/JWT.mjs"; @@ -50,21 +51,47 @@ class JWTSign extends Operation { * @param {Object[]} args * @returns {string} */ - run(input, args) { + async run(input, args) { const [key, algorithm, header] = args; + let secret; try { - return jwt.sign(input, key, { - algorithm: algorithm === "None" ? "none" : algorithm, - header: JSON.parse(header || "{}") - }); + if (key.startsWith("-----BEGIN RSA PRIVATE KEY-----")) { + secret = await createPrivateKey(key); + } else if (key.startsWith("-----BEGIN PRIVATE KEY-----")) { + secret = await importPKCS8(key, algorithm); + } else { + secret = new TextEncoder().encode(key); + } } catch (err) { throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA. ${err}`); } - } + const fullHeader = { alg: algorithm, typ: "JWT" }; + try { + if (header !== "{}") { + Object.assign(fullHeader, JSON.parse(header)); + } + } catch (err) { + throw new OperationError(`Header must be a valid (or empty) json object. + +${err}`); + } + + try { + const token = await new SignJWT(input) + .setProtectedHeader(fullHeader) + .sign(secret); + + return token; + } catch (err) { + throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA. + +${err}`); + } + }; } export default JWTSign; From cc1d4582bd90dc90a321057000348281699c7bd7 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Fri, 29 May 2026 14:51:19 +0000 Subject: [PATCH 4/5] Update tests to match new error output, and add additional test for bad header parameter --- tests/operations/tests/JWTSign.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/JWTSign.mjs b/tests/operations/tests/JWTSign.mjs index 9954174b58..7a1a0a4b93 100644 --- a/tests/operations/tests/JWTSign.mjs +++ b/tests/operations/tests/JWTSign.mjs @@ -89,6 +89,19 @@ TestRegister.addTests([ } ], }, + { + name: "JWT Sign: HS256, invalid header", + input: inputObject, + expectedOutput: `Header must be a valid (or empty) json object. + +SyntaxError: Unexpected token 'h', "this is not JSON" is not valid JSON`, + recipeConfig: [ + { + op: "JWT Sign", + args: [hsKey, "HS256", "this is not JSON"], + } + ], + }, { name: "JWT Sign: HS256 with custom header", input: inputObject, @@ -142,7 +155,7 @@ TestRegister.addTests([ input: inputObject, expectedOutput: `Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA. -Error: "alg" parameter "ES384" requires curve "secp384r1".`, +DataError: Named curve mismatch`, recipeConfig: [ { op: "JWT Sign", @@ -189,7 +202,7 @@ Error: "alg" parameter "ES384" requires curve "secp384r1".`, input: inputObject, expectedOutput: `Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA. -Error: secretOrPrivateKey has a minimum key size of 2048 bits for RS256`, +TypeError: RS256 requires key modulusLength to be 2048 bits or larger`, recipeConfig: [ { op: "JWT Sign", From 3c03e64e6f198a63d9a795c96c9472622100741d Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Fri, 29 May 2026 20:33:33 +0000 Subject: [PATCH 5/5] Replace non-browser-friendly createPrivateKey call with custom function to convert pkcs1 to pkcs8 --- src/core/lib/RSA.mjs | 49 +++++++++++++++++++++++++++++++++ src/core/operations/JWTSign.mjs | 5 ++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/core/lib/RSA.mjs b/src/core/lib/RSA.mjs index 9037379cf8..1909172a32 100644 --- a/src/core/lib/RSA.mjs +++ b/src/core/lib/RSA.mjs @@ -7,6 +7,7 @@ */ import forge from "node-forge"; +import * as asn1js from "asn1js"; export const MD_ALGORITHMS = { "SHA-1": forge.md.sha1, @@ -15,3 +16,51 @@ export const MD_ALGORITHMS = { "SHA-384": forge.md.sha384, "SHA-512": forge.md.sha512, }; + +const rsaEncryptionOID = "1.2.840.113549.1.1.1"; + +/** + * Convert PKCS#1 RSA private key (PEM) to PKCS#8 PEM + * @param {string} originalPem + * @returns {string} + */ +export function pkcs1ToPkcs8(originalPem) { + // remove PEM headers + const b64 = originalPem + .replace(/-----BEGIN RSA PRIVATE KEY-----/g, "") + .replace(/-----END RSA PRIVATE KEY-----/g, "") + .replace(/\s+/g, ""); + + const pkcs1Der = Uint8Array.from(atob(b64), c => c.charCodeAt(0)).buffer; + + // PKCS#8 structure: + // PrivateKeyInfo ::= SEQUENCE { + // version INTEGER, + // privateKeyAlgorithm AlgorithmIdentifier, + // privateKey OCTET STRING + // } + + const pkcs8Schema = new asn1js.Sequence({ + value: [ + new asn1js.Integer({ value: 0 }), + new asn1js.Sequence({ + value: [ + // rsaEncryption OID + new asn1js.ObjectIdentifier({ value: rsaEncryptionOID }), + new asn1js.Null() + ] + }), + new asn1js.OctetString({ valueHex: pkcs1Der }) + ] + }); + + const pkcs8Der = pkcs8Schema.toBER(false); + + const pkcs8B64 = btoa( + String.fromCharCode(...new Uint8Array(pkcs8Der)) + ); + + const lines = pkcs8B64.match(/.{1,64}/g).join("\n"); + + return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`; +} diff --git a/src/core/operations/JWTSign.mjs b/src/core/operations/JWTSign.mjs index bd0ac6627c..f2c81a4087 100644 --- a/src/core/operations/JWTSign.mjs +++ b/src/core/operations/JWTSign.mjs @@ -5,10 +5,9 @@ */ import Operation from "../Operation.mjs"; import { SignJWT, importPKCS8 } from "jose"; -import { createPrivateKey } from "crypto"; import OperationError from "../errors/OperationError.mjs"; import {JWT_ALGORITHMS} from "../lib/JWT.mjs"; - +import {pkcs1ToPkcs8} from "../lib/RSA.mjs"; /** * JWT Sign operation @@ -57,7 +56,7 @@ class JWTSign extends Operation { let secret; try { if (key.startsWith("-----BEGIN RSA PRIVATE KEY-----")) { - secret = await createPrivateKey(key); + secret = await importPKCS8(pkcs1ToPkcs8(key), algorithm); } else if (key.startsWith("-----BEGIN PRIVATE KEY-----")) { secret = await importPKCS8(key, algorithm); } else {