mTLS Engine

The certificate authority behind b.mtlsCa: issuing the CA, signing client certificates, packaging them as PKCS#12, and publishing revocations.

The signature algorithm is chosen by trying the strongest candidate the runtime can actually use, starting at ML-DSA-87 and falling back through the rest. The probe generates a keypair and signs a certificate with it, so the choice is made against what the runtime does rather than what it advertises.

The probe stops there. It does not complete a TLS handshake, so a runtime that can sign a certificate its own TLS stack will not accept keeps the stronger candidate. canVerifyInTls(label) is the separate check that answers the handshake question, and a deployment that needs that guarantee calls it and pins the algorithm it reports.

algorithmEnvelope() is how that selection becomes auditable: it reports which algorithm was chosen, its posture, the whole priority list that was tried, and the PKCS#12 encryption and MAC parameters. An operator asked which algorithm their client certificates use has an answer rather than a default to look up.

b.mtlsEngine.generateCa(opts?) #

stable0.3.37
{
  name:       string,   // CA common name
  generation: number,   // recorded as CAv; default 1
  algorithm:  string,   // exact pin; no fallback. Omit to select by capability
}

Generate a CA keypair and self-signed certificate, and resolve with { caCertPem, caKeyPem }.

Left to itself, the signature algorithm is selected by capability rather than configuration: the strongest candidate the runtime can generate a key for and sign a certificate with wins, and algorithmEnvelope() reports which one that was. The selection does not test a TLS handshake; canVerifyInTls does that separately.

algorithm is an exact pin, not a preference. The named candidate is the one used, with no fallback and no comparison against the others, so naming a classical candidate issues under it even where ML-DSA is available. A name outside the candidates raises mtls-engine/unknown-algorithm listing them, and one the runtime cannot sign with raises mtls-engine/algorithm-unavailable rather than quietly choosing something else. With no algorithm, a runtime that can sign with none of the candidates raises mtls-engine/no-algorithm.

generation is recorded in the subject as CAv, which is how two generations of CA are told apart during a rollover while both are trusted. It defaults to 1.

var b = require("@blamejs/core");
var ca = await b.mtlsEngine.generateCa({ name: "Acme Internal CA" });
b.mtlsEngine.algorithmEnvelope().cert.label;   // → the algorithm used

b.mtlsEngine.signClientCert(opts) #

stable0.3.37
{
  cn:           string,   // required; the subject common name
  caCertPem:    string,   // required; the issuing CA certificate
  caKeyPem:     string,   // required; the issuing CA key
  usage:        string,   // client | server | both; default "client"
  validityDays: number,   // positive integer
  sans:         array,    // subject alternative names; must be an array
  algorithm:    string,   // exact pin; no fallback. Omit to select by capability
}

Sign a leaf certificate under the CA and resolve with the certificate and its key.

cn, caCertPem and caKeyPem are required, raising mtls-engine/missing-arg otherwise. usage is client, server or both and decides the extended key usage; anything else raises mtls-engine/bad-usage rather than producing a certificate with no usage, which a peer would reject in a way that is hard to diagnose.

validityDays must be a positive integer and defaults to the leaf default, which is short: a client certificate is meant to be reissued, and a long one is a revocation problem waiting to happen. A value that is not raises mtls-engine/bad-validity-days.

A cn that is empty once normalized raises mtls-engine/bad-cn. Each entry of sans is a string holding a hostname, an IPv4 or IPv6 address, or either behind a DNS: or IP: prefix, and anything else raises mtls-engine/bad-san, including a null or a number left by a configuration placeholder, which would otherwise become the DNS name "null" or "123". sans itself has to be an array whenever it is present: a bare string, or a null left by a configuration placeholder, raises the same code rather than reading as no sans at all, which issued a certificate for the common name instead of the identity configured. A hostname is 1 to 253 octets in labels of 1 to 63 letters, digits, - or _, with an optional leading * label; a name like "not a host" is refused rather than written into a certificate nothing can match. For usage server or both with no sans, the normalized cn becomes the one dNSName and is held to the same rule. On the algorithm: mtls-engine/unknown-algorithm names one this build does not implement, mtls-engine/algorithm-unavailable one the runtime will not sign with, and mtls-engine/no-algorithm means no candidate survived the capability probe at all.

var b = require("@blamejs/core");
var leaf = await b.mtlsEngine.signClientCert({
  cn:        "worker-7",
  caCertPem: ca.caCertPem,
  caKeyPem:  ca.caKeyPem,
  usage:     "client",
});

b.mtlsEngine.packageP12(opts) #

stable0.3.37
{
  password:     string,   // required; protects the PKCS#12 file
  cn:           string,   // required; the subject common name
  caCertPem:    string,   // required; the issuing CA certificate
  caKeyPem:     string,   // required; the issuing CA key
  usage:        string,   // client | server | both
  validityDays: number,   // positive integer
  sans:         array,    // subject alternative names, as signClientCert takes them
  algorithm:    string,   // exact pin; no fallback. Omit to select by capability
}

