node-opcua-pki 6.14.0 → 6.16.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/dist/bin/pki.mjs +195 -20
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/index.d.mts +152 -1
- package/dist/index.d.ts +152 -1
- package/dist/index.js +195 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +195 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -31,9 +31,17 @@ function adjustDate(params) {
|
|
|
31
31
|
assert(params instanceof Object);
|
|
32
32
|
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
33
33
|
assert(params.startDate instanceof Date);
|
|
34
|
-
params.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (params.validityMs !== void 0) {
|
|
35
|
+
if (params.validityMs <= 0) {
|
|
36
|
+
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
37
|
+
}
|
|
38
|
+
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
39
|
+
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
40
|
+
} else {
|
|
41
|
+
params.validity = params.validity || 365;
|
|
42
|
+
params.endDate = new Date(params.startDate.getTime());
|
|
43
|
+
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
44
|
+
}
|
|
37
45
|
assert(params.endDate instanceof Date);
|
|
38
46
|
assert(params.startDate instanceof Date);
|
|
39
47
|
}
|
|
@@ -120,9 +128,15 @@ function setEnv(varName, value) {
|
|
|
120
128
|
process.env[varName] = value;
|
|
121
129
|
}
|
|
122
130
|
}
|
|
131
|
+
function hasEnv(varName) {
|
|
132
|
+
return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
|
|
133
|
+
}
|
|
123
134
|
function getEnv(varName) {
|
|
124
135
|
return exportedEnvVars[varName];
|
|
125
136
|
}
|
|
137
|
+
function unsetEnv(varName) {
|
|
138
|
+
delete exportedEnvVars[varName];
|
|
139
|
+
}
|
|
126
140
|
function getEnvironmentVarNames() {
|
|
127
141
|
return Object.keys(exportedEnvVars).map((varName) => {
|
|
128
142
|
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
@@ -639,10 +653,17 @@ function openssl_require2DigitYearInDate() {
|
|
|
639
653
|
}
|
|
640
654
|
g_config.opensslVersion = "";
|
|
641
655
|
var _counter = 0;
|
|
656
|
+
function stripConditionalBlocks(template) {
|
|
657
|
+
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
658
|
+
const keep = hasEnv(key) && getEnv(key) !== "";
|
|
659
|
+
return keep ? content : "";
|
|
660
|
+
});
|
|
661
|
+
}
|
|
642
662
|
function generateStaticConfig(configPath, options) {
|
|
643
663
|
const prePath = options?.cwd || "";
|
|
644
664
|
const originalFilename = !path3.isAbsolute(configPath) ? path3.join(prePath, configPath) : configPath;
|
|
645
665
|
let staticConfig = fs5.readFileSync(originalFilename, { encoding: "utf8" });
|
|
666
|
+
staticConfig = stripConditionalBlocks(staticConfig);
|
|
646
667
|
for (const envVar of getEnvironmentVarNames()) {
|
|
647
668
|
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
|
|
648
669
|
}
|
|
@@ -797,7 +818,9 @@ nsComment = ''OpenSSL Generated Certificate''
|
|
|
797
818
|
#nsSslServerName =
|
|
798
819
|
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
|
|
799
820
|
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
800
|
-
|
|
821
|
+
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
822
|
+
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
823
|
+
{{/AIA_VALUE}}
|
|
801
824
|
[ v3_req ]
|
|
802
825
|
basicConstraints = critical, CA:FALSE
|
|
803
826
|
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
@@ -820,10 +843,9 @@ nsComment = "CA Certificate generated by Node-OPCUA Certificate
|
|
|
820
843
|
#nsCertType = sslCA, emailCA
|
|
821
844
|
#issuerAltName = issuer:copy
|
|
822
845
|
#obj = DER:02:03
|
|
823
|
-
crlDistributionPoints =
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
[ v3_selfsigned]
|
|
846
|
+
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
847
|
+
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
848
|
+
{{/AIA_VALUE}}[ v3_selfsigned]
|
|
827
849
|
basicConstraints = critical, CA:FALSE
|
|
828
850
|
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
829
851
|
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
@@ -906,6 +928,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
|
|
|
906
928
|
const subjectOpt = ` -subj "${subject.toString()}" `;
|
|
907
929
|
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
908
930
|
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
931
|
+
certificateAuthority._wireRevocationEnvVars();
|
|
909
932
|
const options = { cwd: caRootDir };
|
|
910
933
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
911
934
|
const configOption = ` -config ${q3(n4(configFile))}`;
|
|
@@ -959,6 +982,33 @@ function parseOpenSSLDate(dateStr) {
|
|
|
959
982
|
const sec = raw.substring(10, 12);
|
|
960
983
|
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
961
984
|
}
|
|
985
|
+
function validateRevocationUrl(url2, fieldName) {
|
|
986
|
+
if (url2 === void 0) {
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
if (url2 === "") {
|
|
990
|
+
throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
|
|
991
|
+
}
|
|
992
|
+
let parsed;
|
|
993
|
+
try {
|
|
994
|
+
parsed = new URL(url2);
|
|
995
|
+
} catch {
|
|
996
|
+
throw new Error(`${fieldName} is not a valid URL: ${url2}`);
|
|
997
|
+
}
|
|
998
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
999
|
+
throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`);
|
|
1000
|
+
}
|
|
1001
|
+
if (!parsed.pathname || parsed.pathname === "/") {
|
|
1002
|
+
throw new Error(`${fieldName} must include a path component (got ${url2})`);
|
|
1003
|
+
}
|
|
1004
|
+
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
|
|
1005
|
+
if (isLoopback) {
|
|
1006
|
+
console.warn(
|
|
1007
|
+
`[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
return url2;
|
|
1011
|
+
}
|
|
962
1012
|
var CertificateAuthority = class {
|
|
963
1013
|
/** RSA key size used when generating the CA private key. */
|
|
964
1014
|
keySize;
|
|
@@ -968,6 +1018,10 @@ var CertificateAuthority = class {
|
|
|
968
1018
|
subject;
|
|
969
1019
|
/** @internal Parent CA (undefined for root CAs). */
|
|
970
1020
|
_issuerCA;
|
|
1021
|
+
/** @internal Configured CDP / AIA URLs (US-202). */
|
|
1022
|
+
_crlDistributionUrl;
|
|
1023
|
+
_ocspResponderUrl;
|
|
1024
|
+
_caIssuersUrl;
|
|
971
1025
|
constructor(options) {
|
|
972
1026
|
assert7(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
973
1027
|
assert7(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
@@ -975,6 +1029,93 @@ var CertificateAuthority = class {
|
|
|
975
1029
|
this.keySize = options.keySize || 2048;
|
|
976
1030
|
this.subject = new Subject2(options.subject || defaultSubject);
|
|
977
1031
|
this._issuerCA = options.issuerCA;
|
|
1032
|
+
if (options.crlDistributionUrl !== void 0) {
|
|
1033
|
+
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
1034
|
+
}
|
|
1035
|
+
if (options.ocspResponderUrl !== void 0) {
|
|
1036
|
+
this.setOcspResponderUrl(options.ocspResponderUrl);
|
|
1037
|
+
}
|
|
1038
|
+
if (options.caIssuersUrl !== void 0) {
|
|
1039
|
+
this.setCaIssuersUrl(options.caIssuersUrl);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Public URL where the CRL produced by this CA is reachable, or
|
|
1044
|
+
* `undefined` if no CDP extension should be emitted on issued certs.
|
|
1045
|
+
*/
|
|
1046
|
+
get crlDistributionUrl() {
|
|
1047
|
+
return this._crlDistributionUrl;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Public URL of the OCSP responder, or `undefined` if no AIA OCSP
|
|
1051
|
+
* leg should be emitted on issued certs.
|
|
1052
|
+
*/
|
|
1053
|
+
get ocspResponderUrl() {
|
|
1054
|
+
return this._ocspResponderUrl;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Public URL where the issuer's certificate can be fetched, or
|
|
1058
|
+
* `undefined` if no AIA caIssuers leg should be emitted.
|
|
1059
|
+
*/
|
|
1060
|
+
get caIssuersUrl() {
|
|
1061
|
+
return this._caIssuersUrl;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Configure the URL embedded as `crlDistributionPoints` in every
|
|
1065
|
+
* subsequently-issued certificate. Pass `undefined` to disable
|
|
1066
|
+
* the extension entirely. Validated synchronously — throws on
|
|
1067
|
+
* empty string, non-http(s) protocol, missing path. Warns (does
|
|
1068
|
+
* not throw) when the URL points at loopback.
|
|
1069
|
+
*
|
|
1070
|
+
* @see US-202
|
|
1071
|
+
*/
|
|
1072
|
+
setCrlDistributionUrl(url2) {
|
|
1073
|
+
this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl");
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Configure the OCSP responder URL embedded as the `OCSP` leg of
|
|
1077
|
+
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
1078
|
+
* certificate. Pass `undefined` to disable.
|
|
1079
|
+
*
|
|
1080
|
+
* @see US-202
|
|
1081
|
+
*/
|
|
1082
|
+
setOcspResponderUrl(url2) {
|
|
1083
|
+
this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl");
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Configure the caIssuers URL embedded as the `caIssuers` leg of
|
|
1087
|
+
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
1088
|
+
* certificate. Pass `undefined` to disable.
|
|
1089
|
+
*
|
|
1090
|
+
* @see US-202
|
|
1091
|
+
*/
|
|
1092
|
+
setCaIssuersUrl(url2) {
|
|
1093
|
+
this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl");
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* @internal
|
|
1097
|
+
* Populate the OpenSSL config substitution env vars (`CDP_URL` and
|
|
1098
|
+
* `AIA_VALUE`) from the configured URLs, or unset them so the
|
|
1099
|
+
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
|
|
1100
|
+
* stripped. MUST be called before every `generateStaticConfig`
|
|
1101
|
+
* invocation that signs a certificate.
|
|
1102
|
+
*/
|
|
1103
|
+
_wireRevocationEnvVars() {
|
|
1104
|
+
unsetEnv("CDP_URL");
|
|
1105
|
+
unsetEnv("AIA_VALUE");
|
|
1106
|
+
if (this._crlDistributionUrl) {
|
|
1107
|
+
setEnv("CDP_URL", this._crlDistributionUrl);
|
|
1108
|
+
}
|
|
1109
|
+
const aiaLegs = [];
|
|
1110
|
+
if (this._ocspResponderUrl) {
|
|
1111
|
+
aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
|
|
1112
|
+
}
|
|
1113
|
+
if (this._caIssuersUrl) {
|
|
1114
|
+
aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (aiaLegs.length > 0) {
|
|
1117
|
+
setEnv("AIA_VALUE", aiaLegs.join(","));
|
|
1118
|
+
}
|
|
978
1119
|
}
|
|
979
1120
|
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
980
1121
|
get rootDir() {
|
|
@@ -1221,14 +1362,15 @@ var CertificateAuthority = class {
|
|
|
1221
1362
|
* @returns the signed certificate as a DER-encoded buffer
|
|
1222
1363
|
*/
|
|
1223
1364
|
async signCertificateRequestFromDER(csrDer, options) {
|
|
1224
|
-
const validity = options?.validity ?? 365;
|
|
1225
1365
|
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-sign-"));
|
|
1226
1366
|
try {
|
|
1227
1367
|
const csrFile = path5.join(tmpDir, "request.csr");
|
|
1228
1368
|
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1229
1369
|
const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
|
|
1230
1370
|
await fs7.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
1231
|
-
const signingParams = {
|
|
1371
|
+
const signingParams = {};
|
|
1372
|
+
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1373
|
+
else signingParams.validity = options?.validity ?? 365;
|
|
1232
1374
|
if (options?.startDate) signingParams.startDate = options.startDate;
|
|
1233
1375
|
if (options?.dns) signingParams.dns = options.dns;
|
|
1234
1376
|
if (options?.ip) signingParams.ip = options.ip;
|
|
@@ -1244,6 +1386,35 @@ var CertificateAuthority = class {
|
|
|
1244
1386
|
});
|
|
1245
1387
|
}
|
|
1246
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Advertise the validity limits this CA can honor.
|
|
1391
|
+
*
|
|
1392
|
+
* Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
|
|
1393
|
+
* clamp a requested validity against these bounds before calling
|
|
1394
|
+
* {@link signCertificateRequestFromDER}, so a misconfigured
|
|
1395
|
+
* `defaultCertValidity` cannot ask the CA for something it cannot
|
|
1396
|
+
* produce.
|
|
1397
|
+
*
|
|
1398
|
+
* Defaults match the OpenSSL-backed implementation:
|
|
1399
|
+
* - `minValidityMs = 60_000` (1 minute) — practical floor; the
|
|
1400
|
+
* X.509 spec floor is 1 second but very short certs are rarely
|
|
1401
|
+
* useful and pathological for any real deployment.
|
|
1402
|
+
* - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
|
|
1403
|
+
* enough for root CAs.
|
|
1404
|
+
* - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
|
|
1405
|
+
* floor on `notBefore` / `notAfter`.
|
|
1406
|
+
* - `nativeUnit = "second"` — what `x509Date()` actually encodes.
|
|
1407
|
+
*
|
|
1408
|
+
* @see US-208 — the consumer-side capability story.
|
|
1409
|
+
*/
|
|
1410
|
+
getCapabilities() {
|
|
1411
|
+
return {
|
|
1412
|
+
minValidityMs: 6e4,
|
|
1413
|
+
maxValidityMs: 10 * 365 * 864e5,
|
|
1414
|
+
validityGranularityMs: 1e3,
|
|
1415
|
+
nativeUnit: "second"
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1247
1418
|
/**
|
|
1248
1419
|
* Generate a new RSA key pair, create an internal CSR, sign it
|
|
1249
1420
|
* with this CA, and return both the certificate and private key
|
|
@@ -1261,7 +1432,6 @@ var CertificateAuthority = class {
|
|
|
1261
1432
|
*/
|
|
1262
1433
|
async generateKeyPairAndSignDER(options) {
|
|
1263
1434
|
const keySize = options.keySize ?? 2048;
|
|
1264
|
-
const validity = options.validity ?? 365;
|
|
1265
1435
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1266
1436
|
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-"));
|
|
1267
1437
|
try {
|
|
@@ -1281,13 +1451,15 @@ var CertificateAuthority = class {
|
|
|
1281
1451
|
purpose: CertificatePurpose.ForApplication
|
|
1282
1452
|
});
|
|
1283
1453
|
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1284
|
-
|
|
1454
|
+
const signingParams = {
|
|
1285
1455
|
applicationUri: options.applicationUri,
|
|
1286
1456
|
dns: options.dns,
|
|
1287
1457
|
ip: options.ip,
|
|
1288
|
-
startDate
|
|
1289
|
-
|
|
1290
|
-
|
|
1458
|
+
startDate
|
|
1459
|
+
};
|
|
1460
|
+
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1461
|
+
else signingParams.validity = options.validity ?? 365;
|
|
1462
|
+
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1291
1463
|
const certPem = readCertificatePEM(certFile);
|
|
1292
1464
|
const certificateDer = convertPEMtoDER(certPem);
|
|
1293
1465
|
const privateKey = readPrivateKey(privateKeyFile);
|
|
@@ -1312,7 +1484,6 @@ var CertificateAuthority = class {
|
|
|
1312
1484
|
*/
|
|
1313
1485
|
async generateKeyPairAndSignPFX(options) {
|
|
1314
1486
|
const keySize = options.keySize ?? 2048;
|
|
1315
|
-
const validity = options.validity ?? 365;
|
|
1316
1487
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1317
1488
|
const passphrase = options.passphrase ?? "";
|
|
1318
1489
|
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-pfx-"));
|
|
@@ -1333,13 +1504,15 @@ var CertificateAuthority = class {
|
|
|
1333
1504
|
purpose: CertificatePurpose.ForApplication
|
|
1334
1505
|
});
|
|
1335
1506
|
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1336
|
-
|
|
1507
|
+
const signingParams = {
|
|
1337
1508
|
applicationUri: options.applicationUri,
|
|
1338
1509
|
dns: options.dns,
|
|
1339
1510
|
ip: options.ip,
|
|
1340
|
-
startDate
|
|
1341
|
-
|
|
1342
|
-
|
|
1511
|
+
startDate
|
|
1512
|
+
};
|
|
1513
|
+
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1514
|
+
else signingParams.validity = options.validity ?? 365;
|
|
1515
|
+
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1343
1516
|
const pfxFile = path5.join(tmpDir, "bundle.pfx");
|
|
1344
1517
|
await createPFX({
|
|
1345
1518
|
certificateFile: certFile,
|
|
@@ -1577,6 +1750,7 @@ var CertificateAuthority = class {
|
|
|
1577
1750
|
async signCACertificateRequest(certFile, csrFile, params) {
|
|
1578
1751
|
const caRootDir = path5.resolve(this.rootDir);
|
|
1579
1752
|
const options = { cwd: caRootDir };
|
|
1753
|
+
this._wireRevocationEnvVars();
|
|
1580
1754
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1581
1755
|
const validity = params.validity ?? 3650;
|
|
1582
1756
|
await execute_openssl(
|
|
@@ -1743,6 +1917,7 @@ var CertificateAuthority = class {
|
|
|
1743
1917
|
ip
|
|
1744
1918
|
};
|
|
1745
1919
|
processAltNames(params);
|
|
1920
|
+
this._wireRevocationEnvVars();
|
|
1746
1921
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1747
1922
|
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
1748
1923
|
const configOption = ` -config ${configFile}`;
|