Skip to content

Conversation

@romanett
Copy link
Contributor

@romanett romanett commented Dec 22, 2025

Proposed changes

Introduce async certificate update events (Started/Completed) in CertificateValidator and ICertificateValidator. Add CertificateUpdateInProgress wait handle for synchronization. Implement CloseAllChannels in transport listeners to force-close connections before certificate updates, and add ForceClose to TcpListenerChannel. Update server logic to close all channels before updating certificates. Refactor and update tests for new async events and certificate update sequencing. Improves security and reliability during certificate rollover.

The behaviour of the Server in case of a Certficate Update now follows the spec more closely:
For each CertificateGroup it may be necessary to call ApplyChanges once the Certificate Update workflow completes. ApplyChanges is required if one or more of the Methods calls returns applyChangesRequired=TRUE.

This step may cause the Server to close its Endpoints and force all Clients to reconnect. If this happens the CertificateManager may need to use the new Certificate to re-establish a Session with the Server.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Introduce async certificate update events (`Started`/`Completed`) in `CertificateValidator` and `ICertificateValidator`. Add `CertificateUpdateInProgress` wait handle for synchronization. Implement `CloseAllChannels` in transport listeners to force-close connections before certificate updates, and add `ForceClose` to `TcpListenerChannel`. Update server logic to close all channels before updating certificates. Refactor and update tests for new async events and certificate update sequencing. Improves security and reliability during certificate rollover.
@romanett romanett changed the title [Server] Handle ApplicationCertificate updates safely using events & close channels on update [Server] Handle ApplicationCertificate updates safely using events: close channels on update and block new Connections / Requests Dec 22, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces async certificate update events (CertificateUpdateStarted and CertificateUpdate) to improve reliability and safety during application certificate updates. The changes ensure that all active channels are properly closed before a certificate update occurs, and new connections are blocked during the update process using a synchronization primitive.

