node-opcua-pki 6.22.1 → 7.0.1

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.
Files changed (138) hide show
  1. package/bin/install_prerequisite.ts +1 -1
  2. package/bin/pki.ts +1 -1
  3. package/bin/pki_config.example.js +0 -1
  4. package/dist/bin/install_prerequisite.d.ts +2 -0
  5. package/dist/bin/install_prerequisite.js +6 -0
  6. package/dist/bin/install_prerequisite.js.map +1 -0
  7. package/dist/bin/pki.d.ts +2 -0
  8. package/dist/bin/pki.js +4 -0
  9. package/dist/bin/pki.js.map +1 -0
  10. package/dist/lib/ca/backends/native_ca_backend.d.ts +49 -0
  11. package/dist/lib/ca/backends/native_ca_backend.js +365 -0
  12. package/dist/lib/ca/backends/native_ca_backend.js.map +1 -0
  13. package/dist/lib/ca/backends/openssl_ca_backend.d.ts +47 -0
  14. package/dist/lib/ca/backends/openssl_ca_backend.js +398 -0
  15. package/dist/lib/ca/backends/openssl_ca_backend.js.map +1 -0
  16. package/dist/lib/ca/certificate_authority.d.ts +62 -0
  17. package/dist/lib/ca/certificate_authority.js +91 -0
  18. package/dist/lib/ca/certificate_authority.js.map +1 -0
  19. package/dist/lib/ca/core/ca_backend.d.ts +89 -0
  20. package/dist/lib/ca/core/ca_backend.js +24 -0
  21. package/dist/lib/ca/core/ca_backend.js.map +1 -0
  22. package/dist/lib/ca/core/ca_database.d.ts +91 -0
  23. package/dist/lib/ca/core/ca_database.js +267 -0
  24. package/dist/lib/ca/core/ca_database.js.map +1 -0
  25. package/dist/{core-BwqSJPHc.d.mts → lib/ca/core/certificate_authority_core.d.ts} +45 -394
  26. package/dist/lib/ca/core/certificate_authority_core.js +1300 -0
  27. package/dist/lib/ca/core/certificate_authority_core.js.map +1 -0
  28. package/dist/lib/ca/core/index.d.ts +17 -0
  29. package/dist/lib/ca/core/index.js +25 -0
  30. package/dist/lib/ca/core/index.js.map +1 -0
  31. package/dist/lib/ca/crypto_create_CA.d.ts +1 -0
  32. package/dist/lib/ca/crypto_create_CA.js +821 -0
  33. package/dist/lib/ca/crypto_create_CA.js.map +1 -0
  34. package/dist/lib/ca/index.d.ts +1 -0
  35. package/dist/lib/ca/index.js +2 -0
  36. package/dist/lib/ca/index.js.map +1 -0
  37. package/dist/lib/ca/templates/ca_config_template.cnf.d.ts +2 -0
  38. package/dist/lib/ca/templates/ca_config_template.cnf.js +170 -0
  39. package/dist/lib/ca/templates/ca_config_template.cnf.js.map +1 -0
  40. package/dist/lib/core.d.ts +33 -0
  41. package/dist/lib/core.js +56 -0
  42. package/dist/lib/core.js.map +1 -0
  43. package/dist/lib/index.d.ts +11 -0
  44. package/dist/lib/index.js +37 -0
  45. package/dist/lib/index.js.map +1 -0
  46. package/dist/lib/misc/applicationurn.d.ts +1 -0
  47. package/dist/lib/misc/applicationurn.js +40 -0
  48. package/dist/lib/misc/applicationurn.js.map +1 -0
  49. package/dist/lib/misc/hostname.d.ts +8 -0
  50. package/dist/lib/misc/hostname.js +82 -0
  51. package/dist/lib/misc/hostname.js.map +1 -0
  52. package/dist/lib/misc/subject.d.ts +2 -0
  53. package/dist/lib/misc/subject.js +2 -0
  54. package/dist/lib/misc/subject.js.map +1 -0
  55. package/dist/{index.d.ts → lib/pki/certificate_manager.d.ts} +24 -248
  56. package/dist/lib/pki/certificate_manager.js +2302 -0
  57. package/dist/lib/pki/certificate_manager.js.map +1 -0
  58. package/dist/lib/pki/templates/simple_config_template.cnf.d.ts +2 -0
  59. package/dist/lib/pki/templates/simple_config_template.cnf.js +74 -0
  60. package/dist/lib/pki/templates/simple_config_template.cnf.js.map +1 -0
  61. package/dist/lib/pki/toolbox_pfx.d.ts +127 -0
  62. package/dist/lib/pki/toolbox_pfx.js +248 -0
  63. package/dist/lib/pki/toolbox_pfx.js.map +1 -0
  64. package/dist/lib/toolbox/common.d.ts +150 -0
  65. package/dist/lib/toolbox/common.js +82 -0
  66. package/dist/lib/toolbox/common.js.map +1 -0
  67. package/dist/lib/toolbox/common2.d.ts +25 -0
  68. package/dist/lib/toolbox/common2.js +97 -0
  69. package/dist/lib/toolbox/common2.js.map +1 -0
  70. package/dist/lib/toolbox/config.d.ts +5 -0
  71. package/dist/lib/toolbox/config.js +28 -0
  72. package/dist/lib/toolbox/config.js.map +1 -0
  73. package/dist/lib/toolbox/debug.d.ts +5 -0
  74. package/dist/lib/toolbox/debug.js +35 -0
  75. package/dist/lib/toolbox/debug.js.map +1 -0
  76. package/dist/lib/toolbox/display.d.ts +4 -0
  77. package/dist/lib/toolbox/display.js +56 -0
  78. package/dist/lib/toolbox/display.js.map +1 -0
  79. package/dist/lib/toolbox/index.d.ts +5 -0
  80. package/dist/lib/toolbox/index.js +28 -0
  81. package/dist/lib/toolbox/index.js.map +1 -0
  82. package/dist/lib/toolbox/with_openssl/_create_random_file.d.ts +4 -0
  83. package/dist/lib/toolbox/with_openssl/_create_random_file.js +50 -0
  84. package/dist/lib/toolbox/with_openssl/_create_random_file.js.map +1 -0
  85. package/dist/lib/toolbox/with_openssl/_env.d.ts +58 -0
  86. package/dist/lib/toolbox/with_openssl/_env.js +126 -0
  87. package/dist/lib/toolbox/with_openssl/_env.js.map +1 -0
  88. package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.d.ts +5 -0
  89. package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js +72 -0
  90. package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js.map +1 -0
  91. package/dist/lib/toolbox/with_openssl/create_private_key.d.ts +5 -0
  92. package/dist/lib/toolbox/with_openssl/create_private_key.js +92 -0
  93. package/dist/lib/toolbox/with_openssl/create_private_key.js.map +1 -0
  94. package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.d.ts +5 -0
  95. package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js +135 -0
  96. package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js.map +1 -0
  97. package/dist/lib/toolbox/with_openssl/execute_openssl.d.ts +67 -0
  98. package/dist/lib/toolbox/with_openssl/execute_openssl.js +246 -0
  99. package/dist/lib/toolbox/with_openssl/execute_openssl.js.map +1 -0
  100. package/dist/lib/toolbox/with_openssl/index.d.ts +5 -0
  101. package/dist/lib/toolbox/with_openssl/index.js +30 -0
  102. package/dist/lib/toolbox/with_openssl/index.js.map +1 -0
  103. package/dist/lib/toolbox/with_openssl/install_prerequisite.d.ts +7 -0
  104. package/dist/lib/toolbox/with_openssl/install_prerequisite.js +358 -0
  105. package/dist/lib/toolbox/with_openssl/install_prerequisite.js.map +1 -0
  106. package/dist/lib/toolbox/with_openssl/toolbox.d.ts +53 -0
  107. package/dist/lib/toolbox/with_openssl/toolbox.js +190 -0
  108. package/dist/lib/toolbox/with_openssl/toolbox.js.map +1 -0
  109. package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.d.ts +5 -0
  110. package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js +69 -0
  111. package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js.map +1 -0
  112. package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.d.ts +3 -0
  113. package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js +78 -0
  114. package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js.map +1 -0
  115. package/dist/lib/toolbox/without_openssl/index.d.ts +2 -0
  116. package/dist/lib/toolbox/without_openssl/index.js +25 -0
  117. package/dist/lib/toolbox/without_openssl/index.js.map +1 -0
  118. package/package.json +12 -31
  119. package/dist/bin/install_prerequisite.mjs +0 -18
  120. package/dist/bin/install_prerequisite.mjs.map +0 -1
  121. package/dist/bin/pki.mjs +0 -5932
  122. package/dist/bin/pki.mjs.map +0 -1
  123. package/dist/chunk-EQ4DGI4M.mjs +0 -1917
  124. package/dist/chunk-EQ4DGI4M.mjs.map +0 -1
  125. package/dist/chunk-GCHH54PS.mjs +0 -30
  126. package/dist/chunk-GCHH54PS.mjs.map +0 -1
  127. package/dist/core-BwqSJPHc.d.ts +0 -1041
  128. package/dist/core.d.mts +0 -2
  129. package/dist/core.d.ts +0 -2
  130. package/dist/core.js +0 -1902
  131. package/dist/core.js.map +0 -1
  132. package/dist/core.mjs +0 -21
  133. package/dist/core.mjs.map +0 -1
  134. package/dist/index.d.mts +0 -1003
  135. package/dist/index.js +0 -5009
  136. package/dist/index.js.map +0 -1
  137. package/dist/index.mjs +0 -3145
  138. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,1300 @@
