Merge 2cbe0d75754b4b2c3cc99051f3a8aa670beda133 into 4290ea753912378913b1f3f54e0fc5720afeda5d

This commit is contained in:
GCHQDeveloper581 2026-08-07 07:29:07 -07:00 committed by GitHub
commit 04ba0d93b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 110 additions and 11 deletions

10
package-lock.json generated
View File

@ -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-ascon": "^1.3.0",
@ -12496,6 +12497,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",

View File

@ -138,6 +138,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-ascon": "^1.3.0",

View File

@ -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-----`;
}

View File

@ -4,10 +4,10 @@
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import jwt from "jsonwebtoken";
import { SignJWT, importPKCS8 } from "jose";
import OperationError from "../errors/OperationError.mjs";
import {JWT_ALGORITHMS} from "../lib/JWT.mjs";
import {pkcs1ToPkcs8} from "../lib/RSA.mjs";
/**
* JWT Sign operation
@ -50,21 +50,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 importPKCS8(pkcs1ToPkcs8(key), algorithm);
} 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;

View File

@ -220,7 +220,7 @@ module.exports = {
// testOp(browser, "JSON to CSV", "test input", "test_output");
testOp(browser, "Jsonata Query", '{"a": "SGVsbG8gV29ybGQh"}', '"Hello World!"', ["$base64decode($.a)"]);
// 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");

View File

@ -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",