Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/pull_requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ jobs:

- name: Set up Docker Buildx
if: success()
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Set up QEMU
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0

- name: Production Image Build
if: success()
id: build-image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
6 changes: 3 additions & 3 deletions .github/workflows/releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ jobs:
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Set up QEMU
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0

- name: Image Metadata
id: image-metadata
Expand All @@ -81,7 +81,7 @@ jobs:
password: ${{ env.REGISTRY_PASSWORD }}

- name: Publish to GHCR
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: true
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds
# This is because npm "chromedriver" package is not compatiable with all platforms
# For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation
FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf AS builder
FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 AS builder

WORKDIR /app

Expand Down
237 changes: 116 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@
"node >= 24"
],
"devDependencies": {
"@babel/eslint-parser": "^8.0.5",
"@babel/plugin-transform-runtime": "^8.0.1",
"@babel/preset-env": "^8.0.5",
"@babel/eslint-parser": "^8.0.6",
"@babel/plugin-transform-runtime": "^8.0.6",
"@babel/preset-env": "^8.0.6",
"@babel/runtime": "^8.0.5",
"@codemirror/commands": "^6.11.1",
"@codemirror/language": "^6.12.4",
"@codemirror/search": "^6.7.2",
"@codemirror/state": "^6.7.5",
"@codemirror/view": "^6.43.12",
"@puppeteer/browsers": "3.2.2",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.13",
"@puppeteer/browsers": "3.2.3",
"autoprefixer": "^10.6.1",
"babel-loader": "^10.1.1",
"babel-plugin-polyfill-corejs3": "^1.0.0",
Expand Down Expand Up @@ -89,7 +89,7 @@
"sitemap": "^9.0.1",
"terser": "^5.51.2",
"webpack": "^5.110.3",
"webpack-bundle-analyzer": "^5.3.3",
"webpack-bundle-analyzer": "^5.4.0",
"webpack-dev-server": "^6.0.0",
"webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8"
Expand Down Expand Up @@ -124,7 +124,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^9.0.0",
"dompurify": "^3.4.15",
"dompurify": "^3.4.16",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
Expand Down
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"name": "Public Key",
"ops": [
"Parse X.509 certificate",
"Parse X.509 certificate bundles",
"Parse X.509 CRL",
"Parse ASN.1 hex string",
"PEM to Hex",
Expand Down Expand Up @@ -622,4 +623,4 @@
"Comment"
]
}
]
]
69 changes: 69 additions & 0 deletions src/core/operations/ParseX509CertificateBundles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @author Pål Sollie [sollie@gmail.com]
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import ParseX509Certificate from "./ParseX509Certificate.mjs";

/**
* Parse X.509 certificate bundles operation
*/
class ParseX509CertificateBundles extends Operation {

/**
* ParseX509CertificateBundles constructor
*/
constructor() {
super();

this.name = "Parse X.509 certificate bundles";
this.module = "PublicKey";
this.description = "Parses a PEM file containing one or more X.509 certificates and displays the validity, issuer, subject, extensions and other details of each certificate in order.";
this.infoURL = "https://wikipedia.org/wiki/X.509";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}

/**
* @param {string} input
* @returns {string}
*/
run(input) {
if (!input.length) return "No input";
if (input.length > 2_000_000) throw new OperationError("Certificate bundle exceeds 2 MB");

const begin = "-----BEGIN CERTIFICATE-----";
const end = "-----END CERTIFICATE-----";
const parser = new ParseX509Certificate();
const output = [];
let position = 0;

while (position < input.length) {
const start = input.indexOf(begin, position);
if (start === -1) break;
if (input.slice(position, start).trim()) throw new OperationError("Invalid certificate bundle content");
if (output.length >= 100) throw new OperationError("Certificate bundle exceeds 100 certificates");

const finish = input.indexOf(end, start + begin.length);
if (finish === -1) throw new OperationError(`Certificate ${output.length + 1}: PEM footer not found`);

try {
if (!/^[A-Za-z0-9+/=\s]+$/.test(input.slice(start + begin.length, finish))) throw new Error("Invalid PEM body");
output.push(`Certificate ${output.length + 1}:\n${parser.run(input.slice(start, finish + end.length), ["PEM"])}`);
} catch (err) {
throw new OperationError(`Certificate ${output.length + 1}: Certificate load error (non-certificate input?)`);
}
position = finish + end.length;
}

if (input.slice(position).trim() || !output.length) throw new OperationError("Invalid certificate bundle content");
return output.join("\n\n");
}

}

export default ParseX509CertificateBundles;
100 changes: 100 additions & 0 deletions tests/operations/tests/ParseX509CertificateBundles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Parse X.509 certificate bundles tests
*
* @author Pål Sollie [sollie@gmail.com]
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/

