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 CHANGED
@@ -2393,8 +2393,7 @@ subjectAltName = $ENV::ALTNAME
2393
2393
  nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
2394
2394
  [ v3_ca ]
2395
2395
  subjectKeyIdentifier = hash
2396
- authorityKeyIdentifier = keyid:always,issuer:always
2397
- # authorityKeyIdentifier = keyid
2396
+ authorityKeyIdentifier = keyid
2398
2397
  basicConstraints = CA:TRUE
2399
2398
  keyUsage = critical, cRLSign, keyCertSign
2400
2399
  nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
@@ -2423,13 +2422,18 @@ authorityKeyIdentifier = keyid:always,issuer:always
2423
2422
  // packages/node-opcua-pki/lib/ca/certificate_authority.ts
2424
2423
  import assert9 from "assert";
2425
2424
  import fs9 from "fs";
2425
+ import os4 from "os";
2426
2426
  import path6 from "path";
2427
2427
  import chalk6 from "chalk";
2428
2428
  import {
2429
+ convertPEMtoDER,
2430
+ exploreCertificate as exploreCertificate2,
2429
2431
  exploreCertificateSigningRequest,
2430
2432
  generatePrivateKeyFile as generatePrivateKeyFile2,
2433
+ readCertificatePEM,
2431
2434
  readCertificateSigningRequest,
2432
- Subject as Subject4
2435
+ Subject as Subject4,
2436
+ toPem as toPem2
2433
2437
  } from "node-opcua-crypto";
