node-opcua-server-configuration 2.186.4 → 2.186.6
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/push_certificate_management_client.d.ts.map +1 -1
- package/dist/clientTools/push_certificate_management_client.js +4 -1
- package/dist/clientTools/push_certificate_management_client.js.map +1 -1
- package/dist/server/application_setup.d.ts +29 -0
- package/dist/server/application_setup.d.ts.map +1 -0
- package/dist/server/application_setup.js +157 -0
- package/dist/server/application_setup.js.map +1 -0
- package/dist/server/install_push_certificate_management.d.ts +46 -0
- package/dist/server/install_push_certificate_management.d.ts.map +1 -1
- package/dist/server/install_push_certificate_management.js +169 -10
- package/dist/server/install_push_certificate_management.js.map +1 -1
- package/dist/server/promote_trust_list.d.ts.map +1 -1
- package/dist/server/promote_trust_list.js +74 -29
- package/dist/server/promote_trust_list.js.map +1 -1
- package/dist/server/push_certificate_manager_helpers.d.ts.map +1 -1
- package/dist/server/push_certificate_manager_helpers.js +4 -0
- package/dist/server/push_certificate_manager_helpers.js.map +1 -1
- package/package.json +2 -2
- package/source/clientTools/push_certificate_management_client.ts +4 -1
- package/source/server/application_setup.ts +169 -0
- package/source/server/install_push_certificate_management.ts +232 -10
- package/source/server/promote_trust_list.ts +85 -35
- package/source/server/push_certificate_manager_helpers.ts +4 -0
|
@@ -16,12 +16,19 @@ import {
|
|
|
16
16
|
resolvePrivateKeyProviderIfNeeded
|
|
17
17
|
} from "node-opcua-common";
|
|
18
18
|
import { readPrivateKey } from "node-opcua-crypto";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type Certificate,
|
|
21
|
+
exploreCertificateInfo,
|
|
22
|
+
makeSHA1Thumbprint,
|
|
23
|
+
split_der,
|
|
24
|
+
verifyCertificateSignature
|
|
25
|
+
} from "node-opcua-crypto/web";
|
|
20
26
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
21
27
|
import type { OPCUAServer, OPCUAServerEndPoint } from "node-opcua-server";
|
|
22
28
|
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
23
29
|
import { type ApplicationDescriptionOptions, ServerState } from "node-opcua-types";
|
|
24
30
|
|
|
31
|
+
import { hasConfirmedTrust, leafThumbprintsIn, recordProvisionalTrust, withdrawProvisionalTrust } from "./application_setup.js";
|
|
25
32
|
import { installPushCertificateManagement } from "./push_certificate_manager_helpers.js";
|
|
26
33
|
import type { ActionQueue, PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
|
|
27
34
|
|
|
@@ -239,6 +246,42 @@ export interface InstallPushCertificateManagementOnServerOptions {
|
|
|
239
246
|
* @defaultValue true
|
|
240
247
|
*/
|
|
241
248
|
closeChannelsOnApplyChanges?: boolean;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* How the server leaves the application setup state
|
|
252
|
+
* (`ServerState.NoConfiguration`, OPC 10000-12 §G.2).
|
|
253
|
+
*/
|
|
254
|
+
applicationSetup?: ApplicationSetupOptions;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ApplicationSetupOptions {
|
|
258
|
+
/**
|
|
259
|
+
* Leave the application setup state automatically once the server is
|
|
260
|
+
* configured. §G.2: "Once an application has been configured it
|
|
261
|
+
* automatically leaves the application setup state. This step is
|
|
262
|
+
* necessary to ensure that security is not compromised."
|
|
263
|
+
*
|
|
264
|
+
* While the server reports `NoConfiguration`, push certificate
|
|
265
|
+
* management admits any client and trusts its certificate, so that a
|
|
266
|
+
* CertificateManager can reach it for the first time (see
|
|
267
|
+
* `installCertificateRelaxationHook`). With this option:
|
|
268
|
+
*
|
|
269
|
+
* - the server moves to `Running` when ApplyChanges has put a
|
|
270
|
+
* CA-issued certificate into service and the TrustList holds a
|
|
271
|
+
* certificate that was not trusted automatically;
|
|
272
|
+
* - on leaving `NoConfiguration` (this way or when the application sets
|
|
273
|
+
* the state itself), the certificates trusted automatically are
|
|
274
|
+
* removed again, except those a TrustList write kept (CloseAndUpdate
|
|
275
|
+
* of the trusted list, AddCertificate). Whoever configured the server
|
|
276
|
+
* stays trusted through the TrustList it wrote; anyone else who
|
|
277
|
+
* connected during the setup does not.
|
|
278
|
+
*
|
|
279
|
+
* Without it the application decides when the setup ends, and the
|
|
280
|
+
* certificates trusted automatically stay trusted.
|
|
281
|
+
*
|
|
282
|
+
* @defaultValue false
|
|
283
|
+
*/
|
|
284
|
+
leaveWhenConfigured?: boolean;
|
|
242
285
|
}
|
|
243
286
|
|
|
244
287
|
export async function installPushCertificateManagementOnServer(
|
|
@@ -246,6 +289,7 @@ export async function installPushCertificateManagementOnServer(
|
|
|
246
289
|
options?: InstallPushCertificateManagementOnServerOptions
|
|
247
290
|
): Promise<void> {
|
|
248
291
|
const closeChannelsOnApplyChanges = options?.closeChannelsOnApplyChanges ?? true;
|
|
292
|
+
const leaveWhenConfigured = options?.applicationSetup?.leaveWhenConfigured ?? false;
|
|
249
293
|
if (!server.engine?.addressSpace) {
|
|
250
294
|
throw new Error(
|
|
251
295
|
"Server must have a valid address space. " +
|
|
@@ -309,6 +353,9 @@ export async function installPushCertificateManagementOnServer(
|
|
|
309
353
|
setImmediate(async () => {
|
|
310
354
|
try {
|
|
311
355
|
await onApplyChangesCompleted(server, closeChannelsOnApplyChanges);
|
|
356
|
+
if (leaveWhenConfigured) {
|
|
357
|
+
await leaveApplicationSetupIfConfigured(server, cm);
|
|
358
|
+
}
|
|
312
359
|
} catch (err) {
|
|
313
360
|
errorLog("onApplyChangesCompleted error:", (err as Error).message);
|
|
314
361
|
}
|
|
@@ -323,8 +370,10 @@ export async function installPushCertificateManagementOnServer(
|
|
|
323
370
|
// write a certificate straight into serverCertificateManager's
|
|
324
371
|
// trusted-certs folder. Only the method path emits "trustListUpdated",
|
|
325
372
|
// so a consumer waiting on it to learn that the server has been
|
|
326
|
-
// provisioned
|
|
327
|
-
// a
|
|
373
|
+
// provisioned would never hear about a TrustList populated the other
|
|
374
|
+
// way. It is not a signal to leave NoConfiguration: the first client
|
|
375
|
+
// trusted automatically populates the TrustList too. Use
|
|
376
|
+
// `applicationSetup.leaveWhenConfigured` for that.
|
|
328
377
|
//
|
|
329
378
|
// This deliberately does NOT emit "applyChangesCompleted": that event
|
|
330
379
|
// means "a certificate rotation was applied" and carries channel
|
|
@@ -375,7 +424,164 @@ export async function installPushCertificateManagementOnServer(
|
|
|
375
424
|
//
|
|
376
425
|
// This hook is ONLY installed when push certificate management
|
|
377
426
|
// is active — bare servers are completely unaffected.
|
|
378
|
-
installCertificateRelaxationHook(server);
|
|
427
|
+
installCertificateRelaxationHook(server, leaveWhenConfigured);
|
|
428
|
+
|
|
429
|
+
// ── Re-evaluate open channels when the TrustList changes ──
|
|
430
|
+
//
|
|
431
|
+
// §7.8.2.5: "When the TrustList changes the Server shall re-evaluate the
|
|
432
|
+
// Certificate associated with any open Sessions and SecureChannels.
|
|
433
|
+
// Sessions or SecureChannels with an untrusted or revoked Certificate
|
|
434
|
+
// shall be closed." The event fires inside the method, before its
|
|
435
|
+
// response is on the wire; the delay gives the Client that changed the
|
|
436
|
+
// TrustList time to receive it before its own channel may be closed
|
|
437
|
+
// ("This process may not complete before the Method returns").
|
|
438
|
+
serverConfigurationPriv.$pushCertificateManager.on("trustListUpdated", () => {
|
|
439
|
+
setTimeout(() => {
|
|
440
|
+
closeChannelsNoLongerTrusted(server).catch((err) =>
|
|
441
|
+
errorLog("[TrustList] cannot re-evaluate open channels:", (err as Error).message)
|
|
442
|
+
);
|
|
443
|
+
}, REEVALUATION_DELAY_MS);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (leaveWhenConfigured) {
|
|
447
|
+
withdrawProvisionalTrustOnLeavingSetup(server, cm);
|
|
448
|
+
// The TrustList may be written after ApplyChanges: without a
|
|
449
|
+
// transaction CloseAndUpdate applies on its own, so check then too.
|
|
450
|
+
serverConfigurationPriv.$pushCertificateManager.on("trustListUpdated", () => {
|
|
451
|
+
setTimeout(() => {
|
|
452
|
+
leaveApplicationSetupIfConfigured(server, cm).catch((err) =>
|
|
453
|
+
errorLog("[ApplicationSetup] cannot evaluate the setup state:", (err as Error).message)
|
|
454
|
+
);
|
|
455
|
+
}, REEVALUATION_DELAY_MS);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Re-evaluating open channels (§7.8.2.5) ─────────────────────
|
|
461
|
+
|
|
462
|
+
const reevaluations = new WeakMap<OPCUAServer, Promise<unknown>>();
|
|
463
|
+
|
|
464
|
+
/** Certificates being re-evaluated: the relaxation hook admits them without trusting them. */
|
|
465
|
+
const reevaluating = new WeakSet<Certificate>();
|
|
466
|
+
|
|
467
|
+
/** How long after a TrustList change open channels are re-evaluated (see the "trustListUpdated" listener). */
|
|
468
|
+
const REEVALUATION_DELAY_MS = 250;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Close every SecureChannel, and the Sessions on it, whose client
|
|
472
|
+
* certificate the server would no longer admit, by the same decision as at
|
|
473
|
+
* OpenSecureChannel: the certificate store, then the endpoint's
|
|
474
|
+
* `onAdjustCertificateStatus` hook (the NoConfiguration relaxation, plus
|
|
475
|
+
* whatever admission policy the application composed with it). In the setup
|
|
476
|
+
* state an untrusted certificate is therefore still admitted. Returns the
|
|
477
|
+
* number of channels closed.
|
|
478
|
+
*
|
|
479
|
+
* The check never trusts a certificate on its own.
|
|
480
|
+
*/
|
|
481
|
+
export function closeChannelsNoLongerTrusted(server: OPCUAServer): Promise<number> {
|
|
482
|
+
const run = (reevaluations.get(server) ?? Promise.resolve()).then(() => _closeChannelsNoLongerTrusted(server));
|
|
483
|
+
reevaluations.set(
|
|
484
|
+
server,
|
|
485
|
+
run.catch(() => undefined)
|
|
486
|
+
);
|
|
487
|
+
return run;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function _closeChannelsNoLongerTrusted(server: OPCUAServer): Promise<number> {
|
|
491
|
+
const cm = server.serverCertificateManager;
|
|
492
|
+
let closed = 0;
|
|
493
|
+
for (const endpoint of server.endpoints as OPCUAServerEndPoint[]) {
|
|
494
|
+
for (const channel of endpoint.getChannels()) {
|
|
495
|
+
const certificate = channel.clientCertificate;
|
|
496
|
+
if (!certificate) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
// The same decision as when the channel was opened: the store,
|
|
500
|
+
// then the endpoint's adjustment hook, which the application may
|
|
501
|
+
// have composed with its own admission policy. Only the
|
|
502
|
+
// NoConfiguration relaxation's side effect (trusting the
|
|
503
|
+
// certificate) is suppressed: a re-evaluation never adds trust.
|
|
504
|
+
let statusCode = await cm.checkCertificate(certificate);
|
|
505
|
+
if (statusCode.isNotGood()) {
|
|
506
|
+
reevaluating.add(certificate);
|
|
507
|
+
try {
|
|
508
|
+
statusCode = await endpoint.adjustCertificateStatus(statusCode, certificate);
|
|
509
|
+
} finally {
|
|
510
|
+
reevaluating.delete(certificate);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (statusCode.isGood()) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
warningLog(`[TrustList] closing SecureChannel ${channel.channelId}: client certificate is now ${statusCode.name}`);
|
|
517
|
+
for (const session of server.engine.getSessions()) {
|
|
518
|
+
if (session.channel === channel) {
|
|
519
|
+
server.engine.closeSession(session.authenticationToken, false, "Terminated");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
channel.close();
|
|
523
|
+
closed++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return closed;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── Leaving the application setup state (§G.2) ─────────────────
|
|
530
|
+
|
|
531
|
+
function isSelfSigned(certificate: Certificate): boolean {
|
|
532
|
+
try {
|
|
533
|
+
return verifyCertificateSignature(certificate, certificate);
|
|
534
|
+
} catch {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Move to Running once ApplyChanges has put a CA-issued certificate into
|
|
541
|
+
* service and the TrustList holds a certificate that was not trusted
|
|
542
|
+
* automatically. Leaving earlier would lock every client out, the
|
|
543
|
+
* CertificateManager included.
|
|
544
|
+
*/
|
|
545
|
+
async function leaveApplicationSetupIfConfigured(server: OPCUAServer, cm: OPCUACertificateManager): Promise<void> {
|
|
546
|
+
if (server.engine.getServerState() !== ServerState.NoConfiguration) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (isSelfSigned(server.getCertificate())) {
|
|
550
|
+
doDebug && debugLog("[ApplicationSetup] still self-signed: staying in NoConfiguration");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!(await hasConfirmedTrust(cm))) {
|
|
554
|
+
warningLog(
|
|
555
|
+
"[ApplicationSetup] a CA-issued certificate is in service but no TrustList was written: staying in NoConfiguration"
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
warningLog("[ApplicationSetup] configured: leaving NoConfiguration");
|
|
560
|
+
server.engine.setServerState(ServerState.Running);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** When the server leaves NoConfiguration, remove the certificates trusted automatically during the setup. */
|
|
564
|
+
function withdrawProvisionalTrustOnLeavingSetup(server: OPCUAServer, cm: OPCUACertificateManager): void {
|
|
565
|
+
const onStateChanged = (oldState: ServerState, newState: ServerState) => {
|
|
566
|
+
if (newState === ServerState.Shutdown) {
|
|
567
|
+
server.engine.removeListener("serverStateChanged", onStateChanged);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (oldState !== ServerState.NoConfiguration || newState === ServerState.NoConfiguration) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
withdrawProvisionalTrust(cm)
|
|
574
|
+
.then(async (withdrawn) => {
|
|
575
|
+
if (withdrawn > 0) {
|
|
576
|
+
warningLog(
|
|
577
|
+
`[ApplicationSetup] setup ended: ${withdrawn} certificate(s) trusted automatically are no longer trusted`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
await closeChannelsNoLongerTrusted(server);
|
|
581
|
+
})
|
|
582
|
+
.catch((err) => errorLog("[ApplicationSetup] cannot withdraw provisional trust:", (err as Error).message));
|
|
583
|
+
};
|
|
584
|
+
server.engine.on("serverStateChanged", onStateChanged);
|
|
379
585
|
}
|
|
380
586
|
|
|
381
587
|
// ── Certificate relaxation for NoConfiguration state ──────────
|
|
@@ -403,7 +609,10 @@ export async function installPushCertificateManagementOnServer(
|
|
|
403
609
|
* relaxation hook returns the original status code unchanged and
|
|
404
610
|
* normal strict certificate validation applies. The accepted
|
|
405
611
|
* certificate is auto-trusted (see `autoTrustCertificateChain`)
|
|
406
|
-
* so that subsequent connections succeed under normal validation
|
|
612
|
+
* so that subsequent connections succeed under normal validation;
|
|
613
|
+
* with `applicationSetup.leaveWhenConfigured` that trust is
|
|
614
|
+
* withdrawn again when the setup ends, unless a TrustList write
|
|
615
|
+
* kept it.
|
|
407
616
|
*
|
|
408
617
|
* Errors that indicate an active security violation (revoked,
|
|
409
618
|
* expired, invalid signature, wrong usage) are **never** relaxed,
|
|
@@ -432,7 +641,7 @@ function isRelaxableCertificateError(statusCode: StatusCode): boolean {
|
|
|
432
641
|
* connection from unintentionally granting trust to every
|
|
433
642
|
* certificate signed by the same CA.
|
|
434
643
|
*/
|
|
435
|
-
async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certificate): Promise<void> {
|
|
644
|
+
async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certificate, recordProvisional: boolean): Promise<void> {
|
|
436
645
|
let chain: Certificate[];
|
|
437
646
|
try {
|
|
438
647
|
chain = split_der(certificate);
|
|
@@ -456,8 +665,12 @@ async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certi
|
|
|
456
665
|
}
|
|
457
666
|
|
|
458
667
|
if (i === 0) {
|
|
459
|
-
// Leaf certificate → trust explicitly
|
|
668
|
+
// Leaf certificate → trust explicitly, and remember that it was
|
|
669
|
+
// trusted only because the server is in the setup state.
|
|
460
670
|
try {
|
|
671
|
+
if (recordProvisional && (await cm.isCertificateTrusted(cert)) !== "Good") {
|
|
672
|
+
await recordProvisionalTrust(cm, "trusted", makeSHA1Thumbprint(cert).toString("hex"));
|
|
673
|
+
}
|
|
461
674
|
await cm.trustCertificate(cert);
|
|
462
675
|
} catch (err) {
|
|
463
676
|
// ENOENT can happen if another concurrent call already
|
|
@@ -470,6 +683,12 @@ async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certi
|
|
|
470
683
|
// Issuer CA certificate → add to issuers/ (chain-building
|
|
471
684
|
// only, does NOT establish a trust anchor)
|
|
472
685
|
try {
|
|
686
|
+
if (recordProvisional) {
|
|
687
|
+
const thumbprint = makeSHA1Thumbprint(cert).toString("hex");
|
|
688
|
+
if (!(await leafThumbprintsIn(cm.issuersCertFolder)).includes(thumbprint)) {
|
|
689
|
+
await recordProvisionalTrust(cm, "issuers", thumbprint);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
473
692
|
await cm.addIssuer(cert);
|
|
474
693
|
} catch (err) {
|
|
475
694
|
warningLog("[NoConfiguration] Failed to add issuer certificate:", (err as Error).message);
|
|
@@ -490,7 +709,7 @@ async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certi
|
|
|
490
709
|
* validation. Issuer CAs are placed in `issuers/` for chain
|
|
491
710
|
* building but do not become trust anchors.
|
|
492
711
|
*/
|
|
493
|
-
function installCertificateRelaxationHook(server: OPCUAServer): void {
|
|
712
|
+
function installCertificateRelaxationHook(server: OPCUAServer, recordProvisional: boolean): void {
|
|
494
713
|
const adjustCertificateStatus = async (statusCode: StatusCode, certificate: Certificate): Promise<StatusCode> => {
|
|
495
714
|
// Only relax in NoConfiguration state
|
|
496
715
|
if (server.engine.getServerState() !== ServerState.NoConfiguration) {
|
|
@@ -509,8 +728,11 @@ function installCertificateRelaxationHook(server: OPCUAServer): void {
|
|
|
509
728
|
"(server is awaiting GDS provisioning)"
|
|
510
729
|
);
|
|
511
730
|
|
|
512
|
-
// Auto-trust the leaf certificate; issuer CAs go to issuers/
|
|
513
|
-
|
|
731
|
+
// Auto-trust the leaf certificate; issuer CAs go to issuers/ (not
|
|
732
|
+
// when an open channel is being re-evaluated: that adds no trust).
|
|
733
|
+
if (!reevaluating.has(certificate)) {
|
|
734
|
+
await autoTrustCertificateChain(server, certificate, recordProvisional);
|
|
735
|
+
}
|
|
514
736
|
|
|
515
737
|
return StatusCodes.Good;
|
|
516
738
|
};
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
} from "node-opcua-address-space";
|
|
22
22
|
import { BinaryStream } from "node-opcua-binary-stream";
|
|
23
23
|
import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
24
|
-
import { split_der, verifyCertificateChain } from "node-opcua-crypto/web";
|
|
24
|
+
import { exploreCertificate, makeSHA1Thumbprint, split_der, verifyCertificateChain } from "node-opcua-crypto/web";
|
|
25
25
|
import { AccessRestrictionsFlag } from "node-opcua-data-model";
|
|
26
26
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
27
27
|
import { type AbstractFs, installFileType, OpenFileMode } from "node-opcua-file-transfer";
|
|
@@ -29,6 +29,7 @@ import { VerificationStatus } from "node-opcua-pki";
|
|
|
29
29
|
import { type CallbackT, type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
30
30
|
import { type CallMethodResultOptions, TrustListDataType } from "node-opcua-types";
|
|
31
31
|
import { DataType, Variant } from "node-opcua-variant";
|
|
32
|
+
import { confirmTrust, isSharedTrustListStore, leafThumbprintsIn } from "./application_setup.js";
|
|
32
33
|
import type { PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
|
|
33
34
|
import { rolePermissionAdminOnly } from "./roles_and_permissions.js";
|
|
34
35
|
import { hasEncryptedChannel, hasExpectedUserAccess } from "./tools.js";
|
|
@@ -274,29 +275,6 @@ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData:
|
|
|
274
275
|
trustListData.specifiedLists |= TrustListMasks.IssuerCertificates;
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
// Process CRLs
|
|
278
|
-
if ((trustListData.specifiedLists & TrustListMasks.IssuerCrls) === TrustListMasks.IssuerCrls) {
|
|
279
|
-
doDebug && debugLog("Processing issuer CRLs");
|
|
280
|
-
await cm.clearRevocationLists("issuers");
|
|
281
|
-
if (trustListData.issuerCrls && trustListData.issuerCrls.length > 0) {
|
|
282
|
-
doDebug && debugLog(` Writing ${trustListData.issuerCrls.length} issuer CRLs`);
|
|
283
|
-
for (const crl of trustListData.issuerCrls) {
|
|
284
|
-
await cm.addRevocationList(crl, "issuers");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if ((trustListData.specifiedLists & TrustListMasks.TrustedCrls) === TrustListMasks.TrustedCrls) {
|
|
290
|
-
doDebug && debugLog("Processing trusted CRLs");
|
|
291
|
-
await cm.clearRevocationLists("trusted");
|
|
292
|
-
if (trustListData.trustedCrls && trustListData.trustedCrls.length > 0) {
|
|
293
|
-
doDebug && debugLog(` Writing ${trustListData.trustedCrls.length} trusted CRLs`);
|
|
294
|
-
for (const crl of trustListData.trustedCrls) {
|
|
295
|
-
await cm.addRevocationList(crl, "trusted");
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
278
|
// Validate all trusted certificates
|
|
301
279
|
if (
|
|
302
280
|
(trustListData.specifiedLists & TrustListMasks.TrustedCertificates) === TrustListMasks.TrustedCertificates &&
|
|
@@ -305,6 +283,9 @@ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData:
|
|
|
305
283
|
for (const cert of trustListData.trustedCertificates) {
|
|
306
284
|
try {
|
|
307
285
|
const certs = split_der(cert);
|
|
286
|
+
// verifyCertificateChain checks nothing on a one-element chain;
|
|
287
|
+
// parsing is what rejects bytes that are not a certificate.
|
|
288
|
+
exploreCertificate(certs[0]);
|
|
308
289
|
const validationResult = await verifyCertificateChain([certs[0]]);
|
|
309
290
|
if (validationResult.status !== "Good") {
|
|
310
291
|
warningLog("Invalid certificate in trust list:", validationResult.status, validationResult.reason);
|
|
@@ -325,6 +306,9 @@ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData:
|
|
|
325
306
|
for (const cert of trustListData.issuerCertificates) {
|
|
326
307
|
try {
|
|
327
308
|
const certs = split_der(cert);
|
|
309
|
+
// verifyCertificateChain checks nothing on a one-element chain;
|
|
310
|
+
// parsing is what rejects bytes that are not a certificate.
|
|
311
|
+
exploreCertificate(certs[0]);
|
|
328
312
|
const validationResult = await verifyCertificateChain([certs[0]]);
|
|
329
313
|
if (validationResult.status !== "Good") {
|
|
330
314
|
warningLog("Invalid issuer certificate in trust list:", validationResult.status, validationResult.reason);
|
|
@@ -337,22 +321,53 @@ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData:
|
|
|
337
321
|
}
|
|
338
322
|
}
|
|
339
323
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
324
|
+
// Nothing below runs unless every uploaded certificate is valid, so an
|
|
325
|
+
// invalid upload leaves the TrustList (CRLs included) as it was.
|
|
326
|
+
// Process CRLs
|
|
327
|
+
if ((trustListData.specifiedLists & TrustListMasks.IssuerCrls) === TrustListMasks.IssuerCrls) {
|
|
328
|
+
doDebug && debugLog("Processing issuer CRLs");
|
|
329
|
+
await cm.clearRevocationLists("issuers");
|
|
330
|
+
if (trustListData.issuerCrls && trustListData.issuerCrls.length > 0) {
|
|
331
|
+
doDebug && debugLog(` Writing ${trustListData.issuerCrls.length} issuer CRLs`);
|
|
332
|
+
for (const crl of trustListData.issuerCrls) {
|
|
333
|
+
await cm.addRevocationList(crl, "issuers");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if ((trustListData.specifiedLists & TrustListMasks.TrustedCrls) === TrustListMasks.TrustedCrls) {
|
|
339
|
+
doDebug && debugLog("Processing trusted CRLs");
|
|
340
|
+
await cm.clearRevocationLists("trusted");
|
|
341
|
+
if (trustListData.trustedCrls && trustListData.trustedCrls.length > 0) {
|
|
342
|
+
doDebug && debugLog(` Writing ${trustListData.trustedCrls.length} trusted CRLs`);
|
|
343
|
+
for (const crl of trustListData.trustedCrls) {
|
|
344
|
+
await cm.addRevocationList(crl, "trusted");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Update certificates. A list whose bit is set is replaced by the one
|
|
350
|
+
// uploaded, not added to (§7.8.2.5: the Server "creates a new TrustList
|
|
351
|
+
// that includes the existing TrustList plus any updates"; §7.8.2.9: a
|
|
352
|
+
// list whose bit is not set is not changed). The uploaded certificates
|
|
353
|
+
// are added before the others are removed: should an add fail, the
|
|
354
|
+
// list is left larger than asked, never emptier, so nobody who was
|
|
355
|
+
// trusted is locked out by a half-applied write.
|
|
356
|
+
if ((trustListData.specifiedLists & TrustListMasks.TrustedCertificates) === TrustListMasks.TrustedCertificates) {
|
|
357
|
+
const uploaded = trustListData.trustedCertificates ?? [];
|
|
358
|
+
for (const cert of uploaded) {
|
|
346
359
|
await cm.trustCertificate(cert);
|
|
347
360
|
}
|
|
361
|
+
await confirmTrust(cm, "trusted", uploaded.map(leafThumbprint));
|
|
362
|
+
await removeCertificatesNotIn(cm, cm.trustedFolder, uploaded, (thumbprint) => cm.removeTrustedCertificate(thumbprint));
|
|
348
363
|
}
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
) {
|
|
353
|
-
for (const cert of trustListData.issuerCertificates) {
|
|
364
|
+
if ((trustListData.specifiedLists & TrustListMasks.IssuerCertificates) === TrustListMasks.IssuerCertificates) {
|
|
365
|
+
const uploaded = trustListData.issuerCertificates ?? [];
|
|
366
|
+
for (const cert of uploaded) {
|
|
354
367
|
await cm.addIssuer(cert);
|
|
355
368
|
}
|
|
369
|
+
await confirmTrust(cm, "issuers", uploaded.map(leafThumbprint));
|
|
370
|
+
await removeCertificatesNotIn(cm, cm.issuersCertFolder, uploaded, (thumbprint) => cm.removeIssuer(thumbprint));
|
|
356
371
|
}
|
|
357
372
|
|
|
358
373
|
return StatusCodes.Good;
|
|
@@ -362,6 +377,38 @@ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData:
|
|
|
362
377
|
}
|
|
363
378
|
}
|
|
364
379
|
|
|
380
|
+
function leafThumbprint(certificateOrChain: Buffer): string {
|
|
381
|
+
return makeSHA1Thumbprint(split_der(certificateOrChain)[0]).toString("hex");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Remove from `folder` every certificate that is not in `uploaded`.
|
|
386
|
+
*
|
|
387
|
+
* Skipped, with a warning, when the store backs another CertificateGroup as
|
|
388
|
+
* well: the certificates of both groups sit in the same folder, and removing
|
|
389
|
+
* what this group's upload leaves out would remove the other group's too.
|
|
390
|
+
* Such a store keeps the previous behaviour, adding only.
|
|
391
|
+
*/
|
|
392
|
+
async function removeCertificatesNotIn(
|
|
393
|
+
cm: OPCUACertificateManager,
|
|
394
|
+
folder: string,
|
|
395
|
+
uploaded: Buffer[],
|
|
396
|
+
remove: (thumbprint: string) => Promise<unknown>
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
if (isSharedTrustListStore(cm)) {
|
|
399
|
+
warningLog(
|
|
400
|
+
`[TrustList] ${cm.rootDir} backs more than one CertificateGroup: the uploaded list is added to it, not replacing it (OPC 10000-12 §7.8.2.5). Give each group its own CertificateManager.`
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const kept = new Set(uploaded.map(leafThumbprint));
|
|
405
|
+
for (const thumbprint of await leafThumbprintsIn(folder)) {
|
|
406
|
+
if (!kept.has(thumbprint)) {
|
|
407
|
+
await remove(thumbprint);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
365
412
|
async function _closeAndUpdate(
|
|
366
413
|
this: UAMethod,
|
|
367
414
|
inputArguments: Variant[],
|
|
@@ -515,6 +562,7 @@ async function _addCertificate(
|
|
|
515
562
|
return { statusCode: verificationStatusToStatusCode(status) };
|
|
516
563
|
}
|
|
517
564
|
|
|
565
|
+
await confirmTrust(cm, "trusted", [leafThumbprint(certificateBuffer)]);
|
|
518
566
|
updateLastUpdateTime(trustList);
|
|
519
567
|
emitTrustListUpdated(trustList);
|
|
520
568
|
|
|
@@ -562,6 +610,7 @@ async function _removeCertificate(
|
|
|
562
610
|
if (!removed) {
|
|
563
611
|
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
564
612
|
}
|
|
613
|
+
await confirmTrust(cm, "trusted", [normalizedThumbprint]);
|
|
565
614
|
} else {
|
|
566
615
|
// Removing an issuer certificate — first check if it's still
|
|
567
616
|
// needed by any trusted certificate
|
|
@@ -580,6 +629,7 @@ async function _removeCertificate(
|
|
|
580
629
|
|
|
581
630
|
// Also remove CRLs issued by this CA
|
|
582
631
|
await cm.removeRevocationListsForIssuer(issuerCert, "issuers");
|
|
632
|
+
await confirmTrust(cm, "issuers", [normalizedThumbprint]);
|
|
583
633
|
}
|
|
584
634
|
|
|
585
635
|
updateLastUpdateTime(trustList);
|
|
@@ -27,6 +27,7 @@ import type { CallMethodResultOptions } from "node-opcua-types";
|
|
|
27
27
|
import { DataType, type Variant, VariantArrayType } from "node-opcua-variant";
|
|
28
28
|
|
|
29
29
|
import type { CreateSigningRequestResult, PushCertificateManager } from "../push_certificate_manager.js";
|
|
30
|
+
import { markSharedTrustListStore } from "./application_setup.js";
|
|
30
31
|
import { installCertificateFileWatcher } from "./install_certificate_file_watcher.js";
|
|
31
32
|
import { installAccessRestrictionOnTrustList, promoteTrustList } from "./promote_trust_list.js";
|
|
32
33
|
import {
|
|
@@ -320,6 +321,9 @@ function bindCertificateManager(addressSpace: AddressSpace, options: PushCertifi
|
|
|
320
321
|
"ServerConfiguration"
|
|
321
322
|
) as UAServerConfiguration;
|
|
322
323
|
|
|
324
|
+
if (options.applicationGroup && options.userTokenGroup) {
|
|
325
|
+
markSharedTrustListStore(options.applicationGroup, options.userTokenGroup);
|
|
326
|
+
}
|
|
323
327
|
const defaultApplicationGroup = serverConfiguration.certificateGroups.getComponentByName(
|
|
324
328
|
"DefaultApplicationGroup"
|
|
325
329
|
) as UACertificateGroup | null;
|