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