Sign a leaf certificate and package it with its key as PKCS#12, the format a browser or an operating system keychain imports.

It takes everything signClientCert takes, plus a non-empty password; without one it raises mtls-engine/no-password, since an unprotected PKCS#12 file is a private key in the clear.

Because it signs the leaf itself, every refusal signClientCert makes reaches this call too: mtls-engine/missing-arg for an absent cn, caCertPem or caKeyPem, mtls-engine/bad-cn for a common name empty once normalized, mtls-engine/bad-usage for a usage outside client, server and both, mtls-engine/bad-validity-days for a validity that is not a positive integer, mtls-engine/bad-san for a subject alternative name that is neither a hostname nor an IP address, and mtls-engine/unknown-algorithm, mtls-engine/algorithm-unavailable or mtls-engine/no-algorithm on the algorithm choice.

The MAC follows the certificate's posture: a post-quantum certificate is packaged with PBMAC1, and a classical one with HMAC at a higher iteration count, because an importer that understands one may not understand the other. algorithmEnvelope() reports both parameter sets.

var b = require("@blamejs/core");
var p12 = await b.mtlsEngine.packageP12({
  cn:        "worker-7",
  caCertPem: ca.caCertPem,
  caKeyPem:  ca.caKeyPem,
  password:  process.env.P12_PASSWORD,
});

b.mtlsEngine.algorithmEnvelope() #

stable0.3.37soc2

Report which signature algorithm the last issuance selected, its posture, the priority list that was tried, and the PKCS#12 encryption and MAC parameters.

This is the answer to "which algorithm are our client certificates signed with", which is otherwise a question about what the runtime happened to support. Because the choice is made by probing rather than from configuration, reporting it is the only way an operator or an auditor can know.

cert.posture is pqc-pure or classical, and cert.priority is the full candidate list in order, so a deployment that fell back can see what it fell back from.

var b = require("@blamejs/core");
var env = b.mtlsEngine.algorithmEnvelope();
env.cert.label;                // → e.g. "ML-DSA-87"
env.cert.posture;              // → "pqc-pure" or "classical"

b.mtlsEngine.generateCrl(opts) #

stable0.6.45
{
  caCertPem:   string,   // required; the issuing CA certificate
  caKeyPem:    string,   // required; the issuing CA key
  crlNumber:   number,   // required; positive, strictly increasing
  revocations: array,    // serials with their revocation times
  thisUpdate:  object,   // Date the list is issued
  nextUpdate:  object,   // Date the list expires
}

Sign a certificate revocation list and resolve with it.

An absent caCertPem or caKeyPem raises mtls-engine/missing-arg.

crlNumber must be a positive integer, raising mtls-engine/bad-crl-number otherwise. RFC 5280 section 5.2.3 also requires it to increase with every CRL an issuer signs, and that part is the caller's to hold: this function keeps no record of what it issued before, so a repeated or lower number is accepted. A relying party given one may keep an older list, which is how a revoked certificate stays accepted, so allocate the number from somewhere that cannot hand out the same one twice.

revocations lists the serials and their revocation times. An omitted revocations is a valid CRL saying nothing is revoked, which is what a fresh issuer publishes, and so is an empty array; anything else present there raises mtls-engine/bad-revocations rather than being read as that empty list, since a null placeholder, a single unwrapped entry or a JSON string would otherwise sign a list asserting that nothing is revoked. thisUpdate and nextUpdate set the validity window and each has to be a Date, raising mtls-engine/bad-crl-date otherwise; an epoch number, which is the shape a config file or an HTTP body carries, previously reached the signer and raised an uncoded TypeError. A relying party is entitled to cache the list until nextUpdate.

var b = require("@blamejs/core");
var crl = await b.mtlsEngine.generateCrl({
  caCertPem:   ca.caCertPem,
  caKeyPem:    ca.caKeyPem,
  crlNumber:   1,
  revocations: [],
});

b.mtlsEngine.canVerifyInTls(label) #

stable0.18.3

Answer whether this runtime can complete a mutually-authenticated TLS handshake using certificates signed with the named algorithm.

It finds out by doing it: a throwaway CA and two one-day leaves are issued under that algorithm and a real handshake is attempted between them. A runtime that can generate a key and sign a certificate may still fail to verify it during a handshake, so asking the TLS stack is the only answer worth having.

It answers false rather than raising, for any failure at any stage, which is what makes it usable as the selection probe behind the algorithm choice.

var b = require("@blamejs/core");
await b.mtlsEngine.canVerifyInTls("ML-DSA-87");   // → true or false here

Last updated 2026-10-12T14:21:56.879Z by seeder.