mTLS CA
Mutual TLS Certificate Authority — internal CA cert issuance, mTLS gate setup, fingerprint pinning.
The framework owns storage, sealed-loading dispatch, generation tagging, and atomic commit. Cert issuance (CA generation, client cert signing, PKCS#12 packaging) delegates to a pluggable engine so the operator chooses the X.509 toolchain. The default pure-JS engine lives in lib/mtls-engine-default.js (backed by the vendored zero-dep @blamejs/pki toolkit); operators with custom requirements pass their own via opts.engine.
Files relative to dataDir: ca.crt (PEM cert, plaintext), ca.key (PEM key, plaintext — refused under caKeySealedMode: "required"), ca.key.sealed (vault.seal of the PEM bytes — the default at-rest shape), revocations.json (revocation registry), ca.crl (signed CRL derived from the registry).
caKeySealedMode defaults to "required" — sealed file required, plaintext refused. The legacy "auto" fallback was removed; it defaulted to writing plaintext on a fresh install, which is the inverse of the framework's security-defaults-on posture for at-rest key material. The "disabled" mode is a dev-only opt-out (operator must justify with audited reason).
Generation tagging: every CA cert issued by the framework embeds an OU=CAv{N} RDN in its subject DN. parseGeneration reads that back so an upgrade flow can detect legacy CAs and prompt regeneration without breaking active mTLS clients.
Engine contract: async generateCa({ generation }) -> { caCertPem, caKeyPem } async signClientCert({ cn, validityDays, caCertPem, caKeyPem }) -> { cert, key, ca, issuedAt, expiresAt } async packageP12({ cn, password, validityDays, caCertPem, caKeyPem }) -> { p12, certPem, issuedAt, expiresAt }
The engine returns the cert PEM but does NOT compute a fingerprint — the framework hashes the certificate's DER via b.crypto.hashCertFingerprint(certPem) (the same value the require-mtls gate pins) so the SHA3-512 posture stays consistent across the stack. Operators who need the X.509- conventional SHA-256 fingerprint (browser cert-details panels, openssl interop) compute it separately from the cert PEM.
b.mtlsCa.parseGeneration(certPem) #
Read the OU=CAv{N} generation tag from a PEM CA certificate's subject DN. Returns the integer N, defaulting to 1 for untagged legacy CAs (so the first regen lifts a legacy CA to generation 2 without misidentifying it as fresh) or 0 when the cert is unreadable. Operators wire this into upgrade flows that detect pre-rotation CAs whose key parameters are below the current bar.
var pem = "-----BEGIN CERTIFICATE-----\n(invalid)\n-----END CERTIFICATE-----\n";
b.mtlsCa.parseGeneration(pem);
// → 0
b.mtlsCa.parseGeneration(null);
// → 0
b.mtlsCa.create(opts) #
{
dataDir: string, // required — base for cert / key / revocation files
paths: { caKey, caKeySealed, caCert, revocations, crl }, // override defaults
vault: object, // b.vault — required when caKeySealedMode = "required"
caKeySealedMode: string, // "required" (default) | "disabled"
generation: number, // current CA generation for OU=CAv{N}
engine: object, // pluggable X.509 engine; default lib/mtls-engine-default
algorithm: string, // pin CA + leaf key algorithm; default ML-DSA-87. Pass "ECDSA-P384-SHA384" for a classical CA when a peer predates OpenSSL 3.5
issuanceStore: object, // bring-your-own { list(), add(entry) } for the issuance ledger revokeGeneration reads; default is a JSON file under dataDir
revocationStore: object, // bring-your-own { list(), add(entry) } for the revocation registry; default is a JSON file under dataDir. For a CLUSTERED deployment (shared store, per-host dataDir) also expose { readGenerationWatermark(), bumpGenerationWatermark(n) } so the issuance-supersede watermark is shared across hosts
The handle also supports a non-breaking CA algorithm migration: status()
reports the stored CA's algorithm / keyType; rotate({ generation, algorithm })
generates and atomically commits a new CA (returning { caCertPem,
previousCaCertPem }) without the algorithm-mismatch initCA raises;
commit({ retainPrevious:true }) + loadTrustBundle() + dropRetained() keep the
superseded CA trusted during a re-enrollment grace window; canVerifyInTls(algorithm?)
runs a loopback mTLS self-test proving node:tls verifies a given algorithm on
this runtime (pass the prospective algorithm to pre-flight a migration before
rotating to it); revokeGeneration(n) revokes every cert the issuance ledger
recorded under a CA generation below n; and importIssuance(entries) backfills
leaf identities the ledger lacks (a pre-upgrade dataDir or out-of-band certs)
so revokeGeneration can sweep them.
}
Build an mTLS CA handle bound to opts.dataDir. The handle owns sealed-loading of the CA private key, generation tagging on issued certs, atomic commit of newly generated material, and a pluggable engine for the X.509 work itself. Returns an object with initCA(), generateClientCert({ cn, validityDays }), generateClientP12({ cn, password, validityDays }), plus revocation helpers.
Throws MtlsCaError at config-time on bad opts (missing dataDir, sealed-mode mismatch, missing vault when seal required).
var fs = require("fs");
var os = require("os");
var path = require("path");
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-mtls-"));
var ca = b.mtlsCa.create({
dataDir: dir,
caKeySealedMode: "disabled",
generation: 1,
});
typeof ca.initCA;
// → "function"
Last updated 2026-08-08T16:39:49.652Z by seeder.