node-opcua-pki 6.8.2 → 6.10.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 +262 -8
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/index.d.mts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +252 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +264 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/readme.md +78 -0
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
2
2
|
import assert6 from "assert";
|
|
3
3
|
import fs6 from "fs";
|
|
4
|
+
import os3 from "os";
|
|
4
5
|
import path5 from "path";
|
|
5
6
|
import chalk5 from "chalk";
|
|
6
7
|
import {
|
|
8
|
+
convertPEMtoDER,
|
|
9
|
+
exploreCertificate,
|
|
7
10
|
exploreCertificateSigningRequest,
|
|
8
11
|
generatePrivateKeyFile,
|
|
12
|
+
readCertificatePEM,
|
|
9
13
|
readCertificateSigningRequest,
|
|
10
|
-
Subject as Subject2
|
|
14
|
+
Subject as Subject2,
|
|
15
|
+
toPem
|
|
11
16
|
} from "node-opcua-crypto";
|
|
12
17
|
|
|
13
18
|
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
@@ -707,8 +712,7 @@ subjectAltName = $ENV::ALTNAME
|
|
|
707
712
|
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
708
713
|
[ v3_ca ]
|
|
709
714
|
subjectKeyIdentifier = hash
|
|
710
|
-
authorityKeyIdentifier = keyid
|
|
711
|
-
# authorityKeyIdentifier = keyid
|
|
715
|
+
authorityKeyIdentifier = keyid
|
|
712
716
|
basicConstraints = CA:TRUE
|
|
713
717
|
keyUsage = critical, cRLSign, keyCertSign
|
|
714
718
|
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
@@ -819,6 +823,18 @@ async function regenerateCrl(revocationList, configOption, options) {
|
|
|
819
823
|
displaySubtitle("Display (Certificate Revocation List)");
|
|
820
824
|
await execute_openssl(`crl -in ${q(n2(revocationList))} -text -noout`, options);
|
|
821
825
|
}
|
|
826
|
+
function parseOpenSSLDate(dateStr) {
|
|
827
|
+
const raw = dateStr?.split(",")[0] ?? "";
|
|
828
|
+
if (raw.length < 12) return "";
|
|
829
|
+
const yy = parseInt(raw.substring(0, 2), 10);
|
|
830
|
+
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
831
|
+
const month = raw.substring(2, 4);
|
|
832
|
+
const day = raw.substring(4, 6);
|
|
833
|
+
const hour = raw.substring(6, 8);
|
|
834
|
+
const min = raw.substring(8, 10);
|
|
835
|
+
const sec = raw.substring(10, 12);
|
|
836
|
+
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
837
|
+
}
|
|
822
838
|
var CertificateAuthority = class {
|
|
823
839
|
/** RSA key size used when generating the CA private key. */
|
|
824
840
|
keySize;
|
|
@@ -866,6 +882,244 @@ var CertificateAuthority = class {
|
|
|
866
882
|
get caCertificateWithCrl() {
|
|
867
883
|
return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
|
|
868
884
|
}
|
|
885
|
+
// ---------------------------------------------------------------
|
|
886
|
+
// Buffer-based accessors (US-059)
|
|
887
|
+
// ---------------------------------------------------------------
|
|
888
|
+
/**
|
|
889
|
+
* Return the CA certificate as a DER-encoded buffer.
|
|
890
|
+
*
|
|
891
|
+
* @throws if the CA certificate file does not exist
|
|
892
|
+
* (call {@link initialize} first).
|
|
893
|
+
*/
|
|
894
|
+
getCACertificateDER() {
|
|
895
|
+
const pem = readCertificatePEM(this.caCertificate);
|
|
896
|
+
return convertPEMtoDER(pem);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Return the CA certificate as a PEM-encoded string.
|
|
900
|
+
*
|
|
901
|
+
* @throws if the CA certificate file does not exist
|
|
902
|
+
* (call {@link initialize} first).
|
|
903
|
+
*/
|
|
904
|
+
getCACertificatePEM() {
|
|
905
|
+
const raw = readCertificatePEM(this.caCertificate);
|
|
906
|
+
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
907
|
+
const idx = raw.indexOf(beginMarker);
|
|
908
|
+
if (idx > 0) {
|
|
909
|
+
return raw.substring(idx);
|
|
910
|
+
}
|
|
911
|
+
return raw;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Return the current Certificate Revocation List as a
|
|
915
|
+
* DER-encoded buffer.
|
|
916
|
+
*
|
|
917
|
+
* Returns an empty buffer if no CRL has been generated yet.
|
|
918
|
+
*/
|
|
919
|
+
getCRLDER() {
|
|
920
|
+
const crlPath = this.revocationListDER;
|
|
921
|
+
if (!fs6.existsSync(crlPath)) {
|
|
922
|
+
return Buffer.alloc(0);
|
|
923
|
+
}
|
|
924
|
+
return fs6.readFileSync(crlPath);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Return the current Certificate Revocation List as a
|
|
928
|
+
* PEM-encoded string.
|
|
929
|
+
*
|
|
930
|
+
* Returns an empty string if no CRL has been generated yet.
|
|
931
|
+
*/
|
|
932
|
+
getCRLPEM() {
|
|
933
|
+
const crlPath = this.revocationList;
|
|
934
|
+
if (!fs6.existsSync(crlPath)) {
|
|
935
|
+
return "";
|
|
936
|
+
}
|
|
937
|
+
const raw = fs6.readFileSync(crlPath, "utf-8");
|
|
938
|
+
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
939
|
+
const idx = raw.indexOf(beginMarker);
|
|
940
|
+
if (idx > 0) {
|
|
941
|
+
return raw.substring(idx);
|
|
942
|
+
}
|
|
943
|
+
return raw;
|
|
944
|
+
}
|
|
945
|
+
// ---------------------------------------------------------------
|
|
946
|
+
// Certificate database API (US-057)
|
|
947
|
+
// ---------------------------------------------------------------
|
|
948
|
+
/**
|
|
949
|
+
* Return a list of all issued certificates recorded in the
|
|
950
|
+
* OpenSSL `index.txt` database.
|
|
951
|
+
*
|
|
952
|
+
* Each entry includes the serial number, subject, status,
|
|
953
|
+
* expiry date, and (for revoked certs) the revocation date.
|
|
954
|
+
*/
|
|
955
|
+
getIssuedCertificates() {
|
|
956
|
+
return this._parseIndexTxt();
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Return the total number of certificates recorded in
|
|
960
|
+
* `index.txt`.
|
|
961
|
+
*/
|
|
962
|
+
getIssuedCertificateCount() {
|
|
963
|
+
return this._parseIndexTxt().length;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Return the status of a certificate by its serial number.
|
|
967
|
+
*
|
|
968
|
+
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
969
|
+
* @returns `"valid"`, `"revoked"`, `"expired"`, or
|
|
970
|
+
* `undefined` if not found
|
|
971
|
+
*/
|
|
972
|
+
getCertificateStatus(serial) {
|
|
973
|
+
const upper = serial.toUpperCase();
|
|
974
|
+
const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
|
|
975
|
+
return record?.status;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Read a specific issued certificate by serial number and
|
|
979
|
+
* return its content as a DER-encoded buffer.
|
|
980
|
+
*
|
|
981
|
+
* OpenSSL stores signed certificates in the `certs/`
|
|
982
|
+
* directory using the naming convention `<SERIAL>.pem`.
|
|
983
|
+
*
|
|
984
|
+
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
985
|
+
* @returns the DER buffer, or `undefined` if not found
|
|
986
|
+
*/
|
|
987
|
+
getCertificateBySerial(serial) {
|
|
988
|
+
const upper = serial.toUpperCase();
|
|
989
|
+
const certFile = path5.join(this.rootDir, "certs", `${upper}.pem`);
|
|
990
|
+
if (!fs6.existsSync(certFile)) {
|
|
991
|
+
return void 0;
|
|
992
|
+
}
|
|
993
|
+
const pem = readCertificatePEM(certFile);
|
|
994
|
+
return convertPEMtoDER(pem);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Path to the OpenSSL certificate database file.
|
|
998
|
+
*/
|
|
999
|
+
get indexFile() {
|
|
1000
|
+
return path5.join(this.rootDir, "index.txt");
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Parse the OpenSSL `index.txt` certificate database.
|
|
1004
|
+
*
|
|
1005
|
+
* Each line has tab-separated fields:
|
|
1006
|
+
* ```
|
|
1007
|
+
* status expiry [revocationDate] serial unknown subject
|
|
1008
|
+
* ```
|
|
1009
|
+
*
|
|
1010
|
+
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
1011
|
+
* - expiry: `YYMMDDHHmmssZ`
|
|
1012
|
+
* - revocationDate: present only for revoked certs
|
|
1013
|
+
* - serial: hex string
|
|
1014
|
+
* - unknown: always `"unknown"`
|
|
1015
|
+
* - subject: X.500 slash-delimited string
|
|
1016
|
+
*/
|
|
1017
|
+
_parseIndexTxt() {
|
|
1018
|
+
const indexPath = this.indexFile;
|
|
1019
|
+
if (!fs6.existsSync(indexPath)) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
const content = fs6.readFileSync(indexPath, "utf-8");
|
|
1023
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
1024
|
+
const records = [];
|
|
1025
|
+
for (const line of lines) {
|
|
1026
|
+
const fields = line.split(" ");
|
|
1027
|
+
if (fields.length < 4) continue;
|
|
1028
|
+
const statusChar = fields[0];
|
|
1029
|
+
const expiryStr = fields[1];
|
|
1030
|
+
let serial;
|
|
1031
|
+
let subject;
|
|
1032
|
+
let revocationDate;
|
|
1033
|
+
if (statusChar === "R") {
|
|
1034
|
+
revocationDate = fields[2];
|
|
1035
|
+
serial = fields[3];
|
|
1036
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
1037
|
+
} else {
|
|
1038
|
+
serial = fields[3];
|
|
1039
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
1040
|
+
}
|
|
1041
|
+
let status;
|
|
1042
|
+
switch (statusChar) {
|
|
1043
|
+
case "V":
|
|
1044
|
+
status = "valid";
|
|
1045
|
+
break;
|
|
1046
|
+
case "R":
|
|
1047
|
+
status = "revoked";
|
|
1048
|
+
break;
|
|
1049
|
+
case "E":
|
|
1050
|
+
status = "expired";
|
|
1051
|
+
break;
|
|
1052
|
+
default:
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
records.push({
|
|
1056
|
+
serial,
|
|
1057
|
+
status,
|
|
1058
|
+
subject,
|
|
1059
|
+
expiryDate: parseOpenSSLDate(expiryStr),
|
|
1060
|
+
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
return records;
|
|
1064
|
+
}
|
|
1065
|
+
// ---------------------------------------------------------------
|
|
1066
|
+
// Buffer-based CA operations (US-058)
|
|
1067
|
+
// ---------------------------------------------------------------
|
|
1068
|
+
/**
|
|
1069
|
+
* Sign a DER-encoded Certificate Signing Request and return
|
|
1070
|
+
* the signed certificate as a DER buffer.
|
|
1071
|
+
*
|
|
1072
|
+
* This method handles temp-file creation and cleanup
|
|
1073
|
+
* internally so that callers can work with in-memory
|
|
1074
|
+
* buffers only.
|
|
1075
|
+
*
|
|
1076
|
+
* @param csrDer - the CSR as a DER-encoded buffer
|
|
1077
|
+
* @param options - signing options
|
|
1078
|
+
* @param options.validity - certificate validity in days
|
|
1079
|
+
* (default: 365)
|
|
1080
|
+
* @returns the signed certificate as a DER-encoded buffer
|
|
1081
|
+
*/
|
|
1082
|
+
async signCertificateRequestFromDER(csrDer, options) {
|
|
1083
|
+
const validity = options?.validity ?? 365;
|
|
1084
|
+
const tmpDir = await fs6.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-sign-"));
|
|
1085
|
+
try {
|
|
1086
|
+
const csrFile = path5.join(tmpDir, "request.csr");
|
|
1087
|
+
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1088
|
+
const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
|
|
1089
|
+
await fs6.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
1090
|
+
await this.signCertificateRequest(certFile, csrFile, { validity });
|
|
1091
|
+
const certPem = readCertificatePEM(certFile);
|
|
1092
|
+
return convertPEMtoDER(certPem);
|
|
1093
|
+
} finally {
|
|
1094
|
+
await fs6.promises.rm(tmpDir, {
|
|
1095
|
+
recursive: true,
|
|
1096
|
+
force: true
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Revoke a DER-encoded certificate and regenerate the CRL.
|
|
1102
|
+
*
|
|
1103
|
+
* Extracts the serial number from the certificate, then
|
|
1104
|
+
* uses the stored cert file at `certs/<serial>.pem` for
|
|
1105
|
+
* revocation — avoiding temp-file PEM format mismatches.
|
|
1106
|
+
*
|
|
1107
|
+
* @param certDer - the certificate as a DER-encoded buffer
|
|
1108
|
+
* @param reason - CRL reason code
|
|
1109
|
+
* (default: `"keyCompromise"`)
|
|
1110
|
+
* @throws if the certificate's serial is not found in the CA
|
|
1111
|
+
*/
|
|
1112
|
+
async revokeCertificateDER(certDer, reason) {
|
|
1113
|
+
const info = exploreCertificate(certDer);
|
|
1114
|
+
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
1115
|
+
const storedCertFile = path5.join(this.rootDir, "certs", `${serial}.pem`);
|
|
1116
|
+
if (!fs6.existsSync(storedCertFile)) {
|
|
1117
|
+
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
1118
|
+
}
|
|
1119
|
+
await this.revokeCertificate(storedCertFile, {
|
|
1120
|
+
reason: reason ?? "keyCompromise"
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
869
1123
|
/**
|
|
870
1124
|
* Initialize the CA directory structure, generate the CA
|
|
871
1125
|
* private key and self-signed certificate if they do not
|
|
@@ -1077,7 +1331,7 @@ import { drainPendingLocks, withLock } from "@ster5/global-mutex";
|
|
|
1077
1331
|
import chalk6 from "chalk";
|
|
1078
1332
|
import chokidar from "chokidar";
|
|
1079
1333
|
import {
|
|
1080
|
-
exploreCertificate,
|
|
1334
|
+
exploreCertificate as exploreCertificate2,
|
|
1081
1335
|
exploreCertificateInfo,
|
|
1082
1336
|
exploreCertificateRevocationList,
|
|
1083
1337
|
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
@@ -1086,7 +1340,7 @@ import {
|
|
|
1086
1340
|
readCertificateAsync,
|
|
1087
1341
|
readCertificateRevocationList,
|
|
1088
1342
|
split_der,
|
|
1089
|
-
toPem,
|
|
1343
|
+
toPem as toPem2,
|
|
1090
1344
|
verifyCertificateChain,
|
|
1091
1345
|
verifyCertificateSignature
|
|
1092
1346
|
} from "node-opcua-crypto";
|
|
@@ -1189,7 +1443,7 @@ function exploreCertificateCached(certificate) {
|
|
|
1189
1443
|
_exploreCache.set(key, cached);
|
|
1190
1444
|
return cached;
|
|
1191
1445
|
}
|
|
1192
|
-
const info =
|
|
1446
|
+
const info = exploreCertificate2(certificate);
|
|
1193
1447
|
_exploreCache.set(key, info);
|
|
1194
1448
|
if (_exploreCache.size > EXPLORE_CACHE_MAX) {
|
|
1195
1449
|
const oldest = _exploreCache.keys().next().value;
|
|
@@ -1503,7 +1757,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
1503
1757
|
}
|
|
1504
1758
|
const filename = path6.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
1505
1759
|
debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
|
|
1506
|
-
await fsWriteFile(filename,
|
|
1760
|
+
await fsWriteFile(filename, toPem2(certificate, "CERTIFICATE"));
|
|
1507
1761
|
this.#thumbs.rejected.set(fingerprint, { certificate, filename });
|
|
1508
1762
|
}
|
|
1509
1763
|
return "BadCertificateUntrusted";
|
|
@@ -1868,7 +2122,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
1868
2122
|
return status;
|
|
1869
2123
|
}
|
|
1870
2124
|
}
|
|
1871
|
-
const pemCertificate =
|
|
2125
|
+
const pemCertificate = toPem2(certificate, "CERTIFICATE");
|
|
1872
2126
|
const fingerprint = makeFingerprint(certificate);
|
|
1873
2127
|
if (this.#thumbs.issuers.certs.has(fingerprint)) {
|
|
1874
2128
|
return "Good" /* Good */;
|
|
@@ -1896,7 +2150,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
1896
2150
|
if (!index.has(key)) {
|
|
1897
2151
|
index.set(key, { crls: [], serialNumbers: {} });
|
|
1898
2152
|
}
|
|
1899
|
-
const pemCertificate =
|
|
2153
|
+
const pemCertificate = toPem2(crl, "X509 CRL");
|
|
1900
2154
|
const filename = path6.join(folder, `crl_${buildIdealCertificateName(crl)}.pem`);
|
|
1901
2155
|
await fs9.promises.writeFile(filename, pemCertificate, "ascii");
|
|
1902
2156
|
await this.#onCrlFileAdded(index, filename);
|
|
@@ -2161,7 +2415,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2161
2415
|
const fingerprint = makeFingerprint(certificate);
|
|
2162
2416
|
let status = await this.#checkRejectedOrTrusted(certificate);
|
|
2163
2417
|
if (status === "unknown") {
|
|
2164
|
-
const pem =
|
|
2418
|
+
const pem = toPem2(certificate, "CERTIFICATE");
|
|
2165
2419
|
const filename = path6.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
2166
2420
|
await fs9.promises.writeFile(filename, pem);
|
|
2167
2421
|
this.#thumbs.rejected.set(fingerprint, { certificate, filename });
|