1
+ // ---------------------------------------------------------------------------------------------------------------------
2
+ // node-opcua
3
+ // ---------------------------------------------------------------------------------------------------------------------
4
+ // Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org
5
+ // Copyright (c) 2022-2026 - Sterfive.com
6
+ // ---------------------------------------------------------------------------------------------------------------------
7
+ //
8
+ // This project is licensed under the terms of the MIT license.
9
+ //
10
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
11
+ // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
12
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
13
+ // permit persons to whom the Software is furnished to do so, subject to the following conditions:
14
+ //
15
+ // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
16
+ // Software.
17
+ //
18
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
19
+ // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20
+ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ // ---------------------------------------------------------------------------------------------------------------------
23
+ import assert from "node:assert";
24
+ import fs from "node:fs";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ import { withLock } from "@ster5/global-mutex";
28
+ import chalk from "chalk";
29
+ import { CertificatePurpose, certificateMatchesPrivateKey, convertPEMtoDER, createCertificateSigningRequest, createPfx, exploreCertificate, exploreCertificateSigningRequest, generatePrivateKeyFile, privateKeyToCryptoKey, readCertificatePEM, readCertificateSigningRequest, readPrivateKey, Subject, toPem, verifyCertificateSignature, writePrivateKeyFile, x509 } from "node-opcua-crypto";
30
+ import { adjustApplicationUri, adjustDate, certificateFileExist, debugLog, displaySubtitle, displayTitle, ensurePrivateDirectory, isEncryptedPrivateKeyFile, makePath, mkdirRecursiveSync, resolvePrivateKeyPassphrase, restrictPrivateFilePermissions, warningLog } from "../../toolbox/index.js";
31
+ import { CaDatabase } from "./ca_database.js";
32
+ /** Default X.500 subject used when no custom subject is provided. */
33
+ export const defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
34
+ import _ca_config_template from "../templates/ca_config_template.cnf.js";
35
+ export const configurationFileTemplate = _ca_config_template;
36
+ /**
37
+ * Escape a value for use inside a double-quoted openssl config value.
38
+ *
39
+ * openssl's config parser (crypto/conf/conf_def.c) treats an unquoted value
40
+ * specially: `$var` / `${var}` / `$(var)` are expanded, and `'`, `"` and the
41
+ * backtick are all quote characters (a backtick pair is silently removed).
42
+ * Inside a double-quoted value none of that applies: characters are copied
43
+ * verbatim except a backslash, which escapes the next character. So a path
44
+ * containing `$`, backticks, `#` or spaces is safe once double-quoted with
45
+ * `\` and `"` escaped.
46
+ */
47
+ export function escapeOpensslConfDoubleQuoted(value) {
48
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
49
+ }
50
+ /**
51
+ * Render the CA openssl configuration for `caRootDir`. The root folder is
52
+ * emitted forward-slashed (openssl accepts that on every platform) and
53
+ * double-quoted, so a CA `location` containing characters that openssl's
54
+ * config syntax would otherwise interpret (`$`, backticks, `#`, quotes) is
55
+ * taken literally.
56
+ */
57
+ export function renderCaConfig(caRootDir) {
58
+ return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
59
+ }
60
+ const config = {
61
+ certificateDir: "INVALID",
62
+ forceCA: false,
63
+ pkiDir: "INVALID"
64
+ };
65
+ /**
66
+ * Upper bound on how long a CA operation waits for `.ca.lock` before
67
+ * throwing. CA operations themselves complete in seconds; a dead holder's
68
+ * lock goes stale and is taken over after ~2 minutes, so 5 minutes covers
69
+ * every legitimate wait with margin while still turning a stuck holder
70
+ * into a loud error instead of a silent fleet-wide hang.
71
+ */
72
+ const CA_LOCK_MAX_WAIT_MS = 5 * 60_000;
73
+ /**
74
+ * How long a waiter sleeps between attempts at the CA directory lock.
75
+ * The lock library's default backoff doubles without an upper bound, so a
76
+ * queue of contending operations ends up sleeping through 1s, 2s, 4s, 8s,
77
+ * 16s ... while the lock is in fact free again after milliseconds: eight
78
+ * queued signings measured over two minutes. Capping the interval keeps a
79
+ * freed lock from going unnoticed; `CA_LOCK_MAX_WAIT_MS` still bounds the
80
+ * total wait.
81
+ */
82
+ const CA_LOCK_RETRY = { minTimeout: 20, maxTimeout: 250 };
83
+ // convert 'c07b9179' to "192.123.145.121"
84
+ function octetStringToIpAddress(a) {
85
+ return (parseInt(a.substring(0, 2), 16).toString() +
86
+ "." +
87
+ parseInt(a.substring(2, 4), 16).toString() +
88
+ "." +
89
+ parseInt(a.substring(4, 6), 16).toString() +
90
+ "." +
91
+ parseInt(a.substring(6, 8), 16).toString());
92
+ }
93
+ assert(octetStringToIpAddress("c07b9179") === "192.123.145.121");
94
+ /**
95
+ * Synchronously validate a revocation-related URL (CDP / OCSP /
96
+ * caIssuers) before it is stored on a {@link CertificateAuthority}.
97
+ *
98
+ * Rules:
99
+ * - `undefined` is a valid input — the matching extension is omitted.
100
+ * - Empty string throws (almost always a config bug — pass `undefined`).
101
+ * - Must parse via `new URL(s)`.
102
+ * - Protocol must be `http:` or `https:`.
103
+ * - Must include a non-trivial path (not `""` or `"/"`).
104
+ * - Loopback hostname produces a warning but does not throw — useful
105
+ * for tests and local dev where pki-server and relying party share
106
+ * a host.
107
+ *
108
+ * @see US-202
109
+ */
110
+ function validateRevocationUrl(url, fieldName) {
111
+ if (url === undefined) {
112
+ return undefined;
113
+ }
114
+ if (url === "") {
115
+ throw new Error(`${fieldName} must not be empty — pass undefined to disable the extension`);
116
+ }
117
+ let parsed;
118
+ try {
119
+ parsed = new URL(url);
120
+ }
121
+ catch {
122
+ throw new Error(`${fieldName} is not a valid URL: ${url}`);
123
+ }
124
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
125
+ throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url})`);
126
+ }
127
+ if (!parsed.pathname || parsed.pathname === "/") {
128
+ throw new Error(`${fieldName} must include a path component (got ${url})`);
129
+ }
130
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
131
+ if (isLoopback) {
132
+ console.warn(`[node-opcua-pki] ${fieldName} points at loopback (${url}) — ` +
133
+ "certificates issued with this URL will be unreachable from any other host.");
134
+ }
135
+ return url;
136
+ }
137
+ export class CertificateAuthorityCore {
138
+ /** RSA key size used when generating the CA private key. */
139
+ keySize;
140
+ /** Root filesystem path of the CA directory structure. */
141
+ location;
142
+ /** X.500 subject of the CA certificate. */
143
+ subject;
144
+ /** @internal Parent CA (undefined for root CAs). */
145
+ _issuerCA;
146
+ /** @internal Configured CDP / AIA URLs (US-202). */
147
+ _crlDistributionUrl;
148
+ _ocspResponderUrl;
149
+ _caIssuersUrl;
150
+ #privateKeyPassphrase;
151
+ /** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
152
+ #resolvedPassphrase;
153
+ #passphraseResolved = false;
154
+ /** Signing backend: `openssl` shells out to the CLI, `native` signs in-process. */
155
+ #backend;
156
+ /** External signing key (HSM/KMS), when one was supplied instead of a key file. */
157
+ #signer;
158
+ /** Read access to `index.txt` / `certs/<SERIAL>.pem`. */
159
+ #db;
160
+ constructor(options) {
161
+ assert(Object.prototype.hasOwnProperty.call(options, "location"));
162
+ assert(Object.prototype.hasOwnProperty.call(options, "keySize"));
163
+ this.location = options.location;
164
+ this.keySize = options.keySize || 2048;
165
+ this.subject = new Subject(options.subject || defaultSubject);
166
+ this._issuerCA = options.issuerCA;
167
+ this.#privateKeyPassphrase = options.privateKeyPassphrase;
168
+ this.#signer = options.signer;
169
+ if (options.signer) {
170
+ // Refuse a signer nothing can use here, where the message can say
171
+ // what is wrong with it, rather than letting it surface as a
172
+ // normalize-algorithm error thrown from inside WebCrypto once the
173
+ // CA directory already exists. The type system says as much for
174
+ // TypeScript callers, but a signer is exactly the sort of object
175
+ // that arrives from untyped glue around a KMS SDK.
176
+ const algorithm = options.signer.algorithm;
177
+ if (algorithm.name !== "RSASSA-PKCS1-v1_5" && algorithm.name !== "ECDSA") {
178
+ throw new Error(`CertificateAuthority: signer algorithm ${algorithm.name} is not supported - ` +
179
+ "use RSASSA-PKCS1-v1_5 or ECDSA.");
180
+ }
181
+ if (algorithm.name === "ECDSA" && !algorithm.namedCurve) {
182
+ throw new Error("CertificateAuthority: an ECDSA signer must declare its namedCurve (P-256, P-384 or P-521) - " +
183
+ "importing the signer's public key needs the curve, and an SPKI import cannot infer it.");
184
+ }
185
+ }
186
+ if (options.signer && !options.backend.supportsExternalSigner) {
187
+ // asked of the backend rather than hardcoded here, so this class
188
+ // keeps working for backends it has never heard of
189
+ throw new Error("CertificateAuthority: this backend cannot sign with an external signer - " +
190
+ "it loads its key from a file. Use a backend that supports one, such as NativeCaBackend.");
191
+ }
192
+ this.#backend = options.backend;
193
+ this.#db = new CaDatabase(this.location);
194
+ if (options.crlDistributionUrl !== undefined) {
195
+ this.setCrlDistributionUrl(options.crlDistributionUrl);
196
+ }
197
+ if (options.ocspResponderUrl !== undefined) {
198
+ this.setOcspResponderUrl(options.ocspResponderUrl);
199
+ }
200
+ if (options.caIssuersUrl !== undefined) {
201
+ this.setCaIssuersUrl(options.caIssuersUrl);
202
+ }
203
+ }
204
+ /**
205
+ * Public URL where the CRL produced by this CA is reachable, or
206
+ * `undefined` if no CDP extension should be emitted on issued certs.
207
+ */
208
+ get crlDistributionUrl() {
209
+ return this._crlDistributionUrl;
210
+ }
211
+ /**
212
+ * Public URL of the OCSP responder, or `undefined` if no AIA OCSP
213
+ * leg should be emitted on issued certs.
214
+ */
215
+ get ocspResponderUrl() {
216
+ return this._ocspResponderUrl;
217
+ }
218
+ /**
219
+ * Public URL where the issuer's certificate can be fetched, or
220
+ * `undefined` if no AIA caIssuers leg should be emitted.
221
+ */
222
+ get caIssuersUrl() {
223
+ return this._caIssuersUrl;
224
+ }
225
+ /**
226
+ * Configure the URL embedded as `crlDistributionPoints` in every
227
+ * subsequently-issued certificate. Pass `undefined` to disable
228
+ * the extension entirely. Validated synchronously — throws on
229
+ * empty string, non-http(s) protocol, missing path. Warns (does
230
+ * not throw) when the URL points at loopback.
231
+ *
232
+ * @see US-202
233
+ */
234
+ setCrlDistributionUrl(url) {
235
+ this._crlDistributionUrl = validateRevocationUrl(url, "crlDistributionUrl");
236
+ }
237
+ /**
238
+ * Configure the OCSP responder URL embedded as the `OCSP` leg of
239
+ * the `authorityInfoAccess` extension on every subsequently-issued
240
+ * certificate. Pass `undefined` to disable.
241
+ *
242
+ * @see US-202
243
+ */
244
+ setOcspResponderUrl(url) {
245
+ this._ocspResponderUrl = validateRevocationUrl(url, "ocspResponderUrl");
246
+ }
247
+ /**
248
+ * Configure the caIssuers URL embedded as the `caIssuers` leg of
249
+ * the `authorityInfoAccess` extension on every subsequently-issued
250
+ * certificate. Pass `undefined` to disable.
251
+ *
252
+ * @see US-202
253
+ */
254
+ setCaIssuersUrl(url) {
255
+ this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
256
+ }
257
+ /** Absolute path to the CA root directory (alias for {@link location}). */
258
+ get rootDir() {
259
+ return this.location;
260
+ }
261
+ /** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
262
+ get configFile() {
263
+ return path.normalize(path.join(this.rootDir, "./conf/caconfig.cnf"));
264
+ }
265
+ /** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
266
+ get privateKey() {
267
+ return path.join(path.resolve(this.rootDir), "private/cakey.pem");
268
+ }
269
+ /**
270
+ * The CA private key, decrypted with the configured `privateKeyPassphrase`
271
+ * if it is encrypted. Fails closed (`PrivateKeyPassphraseRequiredError`)
272
+ * on an encrypted key with no or the wrong passphrase.
273
+ */
274
+ async getPrivateKey() {
275
+ this.#assertKeyIsOnDisk("getPrivateKey");
276
+ return readPrivateKey(this.privateKey, await this._privateKeyPassphrase());
277
+ }
278
+ /**
279
+ * True when this CA signs with an external {@link CaSigner} (HSM/KMS)
280
+ * rather than with `private/cakey.pem`. There is then no private key
281
+ * file on disk, and never was one.
282
+ */
283
+ get hasExternalSigner() {
284
+ return this.#signer !== undefined;
285
+ }
286
+ /**
287
+ * Does `cert` certify the key this CA signs with? With the key on disk
288
+ * that is a private-key match; with a signer there is no private key to
289
+ * match, so compare the certified public key against the signer's own -
290
+ * the same question, asked the only way an HSM allows.
291
+ */
292
+ async #certificateMatchesOurKey(certPem, certDer) {
293
+ if (this.#signer) {
294
+ const ours = Buffer.from(await this.#signer.getPublicKey());
295
+ const certified = Buffer.from(new x509.X509Certificate(certPem).publicKey.rawData);
296
+ return ours.equals(certified);
297
+ }
298
+ return certificateMatchesPrivateKey(certDer, await this.getPrivateKey());
299
+ }
300
+ /** Reject the key-file-only operations up front, rather than failing on a missing file later. */
301
+ #assertKeyIsOnDisk(what) {
302
+ if (this.#signer) {
303
+ throw new Error(`CertificateAuthority.${what} is not available on a signer-backed CA: ` +
304
+ "the private key lives in the signer (HSM/KMS) and is never written to disk.");
305
+ }
306
+ }
307
+ /**
308
+ * @internal The key every signing operation goes through. An injected
309
+ * {@link CaSigner} is returned as-is; otherwise the on-disk key is read
310
+ * (and decrypted) and handed over as a plain `CryptoKey`. Both are
311
+ * accepted by the `node-opcua-crypto` signing primitives, so callers
312
+ * never branch on which one they got.
313
+ */
314
+ async _getSigningKey() {
315
+ if (this.#signer) {
316
+ return this.#signer;
317
+ }
318
+ return privateKeyToCryptoKey(await this.getPrivateKey());
319
+ }
320
+ /**
321
+ * Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
322
+ * (temp file + atomic rename, temp file removed on failure). Only
323
+ * rewrites the file: construct a new `CertificateAuthority` with the new
324
+ * passphrase to continue using it.
325
+ */
326
+ async reencryptPrivateKey(oldPassphrase, newPassphrase) {
327
+ this.#assertKeyIsOnDisk("reencryptPrivateKey");
328
+ const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
329
+ const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
330
+ const key = readPrivateKey(this.privateKey, oldPass);
331
+ await this.#rewritePrivateKeyFile(key, newPass);
332
+ }
333
+ async #rewritePrivateKeyFile(privateKey, passphrase) {
334
+ const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
335
+ try {
336
+ await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
337
+ await fs.promises.rename(tmpFilename, this.privateKey);
338
+ }
339
+ finally {
340
+ await fs.promises.rm(tmpFilename, { force: true });
341
+ }
342
+ }
343
+ /** @internal resolve the configured passphrase, at most once per instance */
344
+ async _privateKeyPassphrase() {
345
+ if (!this.#passphraseResolved) {
346
+ this.#resolvedPassphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
347
+ this.#passphraseResolved = true;
348
+ }
349
+ return this.#resolvedPassphrase;
350
+ }
351
+ /**
352
+ * @internal On an existing key: encrypt it in place if a passphrase is
353
+ * configured and it is still plaintext (secure by default: the option
354
+ * means "protect this key", not "ignore me"), then read it back so a
355
+ * wrong or missing passphrase fails initialize() closed rather than the
356
+ * first signing operation.
357
+ */
358
+ async _ensurePrivateKeyProtection() {
359
+ if (this.#signer || !fs.existsSync(this.privateKey)) {
360
+ return;
361
+ }
362
+ if (this.#privateKeyPassphrase !== undefined && !isEncryptedPrivateKeyFile(this.privateKey)) {
363
+ warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
364
+ const plaintextKey = readPrivateKey(this.privateKey);
365
+ await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
366
+ }
367
+ await this.getPrivateKey();
368
+ }
369
+ /**
370
+ * Acquire a file-based lock on this CA's directory for the duration of
371
+ * `action` — serializes every operation that mutates the certificate
372
+ * database (`index.txt`, `serial`, `crlnumber`, `certs/`,
373
+ * `crl/revocation_list.*`) or the CA's own key/certificate, so
374
+ * concurrent calls on one `CertificateAuthority` (or two instances
375
+ * pointed at the same directory, in this or another process) cannot
376
+ * interleave. Mirrors `CertificateManager.withLock2`.
377
+ *
378
+ * Only ever call this from a top-level public operation — nested calls
379
+ * on the same instance would deadlock, since the underlying file lock
380
+ * is not reentrant.
381
+ *
382
+ * The wait is bounded: a waiter gives up (throws) after
383
+ * `CA_LOCK_MAX_WAIT_MS`. Without a bound, a holder whose openssl child
384
+ * hangs would block every CA operation in every process forever and
385
+ * silently — the lock library's keepalive keeps refreshing the lock
386
+ * file's mtime as long as the holder process is alive, so stale-lock
387
+ * recovery never fires for a live-but-stuck holder. A dead holder's
388
+ * lock goes stale (default 2 minutes) and is taken over well within
389
+ * this bound.
390
+ */
391
+ async #withCaLock(action) {
392
+ const lockFileName = path.join(this.rootDir, ".ca.lock");
393
+ return withLock({
394
+ fileToLock: lockFileName,
395
+ retries: { forever: true, maxRetryTime: CA_LOCK_MAX_WAIT_MS, ...CA_LOCK_RETRY }
396
+ }, action);
397
+ }
398
+ /**
399
+ * @internal Run `action` under this CA's directory lock. For the one
400
+ * legitimate cross-instance use: a subordinate CA's bootstrap signs its
401
+ * certificate with `openssl x509 -CAserial`, which read-increment-writes
402
+ * THIS issuer's `serial` file, and must therefore hold this issuer's
403
+ * lock (ordering is always subordinate -> issuer, so no cycle).
404
+ */
405
+ async _withCaDirectoryLock(action) {
406
+ return this.#withCaLock(action);
407
+ }
408
+ /** Path to the CA certificate in PEM format (`public/cacert.pem`). */
409
+ get caCertificate() {
410
+ // the Certificate Authority Certificate
411
+ return makePath(this.rootDir, "./public/cacert.pem");
412
+ }
413
+ /**
414
+ * Path to the issuer certificate chain (`public/issuer_chain.pem`).
415
+ *
416
+ * This file is created by {@link installCACertificate} when the
417
+ * provided cert file contains additional issuer certificates
418
+ * (e.g. intermediate + root). It is appended to signed certs
419
+ * by {@link constructCertificateChain} to produce a full chain
420
+ * per OPC UA Part 6 §6.2.6.
421
+ */
422
+ get issuerCertificateChain() {
423
+ return makePath(this.rootDir, "./public/issuer_chain.pem");
424
+ }
425
+ /**
426
+ * Path to the current Certificate Revocation List in DER format.
427
+ * (`crl/revocation_list.der`)
428
+ */
429
+ get revocationListDER() {
430
+ return makePath(this.rootDir, "./crl/revocation_list.der");
431
+ }
432
+ /**
433
+ * Path to the current Certificate Revocation List in PEM format.
434
+ * (`crl/revocation_list.crl`)
435
+ */
436
+ get revocationList() {
437
+ return makePath(this.rootDir, "./crl/revocation_list.crl");
438
+ }
439
+ /**
440
+ * Path to the concatenated CA certificate + CRL file.
441
+ * Used by OpenSSL for CRL-based verification.
442
+ */
443
+ get caCertificateWithCrl() {
444
+ return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
445
+ }
446
+ // ---------------------------------------------------------------
447
+ // Buffer-based accessors (US-059)
448
+ // ---------------------------------------------------------------
449
+ /**
450
+ * Return the CA certificate as a DER-encoded buffer.
451
+ *
452
+ * @throws if the CA certificate file does not exist
453
+ * (call {@link initialize} first).
454
+ */
455
+ getCACertificateDER() {
456
+ const pem = readCertificatePEM(this.caCertificate);
457
+ return convertPEMtoDER(pem);
458
+ }
459
+ /**
460
+ * Return the CA certificate as a PEM-encoded string.
461
+ *
462
+ * @throws if the CA certificate file does not exist
463
+ * (call {@link initialize} first).
464
+ */
465
+ getCACertificatePEM() {
466
+ const raw = readCertificatePEM(this.caCertificate);
467
+ // OpenSSL CA cert files may include a human-readable text
468
+ // dump before the PEM block — strip it.
469
+ const beginMarker = "-----BEGIN CERTIFICATE-----";
470
+ const idx = raw.indexOf(beginMarker);
471
+ if (idx > 0) {
472
+ return raw.substring(idx);
473
+ }
474
+ return raw;
475
+ }
476
+ /**
477
+ * Return the current Certificate Revocation List as a
478
+ * DER-encoded buffer.
479
+ *
480
+ * Returns an empty buffer if no CRL has been generated yet.
481
+ */
482
+ getCRLDER() {
483
+ const crlPath = this.revocationListDER;
484
+ if (!fs.existsSync(crlPath)) {
485
+ return Buffer.alloc(0);
486
+ }
487
+ return fs.readFileSync(crlPath);
488
+ }
489
+ /**
490
+ * Return the current Certificate Revocation List as a
491
+ * PEM-encoded string.
492
+ *
493
+ * Returns an empty string if no CRL has been generated yet.
494
+ */
495
+ getCRLPEM() {
496
+ const crlPath = this.revocationList;
497
+ if (!fs.existsSync(crlPath)) {
498
+ return "";
499
+ }
500
+ const raw = fs.readFileSync(crlPath, "utf-8");
501
+ // OpenSSL CRL files may include a human-readable text
502
+ // dump before the PEM block — strip it.
503
+ const beginMarker = "-----BEGIN X509 CRL-----";
504
+ const idx = raw.indexOf(beginMarker);
505
+ if (idx > 0) {
506
+ return raw.substring(idx);
507
+ }
508
+ return raw;
509
+ }
510
+ // ---------------------------------------------------------------
511
+ // Certificate database API (US-057)
512
+ // ---------------------------------------------------------------
513
+ /**
514
+ * Return a list of all issued certificates recorded in the
515
+ * OpenSSL `index.txt` database.
516
+ *
517
+ * Each entry includes the serial number, subject, status,
518
+ * expiry date, and (for revoked certs) the revocation date.
519
+ */
520
+ getIssuedCertificates() {
521
+ return this.#db.readIndex();
522
+ }
523
+ /**
524
+ * Return the total number of certificates recorded in
525
+ * `index.txt`.
526
+ */
527
+ getIssuedCertificateCount() {
528
+ return this.#db.readIndex().length;
529
+ }
530
+ /**
531
+ * Return the status of a certificate by its serial number.
532
+ *
533
+ * @param serial - hex-encoded serial number (e.g. `"1000"`)
534
+ * @returns `"valid"`, `"revoked"`, `"expired"`, or
535
+ * `undefined` if not found
536
+ */
537
+ getCertificateStatus(serial) {
538
+ return this.#db.findBySerial(serial)?.status;
539
+ }
540
+ /**
541
+ * Read a specific issued certificate by serial number and
542
+ * return its content as a DER-encoded buffer.
543
+ *
544
+ * OpenSSL stores signed certificates in the `certs/`
545
+ * directory using the naming convention `<SERIAL>.pem`.
546
+ *
547
+ * @param serial - hex-encoded serial number (e.g. `"1000"`)
548
+ * @returns the DER buffer, or `undefined` if not found
549
+ */
550
+ getCertificateBySerial(serial) {
551
+ return this.#db.getCertificateBySerial(serial);
552
+ }
553
+ /**
554
+ * Path to the OpenSSL certificate database file.
555
+ */
556
+ get indexFile() {
557
+ return this.#db.indexFile;
558
+ }
559
+ // ---------------------------------------------------------------
560
+ // Buffer-based CA operations (US-058)
561
+ // ---------------------------------------------------------------
562
+ /**
563
+ * Sign a DER-encoded Certificate Signing Request and return
564
+ * the signed certificate as a DER buffer.
565
+ *
566
+ * This method handles temp-file creation and cleanup
567
+ * internally so that callers can work with in-memory
568
+ * buffers only.
569
+ *
570
+ * The CA can override fields from the CSR by passing
571
+ * `options.dns`, `options.ip`, `options.applicationUri`,
572
+ * `options.startDate`, or `options.subject`.
573
+ *
574
+ * @param csrDer - the CSR as a DER-encoded buffer
575
+ * @param options - signing options and CA overrides
576
+ * @returns the signed certificate as a DER-encoded buffer
577
+ */
578
+ async signCertificateRequestFromDER(csrDer, options) {
579
+ const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pki-sign-"));
580
+ try {
581
+ const csrFile = path.join(tmpDir, "request.csr");
582
+ const certFile = path.join(tmpDir, "certificate.pem");
583
+ // Write CSR as PEM
584
+ const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
585
+ await fs.promises.writeFile(csrFile, csrPem, "utf-8");
586
+ // Build signing parameters — CA overrides take precedence.
587
+ // validityMs (sub-day capable) overrides validity (days) when
588
+ // both are provided; adjustDate() handles precedence.
589
+ const signingParams = {};
590
+ if (options?.validityMs !== undefined)
591
+ signingParams.validityMs = options.validityMs;
592
+ else
593
+ signingParams.validity = options?.validity ?? 365;
594
+ if (options?.startDate)
595
+ signingParams.startDate = options.startDate;
596
+ if (options?.dns)
597
+ signingParams.dns = options.dns;
598
+ if (options?.ip)
599
+ signingParams.ip = options.ip;
600
+ if (options?.applicationUri)
601
+ signingParams.applicationUri = options.applicationUri;
602
+ if (options?.subject)
603
+ signingParams.subject = options.subject;
604
+ // Delegate to the existing file-based method
605
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
606
+ // Read the signed certificate and convert to DER
607
+ const certPem = readCertificatePEM(certFile);
608
+ return convertPEMtoDER(certPem);
609
+ }
610
+ finally {
611
+ await fs.promises.rm(tmpDir, {
612
+ recursive: true,
613
+ force: true
614
+ });
615
+ }
616
+ }
617
+ /**
618
+ * Advertise the validity limits this CA can honor.
619
+ *
620
+ * Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
621
+ * clamp a requested validity against these bounds before calling
622
+ * {@link signCertificateRequestFromDER}, so a misconfigured
623
+ * `defaultCertValidity` cannot ask the CA for something it cannot
624
+ * produce.
625
+ *
626
+ * Defaults match the OpenSSL-backed implementation:
627
+ * - `minValidityMs = 60_000` (1 minute) — practical floor; the
628
+ * X.509 spec floor is 1 second but very short certs are rarely
629
+ * useful and pathological for any real deployment.
630
+ * - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
631
+ * enough for root CAs.
632
+ * - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
633
+ * floor on `notBefore` / `notAfter`.
634
+ * - `nativeUnit = "second"` — what `x509Date()` actually encodes.
635
+ *
636
+ * @see US-208 — the consumer-side capability story.
637
+ */
638
+ getCapabilities() {
639
+ return {
640
+ minValidityMs: 60_000,
641
+ maxValidityMs: 10 * 365 * 86_400_000,
642
+ validityGranularityMs: 1_000,
643
+ nativeUnit: "second"
644
+ };
645
+ }
646
+ /**
647
+ * Generate a new RSA key pair, create an internal CSR, sign it
648
+ * with this CA, and return both the certificate and private key
649
+ * as DER-encoded buffers.
650
+ *
651
+ * The private key is **never stored** by the CA — it exists only
652
+ * in a temporary directory that is cleaned up after the operation.
653
+ *
654
+ * This is used by `StartNewKeyPairRequest` (OPC UA Part 12) for
655
+ * constrained devices that cannot generate their own keys.
656
+ *
657
+ * @param options - key generation and certificate parameters
658
+ * @returns `{ certificateDer, privateKey }` — certificate as DER,
659
+ * private key as a branded `PrivateKey` buffer
660
+ */
661
+ /**
662
+ * An ephemeral key and a CSR for it, written into `tmpDir`. Both
663
+ * `generateKeyPairAndSign*` methods need exactly this, and neither
664
+ * needs a subprocess for it: the key comes from node's crypto and the
665
+ * request is built and self-signed in process, so no `openssl.cnf` has
666
+ * to be rendered either.
667
+ */
668
+ async #createEphemeralKeyAndCsr(tmpDir, keySize, options) {
669
+ const privateKeyFile = path.join(tmpDir, "private_key.pem");
670
+ await generatePrivateKeyFile(privateKeyFile, keySize);
671
+ const { csr } = await createCertificateSigningRequest({
672
+ privateKey: await privateKeyToCryptoKey(readPrivateKey(privateKeyFile)),
673
+ subject: options.subject ? new Subject(options.subject).toString() : undefined,
674
+ applicationUri: options.applicationUri,
675
+ dns: options.dns ?? [],
676
+ ip: options.ip ?? [],
677
+ purpose: CertificatePurpose.ForApplication
678
+ });
679
+ const csrFile = path.join(tmpDir, "request.csr");
680
+ await fs.promises.writeFile(csrFile, csr);
681
+ return { privateKeyFile, csrFile };
682
+ }
683
+ async generateKeyPairAndSignDER(options) {
684
+ const keySize = options.keySize ?? 2048;
685
+ const startDate = options.startDate ?? new Date();
686
+ const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pki-keygen-"));
687
+ try {
688
+ // 1. Ephemeral key + CSR, both in process
689
+ const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
690
+ // 4. Sign the CSR with this CA — validityMs takes precedence
691
+ // over validity when both are provided (adjustDate handles it).
692
+ const certFile = path.join(tmpDir, "certificate.pem");
693
+ const signingParams = {
694
+ applicationUri: options.applicationUri,
695
+ dns: options.dns,
696
+ ip: options.ip,
697
+ startDate
698
+ };
699
+ if (options.validityMs !== undefined)
700
+ signingParams.validityMs = options.validityMs;
701
+ else
702
+ signingParams.validity = options.validity ?? 365;
703
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
704
+ // 5. Read results
705
+ const certPem = readCertificatePEM(certFile);
706
+ const certificateDer = convertPEMtoDER(certPem);
707
+ const privateKey = readPrivateKey(privateKeyFile);
708
+ return { certificateDer, privateKey };
709
+ }
710
+ finally {
711
+ // 6. Securely clean up — private key is never persisted
712
+ await fs.promises.rm(tmpDir, {
713
+ recursive: true,
714
+ force: true
715
+ });
716
+ }
717
+ }
718
+ /**
719
+ * Generate a new RSA key pair, create an internal CSR, sign it
720
+ * with this CA, and return the result as a PKCS#12 (PFX)
721
+ * buffer bundling the certificate, private key, and CA chain.
722
+ *
723
+ * The private key is **never stored** by the CA — it exists only
724
+ * in a temporary directory that is cleaned up after the operation.
725
+ *
726
+ * @param options - key generation, certificate, and PFX options
727
+ * @returns the PFX as a `Buffer`
728
+ */
729
+ async generateKeyPairAndSignPFX(options) {
730
+ const keySize = options.keySize ?? 2048;
731
+ const startDate = options.startDate ?? new Date();
732
+ const passphrase = options.passphrase ?? "";
733
+ const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pki-keygen-pfx-"));
734
+ try {
735
+ // 1. Ephemeral key + CSR, both in process
736
+ const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
737
+ // 4. Sign the CSR with this CA — validityMs takes precedence
738
+ // over validity when both are provided (adjustDate handles it).
739
+ const certFile = path.join(tmpDir, "certificate.pem");
740
+ const signingParams = {
741
+ applicationUri: options.applicationUri,
742
+ dns: options.dns,
743
+ ip: options.ip,
744
+ startDate
745
+ };
746
+ if (options.validityMs !== undefined)
747
+ signingParams.validityMs = options.validityMs;
748
+ else
749
+ signingParams.validity = options.validity ?? 365;
750
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
751
+ // 5. Bundle into PFX. signCertificateRequest leaves the chain in
752
+ // certFile, so the leading block is the certificate and the rest
753
+ // is already this CA's own - no need to read it back separately.
754
+ const blocks = (await fs.promises.readFile(certFile, "utf-8")).match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
755
+ if (!blocks || blocks.length === 0) {
756
+ throw new Error(`generateKeyPairAndSignPFX: no certificate was produced in ${certFile}`);
757
+ }
758
+ return await createPfx({
759
+ certificate: convertPEMtoDER(blocks[0]),
760
+ certificateChain: blocks.slice(1).map(convertPEMtoDER),
761
+ privateKey: readPrivateKey(privateKeyFile),
762
+ password: passphrase
763
+ });
764
+ }
765
+ finally {
766
+ // 7. Securely clean up — private key is never persisted
767
+ await fs.promises.rm(tmpDir, {
768
+ recursive: true,
769
+ force: true
770
+ });
771
+ }
772
+ }
773
+ /**
774
+ * Revoke a DER-encoded certificate and regenerate the CRL.
775
+ *
776
+ * Extracts the serial number from the certificate, then
777
+ * uses the stored cert file at `certs/<serial>.pem` for
778
+ * revocation — avoiding temp-file PEM format mismatches.
779
+ *
780
+ * @param certDer - the certificate as a DER-encoded buffer
781
+ * @param reason - CRL reason code
782
+ * (default: `"keyCompromise"`)
783
+ * @throws if the certificate's serial is not found in the CA
784
+ */
785
+ async revokeCertificateDER(certDer, reason) {
786
+ // 1. Extract serial from the DER certificate
787
+ const info = exploreCertificate(certDer);
788
+ // exploreCertificate returns serial as "10:00" (colon-hex)
789
+ // openssl stores cert files as "1000.pem" (plain hex upper)
790
+ const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
791
+ // 2. Use the cert file that openssl ca already stored
792
+ const storedCertFile = path.join(this.rootDir, "certs", `${serial}.pem`);
793
+ if (!fs.existsSync(storedCertFile)) {
794
+ throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
795
+ }
796
+ // 3. Delegate to the existing file-based method
797
+ await this.revokeCertificate(storedCertFile, {
798
+ reason: reason ?? "keyCompromise"
799
+ });
800
+ }
801
+ /**
802
+ * Initialize the CA directory structure, generate the CA
803
+ * private key and self-signed certificate if they do not
804
+ * already exist.
805
+ */
806
+ async initialize() {
807
+ // the lock file lives inside rootDir, so rootDir must exist before
808
+ // #withCaLock can even acquire it — on a brand-new CA it doesn't yet
809
+ mkdirRecursiveSync(path.resolve(this.rootDir));
810
+ await this.#withCaLock(() => this.#bootstrap());
811
+ }
812
+ /**
813
+ * @internal Shared (backend-agnostic) part of `initialize()`: directory
814
+ * layout, default database files, the "already initialized" / "partial
815
+ * init" checks, and the openssl config file — then delegates CSR
816
+ * generation, CA-certificate signing, and the initial CRL to
817
+ * {@link CaBackend.bootstrap}. Must be called under {@link #withCaLock}.
818
+ */
819
+ async #bootstrap() {
820
+ const caRootDir = path.resolve(this.rootDir);
821
+ mkdirRecursiveSync(caRootDir);
822
+ ensurePrivateDirectory(path.join(caRootDir, "private"));
823
+ mkdirRecursiveSync(path.join(caRootDir, "public"));
824
+ mkdirRecursiveSync(path.join(caRootDir, "certs"));
825
+ mkdirRecursiveSync(path.join(caRootDir, "crl"));
826
+ mkdirRecursiveSync(path.join(caRootDir, "conf"));
827
+ const serial = path.join(caRootDir, "serial");
828
+ if (!fs.existsSync(serial)) {
829
+ await fs.promises.writeFile(serial, "1000");
830
+ }
831
+ const crlNumber = path.join(caRootDir, "crlnumber");
832
+ if (!fs.existsSync(crlNumber)) {
833
+ await fs.promises.writeFile(crlNumber, "1000");
834
+ }
835
+ const indexFile = path.join(caRootDir, "index.txt");
836
+ if (!fs.existsSync(indexFile)) {
837
+ await fs.promises.writeFile(indexFile, "");
838
+ }
839
+ // A signer-backed CA has no key file and never will: the key is held
840
+ // by the HSM/KMS, so it always "exists" and is never generated here.
841
+ const signerBacked = this.hasExternalSigner;
842
+ const caKeyExists = signerBacked || fs.existsSync(path.join(caRootDir, "private/cakey.pem"));
843
+ const caCertExists = fs.existsSync(path.join(caRootDir, "public/cacert.pem"));
844
+ if (caKeyExists && caCertExists && !config.forceCA) {
845
+ // CA is fully initialized => do not overwrite
846
+ if (!signerBacked) {
847
+ // repair permissions on installs created before this hardening
848
+ restrictPrivateFilePermissions(path.join(caRootDir, "private/cakey.pem"), 0o600);
849
+ // encrypt a plaintext key in place if a passphrase is configured, and
850
+ // fail closed now if the key cannot be read with the configured one
851
+ await this._ensurePrivateKeyProtection();
852
+ }
853
+ debugLog("CA private key and certificate already exist ... skipping");
854
+ return;
855
+ }
856
+ if (!signerBacked && caKeyExists && !caCertExists) {
857
+ // Partial init: key exists but certificate does not.
858
+ // This can happen when a previous CA creation failed
859
+ // (e.g. OpenSSL 3.5 authorityKeyIdentifier error).
860
+ // Remove the stale key so the CA is rebuilt from scratch.
861
+ debugLog("CA private key exists but cacert.pem is missing — rebuilding CA");
862
+ fs.unlinkSync(path.join(caRootDir, "private/cakey.pem"));
863
+ // Also remove the stale CSR if present
864
+ const staleCsr = path.join(caRootDir, "private/cakey.csr");
865
+ if (fs.existsSync(staleCsr)) {
866
+ fs.unlinkSync(staleCsr);
867
+ }
868
+ }
869
+ displayTitle("Create Certificate Authority (CA)");
870
+ const indexFileAttr = path.join(caRootDir, "index.txt.attr");
871
+ if (!fs.existsSync(indexFileAttr)) {
872
+ await fs.promises.writeFile(indexFileAttr, "unique_subject = no");
873
+ }
874
+ const caConfigFile = this.configFile;
875
+ await fs.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
876
+ if (!signerBacked) {
877
+ const privateKeyFilename = path.join(caRootDir, "private/cakey.pem");
878
+ displayTitle(`Generate the CA private Key - ${this.keySize}`);
879
+ // The first step is to create your RSA Private Key.
880
+ // This key is a 1025,2048,3072 or 2038 bit RSA key which is encrypted using
881
+ // Triple-DES and stored in a PEM format so that it is readable as ASCII text.
882
+ await generatePrivateKeyFile(privateKeyFilename, this.keySize, { passphrase: await this._privateKeyPassphrase() });
883
+ restrictPrivateFilePermissions(privateKeyFilename, 0o600);
884
+ }
885
+ // Once the private key is generated a Certificate Signing Request can be generated.
886
+ // The CSR is then used in one of two ways. Ideally, the CSR will be sent to a Certificate Authority, such as
887
+ // Thawte or Verisign who will verify the identity of the requestor and issue a signed certificate.
888
+ // The second option is to self-sign the CSR, which will be demonstrated in the next section
889
+ await this.#backend.bootstrap(this);
890
+ }
891
+ /**
892
+ * Initialize the CA directory structure and generate the
893
+ * private key + CSR **without signing**.
894
+ *
895
+ * Use this when the CA certificate will be signed by an
896
+ * external (third-party) root CA. After receiving the signed
897
+ * certificate, call {@link installCACertificate} to complete
898
+ * the setup.
899
+ *
900
+ * **Idempotent / restart-safe:**
901
+ * - If the CA certificate exists and is valid → `{ status: "ready" }`
902
+ * - If the CA certificate has expired → `{ status: "expired", csrPath, expiryDate }`
903
+ * (a new CSR is generated, preserving the existing private key)
904
+ * - If key + CSR exist but no cert (restart before install) →
905
+ * `{ status: "pending", csrPath }` without regenerating
906
+ * - Otherwise → generates key + CSR → `{ status: "created", csrPath }`
907
+ *
908
+ * @returns an {@link InitializeCSRResult} describing the CA state
909
+ */
910
+ async initializeCSR() {
911
+ const caRootDir = path.resolve(this.rootDir);
912
+ // the lock file lives inside rootDir, so rootDir must exist before
913
+ // #withCaLock can even acquire it — on a brand-new CA it doesn't yet
914
+ mkdirRecursiveSync(caRootDir);
915
+ // Everything below mutates the CA directory (key generation guarded
916
+ // only by an existsSync check, default database files, the openssl
917
+ // config, the CSR) and must run under the same lock initialize()
918
+ // uses, or two concurrent initializeCSR() calls can both pass the
919
+ // key-existence check and overwrite each other's key after one of
920
+ // them has already derived a CSR from it.
921
+ return this.#withCaLock(() => this.#initializeCSRLocked(caRootDir));
922
+ }
923
+ async #initializeCSRLocked(caRootDir) {
924
+ // Ensure directory structure always exists
925
+ for (const dir of ["public", "certs", "crl", "conf"]) {
926
+ mkdirRecursiveSync(path.join(caRootDir, dir));
927
+ }
928
+ ensurePrivateDirectory(path.join(caRootDir, "private"));
929
+ const caCertFile = this.caCertificate;
930
+ const privateKeyFile = path.join(caRootDir, "private/cakey.pem");
931
+ const csrFile = path.join(caRootDir, "private/cakey.csr");
932
+ // a signer-backed CA holds its key externally: it is always
933
+ // "available" and there is no file to generate, protect or permission
934
+ const keyAvailable = this.hasExternalSigner || fs.existsSync(privateKeyFile);
935
+ // repair permissions on installs created before this hardening,
936
+ // encrypt in place if a passphrase is configured, fail closed if unreadable
937
+ if (!this.hasExternalSigner && fs.existsSync(privateKeyFile)) {
938
+ restrictPrivateFilePermissions(privateKeyFile, 0o600);
939
+ await this._ensurePrivateKeyProtection();
940
+ }
941
+ // ── Case 1: cert already exists ──
942
+ if (fs.existsSync(caCertFile)) {
943
+ // Check if the certificate has expired
944
+ const certDer = convertPEMtoDER(readCertificatePEM(caCertFile));
945
+ const certInfo = exploreCertificate(certDer);
946
+ const notAfter = certInfo.tbsCertificate.validity.notAfter;
947
+ if (notAfter.getTime() < Date.now()) {
948
+ // Certificate has expired — regenerate CSR for renewal
949
+ debugLog("CA certificate has expired — generating renewal CSR");
950
+ await this._generateCSR(caRootDir, privateKeyFile, csrFile);
951
+ return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
952
+ }
953
+ debugLog("CA certificate already exists and is valid — ready");
954
+ return { status: "ready" };
955
+ }
956
+ // ── Case 2: key + CSR exist but no cert → pending state ──
957
+ // (restart between initializeCSR and installCACertificate)
958
+ if (keyAvailable && fs.existsSync(csrFile)) {
959
+ debugLog("CA key + CSR already exist — pending external signing");
960
+ return { status: "pending", csrPath: csrFile };
961
+ }
962
+ // ── Case 3: fresh setup — generate key + CSR ──
963
+ // Create default files (serial, crlnumber, index.txt)
964
+ const serial = path.join(caRootDir, "serial");
965
+ if (!fs.existsSync(serial)) {
966
+ await fs.promises.writeFile(serial, "1000");
967
+ }
968
+ const crlNumber = path.join(caRootDir, "crlnumber");
969
+ if (!fs.existsSync(crlNumber)) {
970
+ await fs.promises.writeFile(crlNumber, "1000");
971
+ }
972
+ const indexFile = path.join(caRootDir, "index.txt");
973
+ if (!fs.existsSync(indexFile)) {
974
+ await fs.promises.writeFile(indexFile, "");
975
+ }
976
+ const indexFileAttr = path.join(caRootDir, "index.txt.attr");
977
+ if (!fs.existsSync(indexFileAttr)) {
978
+ await fs.promises.writeFile(indexFileAttr, "unique_subject = no");
979
+ }
980
+ // Write OpenSSL config
981
+ const caConfigFile = this.configFile;
982
+ await fs.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
983
+ // Generate private key
984
+ if (!keyAvailable) {
985
+ await generatePrivateKeyFile(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
986
+ restrictPrivateFilePermissions(privateKeyFile, 0o600);
987
+ }
988
+ // Generate CSR
989
+ await this._generateCSR(caRootDir, privateKeyFile, csrFile);
990
+ return { status: "created", csrPath: csrFile };
991
+ }
992
+ /**
993
+ * Check whether the CA certificate needs renewal and, if so,
994
+ * generate a new CSR for re-signing by the external root CA.
995
+ *
996
+ * Use this while the CA is running to detect upcoming expiry
997
+ * **before** it actually expires. The existing private key is
998
+ * preserved so previously issued certs remain valid.
999
+ *
1000
+ * @param thresholdDays - number of days before expiry at which
1001
+ * to trigger renewal (default: 30)
1002
+ * @returns an {@link InitializeCSRResult} — `"expired"` if
1003
+ * renewal is needed, `"ready"` if the cert is still valid
1004
+ */
1005
+ async renewCSR(thresholdDays = 30) {
1006
+ const caRootDir = path.resolve(this.rootDir);
1007
+ const caCertFile = this.caCertificate;
1008
+ const privateKeyFile = path.join(caRootDir, "private/cakey.pem");
1009
+ const csrFile = path.join(caRootDir, "private/cakey.csr");
1010
+ if (!fs.existsSync(caCertFile)) {
1011
+ // No cert at all — delegate to initializeCSR
1012
+ return this.initializeCSR();
1013
+ }
1014
+ const certDer = convertPEMtoDER(readCertificatePEM(caCertFile));
1015
+ const certInfo = exploreCertificate(certDer);
1016
+ const notAfter = certInfo.tbsCertificate.validity.notAfter;
1017
+ const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000;
1018
+ if (notAfter.getTime() - Date.now() < thresholdMs) {
1019
+ debugLog(`CA certificate expires within ${thresholdDays} days — generating renewal CSR`);
1020
+ await this.#withCaLock(() => this._generateCSR(caRootDir, privateKeyFile, csrFile));
1021
+ return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
1022
+ }
1023
+ return { status: "ready" };
1024
+ }
1025
+ /**
1026
+ * Generate a CSR using the existing private key.
1027
+ * Must be called under {@link #withCaLock} — the lock is taken by the
1028
+ * public callers (`initializeCSR`, `renewCSR`), not here, so that
1029
+ * `initializeCSR` can hold one lock across key generation AND CSR
1030
+ * generation without the non-reentrant file lock deadlocking.
1031
+ * @internal
1032
+ */
1033
+ async _generateCSR(caRootDir, privateKeyFile, csrFile) {
1034
+ await this.#backend.generateCaCsr(this, caRootDir, privateKeyFile, csrFile);
1035
+ }
1036
+ /**
1037
+ * Install an externally-signed CA certificate and generate
1038
+ * the initial CRL.
1039
+ *
1040
+ * Call this after {@link initializeCSR} once the external
1041
+ * root CA has signed the CSR.
1042
+ *
1043
+ * **Safety checks:**
1044
+ * - Verifies that the certificate's public key matches the
1045
+ * CA private key before installing.
1046
+ *
1047
+ * @param signedCertFile - path to the PEM-encoded signed
1048
+ * CA certificate (issued by the external root CA)
1049
+ * @returns an {@link InstallCACertificateResult} with
1050
+ * `status: "success"` or `status: "error"` and a `reason`
1051
+ */
1052
+ async installCACertificate(signedCertFile) {
1053
+ return this.#withCaLock(async () => {
1054
+ const caCertFile = this.caCertificate;
1055
+ // Read the full content once — may contain a chain
1056
+ const fullPem = await fs.promises.readFile(signedCertFile, "utf8");
1057
+ // Split PEM blocks: first cert → cacert.pem, rest → issuer_chain.pem
1058
+ const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
1059
+ if (!pemBlocks || pemBlocks.length === 0) {
1060
+ return {
1061
+ status: "error",
1062
+ reason: "no_certificate_found",
1063
+ message: "The provided file does not contain any PEM-encoded certificate."
1064
+ };
1065
+ }
1066
+ // Verify the first certificate really belongs to this CA's key
1067
+ const certDer = convertPEMtoDER(pemBlocks[0]);
1068
+ if (!(await this.#certificateMatchesOurKey(pemBlocks[0], certDer))) {
1069
+ return {
1070
+ status: "error",
1071
+ reason: "certificate_key_mismatch",
1072
+ message: "The provided certificate does not match the CA " +
1073
+ "private key. Ensure the certificate was signed " +
1074
+ "from the CSR generated by initializeCSR()."
1075
+ };
1076
+ }
1077
+ // Write the first cert (the CA cert itself)
1078
+ await fs.promises.writeFile(caCertFile, `${pemBlocks[0]}\n`);
1079
+ // Write any additional issuer certs to the chain file
1080
+ const issuerChainFile = this.issuerCertificateChain;
1081
+ if (pemBlocks.length > 1) {
1082
+ const issuerPem = `${pemBlocks.slice(1).join("\n")}\n`;
1083
+ await fs.promises.writeFile(issuerChainFile, issuerPem);
1084
+ debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
1085
+ }
1086
+ else {
1087
+ // No issuer chain — remove stale file if present
1088
+ if (fs.existsSync(issuerChainFile)) {
1089
+ await fs.promises.unlink(issuerChainFile);
1090
+ }
1091
+ }
1092
+ // Generate initial CRL
1093
+ await this.#backend.regenerateCrl(this);
1094
+ return { status: "success" };
1095
+ });
1096
+ }
1097
+ /**
1098
+ * Sign a CSR with CA extensions (`v3_ca`), producing a
1099
+ * subordinate CA certificate.
1100
+ *
1101
+ * Unlike {@link signCertificateRequest} which signs with
1102
+ * end-entity extensions (SANs, etc.), this method signs
1103
+ * with `basicConstraints = CA:TRUE` and `keyUsage =
1104
+ * keyCertSign, cRLSign`.
1105
+ *
1106
+ * @param certFile - output path for the signed CA cert (PEM)
1107
+ * @param csrFile - path to the subordinate CA's CSR
1108
+ * @param params - signing parameters
1109
+ */
1110
+ async signCACertificateRequest(certFile, csrFile, params) {
1111
+ await this.#withCaLock(async () => {
1112
+ const validity = params.validity ?? 3650;
1113
+ await this.#backend.signSubordinateCsr(this, csrFile, certFile, validity);
1114
+ // Append this CA's cert chain to the output so the caller
1115
+ // receives a complete chain file ready for installCACertificate.
1116
+ // Chain format: [signedSubordinateCert, thisCA, thisCA's issuers...]
1117
+ await this.constructCertificateChain(certFile);
1118
+ });
1119
+ }
1120
+ /**
1121
+ * Rebuild the combined CA certificate + CRL file.
1122
+ *
1123
+ * This concatenates the CA certificate with the current
1124
+ * revocation list so that OpenSSL can verify certificates
1125
+ * with CRL checking enabled.
1126
+ */
1127
+ async constructCACertificateWithCRL() {
1128
+ const cacertWithCRL = this.caCertificateWithCrl;
1129
+ // note : in order to check if the certificate is revoked,
1130
+ // you need to specify -crl_check and have both the CA cert and the (applicable) CRL in your trust store.
1131
+ // There are two ways to do that:
1132
+ // 1. concatenate cacert.pem and crl.pem into one file and use that for -CAfile.
1133
+ // 2. use some linked
1134
+ // ( from http://security.stackexchange.com/a/58305/59982)
1135
+ if (fs.existsSync(this.revocationList)) {
1136
+ await fs.promises.writeFile(cacertWithCRL, fs.readFileSync(this.caCertificate, "utf8") + fs.readFileSync(this.revocationList, "utf8"));
1137
+ }
1138
+ else {
1139
+ // there is no revocation list yet
1140
+ await fs.promises.writeFile(cacertWithCRL, fs.readFileSync(this.caCertificate));
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Append the CA certificate to a signed certificate file,
1145
+ * creating a PEM certificate chain.
1146
+ *
1147
+ * @param certificate - path to the certificate file to extend
1148
+ */
1149
+ async constructCertificateChain(certificate) {
1150
+ assert(fs.existsSync(certificate));
1151
+ assert(fs.existsSync(this.caCertificate));
1152
+ debugLog(chalk.yellow(" certificate file :"), chalk.cyan(certificate));
1153
+ // Build chain: cert + this CA cert + issuer chain (if available)
1154
+ let chain = await fs.promises.readFile(certificate, "utf8");
1155
+ chain += await fs.promises.readFile(this.caCertificate, "utf8");
1156
+ // Append the issuer certificate chain (e.g. root CA cert)
1157
+ // to produce a complete chain per OPC UA Part 6 §6.2.6
1158
+ if (fs.existsSync(this.issuerCertificateChain)) {
1159
+ chain += await fs.promises.readFile(this.issuerCertificateChain, "utf8");
1160
+ }
1161
+ await fs.promises.writeFile(certificate, chain);
1162
+ }
1163
+ /**
1164
+ * Create a self-signed certificate using OpenSSL.
1165
+ *
1166
+ * @param certificateFile - output path for the signed certificate
1167
+ * @param privateKey - path to the private key file
1168
+ * @param params - certificate parameters (subject, validity, SANs)
1169
+ */
1170
+ async createSelfSignedCertificate(certificateFile, privateKey, params) {
1171
+ assert(typeof privateKey === "string");
1172
+ assert(fs.existsSync(privateKey));
1173
+ if (!certificateFileExist(certificateFile)) {
1174
+ return;
1175
+ }
1176
+ adjustDate(params);
1177
+ adjustApplicationUri(params);
1178
+ params.dns = params.dns || [];
1179
+ params.ip = params.ip || [];
1180
+ await this.#withCaLock(async () => {
1181
+ await this.#backend.createSelfSignedCertificate(this, certificateFile, privateKey, params);
1182
+ });
1183
+ }
1184
+ /**
1185
+ * Regenerate `crl/revocation_list.{crl,der}` from the current database
1186
+ * state, without changing any certificate's status. Normally
1187
+ * unnecessary — `revokeCertificate` already regenerates the CRL as
1188
+ * part of revoking — but useful to force a refresh (e.g. after
1189
+ * switching a CA's `backend` between `"openssl"` and `"native"`).
1190
+ */
1191
+ async regenerateCrl() {
1192
+ await this.#withCaLock(async () => {
1193
+ await this.#backend.regenerateCrl(this);
1194
+ });
1195
+ }
1196
+ /**
1197
+ * Revoke a certificate and regenerate the CRL.
1198
+ *
1199
+ * @param certificate - path to the certificate file to revoke
1200
+ * @param params - revocation parameters
1201
+ * @param params.reason - CRL reason code
1202
+ * (default `"keyCompromise"`)
1203
+ */
1204
+ async revokeCertificate(certificate, params) {
1205
+ const crlReasons = [
1206
+ "unspecified",
1207
+ "keyCompromise",
1208
+ "CACompromise",
1209
+ "affiliationChanged",
1210
+ "superseded",
1211
+ "cessationOfOperation",
1212
+ "certificateHold",
1213
+ "removeFromCRL"
1214
+ ];
1215
+ const reason = params.reason || "keyCompromise";
1216
+ assert(crlReasons.indexOf(reason) >= 0);
1217
+ displayTitle(`Revoking certificate ${certificate}`);
1218
+ await this.#withCaLock(async () => {
1219
+ await this.#backend.revoke(this, certificate, reason);
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Sign a Certificate Signing Request (CSR) with this CA.
1224
+ *
1225
+ * The signed certificate is written to `certificate`, and the
1226
+ * CA certificate chain plus CRL are appended to form a
1227
+ * complete certificate chain.
1228
+ *
1229
+ * @param certificate - output path for the signed certificate
1230
+ * @param certificateSigningRequestFilename - path to the CSR
1231
+ * @param params1 - signing parameters (validity, dates, SANs)
1232
+ * @returns the path to the signed certificate
1233
+ */
1234
+ async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
1235
+ // ask the backend, do not assume: a native CA needs no openssl at all
1236
+ await this.#backend.preflight();
1237
+ assert(fs.existsSync(certificateSigningRequestFilename));
1238
+ if (!certificateFileExist(certificate)) {
1239
+ return "";
1240
+ }
1241
+ adjustDate(params1);
1242
+ adjustApplicationUri(params1);
1243
+ // note :
1244
+ // subjectAltName is not copied across
1245
+ // see https://github.com/openssl/openssl/issues/10458
1246
+ const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
1247
+ const csrInfo = exploreCertificateSigningRequest(csr);
1248
+ const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier
1249
+ ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0]
1250
+ : undefined;
1251
+ if (typeof applicationUri !== "string") {
1252
+ throw new Error("Cannot find applicationUri in CSR");
1253
+ }
1254
+ const dns = csrInfo.extensionRequest.subjectAltName.dNSName || [];
1255
+ let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
1256
+ ip = ip.map(octetStringToIpAddress);
1257
+ const sanOverride = { applicationUri, dns, ip };
1258
+ return this.#withCaLock(async () => {
1259
+ await this.#backend.signEndEntityCsr(this, certificate, certificateSigningRequestFilename, params1, sanOverride);
1260
+ displaySubtitle("- construct CA certificate with CRL");
1261
+ await this.constructCACertificateWithCRL();
1262
+ // construct certificate chain
1263
+ // concatenate certificate with CA Certificate and revocation list
1264
+ displaySubtitle("- construct certificate chain");
1265
+ await this.constructCertificateChain(certificate);
1266
+ // todo
1267
+ displaySubtitle("- verify certificate against the root CA");
1268
+ await this.verifyCertificate(certificate);
1269
+ return certificate;
1270
+ });
1271
+ }
1272
+ /**
1273
+ * Check that `certificate` really was signed by this CA, and throw if it
1274
+ * was not.
1275
+ *
1276
+ * This used to do nothing: `openssl verify` crashes on Windows, so the
1277
+ * check was left as a placeholder. It no longer needs a subprocess -
1278
+ * `verifyCertificateSignature` does it in pure JS, the same way
1279
+ * {@link CertificateManager} already validates a chain - so the check
1280
+ * that `signCertificateRequest` always claimed to perform now actually
1281
+ * happens on every issuance, whichever backend did the signing.
1282
+ *
1283
+ * Only the leading certificate is examined: the file may be a chain,
1284
+ * and the rest of it is this CA's own certificate and its issuers.
1285
+ *
1286
+ * @param certificate - path to the certificate (or chain) to verify
1287
+ */
1288
+ async verifyCertificate(certificate) {
1289
+ const pem = await fs.promises.readFile(certificate, "utf-8");
1290
+ const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
1291
+ if (!blocks || blocks.length === 0) {
1292
+ throw new Error(`verifyCertificate: ${certificate} contains no PEM-encoded certificate`);
1293
+ }
1294
+ const caCertificateDer = convertPEMtoDER(readCertificatePEM(this.caCertificate));
1295
+ if (!verifyCertificateSignature(convertPEMtoDER(blocks[0]), caCertificateDer)) {
1296
+ throw new Error(`verifyCertificate: ${certificate} was not signed by this certificate authority (${this.caCertificate})`);
1297
+ }
1298
+ }
1299
+ }
1300
+ //# sourceMappingURL=certificate_authority_core.js.map