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