node-opcua-server-configuration 2.163.0 → 2.164.2

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.
Files changed (100) hide show
  1. package/dist/clientTools/certificate_types.d.ts +17 -0
  2. package/dist/clientTools/certificate_types.js +20 -0
  3. package/dist/clientTools/certificate_types.js.map +1 -0
  4. package/dist/clientTools/get_certificate_key_type.d.ts +6 -0
  5. package/dist/clientTools/get_certificate_key_type.js +55 -0
  6. package/dist/clientTools/get_certificate_key_type.js.map +1 -0
  7. package/dist/clientTools/index.d.ts +2 -1
  8. package/dist/clientTools/index.js +2 -17
  9. package/dist/clientTools/index.js.map +1 -1
  10. package/dist/clientTools/push_certificate_management_client.d.ts +10 -10
  11. package/dist/clientTools/push_certificate_management_client.js +85 -89
  12. package/dist/clientTools/push_certificate_management_client.js.map +1 -1
  13. package/dist/index.d.ts +9 -7
  14. package/dist/index.js +9 -23
  15. package/dist/index.js.map +1 -1
  16. package/dist/push_certificate_manager.d.ts +4 -4
  17. package/dist/push_certificate_manager.js +1 -2
  18. package/dist/server/certificate_validation.d.ts +15 -0
  19. package/dist/server/certificate_validation.js +76 -0
  20. package/dist/server/certificate_validation.js.map +1 -0
  21. package/dist/server/file_transaction_manager.d.ts +30 -0
  22. package/dist/server/file_transaction_manager.js +223 -0
  23. package/dist/server/file_transaction_manager.js.map +1 -0
  24. package/dist/server/install_certificate_file_watcher.d.ts +1 -1
  25. package/dist/server/install_certificate_file_watcher.js +8 -14
  26. package/dist/server/install_certificate_file_watcher.js.map +1 -1
  27. package/dist/server/install_push_certitifate_management.d.ts +6 -6
  28. package/dist/server/install_push_certitifate_management.js +61 -65
  29. package/dist/server/install_push_certitifate_management.js.map +1 -1
  30. package/dist/server/promote_trust_list.d.ts +1 -1
  31. package/dist/server/promote_trust_list.js +323 -82
  32. package/dist/server/promote_trust_list.js.map +1 -1
  33. package/dist/server/push_certificate_manager/apply_changes.d.ts +3 -0
  34. package/dist/server/push_certificate_manager/apply_changes.js +59 -0
  35. package/dist/server/push_certificate_manager/apply_changes.js.map +1 -0
  36. package/dist/server/push_certificate_manager/create_signing_request.d.ts +5 -0
  37. package/dist/server/push_certificate_manager/create_signing_request.js +108 -0
  38. package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -0
  39. package/dist/server/push_certificate_manager/get_rejected_list.d.ts +3 -0
  40. package/dist/server/push_certificate_manager/get_rejected_list.js +46 -0
  41. package/dist/server/push_certificate_manager/get_rejected_list.js.map +1 -0
  42. package/dist/server/push_certificate_manager/internal_context.d.ts +35 -0
  43. package/dist/server/push_certificate_manager/internal_context.js +45 -0
  44. package/dist/server/push_certificate_manager/internal_context.js.map +1 -0
  45. package/dist/server/push_certificate_manager/subject_to_string.d.ts +3 -0
  46. package/dist/server/push_certificate_manager/subject_to_string.js +27 -0
  47. package/dist/server/push_certificate_manager/subject_to_string.js.map +1 -0
  48. package/dist/server/push_certificate_manager/update_certificate.d.ts +5 -0
  49. package/dist/server/push_certificate_manager/update_certificate.js +132 -0
  50. package/dist/server/push_certificate_manager/update_certificate.js.map +1 -0
  51. package/dist/server/push_certificate_manager/util.d.ts +29 -0
  52. package/dist/server/push_certificate_manager/util.js +117 -0
  53. package/dist/server/push_certificate_manager/util.js.map +1 -0
  54. package/dist/server/push_certificate_manager_helpers.d.ts +5 -2
  55. package/dist/server/push_certificate_manager_helpers.js +109 -112
  56. package/dist/server/push_certificate_manager_helpers.js.map +1 -1
  57. package/dist/server/push_certificate_manager_server_impl.d.ts +16 -29
  58. package/dist/server/push_certificate_manager_server_impl.js +49 -437
  59. package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
  60. package/dist/server/roles_and_permissions.d.ts +1 -1
  61. package/dist/server/roles_and_permissions.js +24 -27
  62. package/dist/server/roles_and_permissions.js.map +1 -1
  63. package/dist/server/tools.d.ts +1 -1
  64. package/dist/server/tools.js +7 -13
  65. package/dist/server/tools.js.map +1 -1
  66. package/dist/server/trust_list_server.d.ts +2 -2
  67. package/dist/server/trust_list_server.js +40 -29
  68. package/dist/server/trust_list_server.js.map +1 -1
  69. package/dist/standard_certificate_types.js +6 -9
  70. package/dist/standard_certificate_types.js.map +1 -1
  71. package/dist/trust_list.d.ts +2 -2
  72. package/dist/trust_list.js +1 -2
  73. package/dist/trust_list_impl.js +1 -2
  74. package/dist/trust_list_impl.js.map +1 -1
  75. package/package.json +29 -30
  76. package/source/clientTools/certificate_types.ts +21 -0
  77. package/source/clientTools/get_certificate_key_type.ts +73 -0
  78. package/source/clientTools/index.ts +2 -1
  79. package/source/clientTools/push_certificate_management_client.ts +49 -44
  80. package/source/index.ts +9 -7
  81. package/source/push_certificate_manager.ts +15 -17
  82. package/source/server/certificate_validation.ts +103 -0
  83. package/source/server/file_transaction_manager.ts +253 -0
  84. package/source/server/install_certificate_file_watcher.ts +15 -11
  85. package/source/server/install_push_certitifate_management.ts +52 -51
  86. package/source/server/promote_trust_list.ts +362 -73
  87. package/source/server/push_certificate_manager/apply_changes.ts +63 -0
  88. package/source/server/push_certificate_manager/create_signing_request.ts +137 -0
  89. package/source/server/push_certificate_manager/get_rejected_list.ts +63 -0
  90. package/source/server/push_certificate_manager/internal_context.ts +63 -0
  91. package/source/server/push_certificate_manager/subject_to_string.ts +25 -0
  92. package/source/server/push_certificate_manager/update_certificate.ts +201 -0
  93. package/source/server/push_certificate_manager/util.ts +145 -0
  94. package/source/server/push_certificate_manager_helpers.ts +61 -51
  95. package/source/server/push_certificate_manager_server_impl.ts +94 -553
  96. package/source/server/roles_and_permissions.ts +7 -8
  97. package/source/server/tools.ts +2 -5
  98. package/source/server/trust_list_server.ts +24 -9
  99. package/source/standard_certificate_types.ts +2 -3
  100. package/source/trust_list.ts +26 -33
