@blamejs/core 0.17.23 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -5
- package/NOTICE +12 -13
- package/README.md +3 -3
- package/lib/audit.js +69 -1
- package/lib/auth/oauth.js +57 -0
- package/lib/cli.js +15 -5
- package/lib/daemon.js +192 -2
- package/lib/guard-tenant-id.js +12 -1
- package/lib/mtls-ca.js +173 -18
- package/lib/mtls-engine-default.js +339 -314
- package/lib/outbox.js +43 -13
- package/lib/redact.js +39 -6
- package/lib/safe-json.js +9 -1
- package/lib/self-update.js +215 -24
- package/lib/vendor/MANIFEST.json +27 -30
- package/lib/vendor/blamejs-pki.cjs +28419 -0
- package/lib/webhook-dispatcher.js +55 -24
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/vendor/pki.cjs +0 -39718
|
@@ -5,42 +5,52 @@
|
|
|
5
5
|
* mtls-engine-default — pure-JS X.509 engine wired into b.mtlsCa.
|
|
6
6
|
*
|
|
7
7
|
* Implements the engine contract documented at the top of lib/mtls-ca.js:
|
|
8
|
-
* generateCa({ generation })
|
|
9
|
-
* signClientCert({ cn, validityDays,
|
|
10
|
-
* caCertPem, caKeyPem })
|
|
11
|
-
* packageP12({ cn, password, validityDays,
|
|
12
|
-
* caCertPem, caKeyPem })
|
|
8
|
+
* generateCa({ generation, algorithm }) -> { caCertPem, caKeyPem }
|
|
9
|
+
* signClientCert({ cn, validityDays, usage, sans, algorithm,
|
|
10
|
+
* caCertPem, caKeyPem }) -> { cert, key, ca, issuedAt, expiresAt }
|
|
11
|
+
* packageP12({ cn, password, validityDays, algorithm,
|
|
12
|
+
* caCertPem, caKeyPem }) -> { p12, certPem, issuedAt, expiresAt }
|
|
13
|
+
* generateCrl({ caCertPem, caKeyPem,
|
|
14
|
+
* revocations, thisUpdate,
|
|
15
|
+
* nextUpdate }) -> CRL PEM
|
|
13
16
|
*
|
|
14
|
-
* Backed by lib/vendor/pki.cjs (
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Backed by lib/vendor/blamejs-pki.cjs (@blamejs/pki — zero-dep pure-CJS
|
|
18
|
+
* X.509 / CRL / PKCS#12 toolkit with a built-in WebCrypto over node:crypto).
|
|
19
|
+
* No openssl CLI is invoked.
|
|
17
20
|
*
|
|
18
21
|
* Algorithm envelope:
|
|
19
|
-
* CA + leaf signatures:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* CA + leaf signatures: ML-DSA-87 by default (FIPS 204). node:tls verifies
|
|
23
|
+
* ML-DSA certificate chains + CertificateVerify on the
|
|
24
|
+
* supported Node LTS (OpenSSL 3.5), so PQC-signed mTLS
|
|
25
|
+
* certificates complete a real mutual-auth handshake.
|
|
26
|
+
* Operators whose peers are not yet on OpenSSL 3.5 pass
|
|
27
|
+
* algorithm: "ECDSA-P384-SHA384" (b.mtlsCa.create({ algorithm })
|
|
28
|
+
* threads it into both CA generation and leaf issuance)
|
|
29
|
+
* for a universally-interoperable classical CA. A pin is
|
|
30
|
+
* per-call: it never mutates the process-wide default, so
|
|
31
|
+
* one classical CA cannot downgrade another CA's ML-DSA-87
|
|
32
|
+
* default. SLH-DSA is intentionally not offered here:
|
|
33
|
+
* OpenSSL rejects it in the TLS handshake ("unknown
|
|
34
|
+
* certificate type").
|
|
35
|
+
* PKCS#12 key + cert bags: PBES2 + AES-256-CBC + PBKDF2-HMAC-SHA-512, 2,000,000 iter
|
|
36
|
+
* PKCS#12 outer MAC : follows the cert tier — PBMAC1 + PBKDF2-HMAC-SHA-512
|
|
37
|
+
* @ 2,000,000 iter (RFC 9579) for the PQC default; the
|
|
38
|
+
* classical ECDSA-P384 bridge uses the legacy-importable
|
|
39
|
+
* RFC 7292 App. B HMAC-SHA-512 MacData so a pre-OpenSSL-3.5
|
|
40
|
+
* peer can verify it
|
|
30
41
|
*/
|
|
31
42
|
|
|
32
43
|
var nodeCrypto = require("node:crypto");
|
|
33
|
-
|
|
34
|
-
var pki = require("./vendor/pki.cjs");
|
|
44
|
+
var pki = require("./vendor/blamejs-pki.cjs");
|
|
35
45
|
|
|
36
46
|
var C = require("./constants");
|
|
37
47
|
var bCrypto = require("./crypto");
|
|
48
|
+
var ipUtils = require("./ip-utils");
|
|
38
49
|
var numericBounds = require("./numeric-bounds");
|
|
50
|
+
var safeBuffer = require("./safe-buffer");
|
|
39
51
|
var { FrameworkError } = require("./framework-error");
|
|
40
52
|
|
|
41
|
-
var
|
|
42
|
-
var pkijs = pki.pkijs;
|
|
43
|
-
var webcrypto = pki.crypto;
|
|
53
|
+
var subtle = pki.webcrypto.subtle;
|
|
44
54
|
|
|
45
55
|
class MtlsEngineError extends FrameworkError {
|
|
46
56
|
constructor(code, message) {
|
|
@@ -53,164 +63,217 @@ class MtlsEngineError extends FrameworkError {
|
|
|
53
63
|
|
|
54
64
|
var CA_KEY_USAGES = ["sign", "verify"];
|
|
55
65
|
|
|
56
|
-
// Algorithm priority — each entry probed at first use; the first one
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
66
|
+
// Algorithm priority — each entry probed at first use; the first one the
|
|
67
|
+
// vendored PKI toolkit AND webcrypto can both honour wins. Ordered
|
|
68
|
+
// highest-PQC-posture first so the engine issues post-quantum certificates
|
|
69
|
+
// by default. Every listed candidate has been confirmed to complete a real
|
|
70
|
+
// node:tls mutual-auth handshake on the supported Node LTS.
|
|
60
71
|
//
|
|
61
|
-
// keyAlg: passed to webcrypto.subtle.generateKey
|
|
62
|
-
//
|
|
63
|
-
// label
|
|
64
|
-
//
|
|
72
|
+
// keyAlg : passed to webcrypto.subtle.generateKey; the certificate signature
|
|
73
|
+
// algorithm is resolved from the signing key by pki.x509.sign.
|
|
74
|
+
// label : surfaced via b.mtlsCa.status() so operators can audit which
|
|
75
|
+
// algorithm the in-flight CA generation is using, and passed to
|
|
76
|
+
// generateCa({ algorithm }) to pin a specific one.
|
|
65
77
|
var ALG_CANDIDATES = [
|
|
66
|
-
// Pure-PQC
|
|
67
|
-
//
|
|
68
|
-
// verification support; currently issuance-only on most stacks.
|
|
69
|
-
{
|
|
70
|
-
label: "SLH-DSA-SHAKE-256f",
|
|
71
|
-
keyAlg: { name: "SLH-DSA-SHAKE-256f" },
|
|
72
|
-
sigAlg: { name: "SLH-DSA-SHAKE-256f" },
|
|
73
|
-
posture: "pqc-pure",
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
label: "SLH-DSA-SHAKE-128f",
|
|
77
|
-
keyAlg: { name: "SLH-DSA-SHAKE-128f" },
|
|
78
|
-
sigAlg: { name: "SLH-DSA-SHAKE-128f" },
|
|
79
|
-
posture: "pqc-pure",
|
|
80
|
-
},
|
|
81
|
-
// Pure-PQC lattice — FIPS 204 (Dilithium family). Smaller than SLH-DSA,
|
|
82
|
-
// accepted by the same emerging cert-store deployments.
|
|
78
|
+
// Pure-PQC lattice — FIPS 204 (ML-DSA / Dilithium family). Verified end to
|
|
79
|
+
// end in a node:tls mutual-auth handshake (chain + CertificateVerify).
|
|
83
80
|
{
|
|
84
81
|
label: "ML-DSA-87",
|
|
85
82
|
keyAlg: { name: "ML-DSA-87" },
|
|
86
|
-
sigAlg: { name: "ML-DSA-87" },
|
|
87
83
|
posture: "pqc-pure",
|
|
88
84
|
},
|
|
89
85
|
{
|
|
90
86
|
label: "ML-DSA-65",
|
|
91
87
|
keyAlg: { name: "ML-DSA-65" },
|
|
92
|
-
sigAlg: { name: "ML-DSA-65" },
|
|
93
88
|
posture: "pqc-pure",
|
|
94
89
|
},
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// KEX; these certs sign with ECDSA P-384 + SHA-384.
|
|
90
|
+
// Classical bridge — for peers not yet on OpenSSL 3.5. Opt in with
|
|
91
|
+
// generateCa({ algorithm: "ECDSA-P384-SHA384" }).
|
|
98
92
|
{
|
|
99
93
|
label: "ECDSA-P384-SHA384",
|
|
100
94
|
keyAlg: { name: "ECDSA", namedCurve: "P-384" },
|
|
101
|
-
sigAlg: { name: "ECDSA", hash: "SHA-384" },
|
|
102
95
|
posture: "classical",
|
|
96
|
+
// The signature digest MUST be passed to pki.x509.sign / pki.crl.sign — the
|
|
97
|
+
// toolkit defaults an EC key to SHA-256, which would silently downgrade this
|
|
98
|
+
// bridge below its SHA-384 posture (and below the framework's no-SHA-256
|
|
99
|
+
// default rule). ML-DSA needs no digest (it is no-prehash: the toolkit forces
|
|
100
|
+
// SHA-512 and ignores an explicit digest), so only the classical entry sets it.
|
|
101
|
+
digest: "sha384",
|
|
103
102
|
},
|
|
104
103
|
];
|
|
105
104
|
|
|
106
|
-
// First-call probe cache
|
|
105
|
+
// First-call probe cache for the process-wide DEFAULT (never written by a per-call
|
|
106
|
+
// pin, so a pin can't downgrade the default). Re-runs after engine reload.
|
|
107
107
|
var _selectedAlg = null;
|
|
108
|
+
// The most-recent RESOLVED algorithm (pinned OR default) — reporting only, so
|
|
109
|
+
// algorithmEnvelope() can describe a pinned selection accurately without poisoning
|
|
110
|
+
// the default cache above.
|
|
111
|
+
var _lastSelectedAlg = null;
|
|
108
112
|
|
|
109
113
|
async function _probeCandidate(c) {
|
|
110
114
|
try {
|
|
111
|
-
var pair = await
|
|
115
|
+
var pair = await subtle.generateKey(c.keyAlg, true, CA_KEY_USAGES);
|
|
116
|
+
/* c8 ignore next -- defensive: webcrypto.generateKey always resolves a valid keypair here */
|
|
112
117
|
if (!pair || !pair.publicKey) return false;
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
await x509.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// Confirm the toolkit can also mint a certificate under this key — some
|
|
119
|
+
// key algorithms keygen in webcrypto but aren't wired through the X.509
|
|
120
|
+
// signer, and selecting one we can't issue with would surface a
|
|
121
|
+
// confusing failure on first real issuance.
|
|
122
|
+
var spki = Buffer.from(await subtle.exportKey("spki", pair.publicKey));
|
|
123
|
+
await pki.x509.sign({
|
|
124
|
+
subject: "probe",
|
|
125
|
+
subjectPublicKey: spki,
|
|
126
|
+
serialNumber: "0x01",
|
|
122
127
|
notBefore: new Date(),
|
|
123
128
|
notAfter: new Date(Date.now() + C.TIME.seconds(1)),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
signingKey: pair.privateKey,
|
|
127
|
-
});
|
|
129
|
+
extensions: { basicConstraints: { cA: true } },
|
|
130
|
+
}, { key: pair.privateKey }, { pem: true });
|
|
128
131
|
return true;
|
|
132
|
+
/* c8 ignore start -- probe never throws: every listed candidate algorithm is honoured by the runtime's OpenSSL 3.5+ */
|
|
129
133
|
} catch (_e) {
|
|
130
134
|
return false;
|
|
131
135
|
}
|
|
136
|
+
/* c8 ignore stop */
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _emitAlgorithmSelected(c, candidatesProbed) {
|
|
140
|
+
setImmediate(function () {
|
|
141
|
+
try {
|
|
142
|
+
var auditMod = require("./audit"); // allow:inline-require — circular-load defense
|
|
143
|
+
auditMod.safeEmit({
|
|
144
|
+
action: "mtls.engine.algorithm_selected",
|
|
145
|
+
outcome: "success",
|
|
146
|
+
metadata: { label: c.label, posture: c.posture, candidatesProbed: candidatesProbed },
|
|
147
|
+
});
|
|
148
|
+
/* c8 ignore next -- belt-and-suspenders: audit.safeEmit is itself drop-silent, so this catch is unreachable */
|
|
149
|
+
} catch (_e) { /* drop-silent */ }
|
|
150
|
+
});
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
// Resolve the signing algorithm. With no argument the highest-posture
|
|
154
|
+
// candidate that probes clean is cached and reused. `preferredLabel` pins a
|
|
155
|
+
// specific candidate (the generateCa({ algorithm }) opt-in) — it must exist
|
|
156
|
+
// and probe clean, else the call throws rather than silently downgrading.
|
|
157
|
+
async function _selectAlgorithm(preferredLabel) {
|
|
158
|
+
if (preferredLabel) {
|
|
159
|
+
var wanted = null;
|
|
160
|
+
for (var w = 0; w < ALG_CANDIDATES.length; w++) {
|
|
161
|
+
if (ALG_CANDIDATES[w].label === preferredLabel || ALG_CANDIDATES[w].keyAlg.name === preferredLabel) {
|
|
162
|
+
wanted = ALG_CANDIDATES[w];
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!wanted) {
|
|
167
|
+
throw new MtlsEngineError("mtls-engine/unknown-algorithm",
|
|
168
|
+
"unknown algorithm " + JSON.stringify(preferredLabel) + " — supported: " +
|
|
169
|
+
ALG_CANDIDATES.map(function (c) { return c.label; }).join(", "));
|
|
170
|
+
}
|
|
171
|
+
/* c8 ignore start -- unreachable: every pinnable candidate probes clean on the runtime's OpenSSL 3.5+ */
|
|
172
|
+
if (!(await _probeCandidate(wanted))) {
|
|
173
|
+
throw new MtlsEngineError("mtls-engine/algorithm-unavailable",
|
|
174
|
+
"algorithm " + JSON.stringify(preferredLabel) + " is not available in this runtime");
|
|
175
|
+
}
|
|
176
|
+
/* c8 ignore stop */
|
|
177
|
+
// A pinned algorithm is per-call and MUST NOT be cached as the process
|
|
178
|
+
// default. `_selectedAlg` is the shared fallback the no-argument path below
|
|
179
|
+
// returns; writing a classical pin here would make a single
|
|
180
|
+
// generateCa({ algorithm: "ECDSA-P384-SHA384" }) silently downgrade every
|
|
181
|
+
// later default CA/leaf off the ML-DSA-87 PQC-first default in the same
|
|
182
|
+
// process. Callers thread a pin explicitly through generateCa /
|
|
183
|
+
// signClientCert instead (b.mtlsCa.create({ algorithm })).
|
|
184
|
+
_lastSelectedAlg = wanted; // reporting only — does NOT touch the default cache
|
|
185
|
+
_emitAlgorithmSelected(wanted, 1);
|
|
186
|
+
return wanted;
|
|
187
|
+
}
|
|
188
|
+
if (_selectedAlg) { _lastSelectedAlg = _selectedAlg; return _selectedAlg; }
|
|
136
189
|
for (var i = 0; i < ALG_CANDIDATES.length; i++) {
|
|
137
190
|
var c = ALG_CANDIDATES[i];
|
|
138
|
-
|
|
139
|
-
if (ok) {
|
|
191
|
+
if (await _probeCandidate(c)) {
|
|
140
192
|
_selectedAlg = c;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Pre-PQC ecosystems land on the ECDSA-P384 bridge silently;
|
|
144
|
-
// this puts the choice on the chain so compliance dashboards
|
|
145
|
-
// alert when an operator's deployment hasn't yet picked up the
|
|
146
|
-
// PQ-signed-cert capability the framework would otherwise
|
|
147
|
-
// prefer.
|
|
148
|
-
setImmediate(function () {
|
|
149
|
-
try {
|
|
150
|
-
var auditMod = require("./audit"); // allow:inline-require — circular-load defense
|
|
151
|
-
auditMod.safeEmit({
|
|
152
|
-
action: "mtls.engine.algorithm_selected",
|
|
153
|
-
outcome: "success",
|
|
154
|
-
metadata: { label: c.label, posture: c.posture, candidatesProbed: i + 1 },
|
|
155
|
-
});
|
|
156
|
-
} catch (_e) { /* drop-silent */ }
|
|
157
|
-
});
|
|
193
|
+
_lastSelectedAlg = c;
|
|
194
|
+
_emitAlgorithmSelected(c, i + 1);
|
|
158
195
|
return c;
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
|
-
|
|
198
|
+
/* c8 ignore start -- unreachable: ECDSA-P384-SHA384 is universal, so the candidate loop always returns first */
|
|
199
|
+
// Should never happen — ECDSA-P384 is universal.
|
|
162
200
|
throw new MtlsEngineError("mtls-engine/no-algorithm",
|
|
163
201
|
"no candidate algorithm passed the webcrypto + x509 probe");
|
|
202
|
+
/* c8 ignore stop */
|
|
164
203
|
}
|
|
165
204
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
var
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// a byte quantity. Hex form per the same rationale as P12_CONTENT_ENC.length.
|
|
179
|
-
var P12_ITER = 0x1E8480;
|
|
205
|
+
// PKCS#12 protection envelope. The cipher / PRF / MAC identifiers are
|
|
206
|
+
// protocol-fixed strings, and the iteration count is a cost parameter, not a
|
|
207
|
+
// byte quantity — hex form keeps it out of the byte-shape detector.
|
|
208
|
+
var P12_CIPHER = "aes-256-cbc";
|
|
209
|
+
var P12_PRF = "hmacWithSHA512";
|
|
210
|
+
var P12_MAC_HASH = "sha512";
|
|
211
|
+
var P12_ITER = 0x1E8480; // 2,000,000 (PBMAC1 outer MAC + PBES2 bag KDF)
|
|
212
|
+
// The classic App. B.2 MacData KDF (RFC 7292) is a synchronous per-iteration
|
|
213
|
+
// hash loop, so it is capped ~10x below PBMAC1's native PBKDF2 — 1,000,000 is
|
|
214
|
+
// the toolkit's classic-MAC ceiling (~1s wall clock). Used only for the
|
|
215
|
+
// classical ECDSA-P384 bridge's legacy-importable MacData.
|
|
216
|
+
var P12_CLASSIC_MAC_ITER = 0xF4240; // 1,000,000
|
|
180
217
|
|
|
181
218
|
var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
|
|
182
219
|
var LEAF_DEFAULT_DAYS = 365;
|
|
183
220
|
var DEFAULT_CA_NAME = "blamejs CA";
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
var
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return "-----BEGIN " + label + "-----\n" + b64.match(/.{1,64}/g).join("\n") + "\n-----END " + label + "-----\n";
|
|
221
|
+
// RFC 5280 removeFromCRL CRLReason — a delta-CRL un-revocation directive invalid
|
|
222
|
+
// in a full CRL; filtered out of full-CRL entries (see generateCrl).
|
|
223
|
+
var CRL_REMOVE_FROM_CRL = 8;
|
|
224
|
+
|
|
225
|
+
function _serial() {
|
|
226
|
+
// pki wants a decimal or 0x-hex integer; generateToken yields hex octets.
|
|
227
|
+
return "0x" + bCrypto.generateToken(C.BYTES.bytes(16));
|
|
192
228
|
}
|
|
193
229
|
|
|
194
|
-
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
publicPem: _pemBlock("PUBLIC KEY", spki),
|
|
200
|
-
};
|
|
230
|
+
function _ekuNames(usage) {
|
|
231
|
+
var names = [];
|
|
232
|
+
if (usage === "client" || usage === "both") names.push("clientAuth");
|
|
233
|
+
if (usage === "server" || usage === "both") names.push("serverAuth");
|
|
234
|
+
return names;
|
|
201
235
|
}
|
|
202
236
|
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
// Pack a textual IP into the DER octet form pki's iPAddress GeneralName
|
|
238
|
+
// expects (4 octets for IPv4, 16 for IPv6). Composes lib/ip-utils.
|
|
239
|
+
function _packIp(value) {
|
|
240
|
+
var s = String(value);
|
|
241
|
+
if (ipUtils.isIPv4(s)) {
|
|
242
|
+
var quads = s.split(".");
|
|
243
|
+
var buf4 = Buffer.alloc(4);
|
|
244
|
+
for (var i = 0; i < 4; i++) {
|
|
245
|
+
var n = parseInt(quads[i], 10);
|
|
246
|
+
/* c8 ignore next -- unreachable: the IPv4 arm is entered only after isIPv4() validated each octet 0-255 */
|
|
247
|
+
if (!(n >= 0 && n <= 255)) throw new MtlsEngineError("mtls-engine/bad-san", "invalid IPv4 SAN " + JSON.stringify(s));
|
|
248
|
+
buf4[i] = n;
|
|
249
|
+
}
|
|
250
|
+
return buf4;
|
|
251
|
+
}
|
|
252
|
+
var groups;
|
|
253
|
+
/* c8 ignore next -- unreachable: expandIpv6Groups returns null for malformed input rather than throwing */
|
|
254
|
+
try { groups = ipUtils.expandIpv6Groups(s); } catch (_e) { groups = null; }
|
|
255
|
+
if (Array.isArray(groups) && groups.length === 8) {
|
|
256
|
+
var buf16 = Buffer.alloc(16);
|
|
257
|
+
for (var g = 0; g < 8; g++) buf16.writeUInt16BE(groups[g] & 0xffff, g * 2);
|
|
258
|
+
return buf16;
|
|
259
|
+
}
|
|
260
|
+
throw new MtlsEngineError("mtls-engine/bad-san", "invalid IP SAN " + JSON.stringify(s));
|
|
210
261
|
}
|
|
211
262
|
|
|
212
|
-
|
|
213
|
-
|
|
263
|
+
// Map operator SAN entries (strings, optionally "DNS:" / "IP:" prefixed) to
|
|
264
|
+
// pki GeneralName objects.
|
|
265
|
+
function _sanEntry(s) {
|
|
266
|
+
var str = String(s);
|
|
267
|
+
if (/^DNS:/i.test(str)) return { dNSName: str.slice(4) };
|
|
268
|
+
if (/^IP:/i.test(str)) return { iPAddress: _packIp(str.slice(3)) };
|
|
269
|
+
if (ipUtils.isIPv4(str)) return { iPAddress: _packIp(str) };
|
|
270
|
+
// A bare IPv6 literal is an IP SAN too — auto-detect it symmetrically with
|
|
271
|
+
// IPv4 (a colon-bearing literal can never be a valid DNS hostname, so it must
|
|
272
|
+
// not fall through to the dNSName default and become an unmatchable SAN).
|
|
273
|
+
var v6 = ipUtils.expandIpv6Groups(str);
|
|
274
|
+
if (Array.isArray(v6) && v6.length === 8) return { iPAddress: _packIp(str) };
|
|
275
|
+
// Bare non-IP entries default to DNS — matches operator expectation.
|
|
276
|
+
return { dNSName: str };
|
|
214
277
|
}
|
|
215
278
|
|
|
216
279
|
function _normaliseCn(cn) {
|
|
@@ -222,33 +285,44 @@ function _normaliseCn(cn) {
|
|
|
222
285
|
return s;
|
|
223
286
|
}
|
|
224
287
|
|
|
288
|
+
// The signature digest a CA private key should sign with. generateCa /
|
|
289
|
+
// signClientCert know their candidate's digest directly; generateCrl only has
|
|
290
|
+
// the CA key, so derive it: an EC (P-384) CA signs with SHA-384, matching the
|
|
291
|
+
// classical bridge's posture; ML-DSA is no-prehash (undefined -> the toolkit
|
|
292
|
+
// forces SHA-512), so return undefined for a non-EC or unparseable key.
|
|
293
|
+
function _digestForKey(caKeyPem) {
|
|
294
|
+
try {
|
|
295
|
+
return nodeCrypto.createPrivateKey(caKeyPem).asymmetricKeyType === "ec" ? "sha384" : undefined;
|
|
296
|
+
/* c8 ignore next -- defensive: the default engine only signs CRLs with the CA key it just generated (always a parseable EC/ML-DSA key), so the parse never throws here */
|
|
297
|
+
} catch (_e) { return undefined; }
|
|
298
|
+
}
|
|
299
|
+
|
|
225
300
|
async function generateCa(opts) {
|
|
226
301
|
opts = opts || {};
|
|
227
302
|
var generation = (typeof opts.generation === "number" && opts.generation >= 1)
|
|
228
303
|
? Math.floor(opts.generation) : 1;
|
|
229
304
|
var caName = opts.name || DEFAULT_CA_NAME;
|
|
230
305
|
|
|
231
|
-
var alg = await _selectAlgorithm();
|
|
232
|
-
|
|
233
|
-
var
|
|
306
|
+
var alg = await _selectAlgorithm(opts.algorithm);
|
|
307
|
+
var keys = await subtle.generateKey(alg.keyAlg, true, CA_KEY_USAGES);
|
|
308
|
+
var spki = Buffer.from(await subtle.exportKey("spki", keys.publicKey));
|
|
234
309
|
var now = new Date();
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
return { caCertPem: ca.toString("pem"), caKeyPem: pem.privatePem };
|
|
310
|
+
|
|
311
|
+
var caCertPem = await pki.x509.sign({
|
|
312
|
+
// Structured DN — a bare string subject is taken by the toolkit as the CN
|
|
313
|
+
// VALUE (so "CN=name,OU=CAvN" would become a single CN literal, double-
|
|
314
|
+
// encoded as CN=CN=name\,OU=CAvN with no real OU). Pass RDN objects so the
|
|
315
|
+
// CN and the OU=CAv{N} generation tag are distinct attributes.
|
|
316
|
+
subject: [{ commonName: caName }, { organizationalUnitName: "CAv" + generation }],
|
|
317
|
+
subjectPublicKey: spki,
|
|
318
|
+
serialNumber: _serial(),
|
|
319
|
+
notBefore: now,
|
|
320
|
+
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
321
|
+
extensions: { basicConstraints: { cA: true, pathLen: 0 }, keyUsage: ["keyCertSign", "cRLSign"] },
|
|
322
|
+
}, { key: keys.privateKey }, { pem: true, digestAlgorithm: alg.digest });
|
|
323
|
+
|
|
324
|
+
var caKeyPem = await pki.key.export(keys.privateKey, { format: "pem" });
|
|
325
|
+
return { caCertPem: caCertPem, caKeyPem: caKeyPem };
|
|
252
326
|
}
|
|
253
327
|
|
|
254
328
|
async function signClientCert(opts) {
|
|
@@ -264,72 +338,57 @@ async function signClientCert(opts) {
|
|
|
264
338
|
var cn = _normaliseCn(opts.cn);
|
|
265
339
|
|
|
266
340
|
// Extended Key Usage: defaults to clientAuth (the historical behaviour).
|
|
267
|
-
//
|
|
268
|
-
// pass `usage: "server"` (sets serverAuth EKU), or `usage: "both"`
|
|
269
|
-
// (clientAuth + serverAuth — dual-purpose certs for service-to-service
|
|
270
|
-
// mTLS where the same workload is both initiator and acceptor).
|
|
341
|
+
// usage: "server" -> serverAuth; "both" -> clientAuth + serverAuth.
|
|
271
342
|
var usage = opts.usage || "client";
|
|
272
|
-
var
|
|
273
|
-
if (
|
|
274
|
-
if (usage === "server" || usage === "both") ekuOids.push(EKU_SERVER_AUTH_OID);
|
|
275
|
-
if (ekuOids.length === 0) {
|
|
343
|
+
var ekuNames = _ekuNames(usage);
|
|
344
|
+
if (ekuNames.length === 0) {
|
|
276
345
|
throw new MtlsEngineError("mtls-engine/bad-usage",
|
|
277
346
|
"signClientCert: opts.usage must be 'client' | 'server' | 'both', got " +
|
|
278
347
|
JSON.stringify(opts.usage));
|
|
279
348
|
}
|
|
280
349
|
|
|
281
|
-
// Subject Alternative Names — required for serverAuth (modern TLS
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
var sanExt = null;
|
|
350
|
+
// Subject Alternative Names — required for serverAuth (modern TLS clients
|
|
351
|
+
// only honor SANs, not CN). Accept opts.sans as an array of strings.
|
|
352
|
+
var sans = null;
|
|
285
353
|
if (Array.isArray(opts.sans) && opts.sans.length > 0) {
|
|
286
|
-
|
|
287
|
-
var str = String(s);
|
|
288
|
-
if (/^DNS:/i.test(str)) return { type: "dns", value: str.slice(4) };
|
|
289
|
-
if (/^IP:/i.test(str)) return { type: "ip", value: str.slice(3) };
|
|
290
|
-
// Bare entries default to DNS — matches operator expectation
|
|
291
|
-
return { type: "dns", value: str };
|
|
292
|
-
});
|
|
293
|
-
sanExt = new x509.SubjectAlternativeNameExtension(sanEntries);
|
|
354
|
+
sans = opts.sans.map(_sanEntry);
|
|
294
355
|
} else if (usage === "server" || usage === "both") {
|
|
295
356
|
// serverAuth without a SAN is unverifiable by modern TLS clients.
|
|
296
|
-
|
|
297
|
-
sanExt = new x509.SubjectAlternativeNameExtension([{ type: "dns", value: cn }]);
|
|
357
|
+
sans = [{ dNSName: cn }];
|
|
298
358
|
}
|
|
299
359
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
var
|
|
304
|
-
var clientKeys = await
|
|
360
|
+
// Leaf key algorithm follows the CA's: an ML-DSA-87 default, or the classical
|
|
361
|
+
// bridge when the caller pinned one (b.mtlsCa threads its create({ algorithm })
|
|
362
|
+
// through here). Undefined selects the process default.
|
|
363
|
+
var alg = await _selectAlgorithm(opts.algorithm);
|
|
364
|
+
var clientKeys = await subtle.generateKey(alg.keyAlg, true, CA_KEY_USAGES);
|
|
365
|
+
var clientSpki = Buffer.from(await subtle.exportKey("spki", clientKeys.publicKey));
|
|
305
366
|
|
|
306
367
|
var now = new Date();
|
|
307
368
|
var notAfter = new Date(now.getTime() + C.TIME.days(validityDays));
|
|
308
|
-
var extensions =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
notBefore:
|
|
323
|
-
notAfter:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
var pem = await _exportKeyPairToPem(clientKeys);
|
|
369
|
+
var extensions = {
|
|
370
|
+
basicConstraints: { cA: false },
|
|
371
|
+
keyUsage: ["digitalSignature", "keyEncipherment"],
|
|
372
|
+
extendedKeyUsage: ekuNames,
|
|
373
|
+
extendedKeyUsageCritical: true,
|
|
374
|
+
};
|
|
375
|
+
if (sans) extensions.subjectAltName = sans;
|
|
376
|
+
|
|
377
|
+
var certPem = await pki.x509.sign({
|
|
378
|
+
// A bare string subject is the CN VALUE (the toolkit wraps it as CN=<value>);
|
|
379
|
+
// passing "CN=" + cn would double-encode to CN=CN=<cn>.
|
|
380
|
+
subject: cn,
|
|
381
|
+
subjectPublicKey: clientSpki,
|
|
382
|
+
serialNumber: _serial(),
|
|
383
|
+
notBefore: now,
|
|
384
|
+
notAfter: notAfter,
|
|
385
|
+
extensions: extensions,
|
|
386
|
+
}, { cert: opts.caCertPem, key: opts.caKeyPem }, { pem: true, digestAlgorithm: alg.digest });
|
|
387
|
+
|
|
388
|
+
var keyPem = await pki.key.export(clientKeys.privateKey, { format: "pem" });
|
|
330
389
|
return {
|
|
331
|
-
cert:
|
|
332
|
-
key:
|
|
390
|
+
cert: certPem,
|
|
391
|
+
key: keyPem,
|
|
333
392
|
ca: opts.caCertPem,
|
|
334
393
|
issuedAt: now.toISOString(),
|
|
335
394
|
expiresAt: notAfter.toISOString(),
|
|
@@ -343,86 +402,35 @@ async function packageP12(opts) {
|
|
|
343
402
|
throw new MtlsEngineError("mtls-engine/no-password",
|
|
344
403
|
"packageP12 requires opts.password (non-empty string)");
|
|
345
404
|
}
|
|
405
|
+
// Resolve the leaf's algorithm tier so the outer MAC matches it. (signClientCert
|
|
406
|
+
// resolves the same alg from opts.algorithm; undefined selects the default.)
|
|
407
|
+
var alg = await _selectAlgorithm(opts.algorithm);
|
|
346
408
|
var leaf = await signClientCert(opts);
|
|
347
409
|
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var
|
|
360
|
-
|
|
361
|
-
var pfx = new pkijs.PFX({
|
|
362
|
-
parsedValue: {
|
|
363
|
-
integrityMode: 0, // PasswordMode (outer HMAC-PBKDF2)
|
|
364
|
-
authenticatedSafe: new pkijs.AuthenticatedSafe({
|
|
365
|
-
parsedValue: {
|
|
366
|
-
safeContents: [
|
|
367
|
-
{
|
|
368
|
-
privacyMode: 1, // PasswordPrivacyMode (PBES2)
|
|
369
|
-
value: new pkijs.SafeContents({
|
|
370
|
-
safeBags: [
|
|
371
|
-
new pkijs.SafeBag({
|
|
372
|
-
bagId: BAG_ID_KEY,
|
|
373
|
-
bagValue: new pkijs.PKCS8ShroudedKeyBag({ parsedValue: privateKeyInfo }),
|
|
374
|
-
}),
|
|
375
|
-
],
|
|
376
|
-
}),
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
privacyMode: 1,
|
|
380
|
-
value: new pkijs.SafeContents({
|
|
381
|
-
safeBags: [
|
|
382
|
-
new pkijs.SafeBag({
|
|
383
|
-
bagId: BAG_ID_CERT,
|
|
384
|
-
bagValue: new pkijs.CertBag({ parsedValue: leafPkijsCert }),
|
|
385
|
-
}),
|
|
386
|
-
new pkijs.SafeBag({
|
|
387
|
-
bagId: BAG_ID_CERT,
|
|
388
|
-
bagValue: new pkijs.CertBag({ parsedValue: caPkijsCert }),
|
|
389
|
-
}),
|
|
390
|
-
],
|
|
391
|
-
}),
|
|
392
|
-
},
|
|
393
|
-
],
|
|
394
|
-
},
|
|
395
|
-
}),
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// Inner protection on the shrouded-key bag itself.
|
|
400
|
-
await pfx.parsedValue.authenticatedSafe.parsedValue.safeContents[0]
|
|
401
|
-
.value.safeBags[0].bagValue.makeInternalValues({
|
|
402
|
-
password: passwordBuf,
|
|
403
|
-
contentEncryptionAlgorithm: P12_CONTENT_ENC,
|
|
404
|
-
hmacHashAlgorithm: P12_KDF_HASH,
|
|
405
|
-
iterationCount: P12_ITER,
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Encrypt each SafeContents envelope.
|
|
409
|
-
await pfx.parsedValue.authenticatedSafe.makeInternalValues({
|
|
410
|
+
var pbe = { password: opts.password, cipher: P12_CIPHER, iterations: P12_ITER, prf: P12_PRF };
|
|
411
|
+
// The integrity MAC follows the cert's interop tier. The PQC-pure default uses
|
|
412
|
+
// PBMAC1 (RFC 9579) — an ML-DSA peer already runs the OpenSSL 3.5 that supports
|
|
413
|
+
// it. The classical ECDSA-P384 bridge exists FOR peers predating OpenSSL 3.5,
|
|
414
|
+
// and PBMAC1 (OpenSSL 3.4+) would make the file unverifiable by exactly those
|
|
415
|
+
// consumers, so it uses the universally-importable RFC 7292 App. B HMAC MacData
|
|
416
|
+
// (at the classic KDF's lower iteration ceiling). PBES2 bag protection is
|
|
417
|
+
// legacy-readable in both tiers, so only the MAC differs.
|
|
418
|
+
var mac = alg.posture === "classical"
|
|
419
|
+
? { algorithm: "hmac", hash: P12_MAC_HASH, iterations: P12_CLASSIC_MAC_ITER }
|
|
420
|
+
: { algorithm: "pbmac1", hash: P12_MAC_HASH, iterations: P12_ITER };
|
|
421
|
+
var p12 = await pki.pkcs12.build({
|
|
410
422
|
safeContents: [
|
|
411
|
-
{
|
|
412
|
-
{
|
|
423
|
+
{ bags: [{ type: "shroudedKey", key: leaf.key, encrypt: pbe }] },
|
|
424
|
+
{ encrypt: pbe, bags: [
|
|
425
|
+
{ type: "cert", cert: leaf.cert },
|
|
426
|
+
{ type: "cert", cert: leaf.ca },
|
|
427
|
+
] },
|
|
413
428
|
],
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Outer integrity MAC.
|
|
417
|
-
await pfx.makeInternalValues({
|
|
418
|
-
password: passwordBuf,
|
|
419
|
-
iterations: P12_ITER,
|
|
420
|
-
pbkdf2HashAlgorithm: P12_KDF_HASH,
|
|
421
|
-
hmacHashAlgorithm: P12_MAC_HASH,
|
|
422
|
-
});
|
|
429
|
+
}, { password: opts.password, mac: mac });
|
|
423
430
|
|
|
424
431
|
return {
|
|
425
|
-
|
|
432
|
+
/* c8 ignore next -- defensive: pki.pkcs12.build always returns a Buffer, so the Buffer.from() arm is unreachable */
|
|
433
|
+
p12: Buffer.isBuffer(p12) ? p12 : Buffer.from(p12),
|
|
426
434
|
certPem: leaf.cert,
|
|
427
435
|
issuedAt: leaf.issuedAt,
|
|
428
436
|
expiresAt: leaf.expiresAt,
|
|
@@ -432,34 +440,42 @@ async function packageP12(opts) {
|
|
|
432
440
|
function algorithmEnvelope() {
|
|
433
441
|
return {
|
|
434
442
|
cert: {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
443
|
+
// Report the most-recent resolved algorithm (a pinned selection or the
|
|
444
|
+
// default), so the envelope is accurate on the pinned issuance path too — not
|
|
445
|
+
// just _selectedAlg (the default cache a pin deliberately never writes).
|
|
446
|
+
keyAlg: _lastSelectedAlg && _lastSelectedAlg.keyAlg,
|
|
447
|
+
label: _lastSelectedAlg && _lastSelectedAlg.label,
|
|
448
|
+
posture: _lastSelectedAlg && _lastSelectedAlg.posture,
|
|
449
|
+
// Operators querying status() before any cert has been issued get the
|
|
450
|
+
// candidate priority list — the engine probes lazily so the chosen
|
|
451
|
+
// algorithm isn't known until first use.
|
|
442
452
|
priority: ALG_CANDIDATES.map(function (c) {
|
|
443
453
|
return { label: c.label, posture: c.posture };
|
|
444
454
|
}),
|
|
445
455
|
},
|
|
446
|
-
p12:
|
|
447
|
-
contentEncryption:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
p12: {
|
|
457
|
+
contentEncryption: P12_CIPHER, // PBES2 + AES-256-CBC bag protection (both tiers)
|
|
458
|
+
kdfPrf: P12_PRF, // PBKDF2-HMAC-SHA-512 (both tiers)
|
|
459
|
+
iterationCount: P12_ITER, // bag KDF iterations (both tiers)
|
|
460
|
+
// The outer integrity MAC is tier-dependent: packageP12 selects it from the
|
|
461
|
+
// cert algorithm's posture — PBMAC1 (RFC 9579) for the PQC default, the
|
|
462
|
+
// traditional RFC 7292 HMAC MacData for the classical bridge so a
|
|
463
|
+
// pre-OpenSSL-3.5 peer can verify it. Both are reported (keyed by posture)
|
|
464
|
+
// so a consumer describes the archive it actually builds under either pin.
|
|
465
|
+
mac: {
|
|
466
|
+
"pqc-pure": { algorithm: "pbmac1", hash: P12_MAC_HASH, iterations: P12_ITER },
|
|
467
|
+
classical: { algorithm: "hmac", hash: P12_MAC_HASH, iterations: P12_CLASSIC_MAC_ITER },
|
|
468
|
+
},
|
|
451
469
|
},
|
|
452
470
|
caValidityDays: CA_VALIDITY_DAYS,
|
|
453
471
|
leafDefaultDays: LEAF_DEFAULT_DAYS,
|
|
454
472
|
};
|
|
455
473
|
}
|
|
456
474
|
|
|
457
|
-
// Generate a signed X.509 CRL (RFC 5280) covering every revoked
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// the
|
|
461
|
-
// algorithm the CA itself was issued under (auto-detected via
|
|
462
|
-
// _selectAlgorithm + cached on first issuance).
|
|
475
|
+
// Generate a signed X.509 CRL (RFC 5280) covering every revoked serial
|
|
476
|
+
// number. pki.crl.sign builds the TBSCertList, populates the entries, and
|
|
477
|
+
// signs under the CA private key — the signature algorithm is resolved from
|
|
478
|
+
// the CA key, matching the algorithm the CA itself was issued under.
|
|
463
479
|
async function generateCrl(opts) {
|
|
464
480
|
opts = opts || {};
|
|
465
481
|
if (!opts.caCertPem || !opts.caKeyPem) {
|
|
@@ -467,30 +483,39 @@ async function generateCrl(opts) {
|
|
|
467
483
|
"generateCrl requires { caCertPem, caKeyPem, revocations, thisUpdate, nextUpdate }");
|
|
468
484
|
}
|
|
469
485
|
var revocations = Array.isArray(opts.revocations) ? opts.revocations : [];
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// X509CrlEntry expects { serialNumber: hex, revocationDate, reason }.
|
|
477
|
-
var entries = revocations.map(function (r) {
|
|
478
|
-
return {
|
|
479
|
-
serialNumber: r.serialNumber,
|
|
480
|
-
revocationDate: new Date(r.revokedAt || Date.now()),
|
|
481
|
-
reason: (typeof r.reasonCode === "number") ? r.reasonCode : 0,
|
|
486
|
+
|
|
487
|
+
var revoked = revocations.map(function (r) {
|
|
488
|
+
var entry = {
|
|
489
|
+
serialNumber: _normaliseRevokedSerial(r.serialNumber),
|
|
490
|
+
revocationDate: new Date(r.revokedAt || Date.now()),
|
|
482
491
|
};
|
|
492
|
+
// A full CRL (the only kind this CA issues) cannot carry removeFromCRL
|
|
493
|
+
// (RFC 5280 code 8) — it is a delta-CRL un-revocation directive the toolkit
|
|
494
|
+
// rejects, failing the ENTIRE CRL. revoke() refuses it going forward, but a
|
|
495
|
+
// registry written by a pre-fix build (or hand-edited) may still hold one;
|
|
496
|
+
// keep the serial revoked (it IS in the store — fail-secure) and DROP only the
|
|
497
|
+
// invalid reason so one legacy entry can't block publishing every other
|
|
498
|
+
// revocation.
|
|
499
|
+
if (typeof r.reasonCode === "number" && r.reasonCode !== CRL_REMOVE_FROM_CRL) {
|
|
500
|
+
entry.reason = r.reasonCode;
|
|
501
|
+
}
|
|
502
|
+
return entry;
|
|
483
503
|
});
|
|
484
504
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
505
|
+
return pki.crl.sign({
|
|
506
|
+
thisUpdate: opts.thisUpdate || new Date(),
|
|
507
|
+
nextUpdate: opts.nextUpdate,
|
|
508
|
+
revoked: revoked,
|
|
509
|
+
}, { cert: opts.caCertPem, key: opts.caKeyPem }, { pem: true, digestAlgorithm: _digestForKey(opts.caKeyPem) });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// A revoked serial arrives as the hex string the engine issued (no 0x
|
|
513
|
+
// prefix). pki wants a decimal or 0x-hex integer, so prefix bare hex.
|
|
514
|
+
function _normaliseRevokedSerial(serial) {
|
|
515
|
+
var s = String(serial == null ? "" : serial);
|
|
516
|
+
if (/^0x/i.test(s)) return s;
|
|
517
|
+
if (safeBuffer.isHex(s)) return "0x" + s;
|
|
518
|
+
return s;
|
|
494
519
|
}
|
|
495
520
|
|
|
496
521
|
module.exports = {
|