node-opcua-pki 6.22.1 → 7.0.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/bin/install_prerequisite.ts +1 -1
- package/bin/pki.ts +1 -1
- package/dist/bin/install_prerequisite.d.ts +2 -0
- package/dist/bin/install_prerequisite.js +6 -0
- package/dist/bin/install_prerequisite.js.map +1 -0
- package/dist/bin/pki.d.ts +2 -0
- package/dist/bin/pki.js +4 -0
- package/dist/bin/pki.js.map +1 -0
- package/dist/lib/ca/backends/native_ca_backend.d.ts +49 -0
- package/dist/lib/ca/backends/native_ca_backend.js +365 -0
- package/dist/lib/ca/backends/native_ca_backend.js.map +1 -0
- package/dist/lib/ca/backends/openssl_ca_backend.d.ts +47 -0
- package/dist/lib/ca/backends/openssl_ca_backend.js +398 -0
- package/dist/lib/ca/backends/openssl_ca_backend.js.map +1 -0
- package/dist/lib/ca/certificate_authority.d.ts +62 -0
- package/dist/lib/ca/certificate_authority.js +91 -0
- package/dist/lib/ca/certificate_authority.js.map +1 -0
- package/dist/lib/ca/core/ca_backend.d.ts +89 -0
- package/dist/lib/ca/core/ca_backend.js +24 -0
- package/dist/lib/ca/core/ca_backend.js.map +1 -0
- package/dist/lib/ca/core/ca_database.d.ts +91 -0
- package/dist/lib/ca/core/ca_database.js +267 -0
- package/dist/lib/ca/core/ca_database.js.map +1 -0
- package/dist/{core-BwqSJPHc.d.mts → lib/ca/core/certificate_authority_core.d.ts} +45 -394
- package/dist/lib/ca/core/certificate_authority_core.js +1300 -0
- package/dist/lib/ca/core/certificate_authority_core.js.map +1 -0
- package/dist/lib/ca/core/index.d.ts +17 -0
- package/dist/lib/ca/core/index.js +25 -0
- package/dist/lib/ca/core/index.js.map +1 -0
- package/dist/lib/ca/crypto_create_CA.d.ts +1 -0
- package/dist/lib/ca/crypto_create_CA.js +821 -0
- package/dist/lib/ca/crypto_create_CA.js.map +1 -0
- package/dist/lib/ca/index.d.ts +1 -0
- package/dist/lib/ca/index.js +2 -0
- package/dist/lib/ca/index.js.map +1 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.d.ts +2 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.js +170 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.js.map +1 -0
- package/dist/lib/core.d.ts +33 -0
- package/dist/lib/core.js +56 -0
- package/dist/lib/core.js.map +1 -0
- package/dist/lib/index.d.ts +11 -0
- package/dist/lib/index.js +37 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/misc/applicationurn.d.ts +1 -0
- package/dist/lib/misc/applicationurn.js +40 -0
- package/dist/lib/misc/applicationurn.js.map +1 -0
- package/dist/lib/misc/hostname.d.ts +8 -0
- package/dist/lib/misc/hostname.js +82 -0
- package/dist/lib/misc/hostname.js.map +1 -0
- package/dist/lib/misc/subject.d.ts +2 -0
- package/dist/lib/misc/subject.js +2 -0
- package/dist/lib/misc/subject.js.map +1 -0
- package/dist/{index.d.ts → lib/pki/certificate_manager.d.ts} +24 -248
- package/dist/lib/pki/certificate_manager.js +2302 -0
- package/dist/lib/pki/certificate_manager.js.map +1 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.d.ts +2 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.js +74 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.js.map +1 -0
- package/dist/lib/pki/toolbox_pfx.d.ts +127 -0
- package/dist/lib/pki/toolbox_pfx.js +248 -0
- package/dist/lib/pki/toolbox_pfx.js.map +1 -0
- package/dist/lib/toolbox/common.d.ts +150 -0
- package/dist/lib/toolbox/common.js +82 -0
- package/dist/lib/toolbox/common.js.map +1 -0
- package/dist/lib/toolbox/common2.d.ts +25 -0
- package/dist/lib/toolbox/common2.js +97 -0
- package/dist/lib/toolbox/common2.js.map +1 -0
- package/dist/lib/toolbox/config.d.ts +5 -0
- package/dist/lib/toolbox/config.js +28 -0
- package/dist/lib/toolbox/config.js.map +1 -0
- package/dist/lib/toolbox/debug.d.ts +5 -0
- package/dist/lib/toolbox/debug.js +35 -0
- package/dist/lib/toolbox/debug.js.map +1 -0
- package/dist/lib/toolbox/display.d.ts +4 -0
- package/dist/lib/toolbox/display.js +56 -0
- package/dist/lib/toolbox/display.js.map +1 -0
- package/dist/lib/toolbox/index.d.ts +5 -0
- package/dist/lib/toolbox/index.js +28 -0
- package/dist/lib/toolbox/index.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.d.ts +4 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.js +50 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/_env.d.ts +58 -0
- package/dist/lib/toolbox/with_openssl/_env.js +126 -0
- package/dist/lib/toolbox/with_openssl/_env.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js +72 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.js +92 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js +135 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.d.ts +67 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.js +246 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/index.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/index.js +30 -0
- package/dist/lib/toolbox/with_openssl/index.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.d.ts +7 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.js +358 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/toolbox.d.ts +53 -0
- package/dist/lib/toolbox/with_openssl/toolbox.js +190 -0
- package/dist/lib/toolbox/with_openssl/toolbox.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.d.ts +5 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js +69 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.d.ts +3 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js +78 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/index.d.ts +2 -0
- package/dist/lib/toolbox/without_openssl/index.js +25 -0
- package/dist/lib/toolbox/without_openssl/index.js.map +1 -0
- package/package.json +11 -30
- package/dist/bin/install_prerequisite.mjs +0 -18
- package/dist/bin/install_prerequisite.mjs.map +0 -1
- package/dist/bin/pki.mjs +0 -5932
- package/dist/bin/pki.mjs.map +0 -1
- package/dist/chunk-EQ4DGI4M.mjs +0 -1917
- package/dist/chunk-EQ4DGI4M.mjs.map +0 -1
- package/dist/chunk-GCHH54PS.mjs +0 -30
- package/dist/chunk-GCHH54PS.mjs.map +0 -1
- package/dist/core-BwqSJPHc.d.ts +0 -1041
- package/dist/core.d.mts +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -1902
- package/dist/core.js.map +0 -1
- package/dist/core.mjs +0 -21
- package/dist/core.mjs.map +0 -1
- package/dist/index.d.mts +0 -1003
- package/dist/index.js +0 -5009
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -3145
- package/dist/index.mjs.map +0 -1
package/dist/chunk-EQ4DGI4M.mjs
DELETED
|
@@ -1,1917 +0,0 @@
|
|
|
1
|
-
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
2
|
-
import assert from "assert";
|
|
3
|
-
function quote(str) {
|
|
4
|
-
return `"${str || ""}"`;
|
|
5
|
-
}
|
|
6
|
-
function isOpaqueSigner(privateKey) {
|
|
7
|
-
return typeof privateKey !== "string" && typeof privateKey.sign === "function";
|
|
8
|
-
}
|
|
9
|
-
async function resolvePrivateKeyPassphrase(passphrase) {
|
|
10
|
-
if (passphrase === void 0) {
|
|
11
|
-
return void 0;
|
|
12
|
-
}
|
|
13
|
-
return typeof passphrase === "function" ? await passphrase() : passphrase;
|
|
14
|
-
}
|
|
15
|
-
function adjustDate(params) {
|
|
16
|
-
assert(params instanceof Object);
|
|
17
|
-
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
18
|
-
assert(params.startDate instanceof Date);
|
|
19
|
-
if (params.validityMs !== void 0) {
|
|
20
|
-
if (params.validityMs <= 0) {
|
|
21
|
-
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
22
|
-
}
|
|
23
|
-
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
24
|
-
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
25
|
-
} else {
|
|
26
|
-
params.validity = params.validity || 365;
|
|
27
|
-
params.endDate = new Date(params.startDate.getTime());
|
|
28
|
-
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
29
|
-
}
|
|
30
|
-
assert(params.endDate instanceof Date);
|
|
31
|
-
assert(params.startDate instanceof Date);
|
|
32
|
-
}
|
|
33
|
-
function adjustApplicationUri(params) {
|
|
34
|
-
const applicationUri = params.applicationUri || "";
|
|
35
|
-
if (applicationUri.length > 200) {
|
|
36
|
-
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// packages/node-opcua-pki/lib/ca/core/ca_database.ts
|
|
41
|
-
import fs from "fs";
|
|
42
|
-
import path from "path";
|
|
43
|
-
import { convertPEMtoDER, readCertificatePEM } from "node-opcua-crypto";
|
|
44
|
-
function parseOpenSSLDate(dateStr) {
|
|
45
|
-
const raw = dateStr?.split(",")[0] ?? "";
|
|
46
|
-
if (raw.length < 12) return "";
|
|
47
|
-
const yy = parseInt(raw.substring(0, 2), 10);
|
|
48
|
-
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
49
|
-
const month = raw.substring(2, 4);
|
|
50
|
-
const day = raw.substring(4, 6);
|
|
51
|
-
const hour = raw.substring(6, 8);
|
|
52
|
-
const min = raw.substring(8, 10);
|
|
53
|
-
const sec = raw.substring(10, 12);
|
|
54
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
55
|
-
}
|
|
56
|
-
function formatOpenSSLDate(date) {
|
|
57
|
-
const yy = String(date.getUTCFullYear() % 100).padStart(2, "0");
|
|
58
|
-
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
59
|
-
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
60
|
-
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
61
|
-
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
62
|
-
const ss = String(date.getUTCSeconds()).padStart(2, "0");
|
|
63
|
-
return `${yy}${mm}${dd}${hh}${mi}${ss}Z`;
|
|
64
|
-
}
|
|
65
|
-
function parseRevocationReason(dateStr) {
|
|
66
|
-
const parts = dateStr?.split(",");
|
|
67
|
-
return parts && parts.length > 1 ? parts[1] : void 0;
|
|
68
|
-
}
|
|
69
|
-
function evenLengthHex(value) {
|
|
70
|
-
const hex = value.toString(16).toUpperCase();
|
|
71
|
-
return hex.length % 2 === 0 ? hex : `0${hex}`;
|
|
72
|
-
}
|
|
73
|
-
function escapeIndexField(value) {
|
|
74
|
-
return value.replace(/[\x00-\x1f\x7f\\]/g, (c) => `\\x${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
75
|
-
}
|
|
76
|
-
function assertHexSerial(serial) {
|
|
77
|
-
if (!/^[0-9A-Fa-f]+$/.test(serial)) {
|
|
78
|
-
throw new Error(`Invalid certificate serial number: ${JSON.stringify(serial)}`);
|
|
79
|
-
}
|
|
80
|
-
return serial.toUpperCase();
|
|
81
|
-
}
|
|
82
|
-
var CaDatabase = class {
|
|
83
|
-
#rootDir;
|
|
84
|
-
constructor(rootDir) {
|
|
85
|
-
this.#rootDir = rootDir;
|
|
86
|
-
}
|
|
87
|
-
/** Path to the OpenSSL certificate database file (`index.txt`). */
|
|
88
|
-
get indexFile() {
|
|
89
|
-
return path.join(this.#rootDir, "index.txt");
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Parse the OpenSSL `index.txt` certificate database.
|
|
93
|
-
*
|
|
94
|
-
* Each line has tab-separated fields:
|
|
95
|
-
* ```
|
|
96
|
-
* status expiry [revocationDate] serial unknown subject
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
100
|
-
* - expiry: `YYMMDDHHmmssZ`
|
|
101
|
-
* - revocationDate: present only for revoked certs
|
|
102
|
-
* - serial: hex string
|
|
103
|
-
* - unknown: always `"unknown"`
|
|
104
|
-
* - subject: X.500 slash-delimited string
|
|
105
|
-
*/
|
|
106
|
-
readIndex() {
|
|
107
|
-
const indexPath = this.indexFile;
|
|
108
|
-
if (!fs.existsSync(indexPath)) {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
const content = fs.readFileSync(indexPath, "utf-8");
|
|
112
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
113
|
-
const records = [];
|
|
114
|
-
for (const line of lines) {
|
|
115
|
-
const fields = line.split(" ");
|
|
116
|
-
if (fields.length < 4) continue;
|
|
117
|
-
const statusChar = fields[0];
|
|
118
|
-
const expiryStr = fields[1];
|
|
119
|
-
let serial;
|
|
120
|
-
let subject;
|
|
121
|
-
let revocationDate;
|
|
122
|
-
if (statusChar === "R") {
|
|
123
|
-
revocationDate = fields[2];
|
|
124
|
-
serial = fields[3];
|
|
125
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
126
|
-
} else {
|
|
127
|
-
serial = fields[3];
|
|
128
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
129
|
-
}
|
|
130
|
-
let status;
|
|
131
|
-
switch (statusChar) {
|
|
132
|
-
case "V":
|
|
133
|
-
status = "valid";
|
|
134
|
-
break;
|
|
135
|
-
case "R":
|
|
136
|
-
status = "revoked";
|
|
137
|
-
break;
|
|
138
|
-
case "E":
|
|
139
|
-
status = "expired";
|
|
140
|
-
break;
|
|
141
|
-
default:
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
records.push({
|
|
145
|
-
serial,
|
|
146
|
-
status,
|
|
147
|
-
subject,
|
|
148
|
-
expiryDate: parseOpenSSLDate(expiryStr),
|
|
149
|
-
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0,
|
|
150
|
-
reason: revocationDate ? parseRevocationReason(revocationDate) : void 0
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return records;
|
|
154
|
-
}
|
|
155
|
-
/** Look up one record by serial number (case-insensitive). */
|
|
156
|
-
findBySerial(serial) {
|
|
157
|
-
const upper = serial.toUpperCase();
|
|
158
|
-
return this.readIndex().find((r) => r.serial.toUpperCase() === upper);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Read a specific issued certificate by serial number.
|
|
162
|
-
*
|
|
163
|
-
* OpenSSL stores signed certificates in the `certs/` directory using
|
|
164
|
-
* the naming convention `<SERIAL>.pem`.
|
|
165
|
-
*
|
|
166
|
-
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
167
|
-
* @returns the DER buffer, or `undefined` if not found
|
|
168
|
-
*/
|
|
169
|
-
getCertificateBySerial(serial) {
|
|
170
|
-
const upper = serial.toUpperCase();
|
|
171
|
-
const certFile = path.join(this.#rootDir, "certs", `${upper}.pem`);
|
|
172
|
-
if (!fs.existsSync(certFile)) {
|
|
173
|
-
return void 0;
|
|
174
|
-
}
|
|
175
|
-
const pem = readCertificatePEM(certFile);
|
|
176
|
-
return convertPEMtoDER(pem);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Read-increment-write a hex counter file (`serial`/`crlnumber`),
|
|
180
|
-
* returning the value to hand out — matching `openssl ca`'s own
|
|
181
|
-
* behavior: the file holds the *next* value to assign, and is bumped
|
|
182
|
-
* to the following one immediately after being read. A `.old` backup
|
|
183
|
-
* of the pre-bump content is kept, as `openssl` itself does.
|
|
184
|
-
*/
|
|
185
|
-
#nextHexCounter(fileName) {
|
|
186
|
-
const counterFile = path.join(this.#rootDir, fileName);
|
|
187
|
-
const current = BigInt(`0x${fs.readFileSync(counterFile, "utf-8").trim()}`);
|
|
188
|
-
fs.copyFileSync(counterFile, `${counterFile}.old`);
|
|
189
|
-
fs.writeFileSync(counterFile, evenLengthHex(current + 1n));
|
|
190
|
-
return evenLengthHex(current);
|
|
191
|
-
}
|
|
192
|
-
/** Hand out the next certificate serial number, bumping the `serial` file. */
|
|
193
|
-
nextSerial() {
|
|
194
|
-
return this.#nextHexCounter("serial");
|
|
195
|
-
}
|
|
196
|
-
/** Hand out the next CRL number, bumping the `crlnumber` file. */
|
|
197
|
-
nextCrlNumber() {
|
|
198
|
-
return this.#nextHexCounter("crlnumber");
|
|
199
|
-
}
|
|
200
|
-
/** Append a newly-issued certificate's `V` (valid) row to `index.txt`. */
|
|
201
|
-
appendIssued(record) {
|
|
202
|
-
const serial = assertHexSerial(record.serial);
|
|
203
|
-
const subject = escapeIndexField(record.subject);
|
|
204
|
-
const line = `V ${formatOpenSSLDate(record.expiryDate)} ${serial} unknown ${subject}
|
|
205
|
-
`;
|
|
206
|
-
fs.appendFileSync(this.indexFile, line);
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Rewrite a certificate's `index.txt` row from `V` to `R` (revoked).
|
|
210
|
-
* Throws if the serial is not found, or is already revoked — the same
|
|
211
|
-
* "already revoked" case `openssl ca -revoke` itself rejects.
|
|
212
|
-
*/
|
|
213
|
-
markRevoked(serial, revocationDate, reason) {
|
|
214
|
-
const upper = assertHexSerial(serial);
|
|
215
|
-
if (!/^[A-Za-z]+$/.test(reason)) {
|
|
216
|
-
throw new Error(`Invalid CRL reason: ${JSON.stringify(reason)}`);
|
|
217
|
-
}
|
|
218
|
-
const lines = fs.readFileSync(this.indexFile, "utf-8").split("\n").filter((l) => l.trim().length > 0);
|
|
219
|
-
let found = false;
|
|
220
|
-
const rewritten = lines.map((line) => {
|
|
221
|
-
const fields = line.split(" ");
|
|
222
|
-
if (fields.length < 4 || fields[3]?.toUpperCase() !== upper) {
|
|
223
|
-
return line;
|
|
224
|
-
}
|
|
225
|
-
if (fields[0] === "R") {
|
|
226
|
-
throw new Error(`Certificate ${upper} is already revoked`);
|
|
227
|
-
}
|
|
228
|
-
found = true;
|
|
229
|
-
const subject = fields.length >= 6 ? fields[5] : "";
|
|
230
|
-
return `R ${fields[1]} ${formatOpenSSLDate(revocationDate)},${reason} ${upper} unknown ${subject}`;
|
|
231
|
-
});
|
|
232
|
-
if (!found) {
|
|
233
|
-
throw new Error(`Certificate ${upper} not found in the certificate database`);
|
|
234
|
-
}
|
|
235
|
-
fs.writeFileSync(this.indexFile, `${rewritten.join("\n")}
|
|
236
|
-
`);
|
|
237
|
-
}
|
|
238
|
-
/** Store a signed certificate's PEM under `certs/<SERIAL>.pem`, as `openssl ca` does. */
|
|
239
|
-
storeCertificate(serial, pem) {
|
|
240
|
-
const certFile = path.join(this.#rootDir, "certs", `${assertHexSerial(serial)}.pem`);
|
|
241
|
-
fs.writeFileSync(certFile, pem);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
// packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts
|
|
246
|
-
import fs3 from "fs";
|
|
247
|
-
import path3 from "path";
|
|
248
|
-
import {
|
|
249
|
-
CertificatePurpose,
|
|
250
|
-
createCertificateFromCsr,
|
|
251
|
-
createCertificateSigningRequest,
|
|
252
|
-
createCrl,
|
|
253
|
-
isCaSigner,
|
|
254
|
-
privateKeyToCryptoKey,
|
|
255
|
-
readPrivateKey,
|
|
256
|
-
Subject,
|
|
257
|
-
x509
|
|
258
|
-
} from "node-opcua-crypto";
|
|
259
|
-
|
|
260
|
-
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
261
|
-
import assert2 from "assert";
|
|
262
|
-
import fs2 from "fs";
|
|
263
|
-
import path2 from "path";
|
|
264
|
-
import chalk from "chalk";
|
|
265
|
-
|
|
266
|
-
// packages/node-opcua-pki/lib/toolbox/config.ts
|
|
267
|
-
var g_config = {
|
|
268
|
-
opensslVersion: "unset",
|
|
269
|
-
silent: process.env.VERBOSE ? !process.env.VERBOSE : true,
|
|
270
|
-
force: false
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// packages/node-opcua-pki/lib/toolbox/debug.ts
|
|
274
|
-
var doDebug = process.env.NODEOPCUAPKIDEBUG || false;
|
|
275
|
-
var displayError = true;
|
|
276
|
-
var displayDebug = !!process.env.NODEOPCUAPKIDEBUG || false;
|
|
277
|
-
function debugLog(...args) {
|
|
278
|
-
if (displayDebug) {
|
|
279
|
-
console.log.apply(null, args);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function warningLog(...args) {
|
|
283
|
-
console.log.apply(null, args);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
287
|
-
function certificateFileExist(certificateFile) {
|
|
288
|
-
if (fs2.existsSync(certificateFile) && !g_config.force) {
|
|
289
|
-
warningLog(
|
|
290
|
-
chalk.yellow(" certificate ") + chalk.cyan(certificateFile) + chalk.yellow(" already exists => do not overwrite")
|
|
291
|
-
);
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
function mkdirRecursiveSync(folder) {
|
|
297
|
-
if (!fs2.existsSync(folder)) {
|
|
298
|
-
debugLog(chalk.white(" .. constructing "), folder);
|
|
299
|
-
fs2.mkdirSync(folder, { recursive: true });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function restrictPrivateFilePermissions(target, mode) {
|
|
303
|
-
if (process.platform === "win32") {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
fs2.chmodSync(target, mode);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
warningLog(chalk.yellow(" could not restrict permissions on "), target, err.message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function ensurePrivateDirectory(dir) {
|
|
313
|
-
if (!fs2.existsSync(dir)) {
|
|
314
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
315
|
-
}
|
|
316
|
-
restrictPrivateFilePermissions(dir, 448);
|
|
317
|
-
}
|
|
318
|
-
function isEncryptedPrivateKeyFile(filename) {
|
|
319
|
-
return fs2.readFileSync(filename, "utf-8").includes("-----BEGIN ENCRYPTED PRIVATE KEY-----");
|
|
320
|
-
}
|
|
321
|
-
function makePath(folderName, filename) {
|
|
322
|
-
let s;
|
|
323
|
-
if (filename) {
|
|
324
|
-
s = path2.join(path2.normalize(folderName), filename);
|
|
325
|
-
} else {
|
|
326
|
-
assert2(folderName);
|
|
327
|
-
s = folderName;
|
|
328
|
-
}
|
|
329
|
-
s = s.replace(/\\/g, "/");
|
|
330
|
-
return s;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// packages/node-opcua-pki/lib/toolbox/display.ts
|
|
334
|
-
import chalk2 from "chalk";
|
|
335
|
-
function displayTitle(str) {
|
|
336
|
-
if (!g_config.silent) {
|
|
337
|
-
warningLog("");
|
|
338
|
-
warningLog(chalk2.yellowBright(str));
|
|
339
|
-
warningLog(chalk2.yellow(new Array(str.length + 1).join("=")), "\n");
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function displaySubtitle(str) {
|
|
343
|
-
if (!g_config.silent) {
|
|
344
|
-
warningLog("");
|
|
345
|
-
warningLog(` ${chalk2.yellowBright(str)}`);
|
|
346
|
-
warningLog(` ${chalk2.white(new Array(str.length + 1).join("-"))}`, "\n");
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
function display(str) {
|
|
350
|
-
if (!g_config.silent) {
|
|
351
|
-
warningLog(` ${str}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts
|
|
356
|
-
var CA_CERT_VALIDITY_DAYS = 3650;
|
|
357
|
-
var RSA_SHA256 = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
|
|
358
|
-
function signingAlgorithmOf(key) {
|
|
359
|
-
return isCaSigner(key) ? key.algorithm : RSA_SHA256;
|
|
360
|
-
}
|
|
361
|
-
function daysFromNow(notBefore, days) {
|
|
362
|
-
return new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
363
|
-
}
|
|
364
|
-
function caSanUri(ca) {
|
|
365
|
-
return `urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
366
|
-
}
|
|
367
|
-
function sanFromCsr(csrPem) {
|
|
368
|
-
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
369
|
-
const extension = request.getExtension("2.5.29.17");
|
|
370
|
-
const dns = [];
|
|
371
|
-
const ip = [];
|
|
372
|
-
let applicationUri;
|
|
373
|
-
for (const name of extension?.names.toJSON() ?? []) {
|
|
374
|
-
if (name.type === "dns") {
|
|
375
|
-
dns.push(name.value);
|
|
376
|
-
} else if (name.type === "ip") {
|
|
377
|
-
ip.push(name.value);
|
|
378
|
-
} else if (name.type === "url" && applicationUri === void 0) {
|
|
379
|
-
applicationUri = name.value;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return { dns, ip, applicationUri };
|
|
383
|
-
}
|
|
384
|
-
var REASON_TO_X509_CRL_REASON = {
|
|
385
|
-
unspecified: x509.X509CrlReason.unspecified,
|
|
386
|
-
keyCompromise: x509.X509CrlReason.keyCompromise,
|
|
387
|
-
CACompromise: x509.X509CrlReason.cACompromise,
|
|
388
|
-
affiliationChanged: x509.X509CrlReason.affiliationChanged,
|
|
389
|
-
superseded: x509.X509CrlReason.superseded,
|
|
390
|
-
cessationOfOperation: x509.X509CrlReason.cessationOfOperation,
|
|
391
|
-
certificateHold: x509.X509CrlReason.certificateHold,
|
|
392
|
-
removeFromCRL: x509.X509CrlReason.removeFromCRL
|
|
393
|
-
};
|
|
394
|
-
function toOpenSslSubjectString(subjectName) {
|
|
395
|
-
const parts = [];
|
|
396
|
-
for (const rdn of subjectName.toJSON()) {
|
|
397
|
-
for (const [type, values] of Object.entries(rdn)) {
|
|
398
|
-
for (const value of values) {
|
|
399
|
-
parts.push(`${type}=${value}`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return `/${parts.join("/")}`;
|
|
404
|
-
}
|
|
405
|
-
var NativeCaBackend = class {
|
|
406
|
-
/** Nothing to check: this backend spawns no process and needs no tool on PATH. */
|
|
407
|
-
/** Signing goes through `ca._getSigningKey()`, which may be an external signer. */
|
|
408
|
-
supportsExternalSigner = true;
|
|
409
|
-
async preflight() {
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Write a CSR for the CA's own key, the `[v3_ca_req]` equivalent. The
|
|
413
|
-
* key comes from the CA rather than from `privateKeyFile`: on a
|
|
414
|
-
* signer-backed CA that file does not exist.
|
|
415
|
-
*/
|
|
416
|
-
async generateCaCsr(ca, _caRootDir, _privateKeyFile, csrFile) {
|
|
417
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
418
|
-
const signingKey = await ca._getSigningKey();
|
|
419
|
-
const { csr } = await createCertificateSigningRequest({
|
|
420
|
-
privateKey: signingKey,
|
|
421
|
-
subject: ca.subject.toString(),
|
|
422
|
-
applicationUri: caSanUri(ca),
|
|
423
|
-
purpose: CertificatePurpose.ForCertificateAuthority
|
|
424
|
-
});
|
|
425
|
-
await fs3.promises.writeFile(csrFile, csr);
|
|
426
|
-
}
|
|
427
|
-
async bootstrap(ca) {
|
|
428
|
-
const caRootDir = path3.resolve(ca.rootDir);
|
|
429
|
-
const csrFile = path3.join(caRootDir, "private/cakey.csr");
|
|
430
|
-
await this.generateCaCsr(ca, caRootDir, path3.join(caRootDir, "private/cakey.pem"), csrFile);
|
|
431
|
-
const issuerCA = ca._issuerCA;
|
|
432
|
-
if (issuerCA) {
|
|
433
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
434
|
-
const signWithIssuer = () => this.signSubordinateCsr(issuerCA, csrFile, ca.caCertificate, CA_CERT_VALIDITY_DAYS);
|
|
435
|
-
if (path3.resolve(issuerCA.rootDir) === caRootDir) {
|
|
436
|
-
await signWithIssuer();
|
|
437
|
-
} else {
|
|
438
|
-
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
442
|
-
const csrPem = await fs3.promises.readFile(csrFile, "utf-8");
|
|
443
|
-
const signingKey = await ca._getSigningKey();
|
|
444
|
-
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
445
|
-
const notBefore = /* @__PURE__ */ new Date();
|
|
446
|
-
const { cert } = await createCertificateFromCsr({
|
|
447
|
-
csr: csrPem,
|
|
448
|
-
// the CSR's own parsed subject, so issuer and subject are
|
|
449
|
-
// byte-identical on a self-signed root: re-encoding a string
|
|
450
|
-
// form could differ and break chain building
|
|
451
|
-
issuerName: request.subjectName,
|
|
452
|
-
issuerPublicKey: request.publicKey,
|
|
453
|
-
signingKey,
|
|
454
|
-
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
455
|
-
notBefore,
|
|
456
|
-
notAfter: daysFromNow(notBefore, CA_CERT_VALIDITY_DAYS),
|
|
457
|
-
purpose: CertificatePurpose.ForCertificateAuthority,
|
|
458
|
-
applicationUri: caSanUri(ca),
|
|
459
|
-
revocation: {
|
|
460
|
-
crlDistributionUrl: ca.crlDistributionUrl,
|
|
461
|
-
ocspResponderUrl: ca.ocspResponderUrl,
|
|
462
|
-
caIssuersUrl: ca.caIssuersUrl
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
await fs3.promises.writeFile(ca.caCertificate, cert);
|
|
466
|
-
}
|
|
467
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
468
|
-
await this.#regenerateCrlLocked(ca, new CaDatabase(ca.rootDir));
|
|
469
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
470
|
-
}
|
|
471
|
-
/** Sign a subordinate CA's request with this CA's key, the `[v3_ca]` equivalent. */
|
|
472
|
-
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
473
|
-
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
474
|
-
const csrPem = await fs3.promises.readFile(csrFile, "utf-8");
|
|
475
|
-
const db = new CaDatabase(ca.rootDir);
|
|
476
|
-
const serialNumber = db.nextSerial();
|
|
477
|
-
const notBefore = /* @__PURE__ */ new Date();
|
|
478
|
-
const notAfter = daysFromNow(notBefore, validityDays);
|
|
479
|
-
const { cert } = await createCertificateFromCsr({
|
|
480
|
-
csr: csrPem,
|
|
481
|
-
issuerName,
|
|
482
|
-
issuerPublicKey,
|
|
483
|
-
signingKey,
|
|
484
|
-
signingAlgorithm,
|
|
485
|
-
serialNumber,
|
|
486
|
-
notBefore,
|
|
487
|
-
notAfter,
|
|
488
|
-
purpose: CertificatePurpose.ForCertificateAuthority,
|
|
489
|
-
// the subordinate's own SAN, not the issuer's
|
|
490
|
-
...sanFromCsr(csrPem),
|
|
491
|
-
revocation: {
|
|
492
|
-
crlDistributionUrl: ca.crlDistributionUrl,
|
|
493
|
-
ocspResponderUrl: ca.ocspResponderUrl,
|
|
494
|
-
caIssuersUrl: ca.caIssuersUrl
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
await fs3.promises.writeFile(certFile, cert);
|
|
498
|
-
this.#record(db, serialNumber, cert, notAfter);
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Self-sign a certificate with a caller-supplied key file and record it
|
|
502
|
-
* in this CA's database: the native equivalent of `openssl req -new`
|
|
503
|
-
* followed by `openssl ca -selfsign`, which likewise applies the
|
|
504
|
-
* end-entity profile and updates `index.txt`.
|
|
505
|
-
*/
|
|
506
|
-
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
507
|
-
displaySubtitle("- the certificate signing request");
|
|
508
|
-
const key = await privateKeyToCryptoKey(readPrivateKey(privateKeyFile, await ca._privateKeyPassphrase()));
|
|
509
|
-
const { csr } = await createCertificateSigningRequest({
|
|
510
|
-
privateKey: key,
|
|
511
|
-
subject: params.subject ? new Subject(params.subject).toString() : ca.subject.toString(),
|
|
512
|
-
dns: params.dns,
|
|
513
|
-
ip: params.ip,
|
|
514
|
-
applicationUri: params.applicationUri,
|
|
515
|
-
purpose: CertificatePurpose.ForApplication
|
|
516
|
-
});
|
|
517
|
-
displaySubtitle("- creating the self-signed certificate");
|
|
518
|
-
const request = new x509.Pkcs10CertificateRequest(csr);
|
|
519
|
-
const db = new CaDatabase(ca.rootDir);
|
|
520
|
-
const serialNumber = db.nextSerial();
|
|
521
|
-
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
522
|
-
const notAfter = params.endDate ?? daysFromNow(notBefore, params.validity ?? 365);
|
|
523
|
-
const { cert } = await createCertificateFromCsr({
|
|
524
|
-
csr,
|
|
525
|
-
issuerName: request.subjectName,
|
|
526
|
-
issuerPublicKey: request.publicKey,
|
|
527
|
-
signingKey: key,
|
|
528
|
-
signingAlgorithm: RSA_SHA256,
|
|
529
|
-
serialNumber,
|
|
530
|
-
notBefore,
|
|
531
|
-
notAfter,
|
|
532
|
-
purpose: CertificatePurpose.ForApplication,
|
|
533
|
-
...sanFromCsr(csr)
|
|
534
|
-
});
|
|
535
|
-
await fs3.promises.writeFile(certificateFile, cert);
|
|
536
|
-
this.#record(db, serialNumber, cert, notAfter);
|
|
537
|
-
}
|
|
538
|
-
/** `certs/<SERIAL>.pem` plus the `V` row, exactly as `openssl ca` writes them. */
|
|
539
|
-
#record(db, serialNumber, certPem, notAfter) {
|
|
540
|
-
db.storeCertificate(serialNumber, certPem);
|
|
541
|
-
db.appendIssued({
|
|
542
|
-
serial: serialNumber,
|
|
543
|
-
expiryDate: notAfter,
|
|
544
|
-
subject: toOpenSslSubjectString(new x509.X509Certificate(certPem).subjectName)
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* The CA's signing key and issuer identity, resolved fresh on every
|
|
549
|
-
* call and never cached across operations. The identity is read back
|
|
550
|
-
* from the CA certificate on disk so that the issuer field of anything
|
|
551
|
-
* this CA signs is byte-identical to that certificate's subject.
|
|
552
|
-
*/
|
|
553
|
-
async #issuerContext(ca) {
|
|
554
|
-
const signingKey = await ca._getSigningKey();
|
|
555
|
-
const caCertPem = await fs3.promises.readFile(ca.caCertificate, "utf-8");
|
|
556
|
-
const caCert = new x509.X509Certificate(caCertPem);
|
|
557
|
-
return {
|
|
558
|
-
signingKey,
|
|
559
|
-
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
560
|
-
issuerPublicKey: caCert.publicKey,
|
|
561
|
-
issuerName: caCert.subjectName
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
async signEndEntityCsr(ca, certificate, csr, params, sanOverride) {
|
|
565
|
-
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
566
|
-
const db = new CaDatabase(ca.rootDir);
|
|
567
|
-
const serialNumber = db.nextSerial();
|
|
568
|
-
const csrPem = await fs3.promises.readFile(csr, "utf-8");
|
|
569
|
-
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
570
|
-
const notAfter = params.endDate ?? new Date(notBefore.getTime() + (params.validity ?? 365) * 24 * 60 * 60 * 1e3);
|
|
571
|
-
const { cert } = await createCertificateFromCsr({
|
|
572
|
-
csr: csrPem,
|
|
573
|
-
issuerName,
|
|
574
|
-
issuerPublicKey,
|
|
575
|
-
signingKey,
|
|
576
|
-
signingAlgorithm,
|
|
577
|
-
serialNumber,
|
|
578
|
-
notBefore,
|
|
579
|
-
notAfter,
|
|
580
|
-
purpose: CertificatePurpose.ForApplication,
|
|
581
|
-
dns: sanOverride.dns,
|
|
582
|
-
ip: sanOverride.ip,
|
|
583
|
-
applicationUri: sanOverride.applicationUri,
|
|
584
|
-
revocation: {
|
|
585
|
-
crlDistributionUrl: ca.crlDistributionUrl,
|
|
586
|
-
ocspResponderUrl: ca.ocspResponderUrl,
|
|
587
|
-
caIssuersUrl: ca.caIssuersUrl
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
await fs3.promises.writeFile(certificate, cert);
|
|
591
|
-
this.#record(db, serialNumber, cert, notAfter);
|
|
592
|
-
}
|
|
593
|
-
async #regenerateCrlLocked(ca, db) {
|
|
594
|
-
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
595
|
-
const crlNumber = db.nextCrlNumber();
|
|
596
|
-
const entries = db.readIndex().filter((r) => r.status === "revoked").map((r) => ({
|
|
597
|
-
serialNumber: r.serial,
|
|
598
|
-
revocationDate: r.revocationDate ? new Date(r.revocationDate) : /* @__PURE__ */ new Date(),
|
|
599
|
-
reason: r.reason ? REASON_TO_X509_CRL_REASON[r.reason] : void 0
|
|
600
|
-
}));
|
|
601
|
-
const { crl } = await createCrl({
|
|
602
|
-
issuerName,
|
|
603
|
-
issuerPublicKey,
|
|
604
|
-
signingKey,
|
|
605
|
-
signingAlgorithm,
|
|
606
|
-
crlNumber: BigInt(`0x${crlNumber}`),
|
|
607
|
-
entries
|
|
608
|
-
});
|
|
609
|
-
await fs3.promises.writeFile(ca.revocationList, crl);
|
|
610
|
-
const der = new x509.X509Crl(crl).rawData;
|
|
611
|
-
await fs3.promises.writeFile(ca.revocationListDER, Buffer.from(der));
|
|
612
|
-
}
|
|
613
|
-
async regenerateCrl(ca) {
|
|
614
|
-
const db = new CaDatabase(ca.rootDir);
|
|
615
|
-
await this.#regenerateCrlLocked(ca, db);
|
|
616
|
-
}
|
|
617
|
-
async revoke(ca, certificate, reason) {
|
|
618
|
-
const certPem = await fs3.promises.readFile(certificate, "utf-8");
|
|
619
|
-
const cert = new x509.X509Certificate(certPem);
|
|
620
|
-
const db = new CaDatabase(ca.rootDir);
|
|
621
|
-
db.markRevoked(cert.serialNumber, /* @__PURE__ */ new Date(), reason);
|
|
622
|
-
await this.#regenerateCrlLocked(ca, db);
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
// packages/node-opcua-pki/lib/misc/subject.ts
|
|
627
|
-
import { Subject as Subject2 } from "node-opcua-crypto";
|
|
628
|
-
|
|
629
|
-
// packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts
|
|
630
|
-
import assert3 from "assert";
|
|
631
|
-
import fs4 from "fs";
|
|
632
|
-
import os from "os";
|
|
633
|
-
import path4 from "path";
|
|
634
|
-
import { withLock } from "@ster5/global-mutex";
|
|
635
|
-
import chalk3 from "chalk";
|
|
636
|
-
import {
|
|
637
|
-
CertificatePurpose as CertificatePurpose2,
|
|
638
|
-
certificateMatchesPrivateKey,
|
|
639
|
-
convertPEMtoDER as convertPEMtoDER2,
|
|
640
|
-
createCertificateSigningRequest as createCertificateSigningRequest2,
|
|
641
|
-
createPfx,
|
|
642
|
-
exploreCertificate,
|
|
643
|
-
exploreCertificateSigningRequest,
|
|
644
|
-
generatePrivateKeyFile,
|
|
645
|
-
privateKeyToCryptoKey as privateKeyToCryptoKey2,
|
|
646
|
-
readCertificatePEM as readCertificatePEM2,
|
|
647
|
-
readCertificateSigningRequest,
|
|
648
|
-
readPrivateKey as readPrivateKey2,
|
|
649
|
-
Subject as Subject3,
|
|
650
|
-
toPem,
|
|
651
|
-
verifyCertificateSignature,
|
|
652
|
-
writePrivateKeyFile,
|
|
653
|
-
x509 as x5092
|
|
654
|
-
} from "node-opcua-crypto";
|
|
655
|
-
|
|
656
|
-
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
657
|
-
var config = `#.........DO NOT MODIFY BY HAND .........................
|
|
658
|
-
[ ca ]
|
|
659
|
-
default_ca = CA_default
|
|
660
|
-
[ CA_default ]
|
|
661
|
-
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
662
|
-
certs = $dir/certs # where to store certificates
|
|
663
|
-
new_certs_dir = $dir/certs #
|
|
664
|
-
database = $dir/index.txt # the certificate database
|
|
665
|
-
serial = $dir/serial # the serial number counter
|
|
666
|
-
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
667
|
-
private_key = $dir/private/cakey.pem # the CA private key
|
|
668
|
-
x509_extensions = usr_cert #
|
|
669
|
-
default_days = 3650 # default validity : 10 years
|
|
670
|
-
|
|
671
|
-
# default_md = sha1
|
|
672
|
-
|
|
673
|
-
default_md = sha256 # The default digest algorithm
|
|
674
|
-
|
|
675
|
-
preserve = no
|
|
676
|
-
policy = policy_match
|
|
677
|
-
# randfile = $dir/random.rnd
|
|
678
|
-
# default_startdate = YYMMDDHHMMSSZ
|
|
679
|
-
# default_enddate = YYMMDDHHMMSSZ
|
|
680
|
-
crl_dir = $dir/crl
|
|
681
|
-
crl_extensions = crl_ext
|
|
682
|
-
crl = $dir/crl/revocation_list.crl # the Revocation list
|
|
683
|
-
crlnumber = $dir/crlnumber # CRL number file
|
|
684
|
-
default_crl_days = 30
|
|
685
|
-
default_crl_hours = 24
|
|
686
|
-
#msie_hack
|
|
687
|
-
|
|
688
|
-
[ policy_match ]
|
|
689
|
-
countryName = optional
|
|
690
|
-
stateOrProvinceName = optional
|
|
691
|
-
localityName = optional
|
|
692
|
-
organizationName = optional
|
|
693
|
-
organizationalUnitName = optional
|
|
694
|
-
commonName = optional
|
|
695
|
-
emailAddress = optional
|
|
696
|
-
|
|
697
|
-
[ req ]
|
|
698
|
-
default_bits = 4096 # Size of keys
|
|
699
|
-
default_keyfile = key.pem # name of generated keys
|
|
700
|
-
distinguished_name = req_distinguished_name
|
|
701
|
-
attributes = req_attributes
|
|
702
|
-
x509_extensions = v3_ca
|
|
703
|
-
#input_password
|
|
704
|
-
#output_password
|
|
705
|
-
string_mask = nombstr # permitted characters
|
|
706
|
-
req_extensions = v3_req
|
|
707
|
-
|
|
708
|
-
[ req_distinguished_name ]
|
|
709
|
-
|
|
710
|
-
#0 countryName = Country Name (2 letter code)
|
|
711
|
-
# countryName_default = FR
|
|
712
|
-
# countryName_min = 2
|
|
713
|
-
# countryName_max = 2
|
|
714
|
-
# stateOrProvinceName = State or Province Name (full name)
|
|
715
|
-
# stateOrProvinceName_default = Ile de France
|
|
716
|
-
# localityName = Locality Name (city, district)
|
|
717
|
-
# localityName_default = Paris
|
|
718
|
-
organizationName = Organization Name (company)
|
|
719
|
-
organizationName_default = NodeOPCUA
|
|
720
|
-
# organizationalUnitName = Organizational Unit Name (department, division)
|
|
721
|
-
# organizationalUnitName_default = R&D
|
|
722
|
-
commonName = Common Name (hostname, FQDN, IP, or your name)
|
|
723
|
-
commonName_max = 256
|
|
724
|
-
commonName_default = NodeOPCUA
|
|
725
|
-
# emailAddress = Email Address
|
|
726
|
-
# emailAddress_max = 40
|
|
727
|
-
# emailAddress_default = node-opcua (at) node-opcua (dot) com
|
|
728
|
-
|
|
729
|
-
[ req_attributes ]
|
|
730
|
-
#challengePassword = A challenge password
|
|
731
|
-
#challengePassword_min = 4
|
|
732
|
-
#challengePassword_max = 20
|
|
733
|
-
#unstructuredName = An optional company name
|
|
734
|
-
[ usr_cert ]
|
|
735
|
-
basicConstraints = critical, CA:FALSE
|
|
736
|
-
subjectKeyIdentifier = hash
|
|
737
|
-
authorityKeyIdentifier = keyid,issuer:always
|
|
738
|
-
#authorityKeyIdentifier = keyid
|
|
739
|
-
subjectAltName = $ENV::ALTNAME
|
|
740
|
-
# issuerAltName = issuer:copy
|
|
741
|
-
nsComment = ''OpenSSL Generated Certificate''
|
|
742
|
-
#nsCertType = client, email, objsign for ''everything including object signing''
|
|
743
|
-
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
|
|
744
|
-
#nsBaseUrl =
|
|
745
|
-
#nsRenewalUrl =
|
|
746
|
-
#nsCaPolicyUrl =
|
|
747
|
-
#nsSslServerName =
|
|
748
|
-
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
|
|
749
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
750
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
751
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
752
|
-
{{/AIA_VALUE}}
|
|
753
|
-
[ v3_req ]
|
|
754
|
-
basicConstraints = critical, CA:FALSE
|
|
755
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
756
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
757
|
-
subjectAltName = $ENV::ALTNAME
|
|
758
|
-
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
759
|
-
[ v3_ca_req ]
|
|
760
|
-
subjectKeyIdentifier = hash
|
|
761
|
-
basicConstraints = CA:TRUE
|
|
762
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
763
|
-
subjectAltName = $ENV::ALTNAME
|
|
764
|
-
nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
|
|
765
|
-
[ v3_ca ]
|
|
766
|
-
subjectKeyIdentifier = hash
|
|
767
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
768
|
-
basicConstraints = CA:TRUE
|
|
769
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
770
|
-
subjectAltName = $ENV::ALTNAME
|
|
771
|
-
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
772
|
-
#nsCertType = sslCA, emailCA
|
|
773
|
-
#issuerAltName = issuer:copy
|
|
774
|
-
#obj = DER:02:03
|
|
775
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
776
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
777
|
-
{{/AIA_VALUE}}[ v3_selfsigned]
|
|
778
|
-
basicConstraints = critical, CA:FALSE
|
|
779
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
780
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
781
|
-
nsComment = "Self-signed certificate, generated by NodeOPCUA"
|
|
782
|
-
subjectAltName = $ENV::ALTNAME
|
|
783
|
-
|
|
784
|
-
[ crl_ext ]
|
|
785
|
-
#issuerAltName = issuer:copy
|
|
786
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
787
|
-
#authorityInfoAccess = @issuer_info`;
|
|
788
|
-
var ca_config_template_cnf_default = config;
|
|
789
|
-
|
|
790
|
-
// packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts
|
|
791
|
-
var defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
792
|
-
var configurationFileTemplate = ca_config_template_cnf_default;
|
|
793
|
-
function escapeOpensslConfDoubleQuoted(value) {
|
|
794
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
795
|
-
}
|
|
796
|
-
function renderCaConfig(caRootDir) {
|
|
797
|
-
return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
|
|
798
|
-
}
|
|
799
|
-
var config2 = {
|
|
800
|
-
certificateDir: "INVALID",
|
|
801
|
-
forceCA: false,
|
|
802
|
-
pkiDir: "INVALID"
|
|
803
|
-
};
|
|
804
|
-
var CA_LOCK_MAX_WAIT_MS = 5 * 6e4;
|
|
805
|
-
var CA_LOCK_RETRY = { minTimeout: 20, maxTimeout: 250 };
|
|
806
|
-
function octetStringToIpAddress(a) {
|
|
807
|
-
return parseInt(a.substring(0, 2), 16).toString() + "." + parseInt(a.substring(2, 4), 16).toString() + "." + parseInt(a.substring(4, 6), 16).toString() + "." + parseInt(a.substring(6, 8), 16).toString();
|
|
808
|
-
}
|
|
809
|
-
assert3(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
810
|
-
function validateRevocationUrl(url, fieldName) {
|
|
811
|
-
if (url === void 0) {
|
|
812
|
-
return void 0;
|
|
813
|
-
}
|
|
814
|
-
if (url === "") {
|
|
815
|
-
throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
|
|
816
|
-
}
|
|
817
|
-
let parsed;
|
|
818
|
-
try {
|
|
819
|
-
parsed = new URL(url);
|
|
820
|
-
} catch {
|
|
821
|
-
throw new Error(`${fieldName} is not a valid URL: ${url}`);
|
|
822
|
-
}
|
|
823
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
824
|
-
throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url})`);
|
|
825
|
-
}
|
|
826
|
-
if (!parsed.pathname || parsed.pathname === "/") {
|
|
827
|
-
throw new Error(`${fieldName} must include a path component (got ${url})`);
|
|
828
|
-
}
|
|
829
|
-
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
|
|
830
|
-
if (isLoopback) {
|
|
831
|
-
console.warn(
|
|
832
|
-
`[node-opcua-pki] ${fieldName} points at loopback (${url}) \u2014 certificates issued with this URL will be unreachable from any other host.`
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
return url;
|
|
836
|
-
}
|
|
837
|
-
var CertificateAuthorityCore = class {
|
|
838
|
-
/** RSA key size used when generating the CA private key. */
|
|
839
|
-
keySize;
|
|
840
|
-
/** Root filesystem path of the CA directory structure. */
|
|
841
|
-
location;
|
|
842
|
-
/** X.500 subject of the CA certificate. */
|
|
843
|
-
subject;
|
|
844
|
-
/** @internal Parent CA (undefined for root CAs). */
|
|
845
|
-
_issuerCA;
|
|
846
|
-
/** @internal Configured CDP / AIA URLs (US-202). */
|
|
847
|
-
_crlDistributionUrl;
|
|
848
|
-
_ocspResponderUrl;
|
|
849
|
-
_caIssuersUrl;
|
|
850
|
-
#privateKeyPassphrase;
|
|
851
|
-
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
852
|
-
#resolvedPassphrase;
|
|
853
|
-
#passphraseResolved = false;
|
|
854
|
-
/** Signing backend: `openssl` shells out to the CLI, `native` signs in-process. */
|
|
855
|
-
#backend;
|
|
856
|
-
/** External signing key (HSM/KMS), when one was supplied instead of a key file. */
|
|
857
|
-
#signer;
|
|
858
|
-
/** Read access to `index.txt` / `certs/<SERIAL>.pem`. */
|
|
859
|
-
#db;
|
|
860
|
-
constructor(options) {
|
|
861
|
-
assert3(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
862
|
-
assert3(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
863
|
-
this.location = options.location;
|
|
864
|
-
this.keySize = options.keySize || 2048;
|
|
865
|
-
this.subject = new Subject3(options.subject || defaultSubject);
|
|
866
|
-
this._issuerCA = options.issuerCA;
|
|
867
|
-
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
868
|
-
this.#signer = options.signer;
|
|
869
|
-
if (options.signer) {
|
|
870
|
-
const algorithm = options.signer.algorithm;
|
|
871
|
-
if (algorithm.name !== "RSASSA-PKCS1-v1_5" && algorithm.name !== "ECDSA") {
|
|
872
|
-
throw new Error(
|
|
873
|
-
`CertificateAuthority: signer algorithm ${algorithm.name} is not supported - use RSASSA-PKCS1-v1_5 or ECDSA.`
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
if (algorithm.name === "ECDSA" && !algorithm.namedCurve) {
|
|
877
|
-
throw new Error(
|
|
878
|
-
"CertificateAuthority: an ECDSA signer must declare its namedCurve (P-256, P-384 or P-521) - importing the signer's public key needs the curve, and an SPKI import cannot infer it."
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
if (options.signer && !options.backend.supportsExternalSigner) {
|
|
883
|
-
throw new Error(
|
|
884
|
-
"CertificateAuthority: this backend cannot sign with an external signer - it loads its key from a file. Use a backend that supports one, such as NativeCaBackend."
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
this.#backend = options.backend;
|
|
888
|
-
this.#db = new CaDatabase(this.location);
|
|
889
|
-
if (options.crlDistributionUrl !== void 0) {
|
|
890
|
-
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
891
|
-
}
|
|
892
|
-
if (options.ocspResponderUrl !== void 0) {
|
|
893
|
-
this.setOcspResponderUrl(options.ocspResponderUrl);
|
|
894
|
-
}
|
|
895
|
-
if (options.caIssuersUrl !== void 0) {
|
|
896
|
-
this.setCaIssuersUrl(options.caIssuersUrl);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
/**
|
|
900
|
-
* Public URL where the CRL produced by this CA is reachable, or
|
|
901
|
-
* `undefined` if no CDP extension should be emitted on issued certs.
|
|
902
|
-
*/
|
|
903
|
-
get crlDistributionUrl() {
|
|
904
|
-
return this._crlDistributionUrl;
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* Public URL of the OCSP responder, or `undefined` if no AIA OCSP
|
|
908
|
-
* leg should be emitted on issued certs.
|
|
909
|
-
*/
|
|
910
|
-
get ocspResponderUrl() {
|
|
911
|
-
return this._ocspResponderUrl;
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Public URL where the issuer's certificate can be fetched, or
|
|
915
|
-
* `undefined` if no AIA caIssuers leg should be emitted.
|
|
916
|
-
*/
|
|
917
|
-
get caIssuersUrl() {
|
|
918
|
-
return this._caIssuersUrl;
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Configure the URL embedded as `crlDistributionPoints` in every
|
|
922
|
-
* subsequently-issued certificate. Pass `undefined` to disable
|
|
923
|
-
* the extension entirely. Validated synchronously — throws on
|
|
924
|
-
* empty string, non-http(s) protocol, missing path. Warns (does
|
|
925
|
-
* not throw) when the URL points at loopback.
|
|
926
|
-
*
|
|
927
|
-
* @see US-202
|
|
928
|
-
*/
|
|
929
|
-
setCrlDistributionUrl(url) {
|
|
930
|
-
this._crlDistributionUrl = validateRevocationUrl(url, "crlDistributionUrl");
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Configure the OCSP responder URL embedded as the `OCSP` leg of
|
|
934
|
-
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
935
|
-
* certificate. Pass `undefined` to disable.
|
|
936
|
-
*
|
|
937
|
-
* @see US-202
|
|
938
|
-
*/
|
|
939
|
-
setOcspResponderUrl(url) {
|
|
940
|
-
this._ocspResponderUrl = validateRevocationUrl(url, "ocspResponderUrl");
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Configure the caIssuers URL embedded as the `caIssuers` leg of
|
|
944
|
-
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
945
|
-
* certificate. Pass `undefined` to disable.
|
|
946
|
-
*
|
|
947
|
-
* @see US-202
|
|
948
|
-
*/
|
|
949
|
-
setCaIssuersUrl(url) {
|
|
950
|
-
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
951
|
-
}
|
|
952
|
-
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
953
|
-
get rootDir() {
|
|
954
|
-
return this.location;
|
|
955
|
-
}
|
|
956
|
-
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
957
|
-
get configFile() {
|
|
958
|
-
return path4.normalize(path4.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
959
|
-
}
|
|
960
|
-
/** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
|
|
961
|
-
get privateKey() {
|
|
962
|
-
return path4.join(path4.resolve(this.rootDir), "private/cakey.pem");
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* The CA private key, decrypted with the configured `privateKeyPassphrase`
|
|
966
|
-
* if it is encrypted. Fails closed (`PrivateKeyPassphraseRequiredError`)
|
|
967
|
-
* on an encrypted key with no or the wrong passphrase.
|
|
968
|
-
*/
|
|
969
|
-
async getPrivateKey() {
|
|
970
|
-
this.#assertKeyIsOnDisk("getPrivateKey");
|
|
971
|
-
return readPrivateKey2(this.privateKey, await this._privateKeyPassphrase());
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* True when this CA signs with an external {@link CaSigner} (HSM/KMS)
|
|
975
|
-
* rather than with `private/cakey.pem`. There is then no private key
|
|
976
|
-
* file on disk, and never was one.
|
|
977
|
-
*/
|
|
978
|
-
get hasExternalSigner() {
|
|
979
|
-
return this.#signer !== void 0;
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Does `cert` certify the key this CA signs with? With the key on disk
|
|
983
|
-
* that is a private-key match; with a signer there is no private key to
|
|
984
|
-
* match, so compare the certified public key against the signer's own -
|
|
985
|
-
* the same question, asked the only way an HSM allows.
|
|
986
|
-
*/
|
|
987
|
-
async #certificateMatchesOurKey(certPem, certDer) {
|
|
988
|
-
if (this.#signer) {
|
|
989
|
-
const ours = Buffer.from(await this.#signer.getPublicKey());
|
|
990
|
-
const certified = Buffer.from(new x5092.X509Certificate(certPem).publicKey.rawData);
|
|
991
|
-
return ours.equals(certified);
|
|
992
|
-
}
|
|
993
|
-
return certificateMatchesPrivateKey(certDer, await this.getPrivateKey());
|
|
994
|
-
}
|
|
995
|
-
/** Reject the key-file-only operations up front, rather than failing on a missing file later. */
|
|
996
|
-
#assertKeyIsOnDisk(what) {
|
|
997
|
-
if (this.#signer) {
|
|
998
|
-
throw new Error(
|
|
999
|
-
`CertificateAuthority.${what} is not available on a signer-backed CA: the private key lives in the signer (HSM/KMS) and is never written to disk.`
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
/**
|
|
1004
|
-
* @internal The key every signing operation goes through. An injected
|
|
1005
|
-
* {@link CaSigner} is returned as-is; otherwise the on-disk key is read
|
|
1006
|
-
* (and decrypted) and handed over as a plain `CryptoKey`. Both are
|
|
1007
|
-
* accepted by the `node-opcua-crypto` signing primitives, so callers
|
|
1008
|
-
* never branch on which one they got.
|
|
1009
|
-
*/
|
|
1010
|
-
async _getSigningKey() {
|
|
1011
|
-
if (this.#signer) {
|
|
1012
|
-
return this.#signer;
|
|
1013
|
-
}
|
|
1014
|
-
return privateKeyToCryptoKey2(await this.getPrivateKey());
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
|
|
1018
|
-
* (temp file + atomic rename, temp file removed on failure). Only
|
|
1019
|
-
* rewrites the file: construct a new `CertificateAuthority` with the new
|
|
1020
|
-
* passphrase to continue using it.
|
|
1021
|
-
*/
|
|
1022
|
-
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
1023
|
-
this.#assertKeyIsOnDisk("reencryptPrivateKey");
|
|
1024
|
-
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
1025
|
-
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
1026
|
-
const key = readPrivateKey2(this.privateKey, oldPass);
|
|
1027
|
-
await this.#rewritePrivateKeyFile(key, newPass);
|
|
1028
|
-
}
|
|
1029
|
-
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
1030
|
-
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
1031
|
-
try {
|
|
1032
|
-
await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
|
|
1033
|
-
await fs4.promises.rename(tmpFilename, this.privateKey);
|
|
1034
|
-
} finally {
|
|
1035
|
-
await fs4.promises.rm(tmpFilename, { force: true });
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
/** @internal resolve the configured passphrase, at most once per instance */
|
|
1039
|
-
async _privateKeyPassphrase() {
|
|
1040
|
-
if (!this.#passphraseResolved) {
|
|
1041
|
-
this.#resolvedPassphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1042
|
-
this.#passphraseResolved = true;
|
|
1043
|
-
}
|
|
1044
|
-
return this.#resolvedPassphrase;
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* @internal On an existing key: encrypt it in place if a passphrase is
|
|
1048
|
-
* configured and it is still plaintext (secure by default: the option
|
|
1049
|
-
* means "protect this key", not "ignore me"), then read it back so a
|
|
1050
|
-
* wrong or missing passphrase fails initialize() closed rather than the
|
|
1051
|
-
* first signing operation.
|
|
1052
|
-
*/
|
|
1053
|
-
async _ensurePrivateKeyProtection() {
|
|
1054
|
-
if (this.#signer || !fs4.existsSync(this.privateKey)) {
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
1058
|
-
warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
1059
|
-
const plaintextKey = readPrivateKey2(this.privateKey);
|
|
1060
|
-
await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
|
|
1061
|
-
}
|
|
1062
|
-
await this.getPrivateKey();
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Acquire a file-based lock on this CA's directory for the duration of
|
|
1066
|
-
* `action` — serializes every operation that mutates the certificate
|
|
1067
|
-
* database (`index.txt`, `serial`, `crlnumber`, `certs/`,
|
|
1068
|
-
* `crl/revocation_list.*`) or the CA's own key/certificate, so
|
|
1069
|
-
* concurrent calls on one `CertificateAuthority` (or two instances
|
|
1070
|
-
* pointed at the same directory, in this or another process) cannot
|
|
1071
|
-
* interleave. Mirrors `CertificateManager.withLock2`.
|
|
1072
|
-
*
|
|
1073
|
-
* Only ever call this from a top-level public operation — nested calls
|
|
1074
|
-
* on the same instance would deadlock, since the underlying file lock
|
|
1075
|
-
* is not reentrant.
|
|
1076
|
-
*
|
|
1077
|
-
* The wait is bounded: a waiter gives up (throws) after
|
|
1078
|
-
* `CA_LOCK_MAX_WAIT_MS`. Without a bound, a holder whose openssl child
|
|
1079
|
-
* hangs would block every CA operation in every process forever and
|
|
1080
|
-
* silently — the lock library's keepalive keeps refreshing the lock
|
|
1081
|
-
* file's mtime as long as the holder process is alive, so stale-lock
|
|
1082
|
-
* recovery never fires for a live-but-stuck holder. A dead holder's
|
|
1083
|
-
* lock goes stale (default 2 minutes) and is taken over well within
|
|
1084
|
-
* this bound.
|
|
1085
|
-
*/
|
|
1086
|
-
async #withCaLock(action) {
|
|
1087
|
-
const lockFileName = path4.join(this.rootDir, ".ca.lock");
|
|
1088
|
-
return withLock(
|
|
1089
|
-
{
|
|
1090
|
-
fileToLock: lockFileName,
|
|
1091
|
-
retries: { forever: true, maxRetryTime: CA_LOCK_MAX_WAIT_MS, ...CA_LOCK_RETRY }
|
|
1092
|
-
},
|
|
1093
|
-
action
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* @internal Run `action` under this CA's directory lock. For the one
|
|
1098
|
-
* legitimate cross-instance use: a subordinate CA's bootstrap signs its
|
|
1099
|
-
* certificate with `openssl x509 -CAserial`, which read-increment-writes
|
|
1100
|
-
* THIS issuer's `serial` file, and must therefore hold this issuer's
|
|
1101
|
-
* lock (ordering is always subordinate -> issuer, so no cycle).
|
|
1102
|
-
*/
|
|
1103
|
-
async _withCaDirectoryLock(action) {
|
|
1104
|
-
return this.#withCaLock(action);
|
|
1105
|
-
}
|
|
1106
|
-
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
1107
|
-
get caCertificate() {
|
|
1108
|
-
return makePath(this.rootDir, "./public/cacert.pem");
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Path to the issuer certificate chain (`public/issuer_chain.pem`).
|
|
1112
|
-
*
|
|
1113
|
-
* This file is created by {@link installCACertificate} when the
|
|
1114
|
-
* provided cert file contains additional issuer certificates
|
|
1115
|
-
* (e.g. intermediate + root). It is appended to signed certs
|
|
1116
|
-
* by {@link constructCertificateChain} to produce a full chain
|
|
1117
|
-
* per OPC UA Part 6 §6.2.6.
|
|
1118
|
-
*/
|
|
1119
|
-
get issuerCertificateChain() {
|
|
1120
|
-
return makePath(this.rootDir, "./public/issuer_chain.pem");
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Path to the current Certificate Revocation List in DER format.
|
|
1124
|
-
* (`crl/revocation_list.der`)
|
|
1125
|
-
*/
|
|
1126
|
-
get revocationListDER() {
|
|
1127
|
-
return makePath(this.rootDir, "./crl/revocation_list.der");
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Path to the current Certificate Revocation List in PEM format.
|
|
1131
|
-
* (`crl/revocation_list.crl`)
|
|
1132
|
-
*/
|
|
1133
|
-
get revocationList() {
|
|
1134
|
-
return makePath(this.rootDir, "./crl/revocation_list.crl");
|
|
1135
|
-
}
|
|
1136
|
-
/**
|
|
1137
|
-
* Path to the concatenated CA certificate + CRL file.
|
|
1138
|
-
* Used by OpenSSL for CRL-based verification.
|
|
1139
|
-
*/
|
|
1140
|
-
get caCertificateWithCrl() {
|
|
1141
|
-
return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
|
|
1142
|
-
}
|
|
1143
|
-
// ---------------------------------------------------------------
|
|
1144
|
-
// Buffer-based accessors (US-059)
|
|
1145
|
-
// ---------------------------------------------------------------
|
|
1146
|
-
/**
|
|
1147
|
-
* Return the CA certificate as a DER-encoded buffer.
|
|
1148
|
-
*
|
|
1149
|
-
* @throws if the CA certificate file does not exist
|
|
1150
|
-
* (call {@link initialize} first).
|
|
1151
|
-
*/
|
|
1152
|
-
getCACertificateDER() {
|
|
1153
|
-
const pem = readCertificatePEM2(this.caCertificate);
|
|
1154
|
-
return convertPEMtoDER2(pem);
|
|
1155
|
-
}
|
|
1156
|
-
/**
|
|
1157
|
-
* Return the CA certificate as a PEM-encoded string.
|
|
1158
|
-
*
|
|
1159
|
-
* @throws if the CA certificate file does not exist
|
|
1160
|
-
* (call {@link initialize} first).
|
|
1161
|
-
*/
|
|
1162
|
-
getCACertificatePEM() {
|
|
1163
|
-
const raw = readCertificatePEM2(this.caCertificate);
|
|
1164
|
-
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
1165
|
-
const idx = raw.indexOf(beginMarker);
|
|
1166
|
-
if (idx > 0) {
|
|
1167
|
-
return raw.substring(idx);
|
|
1168
|
-
}
|
|
1169
|
-
return raw;
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Return the current Certificate Revocation List as a
|
|
1173
|
-
* DER-encoded buffer.
|
|
1174
|
-
*
|
|
1175
|
-
* Returns an empty buffer if no CRL has been generated yet.
|
|
1176
|
-
*/
|
|
1177
|
-
getCRLDER() {
|
|
1178
|
-
const crlPath = this.revocationListDER;
|
|
1179
|
-
if (!fs4.existsSync(crlPath)) {
|
|
1180
|
-
return Buffer.alloc(0);
|
|
1181
|
-
}
|
|
1182
|
-
return fs4.readFileSync(crlPath);
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Return the current Certificate Revocation List as a
|
|
1186
|
-
* PEM-encoded string.
|
|
1187
|
-
*
|
|
1188
|
-
* Returns an empty string if no CRL has been generated yet.
|
|
1189
|
-
*/
|
|
1190
|
-
getCRLPEM() {
|
|
1191
|
-
const crlPath = this.revocationList;
|
|
1192
|
-
if (!fs4.existsSync(crlPath)) {
|
|
1193
|
-
return "";
|
|
1194
|
-
}
|
|
1195
|
-
const raw = fs4.readFileSync(crlPath, "utf-8");
|
|
1196
|
-
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
1197
|
-
const idx = raw.indexOf(beginMarker);
|
|
1198
|
-
if (idx > 0) {
|
|
1199
|
-
return raw.substring(idx);
|
|
1200
|
-
}
|
|
1201
|
-
return raw;
|
|
1202
|
-
}
|
|
1203
|
-
// ---------------------------------------------------------------
|
|
1204
|
-
// Certificate database API (US-057)
|
|
1205
|
-
// ---------------------------------------------------------------
|
|
1206
|
-
/**
|
|
1207
|
-
* Return a list of all issued certificates recorded in the
|
|
1208
|
-
* OpenSSL `index.txt` database.
|
|
1209
|
-
*
|
|
1210
|
-
* Each entry includes the serial number, subject, status,
|
|
1211
|
-
* expiry date, and (for revoked certs) the revocation date.
|
|
1212
|
-
*/
|
|
1213
|
-
getIssuedCertificates() {
|
|
1214
|
-
return this.#db.readIndex();
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Return the total number of certificates recorded in
|
|
1218
|
-
* `index.txt`.
|
|
1219
|
-
*/
|
|
1220
|
-
getIssuedCertificateCount() {
|
|
1221
|
-
return this.#db.readIndex().length;
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Return the status of a certificate by its serial number.
|
|
1225
|
-
*
|
|
1226
|
-
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
1227
|
-
* @returns `"valid"`, `"revoked"`, `"expired"`, or
|
|
1228
|
-
* `undefined` if not found
|
|
1229
|
-
*/
|
|
1230
|
-
getCertificateStatus(serial) {
|
|
1231
|
-
return this.#db.findBySerial(serial)?.status;
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Read a specific issued certificate by serial number and
|
|
1235
|
-
* return its content as a DER-encoded buffer.
|
|
1236
|
-
*
|
|
1237
|
-
* OpenSSL stores signed certificates in the `certs/`
|
|
1238
|
-
* directory using the naming convention `<SERIAL>.pem`.
|
|
1239
|
-
*
|
|
1240
|
-
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
1241
|
-
* @returns the DER buffer, or `undefined` if not found
|
|
1242
|
-
*/
|
|
1243
|
-
getCertificateBySerial(serial) {
|
|
1244
|
-
return this.#db.getCertificateBySerial(serial);
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Path to the OpenSSL certificate database file.
|
|
1248
|
-
*/
|
|
1249
|
-
get indexFile() {
|
|
1250
|
-
return this.#db.indexFile;
|
|
1251
|
-
}
|
|
1252
|
-
// ---------------------------------------------------------------
|
|
1253
|
-
// Buffer-based CA operations (US-058)
|
|
1254
|
-
// ---------------------------------------------------------------
|
|
1255
|
-
/**
|
|
1256
|
-
* Sign a DER-encoded Certificate Signing Request and return
|
|
1257
|
-
* the signed certificate as a DER buffer.
|
|
1258
|
-
*
|
|
1259
|
-
* This method handles temp-file creation and cleanup
|
|
1260
|
-
* internally so that callers can work with in-memory
|
|
1261
|
-
* buffers only.
|
|
1262
|
-
*
|
|
1263
|
-
* The CA can override fields from the CSR by passing
|
|
1264
|
-
* `options.dns`, `options.ip`, `options.applicationUri`,
|
|
1265
|
-
* `options.startDate`, or `options.subject`.
|
|
1266
|
-
*
|
|
1267
|
-
* @param csrDer - the CSR as a DER-encoded buffer
|
|
1268
|
-
* @param options - signing options and CA overrides
|
|
1269
|
-
* @returns the signed certificate as a DER-encoded buffer
|
|
1270
|
-
*/
|
|
1271
|
-
async signCertificateRequestFromDER(csrDer, options) {
|
|
1272
|
-
const tmpDir = await fs4.promises.mkdtemp(path4.join(os.tmpdir(), "pki-sign-"));
|
|
1273
|
-
try {
|
|
1274
|
-
const csrFile = path4.join(tmpDir, "request.csr");
|
|
1275
|
-
const certFile = path4.join(tmpDir, "certificate.pem");
|
|
1276
|
-
const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
|
|
1277
|
-
await fs4.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
1278
|
-
const signingParams = {};
|
|
1279
|
-
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1280
|
-
else signingParams.validity = options?.validity ?? 365;
|
|
1281
|
-
if (options?.startDate) signingParams.startDate = options.startDate;
|
|
1282
|
-
if (options?.dns) signingParams.dns = options.dns;
|
|
1283
|
-
if (options?.ip) signingParams.ip = options.ip;
|
|
1284
|
-
if (options?.applicationUri) signingParams.applicationUri = options.applicationUri;
|
|
1285
|
-
if (options?.subject) signingParams.subject = options.subject;
|
|
1286
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1287
|
-
const certPem = readCertificatePEM2(certFile);
|
|
1288
|
-
return convertPEMtoDER2(certPem);
|
|
1289
|
-
} finally {
|
|
1290
|
-
await fs4.promises.rm(tmpDir, {
|
|
1291
|
-
recursive: true,
|
|
1292
|
-
force: true
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Advertise the validity limits this CA can honor.
|
|
1298
|
-
*
|
|
1299
|
-
* Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
|
|
1300
|
-
* clamp a requested validity against these bounds before calling
|
|
1301
|
-
* {@link signCertificateRequestFromDER}, so a misconfigured
|
|
1302
|
-
* `defaultCertValidity` cannot ask the CA for something it cannot
|
|
1303
|
-
* produce.
|
|
1304
|
-
*
|
|
1305
|
-
* Defaults match the OpenSSL-backed implementation:
|
|
1306
|
-
* - `minValidityMs = 60_000` (1 minute) — practical floor; the
|
|
1307
|
-
* X.509 spec floor is 1 second but very short certs are rarely
|
|
1308
|
-
* useful and pathological for any real deployment.
|
|
1309
|
-
* - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
|
|
1310
|
-
* enough for root CAs.
|
|
1311
|
-
* - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
|
|
1312
|
-
* floor on `notBefore` / `notAfter`.
|
|
1313
|
-
* - `nativeUnit = "second"` — what `x509Date()` actually encodes.
|
|
1314
|
-
*
|
|
1315
|
-
* @see US-208 — the consumer-side capability story.
|
|
1316
|
-
*/
|
|
1317
|
-
getCapabilities() {
|
|
1318
|
-
return {
|
|
1319
|
-
minValidityMs: 6e4,
|
|
1320
|
-
maxValidityMs: 10 * 365 * 864e5,
|
|
1321
|
-
validityGranularityMs: 1e3,
|
|
1322
|
-
nativeUnit: "second"
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Generate a new RSA key pair, create an internal CSR, sign it
|
|
1327
|
-
* with this CA, and return both the certificate and private key
|
|
1328
|
-
* as DER-encoded buffers.
|
|
1329
|
-
*
|
|
1330
|
-
* The private key is **never stored** by the CA — it exists only
|
|
1331
|
-
* in a temporary directory that is cleaned up after the operation.
|
|
1332
|
-
*
|
|
1333
|
-
* This is used by `StartNewKeyPairRequest` (OPC UA Part 12) for
|
|
1334
|
-
* constrained devices that cannot generate their own keys.
|
|
1335
|
-
*
|
|
1336
|
-
* @param options - key generation and certificate parameters
|
|
1337
|
-
* @returns `{ certificateDer, privateKey }` — certificate as DER,
|
|
1338
|
-
* private key as a branded `PrivateKey` buffer
|
|
1339
|
-
*/
|
|
1340
|
-
/**
|
|
1341
|
-
* An ephemeral key and a CSR for it, written into `tmpDir`. Both
|
|
1342
|
-
* `generateKeyPairAndSign*` methods need exactly this, and neither
|
|
1343
|
-
* needs a subprocess for it: the key comes from node's crypto and the
|
|
1344
|
-
* request is built and self-signed in process, so no `openssl.cnf` has
|
|
1345
|
-
* to be rendered either.
|
|
1346
|
-
*/
|
|
1347
|
-
async #createEphemeralKeyAndCsr(tmpDir, keySize, options) {
|
|
1348
|
-
const privateKeyFile = path4.join(tmpDir, "private_key.pem");
|
|
1349
|
-
await generatePrivateKeyFile(privateKeyFile, keySize);
|
|
1350
|
-
const { csr } = await createCertificateSigningRequest2({
|
|
1351
|
-
privateKey: await privateKeyToCryptoKey2(readPrivateKey2(privateKeyFile)),
|
|
1352
|
-
subject: options.subject ? new Subject3(options.subject).toString() : void 0,
|
|
1353
|
-
applicationUri: options.applicationUri,
|
|
1354
|
-
dns: options.dns ?? [],
|
|
1355
|
-
ip: options.ip ?? [],
|
|
1356
|
-
purpose: CertificatePurpose2.ForApplication
|
|
1357
|
-
});
|
|
1358
|
-
const csrFile = path4.join(tmpDir, "request.csr");
|
|
1359
|
-
await fs4.promises.writeFile(csrFile, csr);
|
|
1360
|
-
return { privateKeyFile, csrFile };
|
|
1361
|
-
}
|
|
1362
|
-
async generateKeyPairAndSignDER(options) {
|
|
1363
|
-
const keySize = options.keySize ?? 2048;
|
|
1364
|
-
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1365
|
-
const tmpDir = await fs4.promises.mkdtemp(path4.join(os.tmpdir(), "pki-keygen-"));
|
|
1366
|
-
try {
|
|
1367
|
-
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
1368
|
-
const certFile = path4.join(tmpDir, "certificate.pem");
|
|
1369
|
-
const signingParams = {
|
|
1370
|
-
applicationUri: options.applicationUri,
|
|
1371
|
-
dns: options.dns,
|
|
1372
|
-
ip: options.ip,
|
|
1373
|
-
startDate
|
|
1374
|
-
};
|
|
1375
|
-
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1376
|
-
else signingParams.validity = options.validity ?? 365;
|
|
1377
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1378
|
-
const certPem = readCertificatePEM2(certFile);
|
|
1379
|
-
const certificateDer = convertPEMtoDER2(certPem);
|
|
1380
|
-
const privateKey = readPrivateKey2(privateKeyFile);
|
|
1381
|
-
return { certificateDer, privateKey };
|
|
1382
|
-
} finally {
|
|
1383
|
-
await fs4.promises.rm(tmpDir, {
|
|
1384
|
-
recursive: true,
|
|
1385
|
-
force: true
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
/**
|
|
1390
|
-
* Generate a new RSA key pair, create an internal CSR, sign it
|
|
1391
|
-
* with this CA, and return the result as a PKCS#12 (PFX)
|
|
1392
|
-
* buffer bundling the certificate, private key, and CA chain.
|
|
1393
|
-
*
|
|
1394
|
-
* The private key is **never stored** by the CA — it exists only
|
|
1395
|
-
* in a temporary directory that is cleaned up after the operation.
|
|
1396
|
-
*
|
|
1397
|
-
* @param options - key generation, certificate, and PFX options
|
|
1398
|
-
* @returns the PFX as a `Buffer`
|
|
1399
|
-
*/
|
|
1400
|
-
async generateKeyPairAndSignPFX(options) {
|
|
1401
|
-
const keySize = options.keySize ?? 2048;
|
|
1402
|
-
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1403
|
-
const passphrase = options.passphrase ?? "";
|
|
1404
|
-
const tmpDir = await fs4.promises.mkdtemp(path4.join(os.tmpdir(), "pki-keygen-pfx-"));
|
|
1405
|
-
try {
|
|
1406
|
-
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
1407
|
-
const certFile = path4.join(tmpDir, "certificate.pem");
|
|
1408
|
-
const signingParams = {
|
|
1409
|
-
applicationUri: options.applicationUri,
|
|
1410
|
-
dns: options.dns,
|
|
1411
|
-
ip: options.ip,
|
|
1412
|
-
startDate
|
|
1413
|
-
};
|
|
1414
|
-
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1415
|
-
else signingParams.validity = options.validity ?? 365;
|
|
1416
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1417
|
-
const blocks = (await fs4.promises.readFile(certFile, "utf-8")).match(
|
|
1418
|
-
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g
|
|
1419
|
-
);
|
|
1420
|
-
if (!blocks || blocks.length === 0) {
|
|
1421
|
-
throw new Error(`generateKeyPairAndSignPFX: no certificate was produced in ${certFile}`);
|
|
1422
|
-
}
|
|
1423
|
-
return await createPfx({
|
|
1424
|
-
certificate: convertPEMtoDER2(blocks[0]),
|
|
1425
|
-
certificateChain: blocks.slice(1).map(convertPEMtoDER2),
|
|
1426
|
-
privateKey: readPrivateKey2(privateKeyFile),
|
|
1427
|
-
password: passphrase
|
|
1428
|
-
});
|
|
1429
|
-
} finally {
|
|
1430
|
-
await fs4.promises.rm(tmpDir, {
|
|
1431
|
-
recursive: true,
|
|
1432
|
-
force: true
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Revoke a DER-encoded certificate and regenerate the CRL.
|
|
1438
|
-
*
|
|
1439
|
-
* Extracts the serial number from the certificate, then
|
|
1440
|
-
* uses the stored cert file at `certs/<serial>.pem` for
|
|
1441
|
-
* revocation — avoiding temp-file PEM format mismatches.
|
|
1442
|
-
*
|
|
1443
|
-
* @param certDer - the certificate as a DER-encoded buffer
|
|
1444
|
-
* @param reason - CRL reason code
|
|
1445
|
-
* (default: `"keyCompromise"`)
|
|
1446
|
-
* @throws if the certificate's serial is not found in the CA
|
|
1447
|
-
*/
|
|
1448
|
-
async revokeCertificateDER(certDer, reason) {
|
|
1449
|
-
const info = exploreCertificate(certDer);
|
|
1450
|
-
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
1451
|
-
const storedCertFile = path4.join(this.rootDir, "certs", `${serial}.pem`);
|
|
1452
|
-
if (!fs4.existsSync(storedCertFile)) {
|
|
1453
|
-
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
1454
|
-
}
|
|
1455
|
-
await this.revokeCertificate(storedCertFile, {
|
|
1456
|
-
reason: reason ?? "keyCompromise"
|
|
1457
|
-
});
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* Initialize the CA directory structure, generate the CA
|
|
1461
|
-
* private key and self-signed certificate if they do not
|
|
1462
|
-
* already exist.
|
|
1463
|
-
*/
|
|
1464
|
-
async initialize() {
|
|
1465
|
-
mkdirRecursiveSync(path4.resolve(this.rootDir));
|
|
1466
|
-
await this.#withCaLock(() => this.#bootstrap());
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* @internal Shared (backend-agnostic) part of `initialize()`: directory
|
|
1470
|
-
* layout, default database files, the "already initialized" / "partial
|
|
1471
|
-
* init" checks, and the openssl config file — then delegates CSR
|
|
1472
|
-
* generation, CA-certificate signing, and the initial CRL to
|
|
1473
|
-
* {@link CaBackend.bootstrap}. Must be called under {@link #withCaLock}.
|
|
1474
|
-
*/
|
|
1475
|
-
async #bootstrap() {
|
|
1476
|
-
const caRootDir = path4.resolve(this.rootDir);
|
|
1477
|
-
mkdirRecursiveSync(caRootDir);
|
|
1478
|
-
ensurePrivateDirectory(path4.join(caRootDir, "private"));
|
|
1479
|
-
mkdirRecursiveSync(path4.join(caRootDir, "public"));
|
|
1480
|
-
mkdirRecursiveSync(path4.join(caRootDir, "certs"));
|
|
1481
|
-
mkdirRecursiveSync(path4.join(caRootDir, "crl"));
|
|
1482
|
-
mkdirRecursiveSync(path4.join(caRootDir, "conf"));
|
|
1483
|
-
const serial = path4.join(caRootDir, "serial");
|
|
1484
|
-
if (!fs4.existsSync(serial)) {
|
|
1485
|
-
await fs4.promises.writeFile(serial, "1000");
|
|
1486
|
-
}
|
|
1487
|
-
const crlNumber = path4.join(caRootDir, "crlnumber");
|
|
1488
|
-
if (!fs4.existsSync(crlNumber)) {
|
|
1489
|
-
await fs4.promises.writeFile(crlNumber, "1000");
|
|
1490
|
-
}
|
|
1491
|
-
const indexFile = path4.join(caRootDir, "index.txt");
|
|
1492
|
-
if (!fs4.existsSync(indexFile)) {
|
|
1493
|
-
await fs4.promises.writeFile(indexFile, "");
|
|
1494
|
-
}
|
|
1495
|
-
const signerBacked = this.hasExternalSigner;
|
|
1496
|
-
const caKeyExists = signerBacked || fs4.existsSync(path4.join(caRootDir, "private/cakey.pem"));
|
|
1497
|
-
const caCertExists = fs4.existsSync(path4.join(caRootDir, "public/cacert.pem"));
|
|
1498
|
-
if (caKeyExists && caCertExists && !config2.forceCA) {
|
|
1499
|
-
if (!signerBacked) {
|
|
1500
|
-
restrictPrivateFilePermissions(path4.join(caRootDir, "private/cakey.pem"), 384);
|
|
1501
|
-
await this._ensurePrivateKeyProtection();
|
|
1502
|
-
}
|
|
1503
|
-
debugLog("CA private key and certificate already exist ... skipping");
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
if (!signerBacked && caKeyExists && !caCertExists) {
|
|
1507
|
-
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
1508
|
-
fs4.unlinkSync(path4.join(caRootDir, "private/cakey.pem"));
|
|
1509
|
-
const staleCsr = path4.join(caRootDir, "private/cakey.csr");
|
|
1510
|
-
if (fs4.existsSync(staleCsr)) {
|
|
1511
|
-
fs4.unlinkSync(staleCsr);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
displayTitle("Create Certificate Authority (CA)");
|
|
1515
|
-
const indexFileAttr = path4.join(caRootDir, "index.txt.attr");
|
|
1516
|
-
if (!fs4.existsSync(indexFileAttr)) {
|
|
1517
|
-
await fs4.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
1518
|
-
}
|
|
1519
|
-
const caConfigFile = this.configFile;
|
|
1520
|
-
await fs4.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
1521
|
-
if (!signerBacked) {
|
|
1522
|
-
const privateKeyFilename = path4.join(caRootDir, "private/cakey.pem");
|
|
1523
|
-
displayTitle(`Generate the CA private Key - ${this.keySize}`);
|
|
1524
|
-
await generatePrivateKeyFile(privateKeyFilename, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
1525
|
-
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
1526
|
-
}
|
|
1527
|
-
await this.#backend.bootstrap(this);
|
|
1528
|
-
}
|
|
1529
|
-
/**
|
|
1530
|
-
* Initialize the CA directory structure and generate the
|
|
1531
|
-
* private key + CSR **without signing**.
|
|
1532
|
-
*
|
|
1533
|
-
* Use this when the CA certificate will be signed by an
|
|
1534
|
-
* external (third-party) root CA. After receiving the signed
|
|
1535
|
-
* certificate, call {@link installCACertificate} to complete
|
|
1536
|
-
* the setup.
|
|
1537
|
-
*
|
|
1538
|
-
* **Idempotent / restart-safe:**
|
|
1539
|
-
* - If the CA certificate exists and is valid → `{ status: "ready" }`
|
|
1540
|
-
* - If the CA certificate has expired → `{ status: "expired", csrPath, expiryDate }`
|
|
1541
|
-
* (a new CSR is generated, preserving the existing private key)
|
|
1542
|
-
* - If key + CSR exist but no cert (restart before install) →
|
|
1543
|
-
* `{ status: "pending", csrPath }` without regenerating
|
|
1544
|
-
* - Otherwise → generates key + CSR → `{ status: "created", csrPath }`
|
|
1545
|
-
*
|
|
1546
|
-
* @returns an {@link InitializeCSRResult} describing the CA state
|
|
1547
|
-
*/
|
|
1548
|
-
async initializeCSR() {
|
|
1549
|
-
const caRootDir = path4.resolve(this.rootDir);
|
|
1550
|
-
mkdirRecursiveSync(caRootDir);
|
|
1551
|
-
return this.#withCaLock(() => this.#initializeCSRLocked(caRootDir));
|
|
1552
|
-
}
|
|
1553
|
-
async #initializeCSRLocked(caRootDir) {
|
|
1554
|
-
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
1555
|
-
mkdirRecursiveSync(path4.join(caRootDir, dir));
|
|
1556
|
-
}
|
|
1557
|
-
ensurePrivateDirectory(path4.join(caRootDir, "private"));
|
|
1558
|
-
const caCertFile = this.caCertificate;
|
|
1559
|
-
const privateKeyFile = path4.join(caRootDir, "private/cakey.pem");
|
|
1560
|
-
const csrFile = path4.join(caRootDir, "private/cakey.csr");
|
|
1561
|
-
const keyAvailable = this.hasExternalSigner || fs4.existsSync(privateKeyFile);
|
|
1562
|
-
if (!this.hasExternalSigner && fs4.existsSync(privateKeyFile)) {
|
|
1563
|
-
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1564
|
-
await this._ensurePrivateKeyProtection();
|
|
1565
|
-
}
|
|
1566
|
-
if (fs4.existsSync(caCertFile)) {
|
|
1567
|
-
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
1568
|
-
const certInfo = exploreCertificate(certDer);
|
|
1569
|
-
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
1570
|
-
if (notAfter.getTime() < Date.now()) {
|
|
1571
|
-
debugLog("CA certificate has expired \u2014 generating renewal CSR");
|
|
1572
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1573
|
-
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
1574
|
-
}
|
|
1575
|
-
debugLog("CA certificate already exists and is valid \u2014 ready");
|
|
1576
|
-
return { status: "ready" };
|
|
1577
|
-
}
|
|
1578
|
-
if (keyAvailable && fs4.existsSync(csrFile)) {
|
|
1579
|
-
debugLog("CA key + CSR already exist \u2014 pending external signing");
|
|
1580
|
-
return { status: "pending", csrPath: csrFile };
|
|
1581
|
-
}
|
|
1582
|
-
const serial = path4.join(caRootDir, "serial");
|
|
1583
|
-
if (!fs4.existsSync(serial)) {
|
|
1584
|
-
await fs4.promises.writeFile(serial, "1000");
|
|
1585
|
-
}
|
|
1586
|
-
const crlNumber = path4.join(caRootDir, "crlnumber");
|
|
1587
|
-
if (!fs4.existsSync(crlNumber)) {
|
|
1588
|
-
await fs4.promises.writeFile(crlNumber, "1000");
|
|
1589
|
-
}
|
|
1590
|
-
const indexFile = path4.join(caRootDir, "index.txt");
|
|
1591
|
-
if (!fs4.existsSync(indexFile)) {
|
|
1592
|
-
await fs4.promises.writeFile(indexFile, "");
|
|
1593
|
-
}
|
|
1594
|
-
const indexFileAttr = path4.join(caRootDir, "index.txt.attr");
|
|
1595
|
-
if (!fs4.existsSync(indexFileAttr)) {
|
|
1596
|
-
await fs4.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
1597
|
-
}
|
|
1598
|
-
const caConfigFile = this.configFile;
|
|
1599
|
-
await fs4.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
1600
|
-
if (!keyAvailable) {
|
|
1601
|
-
await generatePrivateKeyFile(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
1602
|
-
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1603
|
-
}
|
|
1604
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1605
|
-
return { status: "created", csrPath: csrFile };
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Check whether the CA certificate needs renewal and, if so,
|
|
1609
|
-
* generate a new CSR for re-signing by the external root CA.
|
|
1610
|
-
*
|
|
1611
|
-
* Use this while the CA is running to detect upcoming expiry
|
|
1612
|
-
* **before** it actually expires. The existing private key is
|
|
1613
|
-
* preserved so previously issued certs remain valid.
|
|
1614
|
-
*
|
|
1615
|
-
* @param thresholdDays - number of days before expiry at which
|
|
1616
|
-
* to trigger renewal (default: 30)
|
|
1617
|
-
* @returns an {@link InitializeCSRResult} — `"expired"` if
|
|
1618
|
-
* renewal is needed, `"ready"` if the cert is still valid
|
|
1619
|
-
*/
|
|
1620
|
-
async renewCSR(thresholdDays = 30) {
|
|
1621
|
-
const caRootDir = path4.resolve(this.rootDir);
|
|
1622
|
-
const caCertFile = this.caCertificate;
|
|
1623
|
-
const privateKeyFile = path4.join(caRootDir, "private/cakey.pem");
|
|
1624
|
-
const csrFile = path4.join(caRootDir, "private/cakey.csr");
|
|
1625
|
-
if (!fs4.existsSync(caCertFile)) {
|
|
1626
|
-
return this.initializeCSR();
|
|
1627
|
-
}
|
|
1628
|
-
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
1629
|
-
const certInfo = exploreCertificate(certDer);
|
|
1630
|
-
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
1631
|
-
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
|
|
1632
|
-
if (notAfter.getTime() - Date.now() < thresholdMs) {
|
|
1633
|
-
debugLog(`CA certificate expires within ${thresholdDays} days \u2014 generating renewal CSR`);
|
|
1634
|
-
await this.#withCaLock(() => this._generateCSR(caRootDir, privateKeyFile, csrFile));
|
|
1635
|
-
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
1636
|
-
}
|
|
1637
|
-
return { status: "ready" };
|
|
1638
|
-
}
|
|
1639
|
-
/**
|
|
1640
|
-
* Generate a CSR using the existing private key.
|
|
1641
|
-
* Must be called under {@link #withCaLock} — the lock is taken by the
|
|
1642
|
-
* public callers (`initializeCSR`, `renewCSR`), not here, so that
|
|
1643
|
-
* `initializeCSR` can hold one lock across key generation AND CSR
|
|
1644
|
-
* generation without the non-reentrant file lock deadlocking.
|
|
1645
|
-
* @internal
|
|
1646
|
-
*/
|
|
1647
|
-
async _generateCSR(caRootDir, privateKeyFile, csrFile) {
|
|
1648
|
-
await this.#backend.generateCaCsr(this, caRootDir, privateKeyFile, csrFile);
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Install an externally-signed CA certificate and generate
|
|
1652
|
-
* the initial CRL.
|
|
1653
|
-
*
|
|
1654
|
-
* Call this after {@link initializeCSR} once the external
|
|
1655
|
-
* root CA has signed the CSR.
|
|
1656
|
-
*
|
|
1657
|
-
* **Safety checks:**
|
|
1658
|
-
* - Verifies that the certificate's public key matches the
|
|
1659
|
-
* CA private key before installing.
|
|
1660
|
-
*
|
|
1661
|
-
* @param signedCertFile - path to the PEM-encoded signed
|
|
1662
|
-
* CA certificate (issued by the external root CA)
|
|
1663
|
-
* @returns an {@link InstallCACertificateResult} with
|
|
1664
|
-
* `status: "success"` or `status: "error"` and a `reason`
|
|
1665
|
-
*/
|
|
1666
|
-
async installCACertificate(signedCertFile) {
|
|
1667
|
-
return this.#withCaLock(async () => {
|
|
1668
|
-
const caCertFile = this.caCertificate;
|
|
1669
|
-
const fullPem = await fs4.promises.readFile(signedCertFile, "utf8");
|
|
1670
|
-
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
1671
|
-
if (!pemBlocks || pemBlocks.length === 0) {
|
|
1672
|
-
return {
|
|
1673
|
-
status: "error",
|
|
1674
|
-
reason: "no_certificate_found",
|
|
1675
|
-
message: "The provided file does not contain any PEM-encoded certificate."
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
const certDer = convertPEMtoDER2(pemBlocks[0]);
|
|
1679
|
-
if (!await this.#certificateMatchesOurKey(pemBlocks[0], certDer)) {
|
|
1680
|
-
return {
|
|
1681
|
-
status: "error",
|
|
1682
|
-
reason: "certificate_key_mismatch",
|
|
1683
|
-
message: "The provided certificate does not match the CA private key. Ensure the certificate was signed from the CSR generated by initializeCSR()."
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
await fs4.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
1687
|
-
`);
|
|
1688
|
-
const issuerChainFile = this.issuerCertificateChain;
|
|
1689
|
-
if (pemBlocks.length > 1) {
|
|
1690
|
-
const issuerPem = `${pemBlocks.slice(1).join("\n")}
|
|
1691
|
-
`;
|
|
1692
|
-
await fs4.promises.writeFile(issuerChainFile, issuerPem);
|
|
1693
|
-
debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
|
|
1694
|
-
} else {
|
|
1695
|
-
if (fs4.existsSync(issuerChainFile)) {
|
|
1696
|
-
await fs4.promises.unlink(issuerChainFile);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
await this.#backend.regenerateCrl(this);
|
|
1700
|
-
return { status: "success" };
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Sign a CSR with CA extensions (`v3_ca`), producing a
|
|
1705
|
-
* subordinate CA certificate.
|
|
1706
|
-
*
|
|
1707
|
-
* Unlike {@link signCertificateRequest} which signs with
|
|
1708
|
-
* end-entity extensions (SANs, etc.), this method signs
|
|
1709
|
-
* with `basicConstraints = CA:TRUE` and `keyUsage =
|
|
1710
|
-
* keyCertSign, cRLSign`.
|
|
1711
|
-
*
|
|
1712
|
-
* @param certFile - output path for the signed CA cert (PEM)
|
|
1713
|
-
* @param csrFile - path to the subordinate CA's CSR
|
|
1714
|
-
* @param params - signing parameters
|
|
1715
|
-
*/
|
|
1716
|
-
async signCACertificateRequest(certFile, csrFile, params) {
|
|
1717
|
-
await this.#withCaLock(async () => {
|
|
1718
|
-
const validity = params.validity ?? 3650;
|
|
1719
|
-
await this.#backend.signSubordinateCsr(this, csrFile, certFile, validity);
|
|
1720
|
-
await this.constructCertificateChain(certFile);
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
/**
|
|
1724
|
-
* Rebuild the combined CA certificate + CRL file.
|
|
1725
|
-
*
|
|
1726
|
-
* This concatenates the CA certificate with the current
|
|
1727
|
-
* revocation list so that OpenSSL can verify certificates
|
|
1728
|
-
* with CRL checking enabled.
|
|
1729
|
-
*/
|
|
1730
|
-
async constructCACertificateWithCRL() {
|
|
1731
|
-
const cacertWithCRL = this.caCertificateWithCrl;
|
|
1732
|
-
if (fs4.existsSync(this.revocationList)) {
|
|
1733
|
-
await fs4.promises.writeFile(
|
|
1734
|
-
cacertWithCRL,
|
|
1735
|
-
fs4.readFileSync(this.caCertificate, "utf8") + fs4.readFileSync(this.revocationList, "utf8")
|
|
1736
|
-
);
|
|
1737
|
-
} else {
|
|
1738
|
-
await fs4.promises.writeFile(cacertWithCRL, fs4.readFileSync(this.caCertificate));
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
/**
|
|
1742
|
-
* Append the CA certificate to a signed certificate file,
|
|
1743
|
-
* creating a PEM certificate chain.
|
|
1744
|
-
*
|
|
1745
|
-
* @param certificate - path to the certificate file to extend
|
|
1746
|
-
*/
|
|
1747
|
-
async constructCertificateChain(certificate) {
|
|
1748
|
-
assert3(fs4.existsSync(certificate));
|
|
1749
|
-
assert3(fs4.existsSync(this.caCertificate));
|
|
1750
|
-
debugLog(chalk3.yellow(" certificate file :"), chalk3.cyan(certificate));
|
|
1751
|
-
let chain = await fs4.promises.readFile(certificate, "utf8");
|
|
1752
|
-
chain += await fs4.promises.readFile(this.caCertificate, "utf8");
|
|
1753
|
-
if (fs4.existsSync(this.issuerCertificateChain)) {
|
|
1754
|
-
chain += await fs4.promises.readFile(this.issuerCertificateChain, "utf8");
|
|
1755
|
-
}
|
|
1756
|
-
await fs4.promises.writeFile(certificate, chain);
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Create a self-signed certificate using OpenSSL.
|
|
1760
|
-
*
|
|
1761
|
-
* @param certificateFile - output path for the signed certificate
|
|
1762
|
-
* @param privateKey - path to the private key file
|
|
1763
|
-
* @param params - certificate parameters (subject, validity, SANs)
|
|
1764
|
-
*/
|
|
1765
|
-
async createSelfSignedCertificate(certificateFile, privateKey, params) {
|
|
1766
|
-
assert3(typeof privateKey === "string");
|
|
1767
|
-
assert3(fs4.existsSync(privateKey));
|
|
1768
|
-
if (!certificateFileExist(certificateFile)) {
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
adjustDate(params);
|
|
1772
|
-
adjustApplicationUri(params);
|
|
1773
|
-
params.dns = params.dns || [];
|
|
1774
|
-
params.ip = params.ip || [];
|
|
1775
|
-
await this.#withCaLock(async () => {
|
|
1776
|
-
await this.#backend.createSelfSignedCertificate(this, certificateFile, privateKey, params);
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
/**
|
|
1780
|
-
* Regenerate `crl/revocation_list.{crl,der}` from the current database
|
|
1781
|
-
* state, without changing any certificate's status. Normally
|
|
1782
|
-
* unnecessary — `revokeCertificate` already regenerates the CRL as
|
|
1783
|
-
* part of revoking — but useful to force a refresh (e.g. after
|
|
1784
|
-
* switching a CA's `backend` between `"openssl"` and `"native"`).
|
|
1785
|
-
*/
|
|
1786
|
-
async regenerateCrl() {
|
|
1787
|
-
await this.#withCaLock(async () => {
|
|
1788
|
-
await this.#backend.regenerateCrl(this);
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
/**
|
|
1792
|
-
* Revoke a certificate and regenerate the CRL.
|
|
1793
|
-
*
|
|
1794
|
-
* @param certificate - path to the certificate file to revoke
|
|
1795
|
-
* @param params - revocation parameters
|
|
1796
|
-
* @param params.reason - CRL reason code
|
|
1797
|
-
* (default `"keyCompromise"`)
|
|
1798
|
-
*/
|
|
1799
|
-
async revokeCertificate(certificate, params) {
|
|
1800
|
-
const crlReasons = [
|
|
1801
|
-
"unspecified",
|
|
1802
|
-
"keyCompromise",
|
|
1803
|
-
"CACompromise",
|
|
1804
|
-
"affiliationChanged",
|
|
1805
|
-
"superseded",
|
|
1806
|
-
"cessationOfOperation",
|
|
1807
|
-
"certificateHold",
|
|
1808
|
-
"removeFromCRL"
|
|
1809
|
-
];
|
|
1810
|
-
const reason = params.reason || "keyCompromise";
|
|
1811
|
-
assert3(crlReasons.indexOf(reason) >= 0);
|
|
1812
|
-
displayTitle(`Revoking certificate ${certificate}`);
|
|
1813
|
-
await this.#withCaLock(async () => {
|
|
1814
|
-
await this.#backend.revoke(this, certificate, reason);
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
/**
|
|
1818
|
-
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
1819
|
-
*
|
|
1820
|
-
* The signed certificate is written to `certificate`, and the
|
|
1821
|
-
* CA certificate chain plus CRL are appended to form a
|
|
1822
|
-
* complete certificate chain.
|
|
1823
|
-
*
|
|
1824
|
-
* @param certificate - output path for the signed certificate
|
|
1825
|
-
* @param certificateSigningRequestFilename - path to the CSR
|
|
1826
|
-
* @param params1 - signing parameters (validity, dates, SANs)
|
|
1827
|
-
* @returns the path to the signed certificate
|
|
1828
|
-
*/
|
|
1829
|
-
async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
|
|
1830
|
-
await this.#backend.preflight();
|
|
1831
|
-
assert3(fs4.existsSync(certificateSigningRequestFilename));
|
|
1832
|
-
if (!certificateFileExist(certificate)) {
|
|
1833
|
-
return "";
|
|
1834
|
-
}
|
|
1835
|
-
adjustDate(params1);
|
|
1836
|
-
adjustApplicationUri(params1);
|
|
1837
|
-
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
1838
|
-
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
1839
|
-
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
1840
|
-
if (typeof applicationUri !== "string") {
|
|
1841
|
-
throw new Error("Cannot find applicationUri in CSR");
|
|
1842
|
-
}
|
|
1843
|
-
const dns = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
1844
|
-
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
1845
|
-
ip = ip.map(octetStringToIpAddress);
|
|
1846
|
-
const sanOverride = { applicationUri, dns, ip };
|
|
1847
|
-
return this.#withCaLock(async () => {
|
|
1848
|
-
await this.#backend.signEndEntityCsr(this, certificate, certificateSigningRequestFilename, params1, sanOverride);
|
|
1849
|
-
displaySubtitle("- construct CA certificate with CRL");
|
|
1850
|
-
await this.constructCACertificateWithCRL();
|
|
1851
|
-
displaySubtitle("- construct certificate chain");
|
|
1852
|
-
await this.constructCertificateChain(certificate);
|
|
1853
|
-
displaySubtitle("- verify certificate against the root CA");
|
|
1854
|
-
await this.verifyCertificate(certificate);
|
|
1855
|
-
return certificate;
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
/**
|
|
1859
|
-
* Check that `certificate` really was signed by this CA, and throw if it
|
|
1860
|
-
* was not.
|
|
1861
|
-
*
|
|
1862
|
-
* This used to do nothing: `openssl verify` crashes on Windows, so the
|
|
1863
|
-
* check was left as a placeholder. It no longer needs a subprocess -
|
|
1864
|
-
* `verifyCertificateSignature` does it in pure JS, the same way
|
|
1865
|
-
* {@link CertificateManager} already validates a chain - so the check
|
|
1866
|
-
* that `signCertificateRequest` always claimed to perform now actually
|
|
1867
|
-
* happens on every issuance, whichever backend did the signing.
|
|
1868
|
-
*
|
|
1869
|
-
* Only the leading certificate is examined: the file may be a chain,
|
|
1870
|
-
* and the rest of it is this CA's own certificate and its issuers.
|
|
1871
|
-
*
|
|
1872
|
-
* @param certificate - path to the certificate (or chain) to verify
|
|
1873
|
-
*/
|
|
1874
|
-
async verifyCertificate(certificate) {
|
|
1875
|
-
const pem = await fs4.promises.readFile(certificate, "utf-8");
|
|
1876
|
-
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
1877
|
-
if (!blocks || blocks.length === 0) {
|
|
1878
|
-
throw new Error(`verifyCertificate: ${certificate} contains no PEM-encoded certificate`);
|
|
1879
|
-
}
|
|
1880
|
-
const caCertificateDer = convertPEMtoDER2(readCertificatePEM2(this.caCertificate));
|
|
1881
|
-
if (!verifyCertificateSignature(convertPEMtoDER2(blocks[0]), caCertificateDer)) {
|
|
1882
|
-
throw new Error(
|
|
1883
|
-
`verifyCertificate: ${certificate} was not signed by this certificate authority (${this.caCertificate})`
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
|
|
1889
|
-
export {
|
|
1890
|
-
quote,
|
|
1891
|
-
isOpaqueSigner,
|
|
1892
|
-
resolvePrivateKeyPassphrase,
|
|
1893
|
-
adjustDate,
|
|
1894
|
-
adjustApplicationUri,
|
|
1895
|
-
g_config,
|
|
1896
|
-
doDebug,
|
|
1897
|
-
displayError,
|
|
1898
|
-
debugLog,
|
|
1899
|
-
warningLog,
|
|
1900
|
-
mkdirRecursiveSync,
|
|
1901
|
-
restrictPrivateFilePermissions,
|
|
1902
|
-
ensurePrivateDirectory,
|
|
1903
|
-
isEncryptedPrivateKeyFile,
|
|
1904
|
-
makePath,
|
|
1905
|
-
displayTitle,
|
|
1906
|
-
displaySubtitle,
|
|
1907
|
-
display,
|
|
1908
|
-
CaDatabase,
|
|
1909
|
-
NativeCaBackend,
|
|
1910
|
-
Subject2 as Subject,
|
|
1911
|
-
defaultSubject,
|
|
1912
|
-
configurationFileTemplate,
|
|
1913
|
-
escapeOpensslConfDoubleQuoted,
|
|
1914
|
-
renderCaConfig,
|
|
1915
|
-
CertificateAuthorityCore
|
|
1916
|
-
};
|
|
1917
|
-
//# sourceMappingURL=chunk-EQ4DGI4M.mjs.map
|