Key changes:

  • Added async event handlers for certificate update lifecycle (started/completed)
  • Implemented CloseAllChannels in transport listeners to force-close connections before certificate updates
  • Added synchronization using ManualResetEventSlim to block new connections during updates

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Stack/Opc.Ua.Core/Security/Certificates/ICertificateValidator.cs Adds new events and WaitHandle property to interface
Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs Implements async event handlers, adds synchronization primitive, changes delegate signature to Task
Stack/Opc.Ua.Core/Stack/Transport/ITransportListener.cs Adds CloseAllChannels method to interface
Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs Implements CloseAllChannels and waits for certificate updates before accepting connections
Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs Adds ForceClose method to immediately close channels
Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs Implements CloseAllChannels (no-op) and waits for certificate updates before processing requests
Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs Adds OnCertificateUpdateStartedAsync handler to close all channels, changes delegate to Task
Libraries/Opc.Ua.Server/Server/StandardServer.cs Registers new CertificateUpdateStarted event handler
Libraries/Opc.Ua.Gds.Client.Common/*.cs Adds BadConnectionClosed to retry logic for connection attempts
Tests/Opc.Ua.Client.Tests/ClientFixture.cs Adds BadConnectionClosed to retry exception handling
Tests/Opc.Ua.Server.Tests/CreateSessionApplicationUriValidationTests.cs Adds BadConnectionClosed to retry exception handling
Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs Updates event handler signature from void to Task
Tests/Opc.Ua.Gds.Tests/PushTest.cs Adds test verification for certificate update events and synchronization
Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs Refactors inline array to static readonly field
Comments suppressed due to low confidence (4)

Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs:86

            string[] domainNames = new[] { "localhost", "127.0.0.1" };

Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs:124

            string[] domainNames = new[] { "localhost", "127.0.0.1" };

Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs:187

            string[] domainNames = new[] { "localhost" };

Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs:219

            string[] domainNames = new[] { "localhost", "testhost.local", "192.168.1.1" };

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.
Comment on lines 3342 to +3343
CertificateValidator.CertificateUpdate += OnCertificateUpdateAsync;
CertificateValidator.CertificateUpdateStarted += OnCertificateUpdateStartedAsync;
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 event handlers registered for CertificateUpdate and CertificateUpdateStarted are never unsubscribed. This can lead to memory leaks if the StandardServer is disposed while the CertificateValidator remains alive, as the validator will hold references to the server through these event handlers.

Consider unsubscribing from these events in the Dispose method or OnServerStoppingAsync to prevent memory leaks.

Copilot uses AI. Check for mistakes.
event CertificateUpdateEventHandler CertificateUpdateStarted;

/// <summary>
/// An event that signals that a certificate update is in progress.
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 documentation comment states "An event that signals that a certificate update is in progress", but the actual behavior is the opposite. The event is signaled (set to true) when no update is in progress, and reset (set to false) during updates. The documentation should accurately describe that this WaitHandle is signaled when NO certificate update is in progress, and unsignaled during an update.

Suggested change
/// An event that signals that a certificate update is in progress.
/// A wait handle that is signaled when no certificate update is in progress
/// and unsignaled while a certificate update is in progress.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
/// An event that signals that a certificate update is in progress.
/// </summary>
WaitHandle CertificateUpdateInProgress { get; }
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 property name "CertificateUpdateInProgress" has inverted semantics. The WaitHandle is signaled (set) when the update is NOT in progress, and unsignaled (reset) when an update IS in progress. This is confusing because code like "CertificateUpdateInProgress.WaitOne()" actually waits for the update to complete, not to be in progress.

Consider renaming to "CertificateUpdateCompleted" or "CertificateUpdateNotInProgress" to better reflect the actual state being signaled. Alternatively, invert the logic by initializing the event as reset (false) and setting it during updates if you want to keep the current name.

Suggested change
/// An event that signals that a certificate update is in progress.
/// </summary>
WaitHandle CertificateUpdateInProgress { get; }
/// A wait handle that is signaled when no certificate update is in progress.
/// </summary>
WaitHandle CertificateUpdateCompleted { get; }

Copilot uses AI. Check for mistakes.

await DisconnectPushClientAsync().ConfigureAwait(false);

var certificateUpdate = new ManualResetEvent(false);
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.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
Comment on lines +1082 to +1117
var certificateUpdate = new ManualResetEvent(false);
var certificateUpdateStarted = new ManualResetEvent(false);
Task OnCertificateUpdateStarted(object sender, EventArgs e)
{
certificateUpdateStarted.Set();
return Task.CompletedTask;
}
Task OnCertificateUpdated(object sender, EventArgs e)
{
certificateUpdate.Set();
return Task.CompletedTask;
}

var validator = m_server.Config.CertificateValidator;
try
{
validator.CertificateUpdateStarted += OnCertificateUpdateStarted;
validator.CertificateUpdate += OnCertificateUpdated;

if (!certificateUpdateStarted.WaitOne(TimeSpan.FromSeconds(10)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not start.");
}

if (!certificateUpdate.WaitOne(TimeSpan.FromSeconds(30)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not complete.");
}
}
finally
{
validator.CertificateUpdateStarted -= OnCertificateUpdateStarted;
validator.CertificateUpdate -= OnCertificateUpdated;
certificateUpdate.Dispose();
certificateUpdateStarted.Dispose();
}
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.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Suggested change
var certificateUpdate = new ManualResetEvent(false);
var certificateUpdateStarted = new ManualResetEvent(false);
Task OnCertificateUpdateStarted(object sender, EventArgs e)
{
certificateUpdateStarted.Set();
return Task.CompletedTask;
}
Task OnCertificateUpdated(object sender, EventArgs e)
{
certificateUpdate.Set();
return Task.CompletedTask;
}
var validator = m_server.Config.CertificateValidator;
try
{
validator.CertificateUpdateStarted += OnCertificateUpdateStarted;
validator.CertificateUpdate += OnCertificateUpdated;
if (!certificateUpdateStarted.WaitOne(TimeSpan.FromSeconds(10)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not start.");
}
if (!certificateUpdate.WaitOne(TimeSpan.FromSeconds(30)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not complete.");
}
}
finally
{
validator.CertificateUpdateStarted -= OnCertificateUpdateStarted;
validator.CertificateUpdate -= OnCertificateUpdated;
certificateUpdate.Dispose();
certificateUpdateStarted.Dispose();
}
using (var certificateUpdate = new ManualResetEvent(false))
using (var certificateUpdateStarted = new ManualResetEvent(false))
{
Task OnCertificateUpdateStarted(object sender, EventArgs e)
{
certificateUpdateStarted.Set();
return Task.CompletedTask;
}
Task OnCertificateUpdated(object sender, EventArgs e)
{
certificateUpdate.Set();
return Task.CompletedTask;
}
var validator = m_server.Config.CertificateValidator;
try
{
validator.CertificateUpdateStarted += OnCertificateUpdateStarted;
validator.CertificateUpdate += OnCertificateUpdated;
if (!certificateUpdateStarted.WaitOne(TimeSpan.FromSeconds(10)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not start.");
}
if (!certificateUpdate.WaitOne(TimeSpan.FromSeconds(30)))
{
NUnit.Framework.Assert.Fail("Server certificate update did not complete.");
}
}
finally
{
validator.CertificateUpdateStarted -= OnCertificateUpdateStarted;
validator.CertificateUpdate -= OnCertificateUpdated;
}
}

Copilot uses AI. Check for mistakes.
@romanett romanett changed the title [Server] Handle ApplicationCertificate updates safely using events: close channels on update and block new Connections / Requests [Server] Handle ApplicationCertificate updates using events: close channels on update and block new Connections / Requests Dec 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants