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.
- package/dist/clientTools/certificate_types.d.ts +17 -0
- package/dist/clientTools/certificate_types.js +20 -0
- package/dist/clientTools/certificate_types.js.map +1 -0
- package/dist/clientTools/get_certificate_key_type.d.ts +6 -0
- package/dist/clientTools/get_certificate_key_type.js +55 -0
- package/dist/clientTools/get_certificate_key_type.js.map +1 -0
- package/dist/clientTools/index.d.ts +2 -1
- package/dist/clientTools/index.js +2 -17
- package/dist/clientTools/index.js.map +1 -1
- package/dist/clientTools/push_certificate_management_client.d.ts +10 -10
- package/dist/clientTools/push_certificate_management_client.js +85 -89
- package/dist/clientTools/push_certificate_management_client.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +9 -23
- package/dist/index.js.map +1 -1
- package/dist/push_certificate_manager.d.ts +4 -4
- package/dist/push_certificate_manager.js +1 -2
- package/dist/server/certificate_validation.d.ts +15 -0
- package/dist/server/certificate_validation.js +76 -0
- package/dist/server/certificate_validation.js.map +1 -0
- package/dist/server/file_transaction_manager.d.ts +30 -0
- package/dist/server/file_transaction_manager.js +223 -0
- package/dist/server/file_transaction_manager.js.map +1 -0
- package/dist/server/install_certificate_file_watcher.d.ts +1 -1
- package/dist/server/install_certificate_file_watcher.js +8 -14
- package/dist/server/install_certificate_file_watcher.js.map +1 -1
- package/dist/server/install_push_certitifate_management.d.ts +6 -6
- package/dist/server/install_push_certitifate_management.js +61 -65
- package/dist/server/install_push_certitifate_management.js.map +1 -1
- package/dist/server/promote_trust_list.d.ts +1 -1
- package/dist/server/promote_trust_list.js +323 -82
- package/dist/server/promote_trust_list.js.map +1 -1
- package/dist/server/push_certificate_manager/apply_changes.d.ts +3 -0
- package/dist/server/push_certificate_manager/apply_changes.js +59 -0
- package/dist/server/push_certificate_manager/apply_changes.js.map +1 -0
- package/dist/server/push_certificate_manager/create_signing_request.d.ts +5 -0
- package/dist/server/push_certificate_manager/create_signing_request.js +108 -0
- package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -0
- package/dist/server/push_certificate_manager/get_rejected_list.d.ts +3 -0
- package/dist/server/push_certificate_manager/get_rejected_list.js +46 -0
- package/dist/server/push_certificate_manager/get_rejected_list.js.map +1 -0
- package/dist/server/push_certificate_manager/internal_context.d.ts +35 -0
- package/dist/server/push_certificate_manager/internal_context.js +45 -0
- package/dist/server/push_certificate_manager/internal_context.js.map +1 -0
- package/dist/server/push_certificate_manager/subject_to_string.d.ts +3 -0
- package/dist/server/push_certificate_manager/subject_to_string.js +27 -0
- package/dist/server/push_certificate_manager/subject_to_string.js.map +1 -0
- package/dist/server/push_certificate_manager/update_certificate.d.ts +5 -0
- package/dist/server/push_certificate_manager/update_certificate.js +132 -0
- package/dist/server/push_certificate_manager/update_certificate.js.map +1 -0
- package/dist/server/push_certificate_manager/util.d.ts +29 -0
- package/dist/server/push_certificate_manager/util.js +117 -0
- package/dist/server/push_certificate_manager/util.js.map +1 -0
- package/dist/server/push_certificate_manager_helpers.d.ts +5 -2
- package/dist/server/push_certificate_manager_helpers.js +109 -112
- package/dist/server/push_certificate_manager_helpers.js.map +1 -1
- package/dist/server/push_certificate_manager_server_impl.d.ts +16 -29
- package/dist/server/push_certificate_manager_server_impl.js +49 -437
- package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
- package/dist/server/roles_and_permissions.d.ts +1 -1
- package/dist/server/roles_and_permissions.js +24 -27
- package/dist/server/roles_and_permissions.js.map +1 -1
- package/dist/server/tools.d.ts +1 -1
- package/dist/server/tools.js +7 -13
- package/dist/server/tools.js.map +1 -1
- package/dist/server/trust_list_server.d.ts +2 -2
- package/dist/server/trust_list_server.js +40 -29
- package/dist/server/trust_list_server.js.map +1 -1
- package/dist/standard_certificate_types.js +6 -9
- package/dist/standard_certificate_types.js.map +1 -1
- package/dist/trust_list.d.ts +2 -2
- package/dist/trust_list.js +1 -2
- package/dist/trust_list_impl.js +1 -2
- package/dist/trust_list_impl.js.map +1 -1
- package/package.json +29 -30
- package/source/clientTools/certificate_types.ts +21 -0
- package/source/clientTools/get_certificate_key_type.ts +73 -0
- package/source/clientTools/index.ts +2 -1
- package/source/clientTools/push_certificate_management_client.ts +49 -44
- package/source/index.ts +9 -7
- package/source/push_certificate_manager.ts +15 -17
- package/source/server/certificate_validation.ts +103 -0
- package/source/server/file_transaction_manager.ts +253 -0
- package/source/server/install_certificate_file_watcher.ts +15 -11
- package/source/server/install_push_certitifate_management.ts +52 -51
- package/source/server/promote_trust_list.ts +362 -73
- package/source/server/push_certificate_manager/apply_changes.ts +63 -0
- package/source/server/push_certificate_manager/create_signing_request.ts +137 -0
- package/source/server/push_certificate_manager/get_rejected_list.ts +63 -0
- package/source/server/push_certificate_manager/internal_context.ts +63 -0
- package/source/server/push_certificate_manager/subject_to_string.ts +25 -0
- package/source/server/push_certificate_manager/update_certificate.ts +201 -0
- package/source/server/push_certificate_manager/util.ts +145 -0
- package/source/server/push_certificate_manager_helpers.ts +61 -51
- package/source/server/push_certificate_manager_server_impl.ts +94 -553
- package/source/server/roles_and_permissions.ts +7 -8
- package/source/server/tools.ts +2 -5
- package/source/server/trust_list_server.ts +24 -9
- package/source/standard_certificate_types.ts +2 -3
- 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
|
+
}
|