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) #

0.7.68

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) #

0.7.68
{
    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.