mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
15eb55c46e
# Description Implements the OPC UA Part 12 §7.10.9 contract on the server (fixes #3562) AND the client-side counterpart for immediate detection of own-cert / trust-list / CRL changes (fixes #3160). ## Server side (#3562) Per Part 12 §7.10.9 the server _shall_ "force existing SecureChannels affected by the changes to renegotiate" once the `ApplyChanges` method response has been delivered. Previously the server reloaded the certificate but did not cut affected channels; clients kept using the old cert until natural rekey, and the code carried an explicit `TODO` next to a 1-second `Task.Delay` hack in `ConfigurationNodeManager.ApplyChanges`. Key server changes: * **New optional capability interface** `Opc.Ua.ITransportListenerCertificateRotation` (single method `CloseChannelsForCertificate(Certificate)`) — implemented by `TcpTransportListener` and `HttpsTransportListener`. Custom transport listeners can opt in without breaking `ITransportListener`. * **TCP path**: new internal `TcpListenerChannel.TryCloseForCertificateRotation(oldThumbprint, out globalChannelId)` matches the channel's negotiated `ServerCertificate` by thumbprint, sends `BadCertificateInvalid`, and calls `ChannelFaulted()` to actually close the socket. `TcpTransportListener.CloseChannelsForCertificate` snapshots `m_channels` outside `m_lock` to avoid lock inversion, invokes the per-channel helper, returns cut global channel ids. **The listener socket stays bound** — clients can reconnect immediately. * **HTTPS path**: HTTPS owns the cert at the Kestrel/HTTP.sys listener level, so a `Stop()`+`Start()` cycle is the only practical force-renegotiate. Moved out of `CertificateUpdate` (which now does only the soft fan-out) into `CloseChannelsForCertificate` so the staging event no longer disrupts in-flight requests prematurely. * **`ConfigurationNodeManager.ApplyChanges`** rewritten: - Captures the per-group pre-transaction certificate (preserved across multi-stage transactions per Part 12 §7.10.2) and the staged `UpdateCertificateData`. - Schedules a deferred apply: waits for the response to flush, re-syncs the registry via `CertificateManager.UpdateAsync`, then calls `CloseChannelsForCertificate` on every listener that implements the capability. - **Configurable grace period**: new `IConfigurationNodeManager.ApplyChangesGracePeriod` (default 250 ms, negative clamped to zero). Hosts running over higher-latency administrative links can raise it. - Exposes `IConfigurationNodeManager.DrainPendingApplyChangesAsync(CancellationToken)` for tests/hosts to deterministically await rotation. - Removes the explicit 1-second `Task.Delay` TODO hack. * **`Opc.Ua.Server.ITransportListenerRegistryProvider`** — narrow opt-in interface on `ServerInternalData` exposing the live transport listener list. Lets `ConfigurationNodeManager` drive the fan-out without expanding `IServerInternal` (binary compat for mocked test fixtures). * **Sessions are NOT closed**. Per spec, the cut is on the SecureChannel only; existing `Session.IsSecureChannelValid` + `Session.Activate` rebind handles transfer over the new channel. ## Client side (#3160) The client SDK previously had no automatic detection of changes to its own certificate, trust list, or CRL — users had to call `Session.ReloadInstanceCertificateAsync` manually and trigger a reconnect. The spec-mandated `ApplicationCertificateUpdated`, `TrustListUpdated`, and `CrlUpdated` enum values existed but only the first was being emitted by `CertificateManager`. Key client/shared changes: * **`CertificateManager` now emits `TrustListUpdated` and `CrlUpdated`** from `WriteTrustListAsync` (per mask) and from `TrustListTransaction.CommitAsync` (after the atomic apply). The new internal `NotifyTrustListChanged` also invalidates cached peer/user/HTTPS validation cores so subsequent validations pick up the fresh trust state. * **`ManagedSession.AutoReconnectOnCertificateChange`** opt-in property (default `false` for backwards compat). When `true`, `ManagedSession` subscribes to `CertificateManager.CertificateChanges` on construction and, on the three relevant kinds: - Calls `Session.ReloadInstanceCertificateAsync` on `ApplicationCertificateUpdated` so the next ActivateSession is signed with the rotated client cert. - Triggers a reconnect via the state machine so the new state is honoured within milliseconds rather than at the next `SecurityTokenLifetime` rekey. * **`ManagedSession.ApplicationCertificateChanged`** event surfaces every observed change regardless of the auto-reconnect policy for diagnostics or custom rotation logic. * Subscription is disposed by the existing `Dispose` / `DisposeAsync` paths. ```csharp using var managed = await ManagedSession.CreateAsync( configuration, endpoint, sessionFactory); managed.AutoReconnectOnCertificateChange = true; managed.ApplicationCertificateChanged += (sender, e) => { Console.WriteLine($"observed {e.Kind} on {e.TrustList}"); }; ``` ## Tests | Suite | net10.0 | net48 | |---|---|---| | `TransportListenerCertificateRotationTests` (new, 6 tests) | ✅ | ✅ | | `CertificateManagerTests` (4 new event tests) | ✅ 22/22 | ✅ 4/4 | | `PushCertManagement` | 50/53 ✅ | 50/53 ✅ | | `GDS PushTest` (focused subset) | 15/15 ✅ | 16/16 ✅ | | `Opc.Ua.Server.Tests` (focused subset) | 38/38 ✅ | 38/38 ✅ | | `GDS PushTest` (full) | 313/338 ✅ | — | Skips are pre-existing (cert-type mismatches, optional methods) — no regressions. New integration test `PushTest.ApplyChangesForcesChannelRenegotiateAsync` positively asserts the stale channel fails with a recognized channel-invalidation status after `ApplyChanges`, and that a fresh reconnect succeeds with the rotated cert. ## Documentation - `Docs/CertificateManager.md` — new sections: - "Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9)" — `ITransportListenerCertificateRotation` contract + custom listener example. - "Client-Side Auto-Detection of Certificate Changes (issue #3160)" — `AutoReconnectOnCertificateChange` + `ApplicationCertificateChanged` event + the three covered scenarios. ## Related Issues - Fixes #3562 - Fixes #3160 ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran focused tests locally using the **UA.slnx** projects against .net **framework 4.8** and .net **10**, and all passed. _(Full UA.slnx test run delegated to CI.)_ - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
156 lines
7.0 KiB
C#
156 lines
7.0 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <summary>
|
|
/// The Server Configuration Node Manager.
|
|
/// </summary>
|
|
public interface IConfigurationNodeManager : IAsyncNodeManager
|
|
{
|
|
/// <summary>
|
|
/// Raised when the <c>DefaultRolePermissions</c> or <c>DefaultUserRolePermissions</c>
|
|
/// property of any <see cref="NamespaceMetadataState"/> node changes.
|
|
/// Subscribers should invalidate any cached role-permission validation results.
|
|
/// </summary>
|
|
event EventHandler DefaultPermissionsChanged;
|
|
|
|
/// <summary>
|
|
/// Gets or creates the <see cref="NamespaceMetadataState"/> node for the specified NamespaceUri.
|
|
/// </summary>
|
|
ValueTask<NamespaceMetadataState> CreateNamespaceMetadataStateAsync(string namespaceUri, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Creates the configuration node for the server.
|
|
/// </summary>
|
|
void CreateServerConfiguration(ServerSystemContext systemContext, ApplicationConfiguration configuration);
|
|
|
|
/// <summary>
|
|
/// Gets and returns the <see cref="NamespaceMetadataState"/> node associated with the specified NamespaceUri
|
|
/// </summary>
|
|
ValueTask<NamespaceMetadataState?> GetNamespaceMetadataStateAsync(string namespaceUri, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets and returns the <see cref="NamespaceMetadataState"/> node associated with the specified namespace index
|
|
/// </summary>
|
|
ValueTask<NamespaceMetadataState?> GetNamespaceMetadataStateAsync(ushort namespaceIndex, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Binds the standard KeyCredentialConfiguration folder to the supplied push subject.
|
|
/// </summary>
|
|
ValueTask BindKeyCredentialPushAsync(
|
|
KeyCredentialPushSubject subject,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Determine if the impersonated user has admin access.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"/>
|
|
/// <seealso cref="StatusCodes.BadUserAccessDenied"/>
|
|
void HasApplicationSecureAdminAccess(ISystemContext context);
|
|
|
|
/// <summary>
|
|
/// Determine if the impersonated user has admin access.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"/>
|
|
/// <seealso cref="StatusCodes.BadUserAccessDenied"/>
|
|
void HasApplicationSecureAdminAccess(ISystemContext context, CertificateStoreIdentifier trustedStore);
|
|
|
|
/// <summary>
|
|
/// Starts periodic evaluation of <c>CertificateExpired</c> and
|
|
/// <c>TrustListOutOfDate</c> alarms per OPC 10000-12 §7.8.3.
|
|
/// Call this after the server is fully started so that
|
|
/// <c>SetActiveState</c> can safely emit event notifications.
|
|
/// </summary>
|
|
/// <param name="interval">
|
|
/// Evaluation interval. A typical value is 60 seconds.
|
|
/// </param>
|
|
void StartAlarmMonitoring(TimeSpan interval);
|
|
|
|
/// <summary>
|
|
/// Stops the periodic alarm evaluation.
|
|
/// </summary>
|
|
void StopAlarmMonitoring();
|
|
|
|
/// <summary>
|
|
/// The grace period that elapses between the
|
|
/// <c>ApplyChanges</c> method response being returned and the
|
|
/// per-channel force-renegotiate fan-out specified by OPC UA
|
|
/// Part 12 §7.10.9. Configurable because the right value depends
|
|
/// on the transport latency between the server and its
|
|
/// administrative client.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Defaults to 250 ms which is adequate for in-process and
|
|
/// LAN deployments. Hosts that run the server behind a higher-
|
|
/// latency link (e.g. WAN-fronted Push Configuration) should
|
|
/// raise this so the <c>ApplyChanges</c> response is flushed
|
|
/// before its SecureChannel is torn down.
|
|
/// </para>
|
|
/// <para>
|
|
/// Setting a non-positive value treats the rotation as
|
|
/// "immediate" and may cause the calling client to receive a
|
|
/// transport error instead of the method response. Tests can
|
|
/// instead await
|
|
/// <see cref="DrainPendingApplyChangesAsync(CancellationToken)"/>
|
|
/// for deterministic completion without relying on the grace
|
|
/// period.
|
|
/// </para>
|
|
/// <para>
|
|
/// TODO: replace the grace period with a transport-level
|
|
/// "response flushed" hook so the channel cut can run exactly
|
|
/// when the response has left the wire — tracked as a follow-up
|
|
/// feature on top of this PR.
|
|
/// </para>
|
|
/// </remarks>
|
|
TimeSpan ApplyChangesGracePeriod { get; set; }
|
|
|
|
/// <summary>
|
|
/// Awaits completion of any pending deferred work scheduled by a
|
|
/// recent <c>ApplyChanges</c> call. The deferred work includes
|
|
/// reloading certificates from disk via
|
|
/// <see cref="CertificateManager.UpdateAsync"/> and forcing
|
|
/// affected SecureChannels to renegotiate per OPC UA Part 12
|
|
/// §7.10.9.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Returns immediately when no deferred work is in flight. Used
|
|
/// by tests and tightly-coupled hosts to deterministically wait
|
|
/// for cert rotation to take effect after a push update.
|
|
/// </remarks>
|
|
/// <param name="cancellationToken">A token to cancel the wait.</param>
|
|
Task DrainPendingApplyChangesAsync(CancellationToken cancellationToken = default);
|
|
}
|
|
}
|