Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ await CoreClientUtils.SelectEndpointAsync(
}
catch (ServiceResultException e) when ((e.StatusCode is
StatusCodes.BadServerHalted or
StatusCodes.BadConnectionClosed or
StatusCodes.BadSecureChannelClosed or
StatusCodes.BadNoCommunication) &&
attempt < maxAttempts)
Expand Down Expand Up @@ -432,6 +433,7 @@ public async Task ConnectAsync(ConfiguredEndpoint endpoint, CancellationToken ct
catch (ServiceResultException e) when ((e.StatusCode is
StatusCodes.BadServerHalted or
StatusCodes.BadSecureChannelClosed or
StatusCodes.BadConnectionClosed or
StatusCodes.BadNoCommunication) &&
attempt < maxAttempts)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ await CoreClientUtils.SelectEndpointAsync(
catch (ServiceResultException e) when ((e.StatusCode is
StatusCodes.BadServerHalted or
StatusCodes.BadSecureChannelClosed or
StatusCodes.BadConnectionClosed or
StatusCodes.BadNoCommunication) &&
attempt < maxAttempts)
{
Expand Down Expand Up @@ -345,6 +346,7 @@ public async Task ConnectAsync(ConfiguredEndpoint endpoint, CancellationToken ct
catch (ServiceResultException e) when ((e.StatusCode is
StatusCodes.BadServerHalted or
StatusCodes.BadSecureChannelClosed or
StatusCodes.BadConnectionClosed or
StatusCodes.BadNoCommunication) &&
attempt < maxAttempts)
{
Expand Down
4 changes: 4 additions & 0 deletions Libraries/Opc.Ua.Server/Server/StandardServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3340,6 +3340,7 @@ await subscriptionManager.StartupAsync(cancellationToken)
}

CertificateValidator.CertificateUpdate += OnCertificateUpdateAsync;
CertificateValidator.CertificateUpdateStarted += OnCertificateUpdateStartedAsync;
}

/// <summary>
Expand Down Expand Up @@ -3397,6 +3398,9 @@ protected override async ValueTask OnServerStoppingAsync(CancellationToken cance
Utils.SilentDispose(m_serverInternal);
m_serverInternal = null;
}

CertificateValidator.CertificateUpdate -= OnCertificateUpdateAsync;
CertificateValidator.CertificateUpdateStarted -= OnCertificateUpdateStartedAsync;
}
}

Expand Down
17 changes: 17 additions & 0 deletions Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ public async Task SendAsync(HttpContext context)
CancellationToken ct = context.RequestAborted;
try
{
ICertificateValidator certificateValidator = m_quotas?.CertificateValidator;
if (certificateValidator == null)
{
// Listener is closed, don't process this connection
throw new InvalidOperationException("CertificateValidator is null, listener is closed.");
}

// wait for certificate update to complete before processing requests.
certificateValidator.CertificateUpdateInProgress.WaitOne();

if (m_callback == null)
{
await WriteResponseAsync(
Expand Down Expand Up @@ -608,6 +618,13 @@ private bool ValidateClientCertificate(
return true;
}


/// <inheritdoc/>
public void CloseAllChannels(string reason)
{
Stop();
}

private EndpointDescriptionCollection m_descriptions;
private ChannelQuotas m_quotas;
private ITransportListenerCallback m_callback;
Expand Down
110 changes: 71 additions & 39 deletions Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ public event CertificateUpdateEventHandler CertificateUpdate
remove => m_CertificateUpdate -= value;
}

/// <summary>
/// Raised before an application certificate update occurs.
/// </summary>
public event CertificateUpdateEventHandler CertificateUpdateStarted
{
add => m_CertificateUpdateStarted += value;
remove => m_CertificateUpdateStarted -= value;
}

/// <summary>
/// Updates the validator with the current state of the configuration.
/// </summary>
Expand Down Expand Up @@ -284,51 +293,69 @@ public virtual async Task UpdateCertificateAsync(
string applicationUri = null,
CancellationToken ct = default)
{
await m_semaphore.WaitAsync(ct).ConfigureAwait(false);
m_updateEvent.Reset();

try
{
m_applicationCertificates.Clear();
//
// crash occurs if the cert is in use still and this has not run yet.
// This might be the intended design but this runs on a free task that
// might not be scheduled right away.
//
// TODO: We need a better way to disconnect all sessions when the cert is
// updated. (See caller of this method)
//
// foreach (CertificateIdentifier applicationCertificate in securityConfiguration
// .ApplicationCertificates)
// {
// applicationCertificate.DisposeCertificate();
// }

foreach (CertificateIdentifier applicationCertificate in securityConfiguration
.ApplicationCertificates)
{
await applicationCertificate
.LoadPrivateKeyExAsync(
securityConfiguration.CertificatePasswordProvider,
applicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
CertificateUpdateEventHandler started_callback = m_CertificateUpdateStarted;
if (started_callback != null)
{
var args = new CertificateUpdateEventArgs(
securityConfiguration,
GetChannelValidator());
await started_callback(this, args).ConfigureAwait(false);
}

await m_semaphore.WaitAsync(ct).ConfigureAwait(false);

try
{
m_applicationCertificates.Clear();
//
// crash occurs if the cert is in use still and this has not run yet.
// This might be the intended design but this runs on a free task that
// might not be scheduled right away.
//
// TODO: We need a better way to disconnect all sessions when the cert is
// updated. (See caller of this method)
//
// foreach (CertificateIdentifier applicationCertificate in securityConfiguration
// .ApplicationCertificates)
// {
// applicationCertificate.DisposeCertificate();
// }

foreach (CertificateIdentifier applicationCertificate in securityConfiguration
.ApplicationCertificates)
{
await applicationCertificate
.LoadPrivateKeyExAsync(
securityConfiguration.CertificatePasswordProvider,
applicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
}
}
finally
{
m_semaphore.Release();
}
}
finally
{
m_semaphore.Release();
}

await UpdateAsync(securityConfiguration, applicationUri, ct).ConfigureAwait(false);
await UpdateAsync(securityConfiguration, applicationUri, ct).ConfigureAwait(false);

CertificateUpdateEventHandler callback = m_CertificateUpdate;
if (callback != null)
CertificateUpdateEventHandler callback = m_CertificateUpdate;
if (callback != null)
{
var args = new CertificateUpdateEventArgs(
securityConfiguration,
GetChannelValidator());
await callback(this, args).ConfigureAwait(false);
}
}
finally
{
var args = new CertificateUpdateEventArgs(
securityConfiguration,
GetChannelValidator());
callback(this, args);
m_updateEvent.Set();
}
}

Expand Down Expand Up @@ -2234,6 +2261,9 @@ public static bool IsECSecureForProfile(
throw new NotSupportedException("Unsupported curve type.");
}

/// <inheritdoc/>
public WaitHandle CertificateUpdateInProgress => m_updateEvent.WaitHandle;

/// <summary>
/// Flag to protect setting by application
/// from a modification by a SecurityConfiguration.
Expand All @@ -2254,13 +2284,15 @@ private enum ProtectFlags
private readonly ILogger m_logger;
private readonly ITelemetryContext m_telemetry;
private readonly Dictionary<string, X509Certificate2> m_validatedCertificates;
private readonly ManualResetEventSlim m_updateEvent = new(true);
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ManualResetEventSlim m_updateEvent is never disposed, which can lead to a resource leak. ManualResetEventSlim implements IDisposable and should be properly disposed when the CertificateValidator is disposed or finalized.

Consider making CertificateValidator implement IDisposable and dispose of m_updateEvent in the Dispose method, or dispose it in a finalizer if IDisposable implementation is not feasible.

Copilot uses AI. Check for mistakes.
private CertificateStoreIdentifier m_trustedCertificateStore;
private CertificateIdentifierCollection m_trustedCertificateList;
private CertificateStoreIdentifier m_issuerCertificateStore;
private CertificateIdentifierCollection m_issuerCertificateList;
private CertificateStoreIdentifier m_rejectedCertificateStore;
private event CertificateValidationEventHandler m_CertificateValidation;
private event CertificateUpdateEventHandler m_CertificateUpdate;
private event CertificateUpdateEventHandler m_CertificateUpdateStarted;
private readonly List<X509Certificate2> m_applicationCertificates;
private ProtectFlags m_protectFlags;
private bool m_autoAcceptUntrustedCertificates;
Expand Down Expand Up @@ -2350,7 +2382,7 @@ public CertificateUpdateEventArgs(
/// <summary>
/// Used to handle certificate update events.
/// </summary>
public delegate void CertificateUpdateEventHandler(
public delegate Task CertificateUpdateEventHandler(
CertificateValidator sender,
CertificateUpdateEventArgs e);
}
24 changes: 24 additions & 0 deletions Stack/Opc.Ua.Core/Security/Certificates/ICertificateValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ namespace Opc.Ua
/// </summary>
public interface ICertificateValidator
{
/// <summary>
/// Raised when an application certificate update occurs.
/// </summary>
event CertificateUpdateEventHandler CertificateUpdate;

/// <summary>
/// Raised before an application certificate update occurs.
/// </summary>
event CertificateUpdateEventHandler CertificateUpdateStarted;

/// <summary>
/// A wait handle that is signaled when no certificate update is in progress
/// and unsignaled while a certificate update is in progress.
/// </summary>
WaitHandle CertificateUpdateInProgress { get; }

/// <summary>
/// Updates the validator with a new application certificate.
/// </summary>
Task UpdateCertificateAsync(
SecurityConfiguration securityConfiguration,
string applicationUri = null,
CancellationToken ct = default);

/// <summary>
/// Validates a certificate.
/// </summary>
Expand Down
21 changes: 20 additions & 1 deletion Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ protected virtual EndpointBase GetEndpointInstance(ServerBase server)
/// <summary>
/// Called after the application certificate update.
/// </summary>
protected virtual async void OnCertificateUpdateAsync(object sender, CertificateUpdateEventArgs e)
protected virtual async Task OnCertificateUpdateAsync(object sender, CertificateUpdateEventArgs e)
{
try
{
Expand Down Expand Up @@ -810,6 +810,25 @@ await InstanceCertificateTypesProvider.LoadCertificateChainAsync(certificate)
}
}

/// <summary>
/// Called before the application certificate update.
/// </summary>
protected virtual Task OnCertificateUpdateStartedAsync(object sender, CertificateUpdateEventArgs e)
{
try
{
foreach (ITransportListener listener in TransportListeners)
{
listener.CloseAllChannels("Update of ApplicationCertificate");
}
}
catch (Exception ex)
{
m_logger.LogError(ex, "Failed to close all channels on certificate update: {EventArgs}", e);
}
return Task.CompletedTask;
}

/// <summary>
/// Create the transport listener for the service host endpoint.
/// </summary>
Expand Down
48 changes: 48 additions & 0 deletions Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ public void IdleCleanup()
}
}

/// <summary>
/// Force the channel to close immediately, e.g. due to certificate update.
/// </summary>
public void ForceClose(string reason)
{
TcpChannelState state;

lock (DataLock)
{
state = State;
if (state is TcpChannelState.Open or TcpChannelState.Connecting)
{
state = State = TcpChannelState.Closing;
}
}

if (state is TcpChannelState.Closing or TcpChannelState.Opening or TcpChannelState.Faulted)
{
OnForceClosed(reason);
}
}

/// <summary>
/// The time in milliseconds elapsed since the channel received or sent messages
/// or received a keep alive.
Expand Down Expand Up @@ -349,6 +371,32 @@ private void OnCleanup(object state)
}
}

/// <summary>
/// Called when the channel is force closed.
/// </summary>
private void OnForceClosed(string reason)
{
lock (DataLock)
{
// nothing to do if the channel is now open or closed.
if (State is TcpChannelState.Closed or TcpChannelState.Open)
{
return;
}

m_logger.LogInformation(
"{Channel} Force Close Socket={SocketHandle:X8}, ChannelId={ChannelId}, TokenId={TokenId}, Reason={Reason}",
ChannelName,
(Socket?.Handle) ?? 0,
CurrentToken != null ? CurrentToken.ChannelId : 0,
CurrentToken != null ? CurrentToken.TokenId : 0,
reason);

// close channel.
ChannelClosed();
}
}

/// <summary>
/// Closes the channel and releases resources.
/// Sets state to Closed and notifies monitors.
Expand Down
Loading
Loading