import TestRegister from "../../lib/TestRegister.mjs";
import ParseX509Certificate from "../../../src/core/operations/ParseX509Certificate.mjs";

const RSA_CERT = `-----BEGIN CERTIFICATE-----
MIIBfTCCASegAwIBAgIUeisK5Nwss2DGg5PCs4uSxxXyyNkwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIUlNBIHRlc3QwHhcNMjExMTE5MTcyMDI2WhcNMzExMTE3
MTcyMDI2WjATMREwDwYDVQQDDAhSU0EgdGVzdDBcMA0GCSqGSIb3DQEBAQUAA0sA
MEgCQQDyq9A6emHSLczn5Omu5muy+AReC53pTGCrW6Bi65OoobahT2RUSzXCYuvB
757fLLTKz+dLeo6sFkNhIzHZI+n7AgMBAAGjUzBRMB0GA1UdDgQWBBRO+jvkqq5p
pnQgwMMnRoun6e7eiTAfBgNVHSMEGDAWgBRO+jvkqq5ppnQgwMMnRoun6e7eiTAP
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EAR/5HAZM5qBhU/ezDUIFx
gmUGoFbIb5kJD41YCnaSdrgWglh4He4melSs42G/oxBBjuCJ0bUpqWnLl+lJkv1z
IA==
-----END CERTIFICATE-----`;

const EC_CERT = `-----BEGIN CERTIFICATE-----
MIIBfzCCASWgAwIBAgIUK4H8J3Hr7NpRLPrACj8Pje4JJJ0wCgYIKoZIzj0EAwIw
FTETMBEGA1UEAwwKUC0yNTYgdGVzdDAeFw0yMTExMTkxNzE5NDVaFw0zMTExMTcx
NzE5NDVaMBUxEzARBgNVBAMMClAtMjU2IHRlc3QwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAAQNRzwDQQM0qgJgg9YwfPXJTOoTmYmC6yBwATwfrzXR+QnxmZM2IIJr
qwuBHa8PVU2HZ2KKtaAo8fg9Uwpq/l7po1MwUTAdBgNVHQ4EFgQU/SxodXrpkybM
gcIgkxnRKd7HMzowHwYDVR0jBBgwFoAU/SxodXrpkybMgcIgkxnRKd7HMzowDwYD
VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiBU9PrOa/kXCpTTBInRf/sN
ac2iDHmbdpWzcXI+xLKNYAIhAIRR1LRSHVwOTLQ/iBXd+8LCkm5aTB27RW46LN80
ylxt
-----END CERTIFICATE-----`;

const parser = new ParseX509Certificate();
const recipeConfig = [{op: "Parse X.509 certificate bundles", args: []}];

TestRegister.addTests([
{
name: "Parse X.509 certificate bundles: single PEM",
input: RSA_CERT,
expectedOutput: `Certificate 1:\n${parser.run(RSA_CERT, ["PEM"])}`,
recipeConfig
},
{
name: "Parse X.509 certificate bundles: ordered PEM file with CRLF",
input: `\r\n${RSA_CERT}\r\n\r\n${EC_CERT}\r\n`.replace(/(?<!\r)\n/g, "\r\n"),
expectedOutput: `Certificate 1:\n${parser.run(RSA_CERT, ["PEM"])}\n\nCertificate 2:\n${parser.run(EC_CERT, ["PEM"])}`,
recipeConfig
},
{
name: "Parse X.509 certificate bundles: empty input",
input: "",
expectedOutput: "No input",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: missing footer",
input: RSA_CERT.replace("-----END CERTIFICATE-----", ""),
expectedOutput: "Certificate 1: PEM footer not found",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: invalid second certificate",
input: `${RSA_CERT}\n-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----`,
expectedOutput: "Certificate 2: Certificate load error (non-certificate input?)",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: nested certificate header",
input: `${RSA_CERT}\n-----BEGIN CERTIFICATE-----\n${EC_CERT}`,
expectedOutput: "Certificate 2: Certificate load error (non-certificate input?)",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: rejects non-certificate content",
input: `${RSA_CERT}\nignored data`,
expectedOutput: "Invalid certificate bundle content",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: rejects no certificates",
input: "not a PEM bundle",
expectedOutput: "Invalid certificate bundle content",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: limits certificate count",
input: Array(101).fill(RSA_CERT).join("\n"),
expectedOutput: "Certificate bundle exceeds 100 certificates",
recipeConfig
},
{
name: "Parse X.509 certificate bundles: limits input size",
input: RSA_CERT + " ".repeat(2_000_000),
expectedOutput: "Certificate bundle exceeds 2 MB",
recipeConfig
}
]);
Loading