@@ -0,0 +1,137 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { CertificateManager } from "node-opcua-certificate-manager";
5
+ import { convertPEMtoDER, type DirectoryName, exploreCertificate, readCertificate } from "node-opcua-crypto";
6
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
7
+ import { NodeId, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
8
+ import type { SubjectOptions } from "node-opcua-pki";
9
+ import { StatusCodes } from "node-opcua-status-code";
10
+
11
+ import type { CreateSigningRequestResult } from "../../push_certificate_manager.js";
12
+ import type { PushCertificateManagerInternalContext } from "./internal_context.js";
13
+ import { subjectToString } from "./subject_to_string.js";
14
+ import { resolveCertificateGroupContext } from "./util.js";
15
+
16
+ const warningLog = make_warningLog("ServerConfiguration");
17
+ const errorLog = make_errorLog("ServerConfiguration");
18
+ const debugLog = make_debugLog("ServerConfiguration");
19
+ const doDebug = checkDebugFlag("ServerConfiguration");
20
+
21
+ export async function executeCreateSigningRequest(
22
+ serverImpl: PushCertificateManagerInternalContext,
23
+ certificateGroupId: NodeId | string,
24
+ certificateTypeId: NodeId | string,
25
+ subjectName: string | SubjectOptions | null,
26
+ regeneratePrivateKey?: boolean,
27
+ nonce?: Buffer
28
+ ): Promise<CreateSigningRequestResult> {
29
+ // Resolve context using our util
30
+ const context = resolveCertificateGroupContext(serverImpl, certificateGroupId);
31
+ if (context.statusCode.isNotGood() || !context.certificateManager) {
32
+ doDebug && debugLog(" cannot find group ", certificateGroupId);
33
+ return { statusCode: StatusCodes.BadInvalidArgument };
34
+ }
35
+
36
+ const { certificateManager, allowedTypes } = context;
37
+
38
+ // Validate Certificate Type
39
+ if (certificateTypeId) {
40
+ let typeNodeId: NodeId;
41
+ if (typeof certificateTypeId === "string") {
42
+ if (certificateTypeId !== "") {
43
+ try {
44
+ typeNodeId = resolveNodeId(certificateTypeId);
45
+ } catch {
46
+ warningLog("Invalid certificateTypeId string:", certificateTypeId);
47
+ return { statusCode: StatusCodes.BadInvalidArgument };
48
+ }
49
+ if (!sameNodeId(typeNodeId, NodeId.nullNodeId)) {
50
+ const isValidType = allowedTypes?.some((t) => sameNodeId(t, typeNodeId));
51
+ if (!isValidType) {
52
+ warningLog("certificateTypeId is not in the allowed types for this certificate group:", certificateTypeId);
53
+ return { statusCode: StatusCodes.BadNotSupported };
54
+ }
55
+ }
56
+ }
57
+ } else {
58
+ typeNodeId = certificateTypeId;
59
+ if (!sameNodeId(typeNodeId, NodeId.nullNodeId)) {
60
+ const isValidType = allowedTypes?.some((t) => sameNodeId(t, typeNodeId));
61
+ if (!isValidType) {
62
+ warningLog("certificateTypeId is not in the allowed types for this certificate group:", certificateTypeId);
63
+ return { statusCode: StatusCodes.BadNotSupported };
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ // Resolve Subject Name
70
+ if (!subjectName) {
71
+ const currentCertificateFilename = path.join(certificateManager.rootDir, "own/certs/certificate.pem");
72
+ try {
73
+ const certificate = readCertificate(currentCertificateFilename);
74
+ const e = exploreCertificate(certificate);
75
+ subjectName = subjectToString(e.tbsCertificate.subject as SubjectOptions & DirectoryName);
76
+ warningLog("reusing existing certificate subjectName = ", subjectName);
77
+ } catch (err) {
78
+ errorLog(
79
+ "Cannot find existing certificate to extract subjectName",
80
+ currentCertificateFilename,
81
+ ":",
82
+ (err as Error).message
83
+ );
84
+ return { statusCode: StatusCodes.BadInvalidState };
85
+ }
86
+ }
87
+
88
+ if (typeof subjectName !== "string") {
89
+ return { statusCode: StatusCodes.BadInternalError };
90
+ }
91
+
92
+ // Regenerate Private Key Logic
93
+ if (regeneratePrivateKey) {
94
+ if (!nonce || nonce.length < 32) {
95
+ warningLog("nonce should be provided when regeneratePrivateKey is set, and length shall be at least 32 bytes");
96
+ return { statusCode: StatusCodes.BadInvalidArgument };
97
+ }
98
+
99
+ const volatileTmp = await serverImpl.fileTransactionManager.getTmpDir();
100
+ const tmpPKI = path.join(volatileTmp, `pki${crypto.randomUUID()}`);
101
+
102
+ const tempCertificateManager = new CertificateManager({
103
+ keySize: certificateManager.keySize,
104
+ location: tmpPKI
105
+ });
106
+
107
+ doDebug && debugLog("generating a new private key ...");
108
+ await tempCertificateManager.initialize();
109
+
110
+ serverImpl.tmpCertificateManager = tempCertificateManager;
111
+
112
+ const generatedPrivateKeyPEM = await fs.promises.readFile(tempCertificateManager.privateKey, "utf8");
113
+ await serverImpl.fileTransactionManager.stageFile(certificateManager.privateKey, generatedPrivateKeyPEM, "utf8");
114
+
115
+ serverImpl.fileTransactionManager.addCleanupTask(async () => {
116
+ await tempCertificateManager.dispose();
117
+ serverImpl.tmpCertificateManager = undefined;
118
+ });
119
+ }
120
+
121
+ const options = {
122
+ applicationUri: serverImpl.applicationUri,
123
+ subject: subjectName
124
+ };
125
+
126
+ const activeCertificateManager = serverImpl.tmpCertificateManager || certificateManager;
127
+
128
+ await activeCertificateManager.initialize();
129
+ const csrFile = await activeCertificateManager.createCertificateRequest(options);
130
+ const csrPEM = await fs.promises.readFile(csrFile, "utf8");
131
+ const certificateSigningRequest = convertPEMtoDER(csrPEM);
132
+
133
+ return {
134
+ certificateSigningRequest,
135
+ statusCode: StatusCodes.Good
136
+ };
137
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { CertificateManager } from "node-opcua-certificate-manager";
4
+ import { convertPEMtoDER } from "node-opcua-crypto";
5
+ import { StatusCodes } from "node-opcua-status-code";
6
+ import type { GetRejectedListResult } from "../../push_certificate_manager.js";
7
+ import type { PushCertificateManagerInternalContext } from "./internal_context.js";
8
+
9
+ interface FileData {
10
+ filename: string;
11
+ stat: {
12
+ mtime: Date;
13
+ };
14
+ }
15
+
16
+ async function extractRejectedList(group: CertificateManager | undefined, certificateList: FileData[]): Promise<void> {
17
+ if (!group) {
18
+ return;
19
+ }
20
+ const rejectedFolder = path.join(group.rootDir, "rejected");
21
+ try {
22
+ const files = await fs.promises.readdir(rejectedFolder);
23
+
24
+ const promises: Promise<fs.Stats>[] = [];
25
+ for (const certFile of files) {
26
+ promises.push(fs.promises.stat(path.join(rejectedFolder, certFile)));
27
+ }
28
+ const stats = await Promise.all(promises);
29
+
30
+ for (let i = 0; i < stats.length; i++) {
31
+ certificateList.push({
32
+ filename: path.join(rejectedFolder, files[i]),
33
+ stat: stats[i]
34
+ });
35
+ }
36
+ } catch (_err) {
37
+ // Directory might not exist yet, ignore
38
+ }
39
+ }
40
+
41
+ export async function executeGetRejectedList(serverImpl: PushCertificateManagerInternalContext): Promise<GetRejectedListResult> {
42
+ const list: FileData[] = [];
43
+
44
+ await extractRejectedList(serverImpl.applicationGroup, list);
45
+ await extractRejectedList(serverImpl.userTokenGroup, list);
46
+ await extractRejectedList(serverImpl.httpsGroup, list);
47
+
48
+ // sort list from newer file to older file
49
+ list.sort((a: FileData, b: FileData) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
50
+
51
+ const promises: Promise<string>[] = [];
52
+ for (const item of list) {
53
+ promises.push(fs.promises.readFile(item.filename, "utf8"));
54
+ }
55
+ const certificatesPEM: string[] = await Promise.all(promises);
56
+
57
+ const certificates: Buffer[] = certificatesPEM.map(convertPEMtoDER);
58
+
59
+ return {
60
+ certificates,
61
+ statusCode: StatusCodes.Good
62
+ };
63
+ }
@@ -0,0 +1,63 @@
1
+ import type { CertificateManager } from "node-opcua-certificate-manager";
2
+ import type { NodeId } from "node-opcua-nodeid";
3
+ import { FileTransactionManager } from "../file_transaction_manager.js";
4
+
5
+ export type ActionQueue = (() => Promise<void>)[];
6
+
7
+ export interface IPushCertificateManagerServer {
8
+ applicationGroup?: CertificateManager;
9
+ userTokenGroup?: CertificateManager;
10
+ httpsGroup?: CertificateManager;
11
+ applicationUri: string;
12
+
13
+ getCertificateManager(groupName: string): CertificateManager | null;
14
+ getCertificateTypes(groupName: string): NodeId[] | undefined;
15
+ emit(eventName: string | symbol, ...args: unknown[]): boolean;
16
+ }
17
+
18
+ export class PushCertificateManagerInternalContext {
19
+ public readonly map: { [key: string]: CertificateManager } = {};
20
+ public readonly certificateTypes: { [key: string]: NodeId[] } = {};
21
+ public readonly fileTransactionManager = new FileTransactionManager();
22
+ public tmpCertificateManager?: CertificateManager;
23
+ public actionQueue: ActionQueue = [];
24
+ public operationInProgress = false;
25
+
26
+ constructor(private readonly server: IPushCertificateManagerServer) {}
27
+
28
+ get applicationGroup() {
29
+ return this.server.applicationGroup;
30
+ }
31
+ get userTokenGroup() {
32
+ return this.server.userTokenGroup;
33
+ }
34
+ get httpsGroup() {
35
+ return this.server.httpsGroup;
36
+ }
37
+ get applicationUri() {
38
+ return this.server.applicationUri;
39
+ }
40
+
41
+ getCertificateManager(groupName: string) {
42
+ return this.server.getCertificateManager(groupName);
43
+ }
44
+ getCertificateTypes(groupName: string) {
45
+ return this.server.getCertificateTypes(groupName);
46
+ }
47
+ emit(eventName: string | symbol, ...args: unknown[]) {
48
+ return this.server.emit(eventName, ...args);
49
+ }
50
+
51
+ public async dispose(): Promise<void> {
52
+ if (this.tmpCertificateManager) {
53
+ await this.tmpCertificateManager.dispose();
54
+ this.tmpCertificateManager = undefined;
55
+ }
56
+
57
+ if (this.fileTransactionManager) {
58
+ await this.fileTransactionManager.abortTransaction();
59
+ }
60
+
61
+ this.actionQueue.length = 0;
62
+ }
63
+ }
@@ -0,0 +1,25 @@
1
+ import type { DirectoryName } from "node-opcua-crypto/web";
2
+ import type { SubjectOptions } from "node-opcua-pki";
3
+
4
+ export function subjectToString(subject: SubjectOptions & DirectoryName): string {
5
+ let s = "";
6
+ if (subject.commonName) s += `/CN=${subject.commonName}`;
7
+
8
+ if (subject.country) s += `/C=${subject.country}`;
9
+ if (subject.countryName) s += `/C=${subject.countryName}`;
10
+
11
+ if (subject.domainComponent) s += `/DC=${subject.domainComponent}`;
12
+
13
+ if (subject.locality) s += `/L=${subject.locality}`;
14
+ if (subject.localityName) s += `/L=${subject.localityName}`;
15
+
16
+ if (subject.organization) s += `/O=${subject.organization}`;
17
+ if (subject.organizationName) s += `/O=${subject.organizationName}`;
18
+
19
+ if (subject.organizationUnitName) s += `/OU=${subject.organizationUnitName}`;
20
+
21
+ if (subject.state) s += `/ST=${subject.state}`;
22
+ if (subject.stateOrProvinceName) s += `/ST=${subject.stateOrProvinceName}`;
23
+
24
+ return s;
25
+ }
@@ -0,0 +1,201 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { assert } from "node-opcua-assert";
4
+ import type { ByteString } from "node-opcua-basic-types";
5
+ import type { CertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
6
+ import { readPrivateKey } from "node-opcua-crypto";
7
+ import {
8
+ certificateMatchesPrivateKey,
9
+ coercePEMorDerToPrivateKey,
10
+ coercePrivateKeyPem,
11
+ makeSHA1Thumbprint,
12
+ type PrivateKey,
13
+ toPem
14
+ } from "node-opcua-crypto/web";
15
+ import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
16
+ import type { NodeId } from "node-opcua-nodeid";
17
+ import { StatusCodes } from "node-opcua-status-code";
18
+
19
+ import type { UpdateCertificateResult } from "../../push_certificate_manager.js";
20
+ import { validateCertificateAndChain } from "../certificate_validation.js";
21
+ import type { PushCertificateManagerInternalContext } from "./internal_context.js";
22
+ import { resolveCertificateGroupContext, validateCertificateType } from "./util.js";
23
+
24
+ const warningLog = make_warningLog("ServerConfiguration");
25
+ const debugLog = make_debugLog("ServerConfiguration");
26
+ const doDebug = checkDebugFlag("ServerConfiguration");
27
+
28
+ // Helper: Stage issuer certificates to temporary files
29
+ async function preInstallIssuerCertificates(
30
+ serverImpl: PushCertificateManagerInternalContext,
31
+ certificateManager: CertificateManager,
32
+ issuerCertificates: ByteString[] | undefined
33
+ ): Promise<void> {
34
+ if (issuerCertificates && issuerCertificates.length > 0) {
35
+ const issuerFolder = certificateManager.issuersCertFolder;
36
+ await fs.promises.mkdir(issuerFolder, { recursive: true });
37
+
38
+ for (let i = 0; i < issuerCertificates.length; i++) {
39
+ const issuerCert = issuerCertificates[i];
40
+ const thumbprint = makeSHA1Thumbprint(issuerCert).toString("hex");
41
+
42
+ const finalIssuerFileDER = path.join(issuerFolder, `issuer_${thumbprint}.der`);
43
+ const finalIssuerFilePEM = path.join(issuerFolder, `issuer_${thumbprint}.pem`);
44
+ const issuerCertPEM = toPem(issuerCert, "CERTIFICATE");
45
+
46
+ await serverImpl.fileTransactionManager.stageFile(finalIssuerFileDER, issuerCert);
47
+ await serverImpl.fileTransactionManager.stageFile(finalIssuerFilePEM, issuerCertPEM, "utf-8");
48
+
49
+ doDebug && debugLog(`Staged issuer certificate ${i + 1}/${issuerCertificates.length}: ${thumbprint}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ // Helper: Stage main certificate to temporary files
55
+ async function preInstallCertificate(
56
+ serverImpl: PushCertificateManagerInternalContext,
57
+ certificateManager: CertificateManager,
58
+ certificate: Buffer
59
+ ): Promise<void> {
60
+ const certFolder = certificateManager.ownCertFolder;
61
+ const destDER = path.join(certFolder, "certificate.der");
62
+ const destPEM = path.join(certFolder, "certificate.pem");
63
+ const certificatePEM = toPem(certificate, "CERTIFICATE");
64
+
65
+ await serverImpl.fileTransactionManager.stageFile(destDER, certificate);
66
+ await serverImpl.fileTransactionManager.stageFile(destPEM, certificatePEM, "utf-8");
67
+ }
68
+
69
+ // Helper: Stage private key to temporary file
70
+ async function preInstallPrivateKey(
71
+ serverImpl: PushCertificateManagerInternalContext,
72
+ certificateManager: CertificateManager,
73
+ privateKeyFormat: string,
74
+ privateKey: Buffer | string | undefined
75
+ ): Promise<void> {
76
+ assert(privateKeyFormat.toUpperCase() === "PEM");
77
+
78
+ if (privateKey) {
79
+ const privateKeyObj = coercePEMorDerToPrivateKey(privateKey as string | Buffer);
80
+ const privateKeyPEM = coercePrivateKeyPem(privateKeyObj);
81
+ await serverImpl.fileTransactionManager.stageFile(certificateManager.privateKey, privateKeyPEM, "utf-8");
82
+ }
83
+ }
84
+
85
+ // Main Execute Function
86
+ export async function executeUpdateCertificate(
87
+ serverImpl: PushCertificateManagerInternalContext,
88
+ certificateGroupId: NodeId | string,
89
+ certificateTypeId: NodeId | string,
90
+ certificate: Buffer,
91
+ issuerCertificates: ByteString[],
92
+ privateKeyFormat?: string,
93
+ privateKey?: Buffer | string
94
+ ): Promise<UpdateCertificateResult> {
95
+ if (serverImpl.operationInProgress) {
96
+ return { statusCode: StatusCodes.BadTooManyOperations, applyChangesRequired: false };
97
+ }
98
+
99
+ serverImpl.operationInProgress = true;
100
+ try {
101
+ const context = resolveCertificateGroupContext(serverImpl, certificateGroupId);
102
+ if (context.statusCode.isNotGood() || !context.certificateManager) {
103
+ debugLog(" cannot find group ", certificateGroupId);
104
+ return { statusCode: StatusCodes.BadInvalidArgument, applyChangesRequired: false };
105
+ }
106
+
107
+ const { certificateManager, allowedTypes } = context;
108
+
109
+ if (!validateCertificateType(certificate, certificateTypeId, allowedTypes ?? [], warningLog)) {
110
+ warningLog(
111
+ `Certificate type ${certificateTypeId} does not match expected certificateTypeId \n allowed types: ${allowedTypes?.map((t) => t.toString()).join(", ")} \n certificate: ${certificate.toString("base64")}`
112
+ );
113
+ return { statusCode: StatusCodes.BadCertificateInvalid, applyChangesRequired: false };
114
+ }
115
+
116
+ const isApplicationGroup = certificateManager === serverImpl.applicationGroup;
117
+ const validationResult = await validateCertificateAndChain(
118
+ certificateManager as OPCUACertificateManager,
119
+ isApplicationGroup,
120
+ certificate,
121
+ issuerCertificates
122
+ );
123
+
124
+ if (validationResult.statusCode !== StatusCodes.Good) {
125
+ await serverImpl.fileTransactionManager.abortTransaction();
126
+ return { statusCode: validationResult.statusCode, applyChangesRequired: false };
127
+ }
128
+
129
+ doDebug && debugLog(" updateCertificate ", makeSHA1Thumbprint(certificate).toString("hex"));
130
+
131
+ const hasPrivateKeyFormat = privateKeyFormat !== undefined && privateKeyFormat !== null && privateKeyFormat !== "";
132
+ const hasPrivateKey =
133
+ privateKey !== undefined &&
134
+ privateKey !== null &&
135
+ privateKey !== "" &&
136
+ !(privateKey instanceof Buffer && privateKey.length === 0);
137
+
138
+ if (hasPrivateKeyFormat !== hasPrivateKey) {
139
+ warningLog("privateKeyFormat and privateKey must both be provided or both be omitted");
140
+ await serverImpl.fileTransactionManager.abortTransaction();
141
+ return { statusCode: StatusCodes.BadInvalidArgument, applyChangesRequired: false };
142
+ }
143
+
144
+ if (!hasPrivateKeyFormat && !hasPrivateKey) {
145
+ const privateKeyObj = readPrivateKey(
146
+ serverImpl.tmpCertificateManager ? serverImpl.tmpCertificateManager.privateKey : certificateManager.privateKey
147
+ );
148
+
149
+ if (!certificateMatchesPrivateKey(certificate, privateKeyObj)) {
150
+ warningLog("certificate doesn't match privateKey");
151
+ await serverImpl.fileTransactionManager.abortTransaction();
152
+ return { statusCode: StatusCodes.BadSecurityChecksFailed, applyChangesRequired: false };
153
+ }
154
+
155
+ await preInstallIssuerCertificates(serverImpl, certificateManager, issuerCertificates);
156
+ await preInstallCertificate(serverImpl, certificateManager, certificate);
157
+ return { statusCode: StatusCodes.Good, applyChangesRequired: true };
158
+ } else {
159
+ if (privateKeyFormat !== "PEM" && privateKeyFormat !== "PFX") {
160
+ warningLog(` the private key format is invalid privateKeyFormat =${privateKeyFormat}`);
161
+ await serverImpl.fileTransactionManager.abortTransaction();
162
+ return { statusCode: StatusCodes.BadNotSupported, applyChangesRequired: false };
163
+ }
164
+ if (privateKeyFormat !== "PEM") {
165
+ warningLog(`in NodeOPCUA we only support PEM for the moment privateKeyFormat =${privateKeyFormat}`);
166
+ await serverImpl.fileTransactionManager.abortTransaction();
167
+ return { statusCode: StatusCodes.BadNotSupported, applyChangesRequired: false };
168
+ }
169
+
170
+ let privateKeyObj: PrivateKey | undefined;
171
+ let tempPrivateKey = privateKey;
172
+
173
+ if (tempPrivateKey instanceof Buffer || typeof tempPrivateKey === "string") {
174
+ if (tempPrivateKey instanceof Buffer) {
175
+ assert(privateKeyFormat === "PEM");
176
+ tempPrivateKey = tempPrivateKey.toString("utf-8");
177
+ }
178
+ privateKeyObj = coercePEMorDerToPrivateKey(tempPrivateKey);
179
+ }
180
+
181
+ if (!privateKeyObj) {
182
+ await serverImpl.fileTransactionManager.abortTransaction();
183
+ return { statusCode: StatusCodes.BadNotSupported, applyChangesRequired: false };
184
+ }
185
+
186
+ if (!certificateMatchesPrivateKey(certificate, privateKeyObj)) {
187
+ warningLog("certificate doesn't match privateKey");
188
+ await serverImpl.fileTransactionManager.abortTransaction();
189
+ return { statusCode: StatusCodes.BadSecurityChecksFailed, applyChangesRequired: false };
190
+ }
191
+
192
+ await preInstallPrivateKey(serverImpl, certificateManager, privateKeyFormat, tempPrivateKey);
193
+ await preInstallIssuerCertificates(serverImpl, certificateManager, issuerCertificates);
194
+ await preInstallCertificate(serverImpl, certificateManager, certificate);
195
+
196
+ return { statusCode: StatusCodes.Good, applyChangesRequired: true };
197
+ }
198
+ } finally {
199
+ serverImpl.operationInProgress = false;
200
+ }
201
+ }
@@ -0,0 +1,145 @@
1
+ import type { CertificateManager } from "node-opcua-certificate-manager";
2
+ import { NodeId, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
3
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
4
+ import { eccCertificateTypesArray, rsaCertificateTypesArray } from "../../clientTools/certificate_types.js";
5
+ import { getCertificateKeyType } from "../../clientTools/get_certificate_key_type.js";
6
+ import type { PushCertificateManagerInternalContext } from "./internal_context.js";
7
+
8
+ const defaultApplicationGroup = resolveNodeId("ServerConfiguration_CertificateGroups_DefaultApplicationGroup");
9
+ const defaultHttpsGroup = resolveNodeId("ServerConfiguration_CertificateGroups_DefaultHttpsGroup");
10
+ const defaultUserTokenGroup = resolveNodeId("ServerConfiguration_CertificateGroups_DefaultUserTokenGroup");
11
+
12
+ /**
13
+ * Find the name of a certificate group based on its NodeId.
14
+ * @param certificateGroupNodeId The NodeId of the certificate group
15
+ * @returns The name of the certificate group (e.g. "DefaultApplicationGroup") or empty string if not recognized
16
+ */
17
+ export function findCertificateGroupName(certificateGroupNodeId: NodeId | string): string {
18
+ // Convert string to NodeId if needed to check for null NodeId
19
+ let nodeId: NodeId;
20
+ if (typeof certificateGroupNodeId === "string") {
21
+ try {
22
+ nodeId = resolveNodeId(certificateGroupNodeId);
23
+ } catch {
24
+ // Invalid NodeId string - treat as literal group name
25
+ return certificateGroupNodeId;
26
+ }
27
+ } else {
28
+ nodeId = certificateGroupNodeId;
29
+ }
30
+
31
+ // Check if it's null NodeId or DefaultApplicationGroup
32
+ if (sameNodeId(nodeId, NodeId.nullNodeId) || sameNodeId(nodeId, defaultApplicationGroup)) {
33
+ return "DefaultApplicationGroup";
34
+ }
35
+ if (sameNodeId(nodeId, defaultHttpsGroup)) {
36
+ return "DefaultHttpsGroup";
37
+ }
38
+ if (sameNodeId(nodeId, defaultUserTokenGroup)) {
39
+ return "DefaultUserTokenGroup";
40
+ }
41
+
42
+ // If it's a valid NodeId but not recognized, return empty string
43
+ // If it was originally a string (and not a standard group), return the string as group name
44
+ return typeof certificateGroupNodeId === "string" ? certificateGroupNodeId : "";
45
+ }
46
+
47
+ /**
48
+ * Validate that the certificate type matches the expected type from certificateTypeId
49
+ *
50
+ * @param certificate The certificate to validate
51
+ * @param certificateTypeId The NodeId of the expected certificate type
52
+ * @param allowedTypes The list of allowed certificate types for this group
53
+ * @param warningLog Function to log warnings
54
+ * @returns true if valid or if validation is not applicable
55
+ */
56
+ export function validateCertificateType(
57
+ certificate: Buffer,
58
+ certificateTypeId: NodeId | string,
59
+ allowedTypes: NodeId[],
60
+ warningLog: (msg: string, ...args: unknown[]) => void
61
+ ): boolean {
62
+ // If certificateTypeId is null or not specified, skip validation
63
+ if (!certificateTypeId || (certificateTypeId instanceof NodeId && sameNodeId(certificateTypeId, NodeId.nullNodeId))) {
64
+ return true;
65
+ }
66
+
67
+ const keyType = getCertificateKeyType(certificate);
68
+ if (!keyType) {
69
+ // If we can't determine the key type, allow it (backward compatibility)
70
+ return true;
71
+ }
72
+
73
+ // Convert to NodeId if string
74
+ let typeNodeId: NodeId;
75
+ if (typeof certificateTypeId === "string") {
76
+ try {
77
+ typeNodeId = resolveNodeId(certificateTypeId);
78
+ } catch {
79
+ // Invalid NodeId string, skip validation
80
+ return true;
81
+ }
82
+ } else {
83
+ typeNodeId = certificateTypeId;
84
+ }
85
+
86
+ // Check again after conversion - empty string becomes null NodeId
87
+ if (sameNodeId(typeNodeId, NodeId.nullNodeId)) {
88
+ return true;
89
+ }
90
+
91
+ // Check if the certificateTypeId is in the list of allowed types for this group
92
+ const isAllowed = allowedTypes.some((t) => sameNodeId(t, typeNodeId));
93
+
94
+ if (!isAllowed) {
95
+ warningLog("Certificate typeId is not in the allowed types for this certificate group:", certificateTypeId);
96
+ return false;
97
+ }
98
+
99
+ // Additional validation: check if the certificate's actual key type matches the declared type
100
+ const isRsaType = rsaCertificateTypesArray.some((t) => sameNodeId(t, typeNodeId));
101
+ const isEccType = eccCertificateTypesArray.some((t) => sameNodeId(t, typeNodeId));
102
+
103
+ if (keyType === "RSA" && !isRsaType) {
104
+ warningLog("Certificate has RSA key but certificateTypeId is not an RSA type:", certificateTypeId);
105
+ return false;
106
+ }
107
+ if (keyType === "ECC" && !isEccType) {
108
+ warningLog("Certificate has ECC key but certificateTypeId is not an ECC type:", certificateTypeId);
109
+ return false;
110
+ }
111
+
112
+ return true;
113
+ }
114
+
115
+ export interface ResolvedGroupContext {
116
+ statusCode: StatusCode;
117
+ certificateManager?: CertificateManager;
118
+ allowedTypes?: NodeId[];
119
+ }
120
+
121
+ /**
122
+ * Resolves the CertificateManager and its allowed types for a given certificate group
123
+ */
124
+ export function resolveCertificateGroupContext(
125
+ serverImpl: PushCertificateManagerInternalContext,
126
+ certificateGroupId: NodeId | string
127
+ ): ResolvedGroupContext {
128
+ const groupName = findCertificateGroupName(certificateGroupId);
129
+ if (!groupName) {
130
+ return { statusCode: StatusCodes.BadInvalidArgument };
131
+ }
132
+
133
+ const certificateManager = serverImpl.getCertificateManager(groupName);
134
+ if (!certificateManager) {
135
+ return { statusCode: StatusCodes.BadInvalidArgument };
136
+ }
137
+
138
+ const allowedTypes = serverImpl.getCertificateTypes(groupName);
139
+
140
+ return {
141
+ statusCode: StatusCodes.Good,
142
+ certificateManager,
143
+ allowedTypes
144
+ };
145
+ }