node-opcua-common 2.165.0 → 2.168.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/applicationurn.js.map +1 -1
- package/dist/certificate_chain_provider.d.ts +52 -0
- package/dist/certificate_chain_provider.js +59 -0
- package/dist/certificate_chain_provider.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +10 -9
- package/dist/index.js.map +1 -1
- package/dist/opcua_secure_object.d.ts +56 -12
- package/dist/opcua_secure_object.js +134 -48
- package/dist/opcua_secure_object.js.map +1 -1
- package/package.json +5 -4
- package/source/applicationurn.ts +1 -2
- package/source/certificate_chain_provider.ts +90 -0
- package/source/index.ts +13 -12
- package/source/opcua_secure_object.ts +161 -61
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applicationurn.js","sourceRoot":"","sources":["../source/applicationurn.ts"],"names":[],"mappings":";;AAOA,
|
|
1
|
+
{"version":3,"file":"applicationurn.js","sourceRoot":"","sources":["../source/applicationurn.ts"],"names":[],"mappings":";;AAOA,gDAeC;AAtBD;;GAEG;AACH,mCAAoC;AAEpC,yDAA2C;AAE3C,SAAgB,kBAAkB,CAAC,QAAgB,EAAE,MAAc;IAC/D,IAAA,0BAAM,EAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,6BAA6B,CAAC,CAAC;IAC7D,sEAAsE;IACtE,2DAA2D;IAC3D,wEAAwE;IACxE,eAAe;IACf,IAAI,YAAY,GAAG,QAAQ,CAAC;IAC5B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QAChD,0DAA0D;QAC1D,kCAAkC;QAClC,YAAY,GAAG,IAAA,mBAAU,EAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,GAAG,GAAG,MAAM,CAAC;IAC5D,IAAA,0BAAM,EAAC,cAAc,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,cAAc,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable certificate chain provider for OPC UA endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts how an endpoint obtains its certificate chain and private key,
|
|
5
|
+
* allowing both static (in-memory) and dynamic (disk-based) strategies
|
|
6
|
+
* without monkey-patching.
|
|
7
|
+
*
|
|
8
|
+
* @module node-opcua-common
|
|
9
|
+
*/
|
|
10
|
+
import type { Certificate, PrivateKey } from "node-opcua-crypto/web";
|
|
11
|
+
import type { ICertificateKeyPairProvider } from "./opcua_secure_object";
|
|
12
|
+
/**
|
|
13
|
+
* Provides a certificate chain and private key to an OPC UA endpoint.
|
|
14
|
+
*
|
|
15
|
+
* Implementations may read from memory, disk, or any other source.
|
|
16
|
+
* See also {@link SecretHolder} which implements this interface for
|
|
17
|
+
* disk-based access with lazy caching.
|
|
18
|
+
*/
|
|
19
|
+
export interface ICertificateChainProvider extends ICertificateKeyPairProvider {
|
|
20
|
+
/**
|
|
21
|
+
* Invalidate any cached values so the next access re-reads
|
|
22
|
+
* from the underlying source. No-op for static providers.
|
|
23
|
+
*/
|
|
24
|
+
invalidate(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Holds a certificate chain and private key in memory.
|
|
28
|
+
*
|
|
29
|
+
* Used as the default provider when push certificate management
|
|
30
|
+
* is NOT installed. The chain can be replaced in-place via `update()`.
|
|
31
|
+
*/
|
|
32
|
+
export declare class StaticCertificateChainProvider implements ICertificateChainProvider {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(chain: Certificate[], key: PrivateKey);
|
|
35
|
+
getCertificate(): Certificate;
|
|
36
|
+
getCertificateChain(): Certificate[];
|
|
37
|
+
getPrivateKey(): PrivateKey;
|
|
38
|
+
/**
|
|
39
|
+
* No-op for static provider — the chain is already in memory.
|
|
40
|
+
* Use `update()` to replace the chain explicitly.
|
|
41
|
+
*/
|
|
42
|
+
invalidate(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Replace the certificate chain and optionally the private key.
|
|
45
|
+
*
|
|
46
|
+
* This immediately affects all consumers that call
|
|
47
|
+
* `getCertificateChain()` on this provider (including
|
|
48
|
+
* endpoint descriptions with dynamic `serverCertificate` getters).
|
|
49
|
+
*/
|
|
50
|
+
update(chain: Certificate[], key?: PrivateKey): void;
|
|
51
|
+
toJSON(): Record<string, string>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StaticCertificateChainProvider = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Holds a certificate chain and private key in memory.
|
|
6
|
+
*
|
|
7
|
+
* Used as the default provider when push certificate management
|
|
8
|
+
* is NOT installed. The chain can be replaced in-place via `update()`.
|
|
9
|
+
*/
|
|
10
|
+
class StaticCertificateChainProvider {
|
|
11
|
+
#chain;
|
|
12
|
+
#key;
|
|
13
|
+
constructor(chain, key) {
|
|
14
|
+
this.#chain = chain;
|
|
15
|
+
this.#key = key;
|
|
16
|
+
}
|
|
17
|
+
getCertificate() {
|
|
18
|
+
return this.#chain[0];
|
|
19
|
+
}
|
|
20
|
+
getCertificateChain() {
|
|
21
|
+
return this.#chain;
|
|
22
|
+
}
|
|
23
|
+
getPrivateKey() {
|
|
24
|
+
return this.#key;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* No-op for static provider — the chain is already in memory.
|
|
28
|
+
* Use `update()` to replace the chain explicitly.
|
|
29
|
+
*/
|
|
30
|
+
invalidate() {
|
|
31
|
+
// nothing to invalidate for a static provider
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Replace the certificate chain and optionally the private key.
|
|
35
|
+
*
|
|
36
|
+
* This immediately affects all consumers that call
|
|
37
|
+
* `getCertificateChain()` on this provider (including
|
|
38
|
+
* endpoint descriptions with dynamic `serverCertificate` getters).
|
|
39
|
+
*/
|
|
40
|
+
update(chain, key) {
|
|
41
|
+
if (chain.length === 0) {
|
|
42
|
+
throw new Error("StaticCertificateChainProvider.update: chain must not be empty");
|
|
43
|
+
}
|
|
44
|
+
this.#chain = chain;
|
|
45
|
+
if (key !== undefined) {
|
|
46
|
+
this.#key = key;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Prevent secrets from leaking through JSON serialization
|
|
50
|
+
toJSON() {
|
|
51
|
+
return { provider: "StaticCertificateChainProvider" };
|
|
52
|
+
}
|
|
53
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
54
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
55
|
+
return "StaticCertificateChainProvider { <in-memory> }";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.StaticCertificateChainProvider = StaticCertificateChainProvider;
|
|
59
|
+
//# sourceMappingURL=certificate_chain_provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certificate_chain_provider.js","sourceRoot":"","sources":["../source/certificate_chain_provider.ts"],"names":[],"mappings":";;;AA4BA;;;;;GAKG;AACH,MAAa,8BAA8B;IACvC,MAAM,CAAgB;IACtB,IAAI,CAAa;IAEjB,YAAY,KAAoB,EAAE,GAAe;QAC7C,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;IACpB,CAAC;IAEM,cAAc;QACjB,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAEM,mBAAmB;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAEM,aAAa;QAChB,OAAO,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED;;;OAGG;IACI,UAAU;QACb,8CAA8C;IAClD,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,KAAoB,EAAE,GAAgB;QAChD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QACpB,CAAC;IACL,CAAC;IAED,0DAA0D;IACnD,MAAM;QACT,OAAO,EAAE,QAAQ,EAAE,gCAAgC,EAAE,CAAC;IAC1D,CAAC;IAED,kEAAkE;IAC3D,CAAC,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC7C,OAAO,gDAAgD,CAAC;IAC5D,CAAC;CACJ;AAvDD,wEAuDC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
/**
|
|
24
24
|
* @module node-opcua-common
|
|
25
25
|
*/
|
|
26
|
-
export {
|
|
26
|
+
export { BuildInfo, DataTypeDefinition, EnumValueType, ModelChangeStructureDataType, // ModelChangeStructure
|
|
27
27
|
RedundantServerDataType, // RedundantServer
|
|
28
|
-
ModelChangeStructureDataType, // ModelChangeStructure
|
|
29
|
-
SubscriptionDiagnosticsDataType, // SubscriptionDiagnostics
|
|
30
28
|
SamplingIntervalDiagnosticsDataType, // SamplingIntervalDiagnostics
|
|
31
29
|
SemanticChangeStructureDataType, // SemanticChangeStructure
|
|
32
|
-
ServerDiagnosticsSummaryDataType,
|
|
30
|
+
ServerDiagnosticsSummaryDataType, ServerState, ServerStatusDataType, // ServerStatus
|
|
31
|
+
ServiceCounterDataType, SessionDiagnosticsDataType, SessionSecurityDiagnosticsDataType, SubscriptionDiagnosticsDataType, // SubscriptionDiagnostics
|
|
32
|
+
TimeZoneDataType } from "node-opcua-types";
|
|
33
33
|
export * from "./applicationurn";
|
|
34
|
+
export * from "./certificate_chain_provider";
|
|
34
35
|
export * from "./opcua_secure_object";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.TimeZoneDataType = exports.
|
|
17
|
+
exports.TimeZoneDataType = exports.SubscriptionDiagnosticsDataType = exports.SessionSecurityDiagnosticsDataType = exports.SessionDiagnosticsDataType = exports.ServiceCounterDataType = exports.ServerStatusDataType = exports.ServerState = exports.ServerDiagnosticsSummaryDataType = exports.SemanticChangeStructureDataType = exports.SamplingIntervalDiagnosticsDataType = exports.RedundantServerDataType = exports.ModelChangeStructureDataType = exports.EnumValueType = exports.DataTypeDefinition = exports.BuildInfo = void 0;
|
|
18
18
|
/*!
|
|
19
19
|
* The MIT License (MIT)
|
|
20
20
|
* Copyright (c) 2022-2025 Sterfive SAS - 833264583 RCS ORLEANS - France (https://www.sterfive.com)
|
|
@@ -41,21 +41,22 @@ exports.TimeZoneDataType = exports.EnumValueType = exports.DataTypeDefinition =
|
|
|
41
41
|
* @module node-opcua-common
|
|
42
42
|
*/
|
|
43
43
|
var node_opcua_types_1 = require("node-opcua-types");
|
|
44
|
-
Object.defineProperty(exports, "
|
|
45
|
-
Object.defineProperty(exports, "
|
|
46
|
-
Object.defineProperty(exports, "
|
|
44
|
+
Object.defineProperty(exports, "BuildInfo", { enumerable: true, get: function () { return node_opcua_types_1.BuildInfo; } });
|
|
45
|
+
Object.defineProperty(exports, "DataTypeDefinition", { enumerable: true, get: function () { return node_opcua_types_1.DataTypeDefinition; } });
|
|
46
|
+
Object.defineProperty(exports, "EnumValueType", { enumerable: true, get: function () { return node_opcua_types_1.EnumValueType; } });
|
|
47
47
|
Object.defineProperty(exports, "ModelChangeStructureDataType", { enumerable: true, get: function () { return node_opcua_types_1.ModelChangeStructureDataType; } });
|
|
48
|
-
Object.defineProperty(exports, "
|
|
48
|
+
Object.defineProperty(exports, "RedundantServerDataType", { enumerable: true, get: function () { return node_opcua_types_1.RedundantServerDataType; } });
|
|
49
49
|
Object.defineProperty(exports, "SamplingIntervalDiagnosticsDataType", { enumerable: true, get: function () { return node_opcua_types_1.SamplingIntervalDiagnosticsDataType; } });
|
|
50
50
|
Object.defineProperty(exports, "SemanticChangeStructureDataType", { enumerable: true, get: function () { return node_opcua_types_1.SemanticChangeStructureDataType; } });
|
|
51
51
|
Object.defineProperty(exports, "ServerDiagnosticsSummaryDataType", { enumerable: true, get: function () { return node_opcua_types_1.ServerDiagnosticsSummaryDataType; } });
|
|
52
|
-
Object.defineProperty(exports, "
|
|
52
|
+
Object.defineProperty(exports, "ServerState", { enumerable: true, get: function () { return node_opcua_types_1.ServerState; } });
|
|
53
|
+
Object.defineProperty(exports, "ServerStatusDataType", { enumerable: true, get: function () { return node_opcua_types_1.ServerStatusDataType; } });
|
|
53
54
|
Object.defineProperty(exports, "ServiceCounterDataType", { enumerable: true, get: function () { return node_opcua_types_1.ServiceCounterDataType; } });
|
|
54
55
|
Object.defineProperty(exports, "SessionDiagnosticsDataType", { enumerable: true, get: function () { return node_opcua_types_1.SessionDiagnosticsDataType; } });
|
|
55
|
-
Object.defineProperty(exports, "
|
|
56
|
-
Object.defineProperty(exports, "
|
|
57
|
-
Object.defineProperty(exports, "EnumValueType", { enumerable: true, get: function () { return node_opcua_types_1.EnumValueType; } });
|
|
56
|
+
Object.defineProperty(exports, "SessionSecurityDiagnosticsDataType", { enumerable: true, get: function () { return node_opcua_types_1.SessionSecurityDiagnosticsDataType; } });
|
|
57
|
+
Object.defineProperty(exports, "SubscriptionDiagnosticsDataType", { enumerable: true, get: function () { return node_opcua_types_1.SubscriptionDiagnosticsDataType; } });
|
|
58
58
|
Object.defineProperty(exports, "TimeZoneDataType", { enumerable: true, get: function () { return node_opcua_types_1.TimeZoneDataType; } });
|
|
59
59
|
__exportStar(require("./applicationurn"), exports);
|
|
60
|
+
__exportStar(require("./certificate_chain_provider"), exports);
|
|
60
61
|
__exportStar(require("./opcua_secure_object"), exports);
|
|
61
62
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;GAEG;AACH,qDAgB0B;AAftB
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;GAEG;AACH,qDAgB0B;AAftB,6GAAA,SAAS,OAAA;AACT,sHAAA,kBAAkB,OAAA;AAClB,iHAAA,aAAa,OAAA;AACb,gIAAA,4BAA4B,OAAA;AAC5B,2HAAA,uBAAuB,OAAA;AACvB,uIAAA,mCAAmC,OAAA;AACnC,mIAAA,+BAA+B,OAAA;AAC/B,oIAAA,gCAAgC,OAAA;AAChC,+GAAA,WAAW,OAAA;AACX,wHAAA,oBAAoB,OAAA;AACpB,0HAAA,sBAAsB,OAAA;AACtB,8HAAA,0BAA0B,OAAA;AAC1B,sIAAA,kCAAkC,OAAA;AAClC,mIAAA,+BAA+B,OAAA;AAC/B,oHAAA,gBAAgB,OAAA;AAGpB,mDAAiC;AACjC,+DAA6C;AAC7C,wDAAsC"}
|
|
@@ -1,32 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-common
|
|
3
3
|
*/
|
|
4
|
-
import { EventEmitter } from "events";
|
|
5
|
-
import { Certificate, PrivateKey } from "node-opcua-crypto/web";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { type Certificate, type PrivateKey } from "node-opcua-crypto/web";
|
|
6
|
+
import type { ICertificateChainProvider } from "./certificate_chain_provider";
|
|
6
7
|
export interface ICertificateKeyPairProvider {
|
|
7
8
|
getCertificate(): Certificate;
|
|
8
|
-
getCertificateChain(): Certificate;
|
|
9
|
+
getCertificateChain(): Certificate[];
|
|
9
10
|
getPrivateKey(): PrivateKey;
|
|
10
11
|
}
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
$$privateKey: null | PrivateKey;
|
|
12
|
+
export interface IHasCertificateFile {
|
|
13
|
+
readonly certificateFile: string;
|
|
14
|
+
readonly privateKeyFile: string;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
18
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
19
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
20
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SecretHolder implements ICertificateChainProvider {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(obj: IHasCertificateFile);
|
|
25
|
+
getCertificate(): Certificate;
|
|
26
|
+
getCertificateChain(): Certificate[];
|
|
27
|
+
getPrivateKey(): PrivateKey;
|
|
28
|
+
/**
|
|
29
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
30
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
31
|
+
*/
|
|
32
|
+
dispose(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Alias for {@link dispose}.
|
|
35
|
+
* Implements `ICertificateChainProvider.invalidate()`.
|
|
36
|
+
*/
|
|
37
|
+
invalidate(): void;
|
|
38
|
+
toJSON(): Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
42
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
43
|
+
* re-reads from disk.
|
|
44
|
+
*
|
|
45
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
46
|
+
* / `$$privateKey = null` pattern.
|
|
47
|
+
*/
|
|
48
|
+
export declare function invalidateCachedSecrets(obj: ICertificateKeyPairProvider): void;
|
|
49
|
+
/**
|
|
50
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
51
|
+
* total size of the chain does not exceed maxSize.
|
|
52
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
53
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
54
|
+
*
|
|
55
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
56
|
+
* @param maxSize - optional byte budget
|
|
57
|
+
* @returns the truncated chain as an array of individual certificates
|
|
58
|
+
*/
|
|
59
|
+
export declare function getPartialCertificateChain(certificateChain?: Certificate | Certificate[] | null, maxSize?: number): Certificate[];
|
|
18
60
|
export interface IOPCUASecureObjectOptions {
|
|
19
61
|
certificateFile?: string;
|
|
20
62
|
privateKeyFile?: string;
|
|
21
63
|
}
|
|
22
64
|
/**
|
|
23
|
-
*
|
|
65
|
+
* An object that provides a certificate and a privateKey.
|
|
66
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
67
|
+
* so they never appear on the instance.
|
|
24
68
|
*/
|
|
25
|
-
export declare class OPCUASecureObject extends EventEmitter implements ICertificateKeyPairProvider {
|
|
69
|
+
export declare class OPCUASecureObject<T extends Record<string | symbol, any> = any> extends EventEmitter<T> implements ICertificateKeyPairProvider, IHasCertificateFile {
|
|
26
70
|
readonly certificateFile: string;
|
|
27
71
|
readonly privateKeyFile: string;
|
|
28
72
|
constructor(options: IOPCUASecureObjectOptions);
|
|
29
73
|
getCertificate(): Certificate;
|
|
30
|
-
getCertificateChain(): Certificate;
|
|
74
|
+
getCertificateChain(): Certificate[];
|
|
31
75
|
getPrivateKey(): PrivateKey;
|
|
32
76
|
}
|
|
@@ -3,52 +3,159 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.OPCUASecureObject = void 0;
|
|
7
|
-
exports.
|
|
6
|
+
exports.OPCUASecureObject = exports.SecretHolder = void 0;
|
|
7
|
+
exports.invalidateCachedSecrets = invalidateCachedSecrets;
|
|
8
8
|
exports.getPartialCertificateChain = getPartialCertificateChain;
|
|
9
9
|
/**
|
|
10
10
|
* @module node-opcua-common
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const node_events_1 = require("node:events");
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_opcua_assert_1 = require("node-opcua-assert");
|
|
15
|
-
const web_1 = require("node-opcua-crypto/web");
|
|
16
15
|
const node_opcua_crypto_1 = require("node-opcua-crypto");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const web_1 = require("node-opcua-crypto/web");
|
|
17
|
+
/**
|
|
18
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
19
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
20
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
21
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
22
|
+
*/
|
|
23
|
+
class SecretHolder {
|
|
24
|
+
#certificateChain = null;
|
|
25
|
+
#privateKey = null;
|
|
26
|
+
#obj;
|
|
27
|
+
constructor(obj) {
|
|
28
|
+
this.#obj = obj;
|
|
29
|
+
}
|
|
30
|
+
getCertificate() {
|
|
31
|
+
// Ensure the chain is loaded before accessing [0]
|
|
32
|
+
const chain = this.getCertificateChain();
|
|
33
|
+
return chain[0];
|
|
34
|
+
}
|
|
35
|
+
getCertificateChain() {
|
|
36
|
+
if (!this.#certificateChain) {
|
|
37
|
+
const file = this.#obj.certificateFile;
|
|
38
|
+
if (!node_fs_1.default.existsSync(file)) {
|
|
39
|
+
throw new Error(`Certificate file must exist: ${file}`);
|
|
40
|
+
}
|
|
41
|
+
const chain = (0, node_opcua_crypto_1.readCertificateChain)(file);
|
|
42
|
+
if (!chain || chain.length === 0) {
|
|
43
|
+
throw new Error(`Invalid certificate chain (length=0) ${file}`);
|
|
44
|
+
}
|
|
45
|
+
this.#certificateChain = chain;
|
|
46
|
+
}
|
|
47
|
+
return this.#certificateChain;
|
|
48
|
+
}
|
|
49
|
+
getPrivateKey() {
|
|
50
|
+
if (!this.#privateKey) {
|
|
51
|
+
const file = this.#obj.privateKeyFile;
|
|
52
|
+
if (!node_fs_1.default.existsSync(file)) {
|
|
53
|
+
throw new Error(`Private key file must exist: ${file}`);
|
|
54
|
+
}
|
|
55
|
+
const key = (0, node_opcua_crypto_1.readPrivateKey)(file);
|
|
56
|
+
if (key instanceof Buffer) {
|
|
57
|
+
throw new Error(`Invalid private key ${file}. Should not be a buffer`);
|
|
58
|
+
}
|
|
59
|
+
this.#privateKey = key;
|
|
60
|
+
}
|
|
61
|
+
return this.#privateKey;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
65
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
66
|
+
*/
|
|
67
|
+
dispose() {
|
|
68
|
+
this.#certificateChain = null;
|
|
69
|
+
this.#privateKey = null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Alias for {@link dispose}.
|
|
73
|
+
* Implements `ICertificateChainProvider.invalidate()`.
|
|
74
|
+
*/
|
|
75
|
+
invalidate() {
|
|
76
|
+
this.dispose();
|
|
77
|
+
}
|
|
78
|
+
// Prevent secrets from leaking through JSON serialization
|
|
79
|
+
toJSON() {
|
|
80
|
+
return { certificateFile: this.#obj.certificateFile, privateKeyFile: this.#obj.privateKeyFile };
|
|
81
|
+
}
|
|
82
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
83
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
84
|
+
return `SecretHolder { certificateFile: "${this.#obj.certificateFile}", privateKeyFile: "${this.#obj.privateKeyFile}" }`;
|
|
85
|
+
}
|
|
20
86
|
}
|
|
21
|
-
|
|
22
|
-
|
|
87
|
+
exports.SecretHolder = SecretHolder;
|
|
88
|
+
/**
|
|
89
|
+
* Module-private WeakMap that associates an ICertificateKeyPairProvider
|
|
90
|
+
* with its SecretHolder. Using a WeakMap means:
|
|
91
|
+
* - The secret holder is invisible from the outside (no enumerable property)
|
|
92
|
+
* - If the owning object is GC'd, the SecretHolder is automatically collected
|
|
93
|
+
*/
|
|
94
|
+
const secretHolders = new WeakMap();
|
|
95
|
+
function getSecretHolder(obj) {
|
|
96
|
+
let holder = secretHolders.get(obj);
|
|
97
|
+
if (!holder) {
|
|
98
|
+
holder = new SecretHolder(obj);
|
|
99
|
+
secretHolders.set(obj, holder);
|
|
100
|
+
}
|
|
101
|
+
return holder;
|
|
23
102
|
}
|
|
24
|
-
|
|
25
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
105
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
106
|
+
* re-reads from disk.
|
|
107
|
+
*
|
|
108
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
109
|
+
* / `$$privateKey = null` pattern.
|
|
110
|
+
*/
|
|
111
|
+
function invalidateCachedSecrets(obj) {
|
|
112
|
+
const holder = secretHolders.get(obj);
|
|
113
|
+
if (holder) {
|
|
114
|
+
holder.dispose();
|
|
115
|
+
}
|
|
26
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
119
|
+
* total size of the chain does not exceed maxSize.
|
|
120
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
121
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
122
|
+
*
|
|
123
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
124
|
+
* @param maxSize - optional byte budget
|
|
125
|
+
* @returns the truncated chain as an array of individual certificates
|
|
126
|
+
*/
|
|
27
127
|
function getPartialCertificateChain(certificateChain, maxSize) {
|
|
28
|
-
if (!certificateChain ||
|
|
29
|
-
|
|
128
|
+
if (!certificateChain ||
|
|
129
|
+
(Array.isArray(certificateChain) && certificateChain.length === 0) ||
|
|
130
|
+
(certificateChain instanceof Buffer && certificateChain.length === 0)) {
|
|
131
|
+
return [];
|
|
30
132
|
}
|
|
133
|
+
const certificates = Array.isArray(certificateChain) ? certificateChain : (0, web_1.split_der)(certificateChain);
|
|
31
134
|
if (maxSize === undefined) {
|
|
32
|
-
return
|
|
135
|
+
return certificates;
|
|
33
136
|
}
|
|
34
|
-
const certificates = (0, web_1.split_der)(certificateChain);
|
|
35
137
|
// at least include first certificate
|
|
36
|
-
|
|
138
|
+
const chainToReturn = [certificates[0]];
|
|
139
|
+
let cumulatedLength = certificates[0].length;
|
|
37
140
|
// Throw if first certificate already exceed maxSize
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${
|
|
141
|
+
if (cumulatedLength > maxSize) {
|
|
142
|
+
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${cumulatedLength}`);
|
|
40
143
|
}
|
|
41
144
|
let index = 1;
|
|
42
|
-
while (index < certificates.length &&
|
|
43
|
-
|
|
145
|
+
while (index < certificates.length && cumulatedLength + certificates[index].length <= maxSize) {
|
|
146
|
+
chainToReturn.push(certificates[index]);
|
|
147
|
+
cumulatedLength += certificates[index].length;
|
|
44
148
|
index++;
|
|
45
149
|
}
|
|
46
|
-
return
|
|
150
|
+
return chainToReturn;
|
|
47
151
|
}
|
|
48
152
|
/**
|
|
49
|
-
*
|
|
153
|
+
* An object that provides a certificate and a privateKey.
|
|
154
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
155
|
+
* so they never appear on the instance.
|
|
50
156
|
*/
|
|
51
|
-
|
|
157
|
+
// biome-ignore lint/suspicious/noExplicitAny: EventEmitter use any
|
|
158
|
+
class OPCUASecureObject extends node_events_1.EventEmitter {
|
|
52
159
|
certificateFile;
|
|
53
160
|
privateKeyFile;
|
|
54
161
|
constructor(options) {
|
|
@@ -59,34 +166,13 @@ class OPCUASecureObject extends events_1.EventEmitter {
|
|
|
59
166
|
this.privateKeyFile = options.privateKeyFile || "invalid private key file";
|
|
60
167
|
}
|
|
61
168
|
getCertificate() {
|
|
62
|
-
|
|
63
|
-
if (!priv.$$certificate) {
|
|
64
|
-
const certChain = this.getCertificateChain();
|
|
65
|
-
priv.$$certificate = (0, web_1.split_der)(certChain)[0];
|
|
66
|
-
}
|
|
67
|
-
return priv.$$certificate;
|
|
169
|
+
return getSecretHolder(this).getCertificate();
|
|
68
170
|
}
|
|
69
171
|
getCertificateChain() {
|
|
70
|
-
|
|
71
|
-
if (!priv.$$certificateChain) {
|
|
72
|
-
(0, node_opcua_assert_1.assert)(fs_1.default.existsSync(this.certificateFile), "Certificate file must exist :" + this.certificateFile);
|
|
73
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
74
|
-
if (priv.$$certificateChain && priv.$$certificateChain.length === 0) {
|
|
75
|
-
// do it again for debug purposes
|
|
76
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
77
|
-
throw new Error("Invalid certificate length = 0 " + this.certificateFile);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return priv.$$certificateChain;
|
|
172
|
+
return getSecretHolder(this).getCertificateChain();
|
|
81
173
|
}
|
|
82
174
|
getPrivateKey() {
|
|
83
|
-
|
|
84
|
-
if (!priv.$$privateKey) {
|
|
85
|
-
(0, node_opcua_assert_1.assert)(fs_1.default.existsSync(this.privateKeyFile), "private file must exist :" + this.privateKeyFile);
|
|
86
|
-
priv.$$privateKey = _load_private_key(this.privateKeyFile);
|
|
87
|
-
}
|
|
88
|
-
(0, node_opcua_assert_1.assert)(!(priv.$$privateKey instanceof Buffer), "should not be a buffer");
|
|
89
|
-
return priv.$$privateKey;
|
|
175
|
+
return getSecretHolder(this).getPrivateKey();
|
|
90
176
|
}
|
|
91
177
|
}
|
|
92
178
|
exports.OPCUASecureObject = OPCUASecureObject;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opcua_secure_object.js","sourceRoot":"","sources":["../source/opcua_secure_object.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"opcua_secure_object.js","sourceRoot":"","sources":["../source/opcua_secure_object.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-common",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.168.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module common",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "mocha",
|
|
7
|
+
"test:check": "tsc --noEmit -p test/tsconfig.json",
|
|
7
8
|
"clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
|
|
8
9
|
"build": "tsc -b",
|
|
9
10
|
"lint": "eslint source/**/*.ts"
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
"types": "./dist/index.d.ts",
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"node-opcua-assert": "2.164.0",
|
|
15
|
-
"node-opcua-crypto": "5.3.
|
|
16
|
-
"node-opcua-types": "2.
|
|
16
|
+
"node-opcua-crypto": "5.3.3",
|
|
17
|
+
"node-opcua-types": "2.168.0"
|
|
17
18
|
},
|
|
18
19
|
"author": "Etienne Rossignon",
|
|
19
20
|
"license": "MIT",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"internet of things"
|
|
31
32
|
],
|
|
32
33
|
"homepage": "http://node-opcua.github.io/",
|
|
33
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "653b6d6df801ca17298308089dee32e5b12102b6",
|
|
34
35
|
"files": [
|
|
35
36
|
"dist",
|
|
36
37
|
"source"
|
package/source/applicationurn.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-common
|
|
3
3
|
*/
|
|
4
|
-
import {createHash} from "crypto";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
5
|
|
|
6
6
|
import { assert } from "node-opcua-assert";
|
|
7
7
|
|
|
8
8
|
export function makeApplicationUrn(hostname: string, suffix: string): string {
|
|
9
|
-
|
|
10
9
|
assert(!suffix.match(/urn:/), "already a application URN ?");
|
|
11
10
|
// beware : Openssl doesn't support urn with length greater than 64 !!
|
|
12
11
|
// sometimes hostname length could be too long ...
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable certificate chain provider for OPC UA endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts how an endpoint obtains its certificate chain and private key,
|
|
5
|
+
* allowing both static (in-memory) and dynamic (disk-based) strategies
|
|
6
|
+
* without monkey-patching.
|
|
7
|
+
*
|
|
8
|
+
* @module node-opcua-common
|
|
9
|
+
*/
|
|
10
|
+
import type { Certificate, PrivateKey } from "node-opcua-crypto/web";
|
|
11
|
+
|
|
12
|
+
import type { ICertificateKeyPairProvider } from "./opcua_secure_object";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Provides a certificate chain and private key to an OPC UA endpoint.
|
|
16
|
+
*
|
|
17
|
+
* Implementations may read from memory, disk, or any other source.
|
|
18
|
+
* See also {@link SecretHolder} which implements this interface for
|
|
19
|
+
* disk-based access with lazy caching.
|
|
20
|
+
*/
|
|
21
|
+
export interface ICertificateChainProvider extends ICertificateKeyPairProvider {
|
|
22
|
+
/**
|
|
23
|
+
* Invalidate any cached values so the next access re-reads
|
|
24
|
+
* from the underlying source. No-op for static providers.
|
|
25
|
+
*/
|
|
26
|
+
invalidate(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Holds a certificate chain and private key in memory.
|
|
31
|
+
*
|
|
32
|
+
* Used as the default provider when push certificate management
|
|
33
|
+
* is NOT installed. The chain can be replaced in-place via `update()`.
|
|
34
|
+
*/
|
|
35
|
+
export class StaticCertificateChainProvider implements ICertificateChainProvider {
|
|
36
|
+
#chain: Certificate[];
|
|
37
|
+
#key: PrivateKey;
|
|
38
|
+
|
|
39
|
+
constructor(chain: Certificate[], key: PrivateKey) {
|
|
40
|
+
this.#chain = chain;
|
|
41
|
+
this.#key = key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public getCertificate(): Certificate {
|
|
45
|
+
return this.#chain[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public getCertificateChain(): Certificate[] {
|
|
49
|
+
return this.#chain;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public getPrivateKey(): PrivateKey {
|
|
53
|
+
return this.#key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* No-op for static provider — the chain is already in memory.
|
|
58
|
+
* Use `update()` to replace the chain explicitly.
|
|
59
|
+
*/
|
|
60
|
+
public invalidate(): void {
|
|
61
|
+
// nothing to invalidate for a static provider
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Replace the certificate chain and optionally the private key.
|
|
66
|
+
*
|
|
67
|
+
* This immediately affects all consumers that call
|
|
68
|
+
* `getCertificateChain()` on this provider (including
|
|
69
|
+
* endpoint descriptions with dynamic `serverCertificate` getters).
|
|
70
|
+
*/
|
|
71
|
+
public update(chain: Certificate[], key?: PrivateKey): void {
|
|
72
|
+
if (chain.length === 0) {
|
|
73
|
+
throw new Error("StaticCertificateChainProvider.update: chain must not be empty");
|
|
74
|
+
}
|
|
75
|
+
this.#chain = chain;
|
|
76
|
+
if (key !== undefined) {
|
|
77
|
+
this.#key = key;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Prevent secrets from leaking through JSON serialization
|
|
82
|
+
public toJSON(): Record<string, string> {
|
|
83
|
+
return { provider: "StaticCertificateChainProvider" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
87
|
+
public [Symbol.for("nodejs.util.inspect.custom")](): string {
|
|
88
|
+
return "StaticCertificateChainProvider { <in-memory> }";
|
|
89
|
+
}
|
|
90
|
+
}
|
package/source/index.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
10
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
11
|
* subject to the following conditions:
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* The above copyright notice and this permission notice shall be included in all
|
|
14
14
|
* copies or substantial portions of the Software.
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
17
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
18
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
@@ -24,22 +24,23 @@
|
|
|
24
24
|
* @module node-opcua-common
|
|
25
25
|
*/
|
|
26
26
|
export {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ModelChangeStructureDataType,
|
|
31
|
-
|
|
27
|
+
BuildInfo,
|
|
28
|
+
DataTypeDefinition,
|
|
29
|
+
EnumValueType,
|
|
30
|
+
ModelChangeStructureDataType, // ModelChangeStructure
|
|
31
|
+
RedundantServerDataType, // RedundantServer
|
|
32
32
|
SamplingIntervalDiagnosticsDataType, // SamplingIntervalDiagnostics
|
|
33
33
|
SemanticChangeStructureDataType, // SemanticChangeStructure
|
|
34
34
|
ServerDiagnosticsSummaryDataType,
|
|
35
|
-
|
|
35
|
+
ServerState,
|
|
36
|
+
ServerStatusDataType, // ServerStatus
|
|
36
37
|
ServiceCounterDataType,
|
|
37
38
|
SessionDiagnosticsDataType,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
TimeZoneDataType,
|
|
39
|
+
SessionSecurityDiagnosticsDataType,
|
|
40
|
+
SubscriptionDiagnosticsDataType, // SubscriptionDiagnostics
|
|
41
|
+
TimeZoneDataType
|
|
42
42
|
} from "node-opcua-types";
|
|
43
43
|
|
|
44
44
|
export * from "./applicationurn";
|
|
45
|
+
export * from "./certificate_chain_provider";
|
|
45
46
|
export * from "./opcua_secure_object";
|
|
@@ -1,57 +1,172 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-common
|
|
3
3
|
*/
|
|
4
|
-
import { EventEmitter } from "events";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import fs from "node:fs";
|
|
7
6
|
import { assert } from "node-opcua-assert";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { readCertificateChain, readPrivateKey } from "node-opcua-crypto";
|
|
8
|
+
import { type Certificate, type PrivateKey, split_der } from "node-opcua-crypto/web";
|
|
9
|
+
|
|
10
|
+
import type { ICertificateChainProvider } from "./certificate_chain_provider";
|
|
11
|
+
|
|
10
12
|
export interface ICertificateKeyPairProvider {
|
|
11
13
|
getCertificate(): Certificate;
|
|
12
|
-
getCertificateChain(): Certificate;
|
|
14
|
+
getCertificateChain(): Certificate[];
|
|
13
15
|
getPrivateKey(): PrivateKey;
|
|
14
16
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
function _load_certificate(certificateFilename: string): Certificate {
|
|
21
|
-
const der = readCertificate(certificateFilename);
|
|
22
|
-
return der;
|
|
17
|
+
|
|
18
|
+
export interface IHasCertificateFile {
|
|
19
|
+
readonly certificateFile: string;
|
|
20
|
+
readonly privateKeyFile: string;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
25
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
26
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
27
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
28
|
+
*/
|
|
29
|
+
export class SecretHolder implements ICertificateChainProvider {
|
|
30
|
+
#certificateChain: Certificate[] | null = null;
|
|
31
|
+
#privateKey: PrivateKey | null = null;
|
|
32
|
+
#obj: IHasCertificateFile;
|
|
33
|
+
|
|
34
|
+
constructor(obj: IHasCertificateFile) {
|
|
35
|
+
this.#obj = obj;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getCertificate(): Certificate {
|
|
39
|
+
// Ensure the chain is loaded before accessing [0]
|
|
40
|
+
const chain = this.getCertificateChain();
|
|
41
|
+
return chain[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public getCertificateChain(): Certificate[] {
|
|
45
|
+
if (!this.#certificateChain) {
|
|
46
|
+
const file = this.#obj.certificateFile;
|
|
47
|
+
if (!fs.existsSync(file)) {
|
|
48
|
+
throw new Error(`Certificate file must exist: ${file}`);
|
|
49
|
+
}
|
|
50
|
+
const chain = readCertificateChain(file);
|
|
51
|
+
if (!chain || chain.length === 0) {
|
|
52
|
+
throw new Error(`Invalid certificate chain (length=0) ${file}`);
|
|
53
|
+
}
|
|
54
|
+
this.#certificateChain = chain;
|
|
55
|
+
}
|
|
56
|
+
return this.#certificateChain;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public getPrivateKey(): PrivateKey {
|
|
60
|
+
if (!this.#privateKey) {
|
|
61
|
+
const file = this.#obj.privateKeyFile;
|
|
62
|
+
if (!fs.existsSync(file)) {
|
|
63
|
+
throw new Error(`Private key file must exist: ${file}`);
|
|
64
|
+
}
|
|
65
|
+
const key = readPrivateKey(file);
|
|
66
|
+
if (key instanceof Buffer) {
|
|
67
|
+
throw new Error(`Invalid private key ${file}. Should not be a buffer`);
|
|
68
|
+
}
|
|
69
|
+
this.#privateKey = key;
|
|
70
|
+
}
|
|
71
|
+
return this.#privateKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
76
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
77
|
+
*/
|
|
78
|
+
public dispose(): void {
|
|
79
|
+
this.#certificateChain = null;
|
|
80
|
+
this.#privateKey = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Alias for {@link dispose}.
|
|
85
|
+
* Implements `ICertificateChainProvider.invalidate()`.
|
|
86
|
+
*/
|
|
87
|
+
public invalidate(): void {
|
|
88
|
+
this.dispose();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prevent secrets from leaking through JSON serialization
|
|
92
|
+
public toJSON(): Record<string, string> {
|
|
93
|
+
return { certificateFile: this.#obj.certificateFile, privateKeyFile: this.#obj.privateKeyFile };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
97
|
+
public [Symbol.for("nodejs.util.inspect.custom")](): string {
|
|
98
|
+
return `SecretHolder { certificateFile: "${this.#obj.certificateFile}", privateKeyFile: "${this.#obj.privateKeyFile}" }`;
|
|
99
|
+
}
|
|
27
100
|
}
|
|
28
101
|
|
|
29
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Module-private WeakMap that associates an ICertificateKeyPairProvider
|
|
104
|
+
* with its SecretHolder. Using a WeakMap means:
|
|
105
|
+
* - The secret holder is invisible from the outside (no enumerable property)
|
|
106
|
+
* - If the owning object is GC'd, the SecretHolder is automatically collected
|
|
107
|
+
*/
|
|
108
|
+
const secretHolders = new WeakMap<object, SecretHolder>();
|
|
30
109
|
|
|
31
|
-
|
|
110
|
+
function getSecretHolder(obj: ICertificateKeyPairProvider & IHasCertificateFile): SecretHolder {
|
|
111
|
+
let holder = secretHolders.get(obj);
|
|
112
|
+
if (!holder) {
|
|
113
|
+
holder = new SecretHolder(obj);
|
|
114
|
+
secretHolders.set(obj, holder);
|
|
115
|
+
}
|
|
116
|
+
return holder;
|
|
32
117
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
121
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
122
|
+
* re-reads from disk.
|
|
123
|
+
*
|
|
124
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
125
|
+
* / `$$privateKey = null` pattern.
|
|
126
|
+
*/
|
|
127
|
+
export function invalidateCachedSecrets(obj: ICertificateKeyPairProvider): void {
|
|
128
|
+
const holder = secretHolders.get(obj);
|
|
129
|
+
if (holder) {
|
|
130
|
+
holder.dispose();
|
|
37
131
|
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
136
|
+
* total size of the chain does not exceed maxSize.
|
|
137
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
138
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
139
|
+
*
|
|
140
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
141
|
+
* @param maxSize - optional byte budget
|
|
142
|
+
* @returns the truncated chain as an array of individual certificates
|
|
143
|
+
*/
|
|
144
|
+
export function getPartialCertificateChain(certificateChain?: Certificate | Certificate[] | null, maxSize?: number): Certificate[] {
|
|
145
|
+
if (
|
|
146
|
+
!certificateChain ||
|
|
147
|
+
(Array.isArray(certificateChain) && certificateChain.length === 0) ||
|
|
148
|
+
(certificateChain instanceof Buffer && certificateChain.length === 0)
|
|
149
|
+
) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const certificates = Array.isArray(certificateChain) ? certificateChain : split_der(certificateChain);
|
|
38
153
|
if (maxSize === undefined) {
|
|
39
|
-
return
|
|
154
|
+
return certificates;
|
|
40
155
|
}
|
|
41
|
-
const certificates = split_der(certificateChain);
|
|
42
156
|
// at least include first certificate
|
|
43
|
-
|
|
157
|
+
const chainToReturn: Certificate[] = [certificates[0]];
|
|
158
|
+
let cumulatedLength = certificates[0].length;
|
|
44
159
|
// Throw if first certificate already exceed maxSize
|
|
45
|
-
if (
|
|
46
|
-
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${
|
|
160
|
+
if (cumulatedLength > maxSize) {
|
|
161
|
+
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${cumulatedLength}`);
|
|
47
162
|
}
|
|
48
163
|
let index = 1;
|
|
49
|
-
while (index < certificates.length &&
|
|
50
|
-
|
|
164
|
+
while (index < certificates.length && cumulatedLength + certificates[index].length <= maxSize) {
|
|
165
|
+
chainToReturn.push(certificates[index]);
|
|
166
|
+
cumulatedLength += certificates[index].length;
|
|
51
167
|
index++;
|
|
52
168
|
}
|
|
53
|
-
return
|
|
54
|
-
|
|
169
|
+
return chainToReturn;
|
|
55
170
|
}
|
|
56
171
|
|
|
57
172
|
export interface IOPCUASecureObjectOptions {
|
|
@@ -60,9 +175,16 @@ export interface IOPCUASecureObjectOptions {
|
|
|
60
175
|
}
|
|
61
176
|
|
|
62
177
|
/**
|
|
63
|
-
*
|
|
178
|
+
* An object that provides a certificate and a privateKey.
|
|
179
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
180
|
+
* so they never appear on the instance.
|
|
64
181
|
*/
|
|
65
|
-
|
|
182
|
+
|
|
183
|
+
// biome-ignore lint/suspicious/noExplicitAny: EventEmitter use any
|
|
184
|
+
export class OPCUASecureObject<T extends Record<string | symbol, any> = any>
|
|
185
|
+
extends EventEmitter<T>
|
|
186
|
+
implements ICertificateKeyPairProvider, IHasCertificateFile
|
|
187
|
+
{
|
|
66
188
|
public readonly certificateFile: string;
|
|
67
189
|
public readonly privateKeyFile: string;
|
|
68
190
|
|
|
@@ -70,41 +192,19 @@ export class OPCUASecureObject extends EventEmitter implements ICertificateKeyPa
|
|
|
70
192
|
super();
|
|
71
193
|
assert(typeof options.certificateFile === "string");
|
|
72
194
|
assert(typeof options.privateKeyFile === "string");
|
|
73
|
-
|
|
74
195
|
this.certificateFile = options.certificateFile || "invalid certificate file";
|
|
75
196
|
this.privateKeyFile = options.privateKeyFile || "invalid private key file";
|
|
76
197
|
}
|
|
77
198
|
|
|
78
199
|
public getCertificate(): Certificate {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return priv.$$certificate;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public getCertificateChain(): Certificate {
|
|
88
|
-
const priv = this as unknown as ICertificateKeyPairProviderPriv;
|
|
89
|
-
if (!priv.$$certificateChain) {
|
|
90
|
-
assert(fs.existsSync(this.certificateFile), "Certificate file must exist :" + this.certificateFile);
|
|
91
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
92
|
-
if (priv.$$certificateChain && priv.$$certificateChain.length === 0) {
|
|
93
|
-
// do it again for debug purposes
|
|
94
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
95
|
-
throw new Error("Invalid certificate length = 0 " + this.certificateFile);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return priv.$$certificateChain;
|
|
200
|
+
return getSecretHolder(this).getCertificate();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public getCertificateChain(): Certificate[] {
|
|
204
|
+
return getSecretHolder(this).getCertificateChain();
|
|
99
205
|
}
|
|
100
206
|
|
|
101
207
|
public getPrivateKey(): PrivateKey {
|
|
102
|
-
|
|
103
|
-
if (!priv.$$privateKey) {
|
|
104
|
-
assert(fs.existsSync(this.privateKeyFile), "private file must exist :" + this.privateKeyFile);
|
|
105
|
-
priv.$$privateKey = _load_private_key(this.privateKeyFile);
|
|
106
|
-
}
|
|
107
|
-
assert(!(priv.$$privateKey instanceof Buffer), "should not be a buffer");
|
|
108
|
-
return priv.$$privateKey;
|
|
208
|
+
return getSecretHolder(this).getPrivateKey();
|
|
109
209
|
}
|
|
110
210
|
}
|