node-forge self-signed certificate: working versions (tested 2026-10-17)
Generate an RSA 2048 keypair, build a self-signed X.509 certificate with subject, issuer, basicConstraints, keyUsage and subjectAltName extensions, round-trip it through PEM, verify its signature and sign/verify data with SHA-256 offline.
passed6 passed · 0 failed2026-10-17 19:19 UTC- Status
- passed
- Last passed
- 2026-10-17 19:19 UTC
- Tests
- 6 / 6
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
node-forge@1.4.0@types/node@26.6.5@types/node-forge@1.3.14tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 39061946720
The code
Exactly the files that passed. Your agent can pull them with get_recipe("node-forge-self-signed-cert") (install), or see them on GitHub.
package.json
{
"name": "node-forge-self-signed-cert",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"node-forge": "1.4.0"
},
"devDependencies": {
"@types/node": "26.6.5",
"@types/node-forge": "1.3.14",
"tsx": "4.23.15"
}
}
src/cert.ts
import forge from "node-forge";
export type Certificate = ReturnType<typeof forge.pki.createCertificate>;
export interface GeneratedCertificate {
privateKeyPem: string;
publicKeyPem: string;
certPem: string;
certificate: Certificate;
}
export function generateSelfSignedCertificate(
commonName: string,
altNames: string[],
days = 365,
): GeneratedCertificate {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + days);
const attributes = [
{ name: "commonName", value: commonName },
{ name: "organizationName", value: "ProvenPin" },
{ shortName: "C", value: "US" },
];
cert.setSubject(attributes);
cert.setIssuer(attributes);
cert.setExtensions([
{ name: "basicConstraints", cA: false },
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
{
name: "subjectAltName",
altNames: altNames.map((value) => ({ type: 2, value })),
},
]);
cert.sign(keys.privateKey, forge.md.sha256.create());
return {
privateKeyPem: forge.pki.privateKeyToPem(keys.privateKey),
publicKeyPem: forge.pki.publicKeyToPem(keys.publicKey),
certPem: forge.pki.certificateToPem(cert),
certificate: cert,
};
}
export function parseCertificate(pem: string): Certificate {
return forge.pki.certificateFromPem(pem);
}
export function verifyCertificate(certificate: Certificate): boolean {
return certificate.verify(certificate);
}
export function signData(privateKeyPem: string, data: string): string {
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
const digest = forge.md.sha256.create();
digest.update(data, "utf8");
return privateKey.sign(digest);
}
export function verifyData(
publicKeyPem: string,
data: string,
signature: string,
): boolean {
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
const digest = forge.md.sha256.create();
digest.update(data, "utf8");
return publicKey.verify(digest.digest().bytes(), signature);
}
test/cert.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import {
generateSelfSignedCertificate,
parseCertificate,
signData,
verifyCertificate,
verifyData,
} from "../src/cert.ts";
const fixture = generateSelfSignedCertificate("example.test", [
"example.test",
"www.example.test",
]);
test("generates a self-signed certificate with subject and issuer", () => {
const { certificate } = fixture;
assert.equal(certificate.subject.getField("CN")?.value, "example.test");
assert.equal(certificate.issuer.getField("CN")?.value, "example.test");
assert.equal(certificate.issuer.getField("O")?.value, "ProvenPin");
});
test("sets the requested validity window", () => {
const { certificate } = fixture;
const { notBefore, notAfter } = certificate.validity;
assert.ok(notAfter.getTime() > notBefore.getTime());
const days = (notAfter.getTime() - notBefore.getTime()) / 86_400_000;
assert.ok(Math.abs(days - 365) < 1);
});
test("includes basicConstraints, keyUsage and subjectAltName extensions", () => {
const { certificate } = fixture;
const constraints = certificate.getExtension("basicConstraints");
assert.equal(constraints.cA, false);
const usage = certificate.getExtension("keyUsage");
assert.equal(usage.digitalSignature, true);
assert.equal(usage.keyEncipherment, true);
const san = certificate.getExtension("subjectAltName");
assert.deepEqual(
san.altNames.map((entry: { value: string }) => entry.value),
["example.test", "www.example.test"],
);
});
test("round-trips through PEM and verifies the certificate signature", () => {
const parsed = parseCertificate(fixture.certPem);
assert.equal(parsed.subject.getField("CN")?.value, "example.test");
assert.equal(parsed.issuer.getField("CN")?.value, "example.test");
assert.equal(verifyCertificate(parsed), true);
});
test("signs and verifies data with SHA-256", () => {
const message = "provenpin";
const signature = signData(fixture.privateKeyPem, message);
assert.equal(typeof signature, "string");
assert.equal(verifyData(fixture.publicKeyPem, message, signature), true);
});
test("rejects tampered data", () => {
const signature = signData(fixture.privateKeyPem, "original");
assert.equal(verifyData(fixture.publicKeyPem, "tampered", signature), false);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.