2434
2438
  function octetStringToIpAddress(a) {
2435
2439
  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();
@@ -2507,6 +2511,18 @@ async function regenerateCrl(revocationList, configOption, options) {
2507
2511
  displaySubtitle("Display (Certificate Revocation List)");
2508
2512
  await execute_openssl(`crl -in ${q3(n4(revocationList))} -text -noout`, options);
2509
2513
  }
2514
+ function parseOpenSSLDate(dateStr) {
2515
+ const raw = dateStr?.split(",")[0] ?? "";
2516
+ if (raw.length < 12) return "";
2517
+ const yy = parseInt(raw.substring(0, 2), 10);
2518
+ const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
2519
+ const month = raw.substring(2, 4);
2520
+ const day = raw.substring(4, 6);
2521
+ const hour = raw.substring(6, 8);
2522
+ const min = raw.substring(8, 10);
2523
+ const sec = raw.substring(10, 12);
2524
+ return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
2525
+ }
2510
2526
  var defaultSubject, configurationFileTemplate, config3, n4, q3, CertificateAuthority;
2511
2527
  var init_certificate_authority = __esm({
2512
2528
  "packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
@@ -2572,6 +2588,244 @@ var init_certificate_authority = __esm({
2572
2588
  get caCertificateWithCrl() {
2573
2589
  return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
2574
2590
  }
2591
+ // ---------------------------------------------------------------
2592
+ // Buffer-based accessors (US-059)
2593
+ // ---------------------------------------------------------------
2594
+ /**
2595
+ * Return the CA certificate as a DER-encoded buffer.
2596
+ *
2597
+ * @throws if the CA certificate file does not exist
2598
+ * (call {@link initialize} first).
2599
+ */
2600
+ getCACertificateDER() {
2601
+ const pem = readCertificatePEM(this.caCertificate);
2602
+ return convertPEMtoDER(pem);
2603
+ }
2604
+ /**
2605
+ * Return the CA certificate as a PEM-encoded string.
2606
+ *
2607
+ * @throws if the CA certificate file does not exist
2608
+ * (call {@link initialize} first).
2609
+ */
2610
+ getCACertificatePEM() {
2611
+ const raw = readCertificatePEM(this.caCertificate);
2612
+ const beginMarker = "-----BEGIN CERTIFICATE-----";
2613
+ const idx = raw.indexOf(beginMarker);
2614
+ if (idx > 0) {
2615
+ return raw.substring(idx);
2616
+ }
2617
+ return raw;
2618
+ }
2619
+ /**
2620
+ * Return the current Certificate Revocation List as a
2621
+ * DER-encoded buffer.
2622
+ *
2623
+ * Returns an empty buffer if no CRL has been generated yet.
2624
+ */
2625
+ getCRLDER() {
2626
+ const crlPath = this.revocationListDER;
2627
+ if (!fs9.existsSync(crlPath)) {
2628
+ return Buffer.alloc(0);
2629
+ }
2630
+ return fs9.readFileSync(crlPath);
2631
+ }
2632
+ /**
2633
+ * Return the current Certificate Revocation List as a
2634
+ * PEM-encoded string.
2635
+ *
2636
+ * Returns an empty string if no CRL has been generated yet.
2637
+ */
2638
+ getCRLPEM() {
2639
+ const crlPath = this.revocationList;
2640
+ if (!fs9.existsSync(crlPath)) {
2641
+ return "";
2642
+ }
2643
+ const raw = fs9.readFileSync(crlPath, "utf-8");
2644
+ const beginMarker = "-----BEGIN X509 CRL-----";
2645
+ const idx = raw.indexOf(beginMarker);
2646
+ if (idx > 0) {
2647
+ return raw.substring(idx);
2648
+ }
2649
+ return raw;
2650
+ }
2651
+ // ---------------------------------------------------------------
2652
+ // Certificate database API (US-057)
2653
+ // ---------------------------------------------------------------
2654
+ /**
2655
+ * Return a list of all issued certificates recorded in the
2656
+ * OpenSSL `index.txt` database.
2657
+ *
2658
+ * Each entry includes the serial number, subject, status,
2659
+ * expiry date, and (for revoked certs) the revocation date.
2660
+ */
2661
+ getIssuedCertificates() {
2662
+ return this._parseIndexTxt();
2663
+ }
2664
+ /**
2665
+ * Return the total number of certificates recorded in
2666
+ * `index.txt`.
2667
+ */
2668
+ getIssuedCertificateCount() {
2669
+ return this._parseIndexTxt().length;
2670
+ }
2671
+ /**
2672
+ * Return the status of a certificate by its serial number.
2673
+ *
2674
+ * @param serial - hex-encoded serial number (e.g. `"1000"`)
2675
+ * @returns `"valid"`, `"revoked"`, `"expired"`, or
2676
+ * `undefined` if not found
2677
+ */
2678
+ getCertificateStatus(serial) {
2679
+ const upper = serial.toUpperCase();
2680
+ const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
2681
+ return record?.status;
2682
+ }
2683
+ /**
2684
+ * Read a specific issued certificate by serial number and
2685
+ * return its content as a DER-encoded buffer.
2686
+ *
2687
+ * OpenSSL stores signed certificates in the `certs/`
2688
+ * directory using the naming convention `<SERIAL>.pem`.
2689
+ *
2690
+ * @param serial - hex-encoded serial number (e.g. `"1000"`)
2691
+ * @returns the DER buffer, or `undefined` if not found
2692
+ */
2693
+ getCertificateBySerial(serial) {
2694
+ const upper = serial.toUpperCase();
2695
+ const certFile = path6.join(this.rootDir, "certs", `${upper}.pem`);
2696
+ if (!fs9.existsSync(certFile)) {
2697
+ return void 0;
2698
+ }
2699
+ const pem = readCertificatePEM(certFile);
2700
+ return convertPEMtoDER(pem);
2701
+ }
2702
+ /**
2703
+ * Path to the OpenSSL certificate database file.
2704
+ */
2705
+ get indexFile() {
2706
+ return path6.join(this.rootDir, "index.txt");
2707
+ }
2708
+ /**
2709
+ * Parse the OpenSSL `index.txt` certificate database.
2710
+ *
2711
+ * Each line has tab-separated fields:
2712
+ * ```
2713
+ * status expiry [revocationDate] serial unknown subject
2714
+ * ```
2715
+ *
2716
+ * - status: `V` (valid), `R` (revoked), `E` (expired)
2717
+ * - expiry: `YYMMDDHHmmssZ`
2718
+ * - revocationDate: present only for revoked certs
2719
+ * - serial: hex string
2720
+ * - unknown: always `"unknown"`
2721
+ * - subject: X.500 slash-delimited string
2722
+ */
2723
+ _parseIndexTxt() {
2724
+ const indexPath = this.indexFile;
2725
+ if (!fs9.existsSync(indexPath)) {
2726
+ return [];
2727
+ }
2728
+ const content = fs9.readFileSync(indexPath, "utf-8");
2729
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
2730
+ const records = [];
2731
+ for (const line of lines) {
2732
+ const fields = line.split(" ");
2733
+ if (fields.length < 4) continue;
2734
+ const statusChar = fields[0];
2735
+ const expiryStr = fields[1];
2736
+ let serial;
2737
+ let subject;
2738
+ let revocationDate;
2739
+ if (statusChar === "R") {
2740
+ revocationDate = fields[2];
2741
+ serial = fields[3];
2742
+ subject = fields.length >= 6 ? fields[5] : "";
2743
+ } else {
2744
+ serial = fields[3];
2745
+ subject = fields.length >= 6 ? fields[5] : "";
2746
+ }
2747
+ let status;
2748
+ switch (statusChar) {
2749
+ case "V":
2750
+ status = "valid";
2751
+ break;
2752
+ case "R":
2753
+ status = "revoked";
2754
+ break;
2755
+ case "E":
2756
+ status = "expired";
2757
+ break;
2758
+ default:
2759
+ continue;
2760
+ }
2761
+ records.push({
2762
+ serial,
2763
+ status,
2764
+ subject,
2765
+ expiryDate: parseOpenSSLDate(expiryStr),
2766
+ revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
2767
+ });
2768
+ }
2769
+ return records;
2770
+ }
2771
+ // ---------------------------------------------------------------
2772
+ // Buffer-based CA operations (US-058)
2773
+ // ---------------------------------------------------------------
2774
+ /**
2775
+ * Sign a DER-encoded Certificate Signing Request and return
2776
+ * the signed certificate as a DER buffer.
2777
+ *
2778
+ * This method handles temp-file creation and cleanup
2779
+ * internally so that callers can work with in-memory
2780
+ * buffers only.
2781
+ *
2782
+ * @param csrDer - the CSR as a DER-encoded buffer
2783
+ * @param options - signing options
2784
+ * @param options.validity - certificate validity in days
2785
+ * (default: 365)
2786
+ * @returns the signed certificate as a DER-encoded buffer
2787
+ */
2788
+ async signCertificateRequestFromDER(csrDer, options) {
2789
+ const validity = options?.validity ?? 365;
2790
+ const tmpDir = await fs9.promises.mkdtemp(path6.join(os4.tmpdir(), "pki-sign-"));
2791
+ try {
2792
+ const csrFile = path6.join(tmpDir, "request.csr");
2793
+ const certFile = path6.join(tmpDir, "certificate.pem");
2794
+ const csrPem = toPem2(csrDer, "CERTIFICATE REQUEST");
2795
+ await fs9.promises.writeFile(csrFile, csrPem, "utf-8");
2796
+ await this.signCertificateRequest(certFile, csrFile, { validity });
2797
+ const certPem = readCertificatePEM(certFile);
2798
+ return convertPEMtoDER(certPem);
2799
+ } finally {
2800
+ await fs9.promises.rm(tmpDir, {
2801
+ recursive: true,
2802
+ force: true
2803
+ });
2804
+ }
2805
+ }
2806
+ /**
2807
+ * Revoke a DER-encoded certificate and regenerate the CRL.
2808
+ *
2809
+ * Extracts the serial number from the certificate, then
2810
+ * uses the stored cert file at `certs/<serial>.pem` for
2811
+ * revocation — avoiding temp-file PEM format mismatches.
2812
+ *
2813
+ * @param certDer - the certificate as a DER-encoded buffer
2814
+ * @param reason - CRL reason code
2815
+ * (default: `"keyCompromise"`)
2816
+ * @throws if the certificate's serial is not found in the CA
2817
+ */
2818
+ async revokeCertificateDER(certDer, reason) {
2819
+ const info = exploreCertificate2(certDer);
2820
+ const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
2821
+ const storedCertFile = path6.join(this.rootDir, "certs", `${serial}.pem`);
2822
+ if (!fs9.existsSync(storedCertFile)) {
2823
+ throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
2824
+ }
2825
+ await this.revokeCertificate(storedCertFile, {
2826
+ reason: reason ?? "keyCompromise"
2827
+ });
2828
+ }
2575
2829
  /**
2576
2830
  * Initialize the CA directory structure, generate the CA
2577
2831
  * private key and self-signed certificate if they do not
@@ -2781,7 +3035,7 @@ var init_certificate_authority = __esm({
2781
3035
  import assert10 from "assert";
2782
3036
  import fs10 from "fs";
2783
3037
  import { createRequire } from "module";
2784
- import os4 from "os";
3038
+ import os5 from "os";
2785
3039
  import path7 from "path";
2786
3040
  import chalk7 from "chalk";
2787
3041
  import { CertificatePurpose as CertificatePurpose2, generatePrivateKeyFile as generatePrivateKeyFile3, Subject as Subject5 } from "node-opcua-crypto";
@@ -2854,7 +3108,7 @@ async function readConfiguration(argv) {
2854
3108
  g_config.silent = false;
2855
3109
  }
2856
3110
  const fqdn2 = await extractFullyQualifiedDomainName();
2857
- const hostname = os4.hostname();
3111
+ const hostname = os5.hostname();
2858
3112
  let certificateDir;
2859
3113
  function performSubstitution(str) {
2860
3114
  str = str.replace("{CWD}", process.cwd());
@@ -2950,7 +3204,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
2950
3204
  const certificate_revoked = makePath(base_name, `${prefix}cert_${key_length}_revoked.pem`);
2951
3205
  const self_signed_certificate_file = makePath(base_name, `${prefix}selfsigned_cert_${key_length}.pem`);
2952
3206
  const fqdn2 = getFullyQualifiedDomainName();
2953
- const hostname = os4.hostname();
3207
+ const hostname = os5.hostname();
2954
3208
  const dns2 = [
2955
3209
  // for conformance reason, localhost shall not be present in the DNS field of COP
2956
3210
  // ***FORBIDEN** "localhost",
@@ -2971,7 +3225,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
2971
3225
  async function createCertificate(certificate, privateKey, applicationUri2, startDate, validity) {
2972
3226
  const certificateSigningRequestFile = `${certificate}.csr`;
2973
3227
  const configFile = makePath(base_name, "../certificates/PKI/own/openssl.cnf");
2974
- const dns3 = [os4.hostname()];
3228
+ const dns3 = [os5.hostname()];
2975
3229
  const ip2 = ["127.0.0.1"];
2976
3230
  const params = {
2977
3231
  applicationUri: applicationUri2,
@@ -3055,7 +3309,7 @@ async function create_default_certificates(dev) {
3055
3309
  let discoveryServerURN;
3056
3310
  wrap(async () => {
3057
3311
  await extractFullyQualifiedDomainName();
3058
- const hostname = os4.hostname();
3312
+ const hostname = os5.hostname();
3059
3313
  const fqdn2 = getFullyQualifiedDomainName();
3060
3314
  warningLog(chalk7.yellow(" hostname = "), chalk7.cyan(hostname));
3061
3315
  warningLog(chalk7.yellow(" fqdn = "), chalk7.cyan(fqdn2));