From e4b880df407e5c2e1ff7544097c7c7c35e7342a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 15:16:59 +0300 Subject: [PATCH 01/16] Bump MQTTnet from 4.3.3.952 to 4.3.6.1152 (#2629) Bumps [MQTTnet](https://github.com/dotnet/MQTTnet) from 4.3.3.952 to 4.3.6.1152. - [Release notes](https://github.com/dotnet/MQTTnet/releases) - [Commits](https://github.com/dotnet/MQTTnet/compare/v4.3.3.952...v4.3.6.1152) --- updated-dependencies: - dependency-name: MQTTnet dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj b/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj index 34632e558..cba0c56ec 100644 --- a/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj +++ b/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj @@ -36,7 +36,7 @@ - + From 1e1de4895d8efd3a90e3efab331557c4c66270fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 16:32:11 +0300 Subject: [PATCH 02/16] Bump BouncyCastle.Cryptography from 2.3.1 to 2.4.0 (#2630) Bumps [BouncyCastle.Cryptography](https://github.com/bcgit/bc-csharp) from 2.3.1 to 2.4.0. - [Commits](https://github.com/bcgit/bc-csharp/compare/release-2.3.1...release-2.4.0) --- updated-dependencies: - dependency-name: BouncyCastle.Cryptography dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj | 2 +- .../Opc.Ua.Security.Certificates.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj b/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj index cc26dafd3..7750c96a6 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj +++ b/Libraries/Opc.Ua.Gds.Server.Common/Opc.Ua.Gds.Server.Common.csproj @@ -26,7 +26,7 @@ - + diff --git a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj index ba3706220..fab90dac5 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj +++ b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj @@ -49,11 +49,11 @@ - + - + From 3ba3b17237ef5f9364f0fd2ed276abfac266d42c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 16:34:58 +0300 Subject: [PATCH 03/16] Bump Microsoft.NET.Test.Sdk from 17.9.0 to 17.10.0 (#2631) Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.9.0 to 17.10.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.9.0...v17.10.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index b80e985a0..d82f82077 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index 1cec7bbba..2fbec7a64 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index b4c571b42..973fbe789 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index ed4d68faf..0c000195e 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index dd4df2055..f3b905127 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index 65a5f6375..0ddbba164 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index c2daf5e7d..7df5a26ff 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 74af9b15a..cecae8ccd 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -8,7 +8,7 @@ - + From ebf8f6265d7bdc3db92eefc7ebe79534ec1aff0f Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 2 Jun 2024 19:05:17 +0200 Subject: [PATCH 04/16] Fix reopen secure channel without activate (#2577) Fix that if a socket is closed, new service calls are queued and a new channel is established without reconnect / activate. Add some logic which ensures that a BeginConnect operation is only started if a valid service call is queued first. The activate is prepared by the session reconnect handler which may take some time to respond. Until then, all other service fail immediately with BadNotConnected. To detect socket failures earlier, the PublishRequest and KeepAlive send are also hooked up to the KeepAliveError to trigger a reconnect. To disable the keep alive timing calcualtion, an additional variable tracks the statuscode of the last error to set KeepAliveStopped to true. --- .../ConsoleReferenceClient/ClientSamples.cs | 6 +- .../ConsoleReferenceClient/Program.cs | 40 +++++- Libraries/Opc.Ua.Client/Session.cs | 114 ++++++++++----- Libraries/Opc.Ua.Client/SessionAsync.cs | 29 +++- .../Opc.Ua.Client/SessionReconnectHandler.cs | 16 ++- .../Opc.Ua.Bindings.Https.csproj | 1 + .../Stack/Https/HttpsTransportChannel.cs | 3 +- Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs | 12 +- .../Stack/Tcp/ChannelAsyncOperation.cs | 12 +- .../Stack/Tcp/UaSCBinaryChannel.cs | 22 +-- .../Stack/Tcp/UaSCBinaryClientChannel.cs | 132 ++++++++++++------ .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 6 +- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 61 +++++--- .../ClientTestFramework.cs | 7 +- Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs | 123 ++++++++-------- Tests/Opc.Ua.Server.Tests/ServerFixture.cs | 9 +- 16 files changed, 391 insertions(+), 202 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ClientSamples.cs b/Applications/ConsoleReferenceClient/ClientSamples.cs index 5730c01c8..4291642e5 100644 --- a/Applications/ConsoleReferenceClient/ClientSamples.cs +++ b/Applications/ConsoleReferenceClient/ClientSamples.cs @@ -699,7 +699,7 @@ public async Task BrowseFullAddressSpaceAsync( /// Outputs elapsed time information for perf testing and lists all /// types that were successfully added to the session encodeable type factory. /// - public async Task LoadTypeSystemAsync(ISession session) + public async Task LoadTypeSystemAsync(ISession session) { m_output.WriteLine("Load the server type system."); @@ -732,6 +732,8 @@ public async Task LoadTypeSystemAsync(ISession session) } } } + + return complexTypeSystem; } #endregion @@ -900,7 +902,7 @@ public async Task SubscribeAllValuesAsync( StartNodeId = item.NodeId, AttributeId = Attributes.Value, SamplingInterval = samplingInterval, - DisplayName = item.DisplayName?.Text ?? item.BrowseName.Name, + DisplayName = item.DisplayName?.Text ?? item.BrowseName?.Name ?? "unknown", QueueSize = queueSize, DiscardOldest = true, MonitoringMode = MonitoringMode.Reporting, diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index 34cbccb86..e0b3cde33 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -234,7 +234,7 @@ public static async Task Main(string[] args) var samples = new ClientSamples(output, ClientBase.ValidateResponse, quitEvent, verbose); if (loadTypes) { - await samples.LoadTypeSystemAsync(uaClient.Session).ConfigureAwait(false); + var complexTypeSystem = await samples.LoadTypeSystemAsync(uaClient.Session).ConfigureAwait(false); } if (browseall || fetchall || jsonvalues) @@ -266,8 +266,8 @@ public static async Task Main(string[] args) if (subscribe && (browseall || fetchall)) { - // subscribe to 100 random variables - const int MaxVariables = 100; + // subscribe to 1000 random variables + const int MaxVariables = 1000; NodeCollection variables = new NodeCollection(); Random random = new Random(62541); if (fetchall) @@ -291,15 +291,41 @@ public static async Task Main(string[] args) await samples.SubscribeAllValuesAsync(uaClient, variableIds: new NodeCollection(variables), - samplingInterval: 1000, - publishingInterval: 5000, + samplingInterval: 100, + publishingInterval: 1000, queueSize: 10, - lifetimeCount: 12, + lifetimeCount: 60, keepAliveCount: 2).ConfigureAwait(false); // Wait for DataChange notifications from MonitoredItems output.WriteLine("Subscribed to {0} variables. Press Ctrl-C to exit.", MaxVariables); - quit = quitEvent.WaitOne(timeout > 0 ? waitTime : Timeout.Infinite); + + // free unused memory + uaClient.Session.NodeCache.Clear(); + + waitTime = timeout - (int)DateTime.UtcNow.Subtract(start).TotalMilliseconds; + DateTime endTime = waitTime > 0 ? DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(waitTime)) : DateTime.MaxValue; + var variableIterator = variables.GetEnumerator(); + while (!quit && endTime > DateTime.UtcNow) + { + if (variableIterator.MoveNext()) + { + try + { + var value = await uaClient.Session.ReadValueAsync(variableIterator.Current.NodeId).ConfigureAwait(false); + output.WriteLine("Value of {0} is {1}", variableIterator.Current.NodeId, value); + } + catch (Exception ex) + { + output.WriteLine("Error reading value of {0}: {1}", variableIterator.Current.NodeId, ex.Message); + } + } + else + { + variableIterator = variables.GetEnumerator(); + } + quit = quitEvent.WaitOne(500); + } } else { diff --git a/Libraries/Opc.Ua.Client/Session.cs b/Libraries/Opc.Ua.Client/Session.cs index c30d5c935..4d06e56ab 100644 --- a/Libraries/Opc.Ua.Client/Session.cs +++ b/Libraries/Opc.Ua.Client/Session.cs @@ -27,6 +27,10 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +#if NET6_0_OR_GREATER +#define PERIODIC_TIMER +#endif + using System; using System.Collections.Generic; using System.Diagnostics; @@ -40,6 +44,7 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Extensions.Logging; +using Opc.Ua.Bindings; namespace Opc.Ua.Client { @@ -741,17 +746,25 @@ public int KeepAliveInterval /// Returns true if the session is not receiving keep alives. /// /// - /// Set to true if the server does not respond for 2 times the KeepAliveInterval. - /// Set to false is communication recovers. + /// Set to true if the server does not respond for 2 times the KeepAliveInterval + /// or if another error was reported. + /// Set to false is communication is ok or recovered. /// public bool KeepAliveStopped { get { - TimeSpan delta = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - Interlocked.Read(ref m_lastKeepAliveTime)); + StatusCode lastKeepAliveErrorStatusCode = m_lastKeepAliveErrorStatusCode; + if (StatusCode.IsGood(lastKeepAliveErrorStatusCode) || lastKeepAliveErrorStatusCode == StatusCodes.BadNoCommunication) + { + TimeSpan delta = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - Interlocked.Read(ref m_lastKeepAliveTime)); - // add a guard band to allow for network lag. - return (m_keepAliveInterval + kKeepAliveGuardBand) <= delta.TotalMilliseconds; + // add a guard band to allow for network lag. + return (m_keepAliveInterval + kKeepAliveGuardBand) <= delta.TotalMilliseconds; + } + + // another error was reported which caused keep alive to stop. + return true; } } @@ -1429,7 +1442,10 @@ private void Reconnect(ITransportWaitingConnection connection, ITransportChannel if (!result.AsyncWaitHandle.WaitOne(kReconnectTimeout / 2)) { - Utils.LogWarning("WARNING: ACTIVATE SESSION timed out. {0}/{1}", GoodPublishRequestCount, OutstandingRequestCount); + var error = ServiceResult.Create(StatusCodes.BadRequestTimeout, "ACTIVATE SESSION timed out. {0}/{1}", GoodPublishRequestCount, OutstandingRequestCount); + Utils.LogWarning("WARNING: {0}", error.ToString()); + var operation = result as ChannelAsyncOperation; + operation?.Fault(false, error); } // reactivate session. @@ -3690,6 +3706,7 @@ private void StartKeepAliveTimer() { int keepAliveInterval = m_keepAliveInterval; + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); m_serverState = ServerState.Unknown; @@ -3709,7 +3726,7 @@ private void StartKeepAliveTimer() { StopKeepAliveTimer(); -#if NET6_0_OR_GREATER +#if PERIODIC_TIMER // start periodic timer loop var keepAliveTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(keepAliveInterval)); _ = Task.Run(() => OnKeepAliveAsync(keepAliveTimer, nodesToRead)); @@ -3821,7 +3838,7 @@ private void AsyncRequestCompleted(IAsyncResult result, uint requestId, uint typ } } -#if NET6_0_OR_GREATER +#if PERIODIC_TIMER /// /// Sends a keep alive by reading from the server. /// @@ -3893,6 +3910,11 @@ private void OnSendKeepAlive(ReadValueIdCollection nodesToRead) AsyncRequestStarted(result, requestHeader.RequestHandle, DataTypes.ReadRequest); } + catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadNotConnected) + { + // recover from error condition when secure channel is still alive + OnKeepAliveError(sre.Result); + } catch (Exception e) { Utils.LogError("Could not send keep alive request: {0} {1}", e.GetType().FullName, e.Message); @@ -3935,10 +3957,10 @@ private void OnKeepAliveComplete(IAsyncResult result) return; } - catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadSessionIdInvalid) + catch (ServiceResultException sre) { // recover from error condition when secure channel is still alive - OnKeepAliveError(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session unavailable for keep alive requests.")); + OnKeepAliveError(sre.Result); } catch (Exception e) { @@ -3960,6 +3982,7 @@ protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTim return; } + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); lock (m_outstandingRequests) @@ -3977,6 +4000,7 @@ protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTim } else { + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); } @@ -4003,14 +4027,20 @@ protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTim /// protected virtual bool OnKeepAliveError(ServiceResult result) { - long delta = DateTime.UtcNow.Ticks - Interlocked.Read(ref m_lastKeepAliveTime); + DateTime now = DateTime.UtcNow; - Utils.LogInfo( - "KEEP ALIVE LATE: {0}s, EndpointUrl={1}, RequestCount={2}/{3}", - ((double)delta) / TimeSpan.TicksPerSecond, - this.Endpoint?.EndpointUrl, - this.GoodPublishRequestCount, - this.OutstandingRequestCount); + m_lastKeepAliveErrorStatusCode = result.StatusCode; + if (result.StatusCode == StatusCodes.BadNoCommunication) + { + // keep alive read timed out + long delta = now.Ticks - Interlocked.Read(ref m_lastKeepAliveTime); + Utils.LogInfo( + "KEEP ALIVE LATE: {0}s, EndpointUrl={1}, RequestCount={2}/{3}", + ((double)delta) / TimeSpan.TicksPerSecond, + this.Endpoint?.EndpointUrl, + this.GoodPublishRequestCount, + this.OutstandingRequestCount); + } KeepAliveEventHandler callback = m_KeepAlive; @@ -4018,7 +4048,7 @@ protected virtual bool OnKeepAliveError(ServiceResult result) { try { - KeepAliveEventArgs args = new KeepAliveEventArgs(result, ServerState.Unknown, DateTime.UtcNow); + KeepAliveEventArgs args = new KeepAliveEventArgs(result, ServerState.Unknown, now); callback(this, args); return !args.CancelKeepAlive; } @@ -5074,14 +5104,18 @@ private void OnPublishComplete(IAsyncResult result) case StatusCodes.BadNoSubscription: case StatusCodes.BadSessionClosed: - case StatusCodes.BadSessionIdInvalid: - case StatusCodes.BadSecureChannelIdInvalid: - case StatusCodes.BadSecureChannelClosed: case StatusCodes.BadSecurityChecksFailed: case StatusCodes.BadCertificateInvalid: case StatusCodes.BadServerHalted: return; + // may require a reconnect or activate to recover + case StatusCodes.BadSessionIdInvalid: + case StatusCodes.BadSecureChannelIdInvalid: + case StatusCodes.BadSecureChannelClosed: + OnKeepAliveError(error); + return; + // Servers may return this error when overloaded case StatusCodes.BadTooManyOperations: case StatusCodes.BadTcpServerTooBusy: @@ -5089,10 +5123,13 @@ private void OnPublishComplete(IAsyncResult result) // throttle the next publish to reduce server load _ = Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); - BeginPublish(OperationTimeout); + QueueBeginPublish(); }); return; + case StatusCodes.BadTimeout: + break; + default: Utils.LogError(e, "PUBLISH #{0} - Unhandled error {1} during Publish.", requestHeader.RequestHandle, error.StatusCode); goto case StatusCodes.BadServerTooBusy; @@ -5100,17 +5137,7 @@ private void OnPublishComplete(IAsyncResult result) } } - int requestCount = GoodPublishRequestCount; - int minPublishRequestCount = GetMinPublishRequestCount(false); - - if (requestCount < minPublishRequestCount) - { - BeginPublish(OperationTimeout); - } - else - { - Utils.LogInfo("PUBLISH - Did not send another publish request. GoodPublishRequestCount={0}, MinPublishRequestCount={1}", requestCount, minPublishRequestCount); - } + QueueBeginPublish(); } /// @@ -5201,6 +5228,24 @@ public bool ResendData(IEnumerable subscriptions, out IList + /// Queues a publish request if there are not enough outstanding requests. + /// + private void QueueBeginPublish() + { + int requestCount = GoodPublishRequestCount; + int minPublishRequestCount = GetMinPublishRequestCount(false); + + if (requestCount < minPublishRequestCount) + { + BeginPublish(OperationTimeout); + } + else + { + Utils.LogInfo("PUBLISH - Did not send another publish request. GoodPublishRequestCount={0}, MinPublishRequestCount={1}", requestCount, minPublishRequestCount); + } + } + /// /// Validates the identity for an open call. /// @@ -6315,9 +6360,10 @@ private static void UpdateLatestSequenceNumberToSend(ref uint latestSequenceNumb private long m_publishCounter; private int m_tooManyPublishRequests; private long m_lastKeepAliveTime; + private StatusCode m_lastKeepAliveErrorStatusCode; private ServerState m_serverState; private int m_keepAliveInterval; -#if NET6_0_OR_GREATER +#if PERIODIC_TIMER private PeriodicTimer m_keepAliveTimer; #else private Timer m_keepAliveTimer; diff --git a/Libraries/Opc.Ua.Client/SessionAsync.cs b/Libraries/Opc.Ua.Client/SessionAsync.cs index f4eb6def5..58bb21e34 100644 --- a/Libraries/Opc.Ua.Client/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/SessionAsync.cs @@ -1332,6 +1332,11 @@ await session.OpenAsync( /// The new session object. public static async Task RecreateAsync(Session sessionTemplate, ITransportChannel transportChannel, CancellationToken ct = default) { + if (transportChannel == null) + { + return await Session.RecreateAsync(sessionTemplate, ct).ConfigureAwait(false); + } + ServiceMessageContext messageContext = sessionTemplate.m_configuration.CreateMessageContext(); messageContext.Factory = sessionTemplate.Factory; @@ -1499,15 +1504,27 @@ private async Task ReconnectAsync(ITransportWaitingConnection connection, ITrans connection, transportChannel); - if (!(result is ChannelAsyncOperation operation)) throw new ArgumentNullException(nameof(result)); - - try + const string timeoutMessage = "ACTIVATE SESSION ASYNC timed out. {0}/{1}"; + if (result is ChannelAsyncOperation operation) { - _ = await operation.EndAsync(kReconnectTimeout / 2, true, ct).ConfigureAwait(false); + try + { + _ = await operation.EndAsync(kReconnectTimeout / 2, true, ct).ConfigureAwait(false); + } + catch (ServiceResultException sre) + { + if (sre.StatusCode == StatusCodes.BadRequestInterrupted) + { + var error = ServiceResult.Create(StatusCodes.BadRequestTimeout, timeoutMessage, + GoodPublishRequestCount, OutstandingRequestCount); + Utils.LogWarning("WARNING: {0}", error.ToString()); + operation.Fault(false, error); + } + } } - catch (ServiceResultException) + else if (!result.AsyncWaitHandle.WaitOne(kReconnectTimeout / 2)) { - Utils.LogWarning("WARNING: ACTIVATE SESSION {0} timed out. {1}/{2}", SessionId, GoodPublishRequestCount, OutstandingRequestCount); + Utils.LogWarning(timeoutMessage, GoodPublishRequestCount, OutstandingRequestCount); } // reactivate session. diff --git a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs index 9ec0ac072..eb2e9d4ca 100644 --- a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs +++ b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs @@ -315,8 +315,8 @@ private async void OnReconnectAsync(object state) keepaliveRecovered = true; // breaking change, the callback must only assign the new // session if the property is != null - m_session = null; Utils.LogInfo("Reconnect {0} aborted, KeepAlive recovered.", m_session?.SessionId); + m_session = null; } else { @@ -375,6 +375,7 @@ private async Task DoReconnectAsync() // helper to override operation timeout int operationTimeout = m_session.OperationTimeout; int reconnectOperationTimeout = Math.Max(m_reconnectPeriod, MinReconnectOperationTimeout); + ITransportChannel transportChannel = null; // try a reconnect. if (!m_reconnectFailed) @@ -429,10 +430,15 @@ private async Task DoReconnectAsync() m_updateFromServer = true; Utils.LogInfo("Reconnect failed due to security check. Request endpoint update from server. {0}", sre.Message); } - // wait for next scheduled reconnect if connection failed, - // otherwise recreate session immediately - else if (sre.StatusCode != StatusCodes.BadSessionIdInvalid) + // recreate session immediately, use existing channel + else if (sre.StatusCode == StatusCodes.BadSessionIdInvalid) { + transportChannel = m_session.NullableTransportChannel; + m_session.DetachChannel(); + } + else + { + // wait for next scheduled reconnect if connection failed, // next attempt is to recreate session m_reconnectFailed = true; return false; @@ -492,7 +498,7 @@ await endpoint.UpdateFromServerAsync( m_updateFromServer = false; } - session = await m_session.SessionFactory.RecreateAsync(m_session).ConfigureAwait(false); + session = await m_session.SessionFactory.RecreateAsync(m_session, transportChannel).ConfigureAwait(false); } // note: the template session is not connected at this point // and must be disposed by the owner diff --git a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj index 02b621cfa..73b3dc781 100644 --- a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj +++ b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj @@ -1,6 +1,7 @@ + true $(HttpsTargetFrameworks) Opc.Ua.Bindings.Https OPCFoundation.NetStandard.Opc.Ua.Bindings.Https diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs index f7061f1d2..21601552d 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs @@ -17,9 +17,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Net.Http; using System.Net.Http.Headers; using System.Net.Security; -using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs index f5d033e86..61e5df64f 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs @@ -93,7 +93,7 @@ public ITransportChannel NullableTransportChannel { if (m_disposed) { - throw new ObjectDisposedException("ClientBase has been disposed."); + throw new ServiceResultException(StatusCodes.BadSecureChannelClosed, "Channel has been closed."); } } @@ -110,17 +110,13 @@ public ITransportChannel TransportChannel if (channel != null) { - if (m_disposed) + if (!m_disposed) { - throw new ObjectDisposedException("ClientBase has been disposed."); + return channel; } } - else - { - throw new ServiceResultException(StatusCodes.BadSecureChannelClosed, "Channel has been closed."); - } - return channel; + throw new ServiceResultException(StatusCodes.BadSecureChannelClosed, "Channel has been closed."); } protected set diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs index b3e6e110b..891a80252 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelAsyncOperation.cs @@ -311,6 +311,11 @@ public IDictionary Properties } } } + + /// + /// Return the result of the operation. + /// + public ServiceResult Error => m_error ?? ServiceResult.Good; #endregion #region IAsyncResult Members @@ -421,19 +426,20 @@ protected virtual bool InternalComplete(bool doNotBlock, object result) } } - if (m_callback != null) + AsyncCallback callback = m_callback; + if (callback != null) { if (doNotBlock) { Task.Run(() => { - m_callback(this); + callback(this); }); } else { try { - m_callback(this); + callback(this); } catch (Exception e) { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 6cb6b1f14..ccd5b9845 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -99,7 +99,7 @@ public UaSCUaBinaryChannel( m_discoveryOnly = false; m_uninitialized = true; - m_state = TcpChannelState.Closed; + m_state = (int)TcpChannelState.Closed; m_receiveBufferSize = quotas.MaxBufferSize; m_sendBufferSize = quotas.MaxBufferSize; m_activeWriteRequests = 0; @@ -447,10 +447,15 @@ protected virtual void OnWriteComplete(object sender, IMessageSocketAsyncEventAr protected void BeginWriteMessage(ArraySegment buffer, object state) { ServiceResult error = ServiceResult.Good; - IMessageSocketAsyncEventArgs args = null; + IMessageSocketAsyncEventArgs args = m_socket?.MessageSocketEventArgs(); + + if (args == null) + { + throw ServiceResultException.Create(StatusCodes.BadConnectionClosed, "The socket was closed by the remote application."); + } + try { - args = m_socket.MessageSocketEventArgs(); Interlocked.Increment(ref m_activeWriteRequests); args.SetBuffer(buffer.Array, buffer.Offset, buffer.Count); args.Completed += OnWriteComplete; @@ -737,16 +742,14 @@ protected int MaxResponseChunkCount /// protected TcpChannelState State { - get { return m_state; } + get => (TcpChannelState)m_state; set { - if (m_state != value) + if (Interlocked.Exchange(ref m_state, (int)value) != (int)value) { Utils.LogTrace("ChannelId {0}: in {1} state.", ChannelId, value); } - - m_state = value; } } @@ -874,7 +877,8 @@ public void UpdateLastActiveTime() private int m_maxResponseChunkCount; private string m_contextId; - private TcpChannelState m_state; + // treat TcpChannelState as int to use Interlocked + private int m_state; private uint m_channelId; private string m_globalChannelId; private long m_sequenceNumber; @@ -892,7 +896,7 @@ public void UpdateLastActiveTime() /// /// The possible channel states. /// - public enum TcpChannelState + public enum TcpChannelState : int { /// /// The channel is closed. diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index fb0f40531..a77a832a9 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -144,6 +144,10 @@ public IAsyncResult BeginConnect(Uri url, int timeout, AsyncCallback callback, o m_handshakeOperation = operation; State = TcpChannelState.Connecting; + + // set the state. + ChannelStateChanged(TcpChannelState.Connecting, ServiceResult.Good); + if (ReverseSocket) { if (Socket != null) @@ -219,24 +223,8 @@ public async Task CloseAsync(int timeout, CancellationToken ct = default) { try { - _ = await operation.EndAsync(timeout, true, ct).ConfigureAwait(false); - } - catch (ServiceResultException e) - { - switch (e.StatusCode) - { - case StatusCodes.BadRequestInterrupted: - case StatusCodes.BadSecureChannelClosed: - { - break; - } - - default: - { - Utils.LogWarning(e, "ChannelId {0}: Could not gracefully close the channel. Reason={1}", ChannelId, e.Result.StatusCode); - break; - } - } + _ = await operation.EndAsync(timeout, false, ct).ConfigureAwait(false); + ValidateChannelCloseError(operation.Error); } catch (Exception e) { @@ -261,23 +249,7 @@ public void Close(int timeout) try { operation.End(timeout, false); - } - catch (ServiceResultException e) - { - switch (e.StatusCode) - { - case StatusCodes.BadRequestInterrupted: - case StatusCodes.BadSecureChannelClosed: - { - break; - } - - default: - { - Utils.LogWarning(e, "ChannelId {0}: Could not gracefully close the channel. Reason={1}", ChannelId, e.Result.StatusCode); - break; - } - } + ValidateChannelCloseError(operation.Error); } catch (Exception e) { @@ -323,9 +295,10 @@ public IAsyncResult BeginSendRequest(IServiceRequest request, int timeout, Async if (m_queuedOperations != null) { operation = BeginOperation(timeout, callback, state); - m_queuedOperations.Add(new QueuedOperation(operation, timeout, request)); - if (firstCall) + bool validConnectOperation = QueueConnectOperation(operation, timeout, request); + + if (firstCall && validConnectOperation) { BeginConnect(m_url, timeout, OnConnectOnDemandComplete, null); } @@ -516,6 +489,9 @@ private bool ProcessAcknowledgeMessage(ArraySegment messageChunk) // ready to open the channel. State = TcpChannelState.Opening; + // set the state. + ChannelStateChanged(TcpChannelState.Opening, ServiceResult.Good); + try { // check if reconnecting after a socket failure. @@ -699,6 +675,9 @@ private bool ProcessOpenSecureChannelResponse(uint messageType, ArraySegment + /// Validates the result of a channel close operation. + /// + private void ValidateChannelCloseError(ServiceResult error) + { + if (ServiceResult.IsBad(error)) + { + StatusCode statusCode = error.StatusCode; + switch ((uint)statusCode) + { + case StatusCodes.BadRequestInterrupted: + case StatusCodes.BadSecureChannelClosed: + { + break; + } + + default: + { + Utils.LogWarning("ChannelId {0}: Could not gracefully close the channel. Reason={1}", ChannelId, error); + break; + } + } + } + } + + /// + /// Queues an operation for sending after the channel is connected. + /// Inserts operations that create or activate a session or don't require a session first. + /// + /// true if a valid service call for BeginConnect is queued. + private bool QueueConnectOperation(WriteOperation operation, int timeout, IServiceRequest request) + { + var queuedOperation = new QueuedOperation(operation, timeout, request); + + // operations that must be sent first and which allow for a connect. + if (request.TypeId == DataTypeIds.ActivateSessionRequest || + request.TypeId == DataTypeIds.CreateSessionRequest || + request.TypeId == DataTypeIds.GetEndpointsRequest || + request.TypeId == DataTypeIds.FindServersOnNetworkRequest || + request.TypeId == DataTypeIds.FindServersRequest) + { + m_queuedOperations.Insert(0, queuedOperation); + return true; + } + + // fail until a valid service call for BeginConnect is queued. + if (m_queuedOperations.Count == 0) + { + operation.Fault(StatusCodes.BadSecureChannelClosed); + throw new ServiceResultException(StatusCodes.BadNotConnected); + } + else + { + m_queuedOperations.Add(queuedOperation); + } + + return false; + } + /// /// Called when the socket is connected. /// @@ -807,8 +845,7 @@ private void OnConnectComplete(object sender, IMessageSocketAsyncEventArgs e) { WriteOperation operation = (WriteOperation)e.UserToken; - // dual stack ConnectAsync may call in with null UserToken if - // one connection attempt timed out but the other succeeded + // ConnectAsync may call in with a null UserToken, ignore if (operation == null) { return; @@ -906,6 +943,9 @@ private void OnScheduledHandshake(object state) Socket = null; } + // set the state. + ChannelStateChanged(TcpChannelState.Closed, ServiceResult.Good); + if (!ReverseSocket) { // create an operation. @@ -913,6 +953,10 @@ private void OnScheduledHandshake(object state) State = TcpChannelState.Connecting; Socket = m_socketFactory.Create(this, BufferManager, Quotas.MaxBufferSize); + + // set the state. + ChannelStateChanged(TcpChannelState.Connecting, ServiceResult.Good); + Socket.BeginConnect(m_via, m_ConnectCallback, m_handshakeOperation); } } @@ -1062,7 +1106,7 @@ private void Shutdown(ServiceResult reason) } // halt any existing handshake. - if (m_handshakeOperation != null && !m_handshakeOperation.IsCompleted) + if (m_handshakeOperation?.IsCompleted == false) { m_handshakeOperation.Fault(reason); } @@ -1287,14 +1331,15 @@ private void OnConnectOnDemandComplete(object state) } catch (Exception e) { - request.Operation.Fault(e, StatusCodes.BadNoCommunication, "Error establishing a connection: " + e.Message); - break; + request.Operation.Fault(StatusCodes.BadNoCommunication, "Error establishing a connection: " + e.Message); + continue; } } if (this.CurrentToken == null) { request.Operation.Fault(StatusCodes.BadConnectionClosed, "Could not send request because connection is closed."); + continue; } try @@ -1336,6 +1381,9 @@ private WriteOperation InternalClose(int timeout) State = TcpChannelState.Closing; operation = BeginOperation(timeout, null, null); SendCloseSecureChannelRequest(operation); + + // set the state. + ChannelStateChanged(TcpChannelState.Closing, ServiceResult.Good); } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index e1f82b34a..b7a76f64f 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -359,9 +359,11 @@ public IAsyncResult BeginSendRequest(IServiceRequest request, AsyncCallback call if (channel == null) { channel = CreateChannel(); - if (Interlocked.CompareExchange(ref m_channel, channel, null) != null) + var currentChannel = Interlocked.CompareExchange(ref m_channel, channel, null); + if (currentChannel != null) { - channel = m_channel; + Utils.SilentDispose(channel); + channel = currentChannel; } } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index b1247370a..65196b005 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -32,14 +32,12 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Moq; -using Newtonsoft.Json.Linq; using NUnit.Framework; using Opc.Ua.Bindings; using Opc.Ua.Configuration; @@ -285,7 +283,7 @@ public void ReadOnDiscoveryChannel(int readCount) // client may report channel closed instead of security policy rejected if (StatusCodes.BadSecureChannelClosed == sre.StatusCode) { - Assert.Inconclusive($"Unexpected Status: {sre}" ); + Assert.Inconclusive($"Unexpected Status: {sre}"); } Assert.AreEqual((StatusCode)StatusCodes.BadSecurityPolicyRejected, (StatusCode)sre.StatusCode, "Unexpected Status: {0}", sre); } @@ -314,7 +312,7 @@ public void GetEndpointsOnDiscoveryChannel() // client may report channel closed instead of security policy rejected if (StatusCodes.BadSecureChannelClosed == sre.StatusCode) { - Assert.Inconclusive($"Unexpected Status: {sre}" ); + Assert.Inconclusive($"Unexpected Status: {sre}"); } Assert.AreEqual((StatusCode)StatusCodes.BadSecurityPolicyRejected, (StatusCode)sre.StatusCode, "Unexpected Status: {0}", sre); } @@ -614,7 +612,7 @@ public async Task ConnectMultipleSessionsAsync() /// Close the first channel before or after the new channel is activated. /// [Theory, Order(250)] - public async Task ReconnectSessionOnAlternateChannel(bool closeChannel) + public async Task ReconnectSessionOnAlternateChannel(bool closeChannel, bool asyncReconnect) { ServiceResultException sre; @@ -649,7 +647,14 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel) Assert.NotNull(channel2); // activate the session on the new channel - session1.Reconnect(channel2); + if (asyncReconnect) + { + await session1.ReconnectAsync(channel2, CancellationToken.None).ConfigureAwait(false); + } + else + { + session1.Reconnect(channel2); + } // test by reading a value ServerStatusDataType value2 = (ServerStatusDataType)session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); @@ -669,25 +674,39 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel) Assert.AreEqual(value1.State, value3.State); // close the session, keep the channel open - session1.Close(closeChannel: false); + if (asyncReconnect) + { + await session1.CloseAsync(closeChannel: false, CancellationToken.None).ConfigureAwait(false); + } + else + { + session1.Close(closeChannel: false); + } // cannot read using a closed session, validate the status code sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); Assert.AreEqual((StatusCode)StatusCodes.BadSessionIdInvalid, (StatusCode)sre.StatusCode, sre.Message); // close the channel - channel2.Close(); + if (asyncReconnect) + { + await channel2.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } + else + { + channel2.Close(); + } channel2.Dispose(); // cannot read using a closed channel, validate the status code sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); - // TODO: Both channel should return BadSecureChannelClosed + // TODO: Both channel should return BadNotConnected if (!(StatusCodes.BadSecureChannelClosed == sre.StatusCode)) { if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) { - Assert.AreEqual((StatusCode)StatusCodes.BadSessionIdInvalid, (StatusCode)sre.StatusCode, sre.Message); + Assert.AreEqual((StatusCode)StatusCodes.BadNotConnected, (StatusCode)sre.StatusCode, sre.Message); } else { @@ -701,11 +720,13 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel) /// the same session on a new channel with saved session secrets /// [Test, Order(260)] - [TestCase(SecurityPolicies.None, true)] - [TestCase(SecurityPolicies.None, false)] - [TestCase(SecurityPolicies.Basic256Sha256, true)] - [TestCase(SecurityPolicies.Basic256Sha256, false)] - public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(string securityPolicy, bool anonymous) + [TestCase(SecurityPolicies.None, true, false)] + [TestCase(SecurityPolicies.None, false, false)] + [TestCase(SecurityPolicies.None, false, true)] + [TestCase(SecurityPolicies.Basic256Sha256, true, false)] + [TestCase(SecurityPolicies.Basic256Sha256, false, false)] + [TestCase(SecurityPolicies.Basic256Sha256, false, true)] + public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(string securityPolicy, bool anonymous, bool asyncReconnect) { ServiceResultException sre; @@ -756,8 +777,14 @@ public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(stri }; // activate the session from saved session secrets on the new channel - session2.Reconnect(channel2); - + if (asyncReconnect) + { + await session2.ReconnectAsync(channel2, CancellationToken.None).ConfigureAwait(false); + } + else + { + session2.Reconnect(channel2); + } Thread.Sleep(500); Assert.AreEqual(session1.SessionId, session2.SessionId); diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index 8e72573a6..69bb37472 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -60,6 +60,7 @@ public class ClientTestFramework public TokenValidatorMock TokenValidator { get; set; } = new TokenValidatorMock(); public bool SingleSession { get; set; } = true; + public int MaxChannelCount { get; set; } = 10; public bool SupportsExternalServerUrl { get; set; } = false; public ServerFixture ServerFixture { get; set; } public ClientFixture ClientFixture { get; set; } @@ -135,13 +136,13 @@ public async Task OneTimeSetUpAsync(TextWriter writer = null, bool securityNone if (customUrl == null) { // start Ref server - ServerFixture = new ServerFixture(enableTracing, disableActivityLogging) - { + ServerFixture = new ServerFixture(enableTracing, disableActivityLogging) { UriScheme = UriScheme, SecurityNone = securityNone, AutoAccept = true, AllNodeManagers = true, - OperationLimits = true + OperationLimits = true, + MaxChannelCount = MaxChannelCount, }; if (writer != null) diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index b2fe2f28e..cd7e0fc13 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -62,6 +62,7 @@ public class SubscriptionTest : ClientTestFramework SupportsExternalServerUrl = true; // create a new session for every test SingleSession = false; + MaxChannelCount = 1000; return base.OneTimeSetUpAsync(null, true); } @@ -503,6 +504,7 @@ public async Task ReconnectWithSavedSessionSecrets( // the active channel ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity).ConfigureAwait(false); Assert.NotNull(session1); + var sessionId1 = session1.SessionId; int session1ConfigChanged = 0; session1.SessionConfigurationChanged += (object sender, EventArgs e) => { session1ConfigChanged++; }; @@ -616,80 +618,85 @@ public async Task ReconnectWithSavedSessionSecrets( await Task.Delay(2 * kDelay).ConfigureAwait(false); - Assert.AreEqual(session1.SessionId, session2.SessionId); - - if (asyncTest) - { - DataValue value2 = await session2.ReadValueAsync(VariableIds.Server_ServerStatus).ConfigureAwait(false); - Assert.NotNull(value2); - } - else - { - ServerStatusDataType value2 = (ServerStatusDataType)session2.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); - Assert.NotNull(value2); - } - - for (ii = 0; ii < kTestSubscriptions; ii++) + try { - var monitoredItemCount = restoredSubscriptions[ii].MonitoredItemCount; - string errorText = $"Error in test subscription {ii}"; + Assert.AreEqual(sessionId1, session2.SessionId); - // the static subscription doesn't resend data until there is a data change - if (ii == 0 && !sendInitialValues) + if (asyncTest) { - Assert.AreEqual(0, targetSubscriptionCounters[ii], errorText); - Assert.AreEqual(0, targetSubscriptionFastDataCounters[ii], errorText); + DataValue value2 = await session2.ReadValueAsync(VariableIds.Server_ServerStatus).ConfigureAwait(false); + Assert.NotNull(value2); } - else if (ii == 0) + else { - Assert.AreEqual(monitoredItemCount, targetSubscriptionCounters[ii], errorText); - Assert.AreEqual(1, targetSubscriptionFastDataCounters[ii], errorText); + ServerStatusDataType value2 = (ServerStatusDataType)session2.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + Assert.NotNull(value2); } - else + + for (ii = 0; ii < kTestSubscriptions; ii++) { - Assert.LessOrEqual(monitoredItemCount, targetSubscriptionCounters[ii], errorText); - Assert.LessOrEqual(1, targetSubscriptionFastDataCounters[ii], errorText); + var monitoredItemCount = restoredSubscriptions[ii].MonitoredItemCount; + string errorText = $"Error in test subscription {ii}"; + + // the static subscription doesn't resend data until there is a data change + if (ii == 0 && !sendInitialValues) + { + Assert.AreEqual(0, targetSubscriptionCounters[ii], errorText); + Assert.AreEqual(0, targetSubscriptionFastDataCounters[ii], errorText); + } + else if (ii == 0) + { + Assert.AreEqual(monitoredItemCount, targetSubscriptionCounters[ii], errorText); + Assert.AreEqual(1, targetSubscriptionFastDataCounters[ii], errorText); + } + else + { + Assert.LessOrEqual(monitoredItemCount, targetSubscriptionCounters[ii], errorText); + Assert.LessOrEqual(1, targetSubscriptionFastDataCounters[ii], errorText); + } } - } - await Task.Delay(kDelay).ConfigureAwait(false); + await Task.Delay(kDelay).ConfigureAwait(false); - // verify that reconnect created subclassed version of subscription and monitored item - foreach (var s in session2.Subscriptions) - { - Assert.AreEqual(typeof(TestableSubscription), s.GetType()); - foreach (var m in s.MonitoredItems) + // verify that reconnect created subclassed version of subscription and monitored item + foreach (var s in session2.Subscriptions) { - Assert.AreEqual(typeof(TestableMonitoredItem), m.GetType()); + Assert.AreEqual(typeof(TestableSubscription), s.GetType()); + foreach (var m in s.MonitoredItems) + { + Assert.AreEqual(typeof(TestableMonitoredItem), m.GetType()); + } } - } - - // cannot read using a closed channel, validate the status code - if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) - { - sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); - Assert.AreEqual((StatusCode)StatusCodes.BadSecureChannelIdInvalid, (StatusCode)sre.StatusCode, sre.Message); - } - else - { - var result = session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); - Assert.NotNull(result); - } - session1.DeleteSubscriptionsOnClose = true; - session2.DeleteSubscriptionsOnClose = true; - if (asyncTest) - { - await session1.CloseAsync(1000).ConfigureAwait(false); - await session2.CloseAsync(1000).ConfigureAwait(false); + // cannot read using a closed channel, validate the status code + if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) + { + sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); + Assert.AreEqual((StatusCode)StatusCodes.BadSecureChannelClosed, (StatusCode)sre.StatusCode, sre.Message); + } + else + { + var result = session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + Assert.NotNull(result); + } } - else + finally { - session1.Close(1000); - session2.Close(1000); + session1.DeleteSubscriptionsOnClose = true; + session2.DeleteSubscriptionsOnClose = true; + if (asyncTest) + { + await session1.CloseAsync(1000, true).ConfigureAwait(false); + await session2.CloseAsync(1000, true).ConfigureAwait(false); + } + else + { + session1.Close(1000, true); + session2.Close(1000, true); + } + Utils.SilentDispose(session1); + Utils.SilentDispose(session2); } - Utils.SilentDispose(session1); - Utils.SilentDispose(session2); Assert.AreEqual(0, session1ConfigChanged); Assert.Less(0, session2ConfigChanged); diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs index 798679120..7e7fb76bb 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs @@ -48,6 +48,7 @@ namespace Opc.Ua.Server.Tests public bool LogConsole { get; set; } public bool AutoAccept { get; set; } public bool OperationLimits { get; set; } + public int MaxChannelCount { get; set; } = 10; public int ReverseConnectTimeout { get; set; } public bool AllNodeManagers { get; set; } public int TraceMasks { get; set; } = Utils.TraceMasks.Error | Utils.TraceMasks.StackTrace | Utils.TraceMasks.Security | Utils.TraceMasks.Information; @@ -129,10 +130,10 @@ public async Task LoadConfiguration(string pkiRoot = null) }); } - serverConfig.SetMaxMessageQueueSize(20); - serverConfig.SetDiagnosticsEnabled(true); - serverConfig.SetAuditingEnabled(true); - serverConfig.SetMaxChannelCount(10); + serverConfig.SetMaxChannelCount(MaxChannelCount) + .SetMaxMessageQueueSize(20) + .SetDiagnosticsEnabled(true) + .SetAuditingEnabled(true); if (ReverseConnectTimeout != 0) { From 8a1df6d3dc8f2bb195946944b91d17ff0dd94ada Mon Sep 17 00:00:00 2001 From: romanett Date: Wed, 5 Jun 2024 07:10:07 +0200 Subject: [PATCH 05/16] [Client] Fix UserIdentity for CertificateIdentifer & add parameter for console reference client to specify UserCertificate (#2624) - Fix the UserIdentity constructor to load the private key of a provided certificateIdenifier. - make UserIdentity constructor throw a ServiceResultException when a Certificate with no private key is specified. - extend console reference client with two new parameters: -uc Thumbprint of a user certifiate located in the TrustedUserCertificatesStore -ucp Password of the private key of the user certificate - add documentation for console reference client --- .../ConsoleReferenceClient/Program.cs | 59 +++++++++++++++++-- Applications/ConsoleReferenceClient/README.md | 20 +++++++ Docs/README.md | 1 + .../Opc.Ua.Core/Stack/Client/UserIdentity.cs | 18 +++++- 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 Applications/ConsoleReferenceClient/README.md diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index e0b3cde33..ebe28e873 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -32,6 +32,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -69,6 +70,8 @@ public static async Task Main(string[] args) bool autoAccept = false; string username = null; string userpassword = null; + string userCertificateThumbprint = null; + string userCertificatePassword = null; bool logConsole = false; bool appLog = false; bool renewCertificate = false; @@ -93,6 +96,8 @@ public static async Task Main(string[] args) { "nsec|nosecurity", "select endpoint with security NONE, least secure if unavailable", s => noSecurity = s != null }, { "un|username=", "the name of the user identity for the connection", (string u) => username = u }, { "up|userpassword=", "the password of the user identity for the connection", (string u) => userpassword = u }, + { "uc|usercertificate=", "the thumbprint of the user certificate for the user identity", (string u) => userCertificateThumbprint = u }, + { "ucp|usercertificatepassword=", "the password of the user certificate for the user identity", (string u) => userCertificatePassword = u }, { "c|console", "log to console", c => logConsole = c != null }, { "l|log", "log app output", c => appLog = c != null }, { "p|password=", "optional password for private key", (string p) => password = p }, @@ -195,12 +200,12 @@ public static async Task Main(string[] args) { if (!forever) { - break; - } + break; + } else { waitTime = 0; - } + } } if (forever) @@ -215,12 +220,29 @@ public static async Task Main(string[] args) SessionLifeTime = 60_000, }) { - // set user identity - if (!String.IsNullOrEmpty(username)) + // set user identity of type username/pw + if (!string.IsNullOrEmpty(username)) { uaClient.UserIdentity = new UserIdentity(username, userpassword ?? string.Empty); } + // set user identity of type certificate + if (!string.IsNullOrEmpty(userCertificateThumbprint)) + { + CertificateIdentifier userCertificateIdentifier = + await FindUserCertificateIdentifierAsync(userCertificateThumbprint, + application.ApplicationConfiguration.SecurityConfiguration.TrustedUserCertificates); + + if (userCertificateIdentifier != null) + { + uaClient.UserIdentity = new UserIdentity(userCertificateIdentifier, new CertificatePasswordProvider(userCertificatePassword ?? string.Empty)); + } + else + { + output.WriteLine($"Failed to load user certificate with Thumbprint {userCertificateThumbprint}"); + } + } + bool connected = await uaClient.ConnectAsync(serverUrl.ToString(), !noSecurity, quitCTS.Token).ConfigureAwait(false); if (connected) { @@ -372,5 +394,32 @@ await samples.SubscribeAllValuesAsync(uaClient, output.Close(); } } + /// + /// returns a CertificateIdentifier of the Certificate with the specified thumbprint if it is found in the trustedUserCertificates TrustList + /// + /// the thumbprint of the certificate to select + /// the trustlist of the user certificates + /// Certificate Identifier + private static async Task FindUserCertificateIdentifierAsync(string thumbprint, CertificateTrustList trustedUserCertificates) + { + CertificateIdentifier userCertificateIdentifier = null; + + // get user certificate with matching thumbprint + X509Certificate2Collection userCertifiactesWithMatchingThumbprint = + (await trustedUserCertificates + .GetCertificates()) + .Find(X509FindType.FindByThumbprint, thumbprint, false); + + // create Certificate Identifier + if (userCertifiactesWithMatchingThumbprint.Count == 1) + { + userCertificateIdentifier = new CertificateIdentifier(userCertifiactesWithMatchingThumbprint[0]) { + StorePath = trustedUserCertificates.StorePath, + StoreType = trustedUserCertificates.StoreType + }; + } + + return userCertificateIdentifier; + } } } diff --git a/Applications/ConsoleReferenceClient/README.md b/Applications/ConsoleReferenceClient/README.md new file mode 100644 index 000000000..2234e5c2c --- /dev/null +++ b/Applications/ConsoleReferenceClient/README.md @@ -0,0 +1,20 @@ +# OPC Foundation UA .NET Standard Reference Client + +## Introduction + +The console reference client can be configured using several console parameters. +Some of these parameters are explained in more detail below. + +To see all available parameters call console reference client the with the parameter `-h`. + +### How to specify User Identity +#### Username & Password +Specify as console parameters: + `-un YourUsername` + `-up YourPassword` + +#### Certificate +Place your user certificate in the TrustedUserCertificatesStore (the path can be found in the client configuration XML). Make shure to include an accessible private key with the certificate. +Specify console parameters: + `-uc Thumbprint` (of the user certificate to select) + `-ucp Password` (of the user certificates private key (optional)) \ No newline at end of file diff --git a/Docs/README.md b/Docs/README.md index bbf2402aa..dc0a13dfc 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -14,6 +14,7 @@ UA Core stack related: Reference application related: * [Reference Server](../Applications/ReferenceServer/README.md) documentation for running against CTT. +* [Reference Client](../Applications/ConsoleReferenceClient/README.md) documentation for configuration of the console reference client using parameters. * Using the [Container support](ContainerReferenceServer.md) of the Reference Server in VS2022 and for local testing. For the PubSub support library: diff --git a/Stack/Opc.Ua.Core/Stack/Client/UserIdentity.cs b/Stack/Opc.Ua.Core/Stack/Client/UserIdentity.cs index 696c0fb1d..ada57ff06 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/UserIdentity.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/UserIdentity.cs @@ -60,14 +60,25 @@ public UserIdentity(IssuedIdentityToken issuedToken) /// Initializes the object with an X509 certificate identifier /// public UserIdentity(CertificateIdentifier certificateId) + : this(certificateId, new CertificatePasswordProvider(string.Empty)) + { + } + + /// + /// Initializes the object with an X509 certificate identifier and a CertificatePasswordProvider + /// + public UserIdentity(CertificateIdentifier certificateId, CertificatePasswordProvider certificatePasswordProvider) { if (certificateId == null) throw new ArgumentNullException(nameof(certificateId)); - X509Certificate2 certificate = certificateId.Find().Result; - if (certificate != null) + X509Certificate2 certificate = certificateId.LoadPrivateKeyEx(certificatePasswordProvider).Result; + + if (certificate == null || !certificate.HasPrivateKey) { - Initialize(certificate); + throw new ServiceResultException("Cannot create User Identity with CertificateIdentifier that does not contain a private key"); } + + Initialize(certificate); } /// @@ -76,6 +87,7 @@ public UserIdentity(CertificateIdentifier certificateId) public UserIdentity(X509Certificate2 certificate) { if (certificate == null) throw new ArgumentNullException(nameof(certificate)); + if (!certificate.HasPrivateKey) throw new ServiceResultException("Cannot create User Identity with Certificate that does not have a private key"); Initialize(certificate); } From 8a0349ef9cf8b47278d10da4e022937324d535fe Mon Sep 17 00:00:00 2001 From: romanett Date: Wed, 5 Jun 2024 07:13:30 +0200 Subject: [PATCH 06/16] [Client] select the most secure User Identity Token if a server offers multiple ones (#2611) This fix makes the client select the most secure UserIdentity Token if a server offers more than one. Before the client just selected the first UserIdentity Token offered by the server. Also calculate the security level by the client, as the unsecure response of a server could be spoofed. --- Libraries/Opc.Ua.Client/CoreClientUtils.cs | 13 ++-- .../Schema/ApplicationConfiguration.cs | 27 +------ .../Schema/SecuredApplicationHelpers.cs | 34 +++++++++ .../Configuration/EndpointDescription.cs | 7 +- .../Schema/SecuredApplicationHelpersTests.cs | 71 +++++++++++++++++++ 5 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 Tests/Opc.Ua.Core.Tests/Schema/SecuredApplicationHelpersTests.cs diff --git a/Libraries/Opc.Ua.Client/CoreClientUtils.cs b/Libraries/Opc.Ua.Client/CoreClientUtils.cs index adbb9184c..b73f01aba 100644 --- a/Libraries/Opc.Ua.Client/CoreClientUtils.cs +++ b/Libraries/Opc.Ua.Client/CoreClientUtils.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Opc.Ua.Security; namespace Opc.Ua.Client { @@ -280,14 +281,10 @@ public static EndpointDescription SelectEndpoint( selectedEndpoint = endpoint; } - // The security level is a relative measure assigned by the server to the - // endpoints that it returns. Clients should always pick the highest level - // unless they have a reason not too. - // Some servers however, mess this up a bit. So prefer a higher SecurityMode - // over the SecurityLevel. - if (endpoint.SecurityMode > selectedEndpoint.SecurityMode - || (endpoint.SecurityMode == selectedEndpoint.SecurityMode - && endpoint.SecurityLevel > selectedEndpoint.SecurityLevel)) + + //Select endpoint if it has a higher calculated security level, than the previously selected one + if (SecuredApplication.CalculateSecurityLevel(endpoint.SecurityMode, endpoint.SecurityPolicyUri) + > SecuredApplication.CalculateSecurityLevel(selectedEndpoint.SecurityMode, selectedEndpoint.SecurityPolicyUri)) { selectedEndpoint = endpoint; } diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 621ca7840..1e7270934 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using Opc.Ua.Bindings; +using Opc.Ua.Security; namespace Opc.Ua { @@ -650,7 +651,7 @@ public TransportConfigurationCollection(int capacity) : base(capacity) { } #region ServerSecurityPolicy Class /// - /// A class that defines a group of sampling rates supported by the server. + /// A class that defines a group of security policies supported by the server. /// [DataContract(Namespace = Namespaces.OpcUaConfig)] public class ServerSecurityPolicy @@ -689,29 +690,7 @@ private void Initialize() /// public static byte CalculateSecurityLevel(MessageSecurityMode mode, string policyUri) { - if ((mode == MessageSecurityMode.Invalid) || (mode == MessageSecurityMode.None)) - { - return 0; - } - - byte result = 0; - switch (policyUri) - { - case SecurityPolicies.Basic128Rsa15: result = 2; break; - case SecurityPolicies.Basic256: result = 4; break; - case SecurityPolicies.Basic256Sha256: result = 6; break; - case SecurityPolicies.Aes128_Sha256_RsaOaep: result = 8; break; - case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; - case SecurityPolicies.None: - default: return 0; - } - - if (mode == MessageSecurityMode.SignAndEncrypt) - { - result += 100; - } - - return result; + return SecuredApplication.CalculateSecurityLevel(mode, policyUri); } /// diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index ca51be734..bcae7b622 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -371,6 +371,40 @@ public static ServerSecurityPolicyCollection FromListOfSecurityProfiles(ListOfSe return policies; } + /// + /// Calculates the security level, given the security mode and policy + /// Invalid and none is discouraged + /// Just signing is always weaker than any use of encryption + /// + public static byte CalculateSecurityLevel(MessageSecurityMode mode, string policyUri) + { + if ((mode != MessageSecurityMode.Sign) && (mode != MessageSecurityMode.SignAndEncrypt)) + { + return 0; + } + + byte result = 0; + switch (policyUri) + { + case SecurityPolicies.Basic128Rsa15: result = 2; break; + case SecurityPolicies.Basic256: result = 4; break; + case SecurityPolicies.Basic256Sha256: result = 6; break; + case SecurityPolicies.Aes128_Sha256_RsaOaep: result = 8; break; + case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; + case SecurityPolicies.None: + default: + Utils.LogWarning("Security level requested for unknown Security Policy {policy}. Returning security level 0", policyUri); + return 0; + } + + if (mode == MessageSecurityMode.SignAndEncrypt) + { + result += 100; + } + + return result; + } + /// /// Creates a new policy object. /// Always uses sign and encrypt for all security policies except none diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 1d848f341..8de37f269 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -11,8 +11,9 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; -using System.Collections.Generic; +using System.Linq; using System.Xml; +using Opc.Ua.Security; namespace Opc.Ua { @@ -124,8 +125,8 @@ public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, string issue // construct issuer type. string issuedTokenTypeText = issuedTokenType; - // find matching policy. - foreach (UserTokenPolicy policy in m_userIdentityTokens) + // find matching policy, return most secure matching policy first. + foreach (UserTokenPolicy policy in m_userIdentityTokens.OrderByDescending(token => SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, token.SecurityPolicyUri))) { // check token type. if (tokenType != policy.TokenType) diff --git a/Tests/Opc.Ua.Core.Tests/Schema/SecuredApplicationHelpersTests.cs b/Tests/Opc.Ua.Core.Tests/Schema/SecuredApplicationHelpersTests.cs new file mode 100644 index 000000000..49400b117 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Schema/SecuredApplicationHelpersTests.cs @@ -0,0 +1,71 @@ +using Opc.Ua.Security; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Schema +{ + /// + /// Tests for the CertificateValidator class. + /// + [TestFixture, Category("SecuredApplicationHelpers")] + [Parallelizable] + [SetCulture("en-us")] + public class SecuredApplicationHelpersTests + { + /// + /// Verify CalculateSecurityLevel encryption is a higher security Level than signing + /// + [Test] + public void CalculateSecurityLevelEncryptionStrongerSigning() + { + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15)); + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Aes256_Sha256_RsaPss)); + } + /// + /// Verify CalculateSecurityLevel none or Invalid MessageSecurityMode return 0 + /// + [Test] + public void CalculateSecurityLevelNoneOrInvalidZero() + { + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.None, SecurityPolicies.Basic128Rsa15) + == 0); + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Invalid, SecurityPolicies.Basic128Rsa15) + == 0); + } + + /// + /// Verify CalculateSecurityLevel none or Invalid MessageSecurityMode return 0 + /// + [Test] + public void CalculateSecurityLevelOrderValid() + { + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic256)); + + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic256) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256)); + + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Aes128_Sha256_RsaOaep)); + + Assert.That( + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Aes128_Sha256_RsaOaep) + < + SecuredApplication.CalculateSecurityLevel(MessageSecurityMode.Sign, SecurityPolicies.Aes256_Sha256_RsaPss)); + + } + } +} From 6ad287396ed5cf83805b8248d96f185ae592c6ce Mon Sep 17 00:00:00 2001 From: romanett Date: Wed, 5 Jun 2024 07:40:01 +0200 Subject: [PATCH 07/16] [GDS] Add Method GetCertificates to GDS for Pull Support and ServerConfiguration for Push Support / Fix CheckRevocationStatus (#2553) * add GetCertificates Method to standard Server & GDS * fix CheckRevocationStatus to use X509 Chain with Online Revocation Check & remove Check from GetCertificates --- .../GlobalDiscoveryServerClient.cs | 37 +++++ .../ServerPushConfigurationClient.cs | 41 +++++ .../LinqApplicationsDatabase.cs | 1 - .../ApplicationsNodeManager.cs | 146 ++++++++++++++++-- .../Configuration/ConfigurationNodeManager.cs | 41 +++++ .../Opc.Ua.Gds.Tests/CertificateGroupTests.cs | 10 +- Tests/Opc.Ua.Gds.Tests/ClientTest.cs | 32 +++- Tests/Opc.Ua.Gds.Tests/PushTest.cs | 20 +++ 8 files changed, 305 insertions(+), 23 deletions(-) diff --git a/Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs b/Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs index 35d59c871..cdced88f6 100644 --- a/Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs +++ b/Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs @@ -584,6 +584,43 @@ public NodeId RegisterApplication(ApplicationRecordDataType application) return null; } + /// + /// Returns the Certificates assigned to Application and associated with the CertificateGroup. + /// + /// The identifier assigned to the Application by the GDS. + /// An identifier for the CertificateGroup that the Certificates belong to. + ///If null, the CertificateManager shall return the Certificates for all CertificateGroups assigned to the Application. + /// The CertificateTypes that currently have a Certificate assigned. + /// The length of this list is the same as the length as certificates list. + /// A list of DER encoded Certificates assigned to Application. + /// This list only includes Certificates that are currently valid. + public void GetCertificates( + NodeId applicationId, + NodeId certificateGroupId, + out NodeId[] certificateTypeIds, + out byte[][] certificates) + { + certificateTypeIds = Array.Empty(); + certificates = Array.Empty(); + + if (!IsConnected) + { + Connect(); + } + + var outputArguments = Session.Call( + ExpandedNodeId.ToNodeId(Opc.Ua.Gds.ObjectIds.Directory, Session.NamespaceUris), + ExpandedNodeId.ToNodeId(Opc.Ua.Gds.MethodIds.CertificateDirectoryType_GetCertificates, Session.NamespaceUris), + applicationId, + certificateGroupId); + + if (outputArguments.Count >= 2) + { + certificateTypeIds = outputArguments[0] as NodeId[]; + certificates = outputArguments[1] as byte[][]; + } + } + /// /// Checks the provided certificate for validity /// diff --git a/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs b/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs index 32d69d905..40940834f 100644 --- a/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs +++ b/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs @@ -561,6 +561,47 @@ public void RemoveCertificate(string thumbprint, bool isTrustedCertificate) } } + /// + /// returns the Certificates assigned to CertificateTypes associated with a CertificateGroup. + /// + /// The identifier for the CertificateGroup. + /// The CertificateTypes that currently have a Certificate assigned. + ///The length of this list is the same as the length as certificates list. + ///An empty list if the CertificateGroup does not have any CertificateTypes. + /// A list of DER encoded Certificates assigned to CertificateGroup. + ///The certificateType for the Certificate is specified by the corresponding element in the certificateTypes parameter. + public void GetCertificates( + NodeId certificateGroupId, + out NodeId[] certificateTypeIds, + out byte[][] certificates) + { + certificateTypeIds = Array.Empty(); + certificates = Array.Empty(); + if (!IsConnected) + { + Connect(); + } + + IUserIdentity oldUser = ElevatePermissions(); + try + { + var outputArguments = m_session.Call( + ExpandedNodeId.ToNodeId(Opc.Ua.ObjectIds.ServerConfiguration, m_session.NamespaceUris), + ExpandedNodeId.ToNodeId(Opc.Ua.MethodIds.ServerConfigurationType_GetCertificates, m_session.NamespaceUris), + certificateGroupId + ); + if (outputArguments.Count >= 2) + { + certificateTypeIds = outputArguments[0] as NodeId[]; + certificates = outputArguments[1] as byte[][]; + } + } + finally + { + RevertPermissions(oldUser); + } + } + /// /// Creates the CSR. /// diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs index 235b5d2b0..56692e9ed 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs @@ -672,7 +672,6 @@ public override bool GetApplicationCertificate( Guid id = GetNodeIdGuid(applicationId); - List certificates = new List(); lock (Lock) { diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs index 51ede019f..0207479b0 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs @@ -31,12 +31,14 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Opc.Ua.Gds.Server.Database; using Opc.Ua.Gds.Server.Diagnostics; using Opc.Ua.Server; +using Org.BouncyCastle.Tls; namespace Opc.Ua.Gds.Server { @@ -389,7 +391,8 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context Opc.Ua.Gds.CertificateDirectoryState activeNode = new Opc.Ua.Gds.CertificateDirectoryState(passiveNode.Parent); activeNode.RevokeCertificate = new RevokeCertificateMethodState(passiveNode); - activeNode.CheckRevocationStatus = new CheckRevocationStatusMethodState(passiveNode.Parent); + activeNode.CheckRevocationStatus = new CheckRevocationStatusMethodState(passiveNode); + activeNode.GetCertificates = new GetCertificatesMethodState(passiveNode); activeNode.Create(context, passiveNode); activeNode.QueryServers.OnCall = new QueryServersMethodStateMethodCallHandler(OnQueryServers); @@ -407,6 +410,7 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context activeNode.StartSigningRequest.OnCall = new StartSigningRequestMethodStateMethodCallHandler(OnStartSigningRequest); activeNode.RevokeCertificate.OnCall = new RevokeCertificateMethodStateMethodCallHandler(OnRevokeCertificate); activeNode.CheckRevocationStatus.OnCall = new CheckRevocationStatusMethodStateMethodCallHandler(OnCheckRevocationStatus); + activeNode.GetCertificates.OnCall = new GetCertificatesMethodStateMethodCallHandler(OnGetCertificates); activeNode.CertificateGroups.DefaultApplicationGroup.CertificateTypes.Value = new NodeId[] { Opc.Ua.ObjectTypeIds.RsaSha256ApplicationCertificateType }; activeNode.CertificateGroups.DefaultApplicationGroup.TrustList.LastUpdateTime.Value = DateTime.UtcNow; @@ -659,31 +663,141 @@ private ServiceResult OnCheckRevocationStatus( { AuthorizationHelper.HasAuthenticatedSecureChannel(context); - //create CertificateValidator initialized with GDS CAs - var certificateValidator = new CertificateValidator(); - var authorities = new CertificateTrustList() { - StorePath = m_globalDiscoveryServerConfiguration.AuthoritiesStorePath, - StoreType = CertificateStoreIdentifier.DetermineStoreType(m_globalDiscoveryServerConfiguration.AuthoritiesStorePath) - }; - certificateValidator.Update(null, authorities, null); - - //TODO return validityTime of Certificate once CertificateValidator supports it + //TODO return When the result expires and should be rechecked. validityTime = DateTime.MinValue; - using (var x509 = new X509Certificate2(certificate)) + try { - try + //create chain to validate Certificate against it + var chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; + + //add GDS Issuer Cert Store Certificates to the Chain validation for consitent behaviour on all Platforms + using (ICertificateStore store = CertificateStoreIdentifier.OpenStore(m_configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath)) { - certificateValidator.Validate(x509); + chain.ChainPolicy.ExtraStore.AddRange(store.Enumerate().GetAwaiter().GetResult()); } - catch (ServiceResultException se) + using (var x509 = new X509Certificate2(certificate)) { - certificateStatus = se.StatusCode; + if (chain.Build(x509)) + { + certificateStatus = StatusCodes.Good; + return ServiceResult.Good; + } + else + { + //Assing certificateStatus for invalid chain if no matching found use StatusCodes.BadCertificateRevoked + switch (chain.ChainStatus.FirstOrDefault().Status) + { + case X509ChainStatusFlags.NotTimeValid: + certificateStatus = StatusCodes.BadCertificateTimeInvalid; + break; + case X509ChainStatusFlags.Revoked: + certificateStatus = StatusCodes.BadCertificateRevoked; + break; + case X509ChainStatusFlags.NotSignatureValid: + certificateStatus = StatusCodes.BadCertificateInvalid; + break; + case X509ChainStatusFlags.NotValidForUsage: + certificateStatus = StatusCodes.BadCertificateUseNotAllowed; + break; + case X509ChainStatusFlags.RevocationStatusUnknown: + certificateStatus = StatusCodes.BadCertificateRevocationUnknown; + break; + case X509ChainStatusFlags.PartialChain: + certificateStatus = StatusCodes.BadCertificateChainIncomplete; + break; + case X509ChainStatusFlags.ExplicitDistrust: + certificateStatus = StatusCodes.BadCertificateUntrusted; + break; + //cases not in the OPC UA Status codes -> default to BadCertificateRevoked + case X509ChainStatusFlags.NoError: + case X509ChainStatusFlags.UntrustedRoot: + case X509ChainStatusFlags.NotTimeNested: + case X509ChainStatusFlags.Cyclic: + case X509ChainStatusFlags.InvalidExtension: + case X509ChainStatusFlags.InvalidPolicyConstraints: + case X509ChainStatusFlags.InvalidBasicConstraints: + case X509ChainStatusFlags.InvalidNameConstraints: + case X509ChainStatusFlags.HasNotSupportedNameConstraint: + case X509ChainStatusFlags.HasNotDefinedNameConstraint: + case X509ChainStatusFlags.HasNotPermittedNameConstraint: + case X509ChainStatusFlags.HasExcludedNameConstraint: + case X509ChainStatusFlags.CtlNotTimeValid: + case X509ChainStatusFlags.CtlNotSignatureValid: + case X509ChainStatusFlags.CtlNotValidForUsage: + case X509ChainStatusFlags.OfflineRevocation: + case X509ChainStatusFlags.NoIssuanceChainPolicy: + case X509ChainStatusFlags.HasNotSupportedCriticalExtension: + case X509ChainStatusFlags.HasWeakSignature: + default: + certificateStatus = StatusCodes.BadCertificateRevoked; + break; + } + } } } + catch (CryptographicException) + { + certificateStatus = StatusCodes.BadCertificateRevoked; + } + return ServiceResult.Good; } + private ServiceResult OnGetCertificates( + ISystemContext context, + MethodState method, + NodeId objectId, + NodeId applicationId, + NodeId certificateGroupId, + ref NodeId[] certificateTypeIds, + ref byte[][] certificates) + { + AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.CertificateAuthorityAdminOrSelfAdmin); + + var certificateTypeIdsList = new List(); + var certificatesList = new List(); + + if (m_database.GetApplication(applicationId) == null) + { + return new ServiceResult(StatusCodes.BadNotFound, "The ApplicationId does not refer to a registered application."); + } + + //If CertificateGroupId is null, the CertificateManager shall return the Certificates for all CertificateGroups assigned to the Application. + if (certificateGroupId == null) + { + foreach (KeyValuePair certType in m_certTypeMap) + { + if (m_database.GetApplicationCertificate(applicationId, certType.Value, out byte[] certificate) && certificate != null) + { + certificateTypeIdsList.Add(certType.Key); + certificatesList.Add(certificate); + } + } + } + //get only Certificate of the provided CertificateGroup + else + { + if (!m_certificateGroups.TryGetValue(certificateGroupId, out ICertificateGroup certificateGroup) + || !m_certTypeMap.TryGetValue(certificateGroup.CertificateType, out string certificateTypeId)) + { + return new ServiceResult(StatusCodes.BadInvalidArgument, "The CertificateGroupId is not recognized or not valid for the Application."); + } + if (m_database.GetApplicationCertificate(applicationId, certificateTypeId, out byte[] certificate) && certificate != null) + { + certificateTypeIdsList.Add(certificateGroup.CertificateType); + certificatesList.Add(certificate); + } + } + + certificates = certificatesList.ToArray(); + certificateTypeIds = certificateTypeIdsList.ToArray(); + + return ServiceResult.Good; + } + private ServiceResult CheckHttpsDomain(ApplicationRecordDataType application, string commonName) { if (application.ApplicationType == ApplicationType.Client) @@ -1391,7 +1505,7 @@ protected override NodeState ValidateNode( handle.Validated = true; return handle.Node; } - #endregion +#endregion #region Overridden Methods #endregion diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 7d380be41..68cc77dbc 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -105,6 +105,9 @@ protected override NodeState AddBehaviourToPredefinedNode( case ObjectTypes.ServerConfigurationType: { ServerConfigurationState activeNode = new ServerConfigurationState(passiveNode.Parent); + + activeNode.GetCertificates = new GetCertificatesMethodState(activeNode); + activeNode.Create(context, passiveNode); m_serverConfigurationNode = activeNode; @@ -202,6 +205,7 @@ public void CreateServerConfiguration( m_serverConfigurationNode.CreateSigningRequest.OnCall = new CreateSigningRequestMethodStateMethodCallHandler(CreateSigningRequest); m_serverConfigurationNode.ApplyChanges.OnCallMethod = new GenericMethodCalledEventHandler(ApplyChanges); m_serverConfigurationNode.GetRejectedList.OnCall = new GetRejectedListMethodStateMethodCallHandler(GetRejectedList); + m_serverConfigurationNode.GetCertificates.OnCall = new GetCertificatesMethodStateMethodCallHandler(GetCertificates); m_serverConfigurationNode.ClearChangeMasks(systemContext, true); // setup certificate group trust list handlers @@ -227,6 +231,8 @@ public void CreateServerConfiguration( } } + + /// /// Gets and returns the node associated with the specified NamespaceUri /// @@ -611,6 +617,41 @@ private ServiceResult GetRejectedList( return StatusCodes.Good; } + private ServiceResult GetCertificates( + ISystemContext context, + MethodState method, + NodeId objectId, + NodeId certificateGroupId, + ref NodeId[] certificateTypeIds, + ref byte[][] certificates) + { + HasApplicationSecureAdminAccess(context); + + ServerCertificateGroup certificateGroup = m_certificateGroups.FirstOrDefault(group => Utils.IsEqual(group.NodeId, certificateGroupId)); + if (certificateGroup == null) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Certificate group invalid."); + } + + NodeId certificateTypeId = certificateGroup.CertificateTypes.FirstOrDefault(); + + //TODO support multiple Application Instance Certificates + if (certificateTypeId != null) + { + certificateTypeIds = new NodeId[1] {certificateTypeId }; + certificates = new byte[1][]; + certificates[0] = certificateGroup.ApplicationCertificate.Certificate.GetRawCertData(); + } + else + { + certificateTypeIds = new NodeId[0]; + certificates = new byte[0][]; + } + + return ServiceResult.Good; + } + + private ServerCertificateGroup VerifyGroupAndTypeId( NodeId certificateGroupId, NodeId certificateTypeId diff --git a/Tests/Opc.Ua.Gds.Tests/CertificateGroupTests.cs b/Tests/Opc.Ua.Gds.Tests/CertificateGroupTests.cs index 8c8d3375e..9b84b8351 100644 --- a/Tests/Opc.Ua.Gds.Tests/CertificateGroupTests.cs +++ b/Tests/Opc.Ua.Gds.Tests/CertificateGroupTests.cs @@ -20,14 +20,18 @@ public class CertificateGroupTests private string _path; - public CertificateGroupTests() + [SetUp] + public void Setup() { _path = Utils.ReplaceSpecialFolderNames("%LocalApplicationData%/OPC/GDS/TestStore"); } - + [TearDown] public void Dispose() { - Directory.Delete(_path, true); + if (Directory.Exists(_path)) + { + Directory.Delete(_path, true); + } } #region Test Methods diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index d39affd48..cbbe57d99 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -856,6 +856,31 @@ out byte[][] issuerCertificates } + + [Test, Order(540)] + public void GetGoodCertificates() + { + AssertIgnoreTestWithoutGoodRegistration(); + AssertIgnoreTestWithoutGoodNewKeyPairRequest(); + ConnectGDS(true); + + Assert.That(() => { + m_gdsClient.GDSClient.GetCertificates(null, null, out var _, out var _); + }, Throws.Exception); + + foreach (var application in m_goodApplicationTestSet) + { + m_gdsClient.GDSClient.GetCertificates(application.ApplicationRecord.ApplicationId, null, out NodeId[] certificateTypeIds, out byte[][] certificates); + Assert.That(certificateTypeIds.Length == 1); + Assert.NotNull(certificates[0]); + Assert.AreEqual(certificates[0], application.Certificate); + m_gdsClient.GDSClient.GetCertificates(application.ApplicationRecord.ApplicationId, application.CertificateGroupId, out NodeId[] certificateTypeIds2, out byte[][] certificates2); + Assert.That(certificateTypeIds2.Length == 1); + Assert.NotNull(certificates2[0]); + Assert.AreEqual(certificates2[0], application.Certificate); + } + } + [Test, Order(550)] public void StartGoodSigningRequestWithInvalidAppURI() { @@ -1308,7 +1333,8 @@ public void CheckGoodRevocationStatus() foreach (var application in m_goodApplicationTestSet) { m_gdsClient.GDSClient.CheckRevocationStatus(application.Certificate, out StatusCode certificateStatus, out DateTime validityTime); - Assert.AreEqual((StatusCode)StatusCodes.Good, (StatusCode)certificateStatus.Code); + //Status code needs to be Bad as the method builds a custom chain that does not know about the custom cert stores. + Assert.True(StatusCode.IsBad(certificateStatus.Code)); Assert.NotNull(validityTime); } } @@ -1330,7 +1356,7 @@ public void RevokeGoodCertificates() }, Throws.Exception); } } - + [Test, Order(900)] public void UnregisterGoodApplications() { @@ -1350,7 +1376,7 @@ public void CheckRevocationStatusUnregisteredApplications() foreach (var application in m_goodApplicationTestSet) { m_gdsClient.GDSClient.CheckRevocationStatus(application.Certificate, out StatusCode certificateStatus, out DateTime validityTime); - Assert.AreEqual((StatusCode)StatusCodes.BadCertificateRevoked, (StatusCode)certificateStatus.Code); + Assert.IsTrue(((StatusCode)certificateStatus.Code).ToString().StartsWith("BadCertificate")); Assert.NotNull(validityTime); } } diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 58971815e..47080f98a 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -603,6 +603,25 @@ public void GetRejectedList() Assert.NotNull(collection); } + [Test, Order(610)] + public void GetCertificates() + { + ConnectPushClient(true); + + Assert.That(() => { + m_pushClient.PushClient.GetCertificates(null, out var _, out var _); + }, Throws.Exception); + + m_pushClient.PushClient.GetCertificates(m_pushClient.PushClient.DefaultApplicationGroup, out NodeId[] certificateTypeIds, out byte[][] certificates); + + Assert.That(certificateTypeIds.Length == 1); + Assert.NotNull(certificates[0]); + using (var x509 = new X509Certificate2(certificates[0])) + { + Assert.NotNull(x509); + } + } + [Test, Order(700)] public void ApplyChanges() { @@ -616,6 +635,7 @@ public void VerifyNoUserAccess() ConnectPushClient(false); Assert.That(() => { m_pushClient.PushClient.ApplyChanges(); }, Throws.Exception); Assert.That(() => { m_pushClient.PushClient.GetRejectedList(); }, Throws.Exception); + Assert.That(() => { m_pushClient.PushClient.GetCertificates(null, out _, out _); }, Throws.Exception); Assert.That(() => { m_pushClient.PushClient.UpdateCertificate(null, null, m_selfSignedServerCert.RawData, null, null, null); }, Throws.Exception); Assert.That(() => { m_pushClient.PushClient.CreateSigningRequest(null, null, null, false, null); }, Throws.Exception); Assert.That(() => { m_pushClient.PushClient.ReadTrustList(); }, Throws.Exception); From 33319d24186359bdd8e9df74bb194d6dd2ba6507 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:19:18 +0200 Subject: [PATCH 08/16] Bump Serilog and System.Diagnostics.DiagnosticSource (#2633) Bumps [Serilog](https://github.com/serilog/serilog) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog` from 3.1.1 to 4.0.0 - [Release notes](https://github.com/serilog/serilog/releases) - [Commits](https://github.com/serilog/serilog/compare/v3.1.1...v4.0.0) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog dependency-type: direct:production update-type: version-update:semver-major - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Applications/ReferenceServer/Reference Server.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index 8b9d99348..818599906 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -24,7 +24,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 28c81fb76..e12e841d5 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Applications/ReferenceServer/Reference Server.csproj b/Applications/ReferenceServer/Reference Server.csproj index d3773e6f2..7a9acf87c 100644 --- a/Applications/ReferenceServer/Reference Server.csproj +++ b/Applications/ReferenceServer/Reference Server.csproj @@ -159,7 +159,7 @@ - 3.1.1 + 4.0.0 2.0.0 diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 258bf0027..718ddeb86 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -22,7 +22,7 @@ - + From b33c250c2f78d930c1591349aa964712156cf6bf Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 11 Jun 2024 10:37:28 +0200 Subject: [PATCH 09/16] Fix special case for reconnect without activate (#2643) -Due to a race condition the channel may never reconnect -Remove a session.dispose in keep alive which caused flaky tests as a side effect --- Libraries/Opc.Ua.Client/Session.cs | 2 +- Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs | 4 ++-- Tests/Opc.Ua.Client.Tests/ClientFixture.cs | 3 +-- Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session.cs b/Libraries/Opc.Ua.Client/Session.cs index 4d06e56ab..1cb430bfb 100644 --- a/Libraries/Opc.Ua.Client/Session.cs +++ b/Libraries/Opc.Ua.Client/Session.cs @@ -1413,7 +1413,7 @@ public void Reconnect(ITransportChannel channel) /// /// Reconnects to the server after a network failure using a waiting connection. /// - private void Reconnect(ITransportWaitingConnection connection, ITransportChannel transportChannel = null) + private void Reconnect(ITransportWaitingConnection connection, ITransportChannel transportChannel) { bool resetReconnect = false; try diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index a77a832a9..3e22c3fb7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -286,9 +286,9 @@ public IAsyncResult BeginSendRequest(IServiceRequest request, int timeout, Async { if (m_queuedOperations == null) { - firstCall = true; m_queuedOperations = new List(); } + firstCall = m_queuedOperations.Count == 0; } // queue operations until connect completes. @@ -820,7 +820,7 @@ private bool QueueConnectOperation(WriteOperation operation, int timeout, IServi request.TypeId == DataTypeIds.FindServersOnNetworkRequest || request.TypeId == DataTypeIds.FindServersRequest) { - m_queuedOperations.Insert(0, queuedOperation); + m_queuedOperations.Add(queuedOperation); return true; } diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index f400e2012..c4091e31e 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -394,7 +394,6 @@ public void StartActivityListenerInternal(bool disableActivityLogging) ActivitySource.AddActivityListener(ActivityListener); } - /// /// Disposes Activity Listener and unregisters from Activity Source. /// @@ -410,7 +409,7 @@ private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) { if (ServiceResult.IsBad(e.Status)) { - session?.Dispose(); + Utils.LogError("Session '{0}' keep alive error: {1}", session.SessionName, e.Status); } } #endregion diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index cd7e0fc13..4888533bf 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -672,7 +672,7 @@ public async Task ReconnectWithSavedSessionSecrets( if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) { sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); - Assert.AreEqual((StatusCode)StatusCodes.BadSecureChannelClosed, (StatusCode)sre.StatusCode, sre.Message); + Assert.AreEqual((StatusCode)StatusCodes.BadSecureChannelIdInvalid, (StatusCode)sre.StatusCode, sre.Message); } else { From 10dcbfe1f0ed1117bc7fc3ede06ba51b94eea7d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:42:25 +0200 Subject: [PATCH 10/16] Bump Serilog.Sinks.Console and System.Diagnostics.DiagnosticSource (#2642) Bumps [Serilog.Sinks.Console](https://github.com/serilog/serilog-sinks-console) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog.Sinks.Console` from 5.0.0 to 6.0.0 - [Release notes](https://github.com/serilog/serilog-sinks-console/releases) - [Commits](https://github.com/serilog/serilog-sinks-console/compare/v5.0.0...v6.0.0) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog.Sinks.Console dependency-type: direct:production update-type: version-update:semver-major - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index 818599906..bfe763ec2 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -26,7 +26,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index e12e841d5..3ff6ef968 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -35,7 +35,7 @@ - + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 718ddeb86..d8ee8e3f0 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -24,7 +24,7 @@ - + From dddc1cb4abbaf4cac1ba5653edf2b08c73b12795 Mon Sep 17 00:00:00 2001 From: romanett Date: Tue, 11 Jun 2024 16:17:02 +0200 Subject: [PATCH 11/16] Make X509CertificateStore support CRLs on Windows (#2571) This PR extends the existing X509 CertificateStore building on top of the .NET X509Store Class to support Certificate Revocation Lists (CRLs) This is important to make the X509Store viable for use as Trust and Issuer Store. Considerations: - only Works on Windows, on all other OS the "SupportsCRLs"-Property of the Store still returns false - builds on top of the Win32 API - uses source generated P-Invoke calls to call into Win32 API (https://github.com/microsoft/CsWin32) - makes use of Extension Methods of .NET X509Store Class to make the CRL Enumerate / Add / Delete accessible for the Store Implementation - guards ever call with OS Support calls and throws PlatformNotSupportedException - Behaviour of the X509CertificateStore for Linux/MacOS is unchanged - Added unit tests for the Certificate Store testing all relvant implemented crl functions - Extended GDS Tests for Integration Testing by utilizing Client & Server making second run on windows with only X509 Stores --- Docs/Certificates.md | 4 + README.md | 3 + Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 1 + .../Extensions/Internal/PInvokeHelper.cs | 198 ++++++++++++++ .../Extensions/Internal/X509CrlHelper.cs | 253 ++++++++++++++++++ .../Extensions/PlatformHelper.cs | 59 ++++ .../Extensions/X509StoreExtensions.cs | 114 ++++++++ .../X509CertificateStore.cs | 173 ++++++++++-- .../Certificates/CertificateStoreTest.cs | 251 ++++++++++++++++- .../Certificates/TemporaryCertValidator.cs | 12 +- .../Security/Certificates/TestUtils.cs | 17 +- Tests/Opc.Ua.Gds.Tests/ClientTest.cs | 24 +- Tests/Opc.Ua.Gds.Tests/Common.cs | 13 +- .../GlobalDiscoveryTestClient.cs | 19 +- .../GlobalDiscoveryTestServer.cs | 15 +- .../Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 10 +- ...alDiscoveryTestClientX509Stores.Config.xml | 112 ++++++++ ...alDiscoveryTestServerX509Stores.Config.xml | 174 ++++++++++++ 18 files changed, 1407 insertions(+), 45 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/PInvokeHelper.cs create mode 100644 Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/X509CrlHelper.cs create mode 100644 Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/PlatformHelper.cs create mode 100644 Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/X509StoreExtensions.cs rename Stack/Opc.Ua.Core/Security/Certificates/{ => X509CertificateStore}/X509CertificateStore.cs (58%) create mode 100644 Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestClientX509Stores.Config.xml create mode 100644 Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServerX509Stores.Config.xml diff --git a/Docs/Certificates.md b/Docs/Certificates.md index e06570af1..23c38a502 100644 --- a/Docs/Certificates.md +++ b/Docs/Certificates.md @@ -31,6 +31,10 @@ The UA .NET Standard stack supports the following certificate stores: - The **Trusted Https** store `/trustedHttps` which contains https certificates which are trusted by an application. To establish trust, the same rules apply as explained for the *Trusted* and the *Issuer* store. +### X509Store on Windows +Starting with Version 1.5.xx of the UA .NET Standard Stack the X509Store supports the storage and retrieval of CRLS, if used on the **Windows OS**. +This enables the usage of the X509Store instead of the Directory Store for stores requiring the use of crls, e.g. the issuer or the directory Store. + ### Windows .NET applications By default the self signed certificates are stored in a **X509Store** called **CurrentUser\\UA_MachineDefault**. The certificates can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc). The *trusted*, *issuer* and *rejected* stores remain in a folder called **OPC Foundation\pki** with a root folder which is specified by the `SpecialFolder` variable **%CommonApplicationData%**. On Windows 7/8/8.1/10 this is usually the invisible folder **C:\ProgramData**. diff --git a/README.md b/README.md index 9b106bb3d..c780b5ba8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ More samples based on the official [Nuget](https://www.nuget.org/packages/OPCFou * Sessions and Subscriptions. * A [PubSub](Docs/PubSub.md) library with samples. +#### **New in 1.05.xxx** +* CRL Support for the X509Store on Windows + #### **New in 1.05.373** * 1.05 Nodeset * Support for [WellKnownRoles & RoleBasedUserManagement](Docs/RoleBasedUserManagement.md). diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 4874bd731..2498aa3c4 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -92,6 +92,7 @@ $(BaseIntermediateOutputPath)/zipnodeset2 Schema/Opc.Ua.NodeSet2.xml + true diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/PInvokeHelper.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/PInvokeHelper.cs new file mode 100644 index 000000000..ae6aebbe0 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/PInvokeHelper.cs @@ -0,0 +1,198 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 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/ + * ======================================================================*/ + +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ + +#pragma warning disable CS1591,CS1573,CS0465,CS0649,CS8019,CS1570,CS1584,CS1658,CS0436,CS8981 +using global::System; +using global::System.Diagnostics; +using global::System.Diagnostics.CodeAnalysis; +using global::System.Runtime.CompilerServices; +using global::System.Runtime.InteropServices; +using global::System.Runtime.Versioning; +using winmdroot = global::Windows.Win32; +namespace Windows.Win32 +{ + + /// + /// Contains extern methods from "CRYPT32.dll". + /// + [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.49-beta+91f5c15987")] + internal static partial class PInvokeHelper + { + /// The CertEnumCRLsInStore function retrieves the first or next certificate revocation list (CRL) context in a certificate store. Used in a loop, this function can retrieve in sequence all CRL contexts in a certificate store. + /// Handle of a certificate store. + /// + /// A pointer to the previous CRL_CONTEXT structure found. The pPrevCrlContext parameter must be NULL to get the first CRL in the store. Successive CRLs are enumerated by setting pPrevCrlContext to the pointer returned by a previous call to the function. This function frees the CRL_CONTEXT referenced by non-NULL values of this parameter. The enumeration skips any CRLs previously deleted by CertDeleteCRLFromStore. + /// Read more on docs.microsoft.com. + /// + /// + /// If the function succeeds, the return value is a pointer to the next CRL_CONTEXT in the store. NULL is returned if the function fails. For extended error information, call GetLastError. Some possible error codes follow. + /// This doc was truncated. + /// + /// + /// The returned pointer is freed when it is passed as the pPrevCrlContext on a subsequent call to the function. Otherwise, the pointer must explicitly be freed by calling CertFreeCRLContext. A pPrevCrlContext that is not NULL is always freed when passed to this function through a call to CertFreeCRLContext, even if the function itself returns an error. A duplicate of the CRL context returned by this function can be made by calling CertDuplicateCRLContext. + /// Read more on docs.microsoft.com. + /// + [DllImport("CRYPT32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static extern unsafe winmdroot.Security.Cryptography.CRL_CONTEXT* CertEnumCRLsInStore(winmdroot.Security.Cryptography.HCERTSTORE hCertStore, [Optional] winmdroot.Security.Cryptography.CRL_CONTEXT* pPrevCrlContext); + + /// Creates a certificate revocation list (CRL) context from an encoded CRL and adds it to the certificate store. + /// Handle of a certificate store. + /// + /// Specifies the type of encoding used. It is always acceptable to specify both the certificate and message encoding types by combining them with a bitwise-OR operation as shown in the following example: X509_ASN_ENCODING | PKCS_7_ASN_ENCODING Currently defined encoding types are: + /// This doc was truncated. + /// Read more on docs.microsoft.com. + /// + /// A pointer to a buffer containing the encoded CRL to be added to the certificate store. + /// The size, in bytes, of the pbCrlEncoded buffer. + /// + /// Specifies the action to take if a matching CRL or a link to a matching CRL already exists in the store. Currently defined disposition values and their uses are as follows. + /// This doc was truncated. + /// Read more on docs.microsoft.com. + /// + /// + /// A pointer to a pointer to the decoded CRL_CONTEXT structure. This is an optional parameter that can be NULL, indicating that the calling application does not require a copy of the new or existing CRL. If a copy is made, that context must be freed using CertFreeCRLContext. + /// Read more on docs.microsoft.com. + /// + /// + /// If the function succeeds, the return value is TRUE. If the function fails, the return value is FALSE. For extended error information, call GetLastError. Some possible error codes follow. + /// This doc was truncated. + /// + /// + /// Learn more about this API from docs.microsoft.com. + /// + [DllImport("CRYPT32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static extern unsafe winmdroot.Foundation.BOOL CertAddEncodedCRLToStore(winmdroot.Security.Cryptography.HCERTSTORE hCertStore, winmdroot.Security.Cryptography.CERT_QUERY_ENCODING_TYPE dwCertEncodingType, byte* pbCrlEncoded, uint cbCrlEncoded, uint dwAddDisposition, [Optional] winmdroot.Security.Cryptography.CRL_CONTEXT** ppCrlContext); + + /// The CertDeleteCRLFromStore function deletes the specified certificate revocation list (CRL) context from the certificate store. + /// + /// A pointer to the CRL_CONTEXT structure to be deleted. + /// Read more on docs.microsoft.com. + /// + /// + /// If the function succeeds, the return value is TRUE. If the function fails, the return value is FALSE. For extended error information, call GetLastError. One possible error code is the following. + /// This doc was truncated. + /// + /// + /// All subsequent get or find operations for the CRL in this store fail. However, memory allocated for the CRL is not freed until all duplicated contexts have also been freed. The pCrlContext parameter is always freed by this function by using CertFreeCRLContext, even for an error. + /// Read more on docs.microsoft.com. + /// + [DllImport("CRYPT32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static extern unsafe winmdroot.Foundation.BOOL CertDeleteCRLFromStore(winmdroot.Security.Cryptography.CRL_CONTEXT* pCrlContext); + } + + namespace Security.Cryptography + { + /// The CRL_CONTEXT structure contains both the encoded and decoded representations of a certificate revocation list (CRL). CRL contexts returned by any CryptoAPI function must be freed by calling the CertFreeCRLContext function. + /// + /// Learn more about this API from docs.microsoft.com. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.49-beta+91f5c15987")] + internal partial struct CRL_CONTEXT + { + /// + /// Type of encoding used. It is always acceptable to specify both the certificate and message encoding types by combining them with a bitwise-OR operation as shown in the following example: X509_ASN_ENCODING | PKCS_7_ASN_ENCODING Currently defined encoding types are: + /// This doc was truncated. + /// Read more on docs.microsoft.com. + /// + internal winmdroot.Security.Cryptography.CERT_QUERY_ENCODING_TYPE dwCertEncodingType; + + /// A pointer to the encoded CRL information. + internal unsafe byte* pbCrlEncoded; + + /// The size, in bytes, of the encoded CRL information. + internal uint cbCrlEncoded; + + /// + /// A pointer to CRL_INFO structure containing the CRL information. + /// Read more on docs.microsoft.com. + /// + internal unsafe Byte* pCrlInfo; + + /// A handle to the certificate store. + internal winmdroot.Security.Cryptography.HCERTSTORE hCertStore; + } + + + + [DebuggerDisplay("{Value}")] + [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.49-beta+91f5c15987")] + internal unsafe readonly partial struct HCERTSTORE + { + internal readonly void* Value; + + internal HCERTSTORE(void* value) => this.Value = value; + + public static implicit operator void*(HCERTSTORE value) => value.Value; + + public static explicit operator HCERTSTORE(void* value) => new HCERTSTORE(value); + + } + + [Flags] + [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.49-beta+91f5c15987")] + internal enum CERT_QUERY_ENCODING_TYPE : uint + { + X509_ASN_ENCODING = 0x00000001, + PKCS_7_ASN_ENCODING = 0x00010000, + } + } + + namespace Foundation + { + [DebuggerDisplay("{Value}")] + [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.49-beta+91f5c15987")] + internal readonly partial struct BOOL + { + internal readonly int Value; + + internal BOOL(int value) => this.Value = value; + + public static implicit operator int(BOOL value) => value.Value; + + public static explicit operator BOOL(int value) => new BOOL(value); + internal BOOL(bool value) => this.Value = value ? 1 : 0; + + public static implicit operator bool(BOOL value) => value.Value != 0; + + public static implicit operator BOOL(bool value) => new BOOL(value); + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/X509CrlHelper.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/X509CrlHelper.cs new file mode 100644 index 000000000..957181cbd --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/Internal/X509CrlHelper.cs @@ -0,0 +1,253 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 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.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Security.Cryptography; + +namespace Opc.Ua.X509StoreExtensions.Internal +{ + /// + /// Helper functions to access Crls in a Windows X509 Store + /// + internal static unsafe class X509CrlHelper + { + /// + /// Gets all crls from the provided X509 Store on Windows + /// + /// HCERTSTORE Handle to X509 Store + /// array of all found crls as byte array + public static byte[][] GetCrls(IntPtr storeHandle) + { + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + + var crls = new List(); + + CRL_CONTEXT* crlContext = (CRL_CONTEXT*)IntPtr.Zero; + try + { + //read until Pointer to crlContext is NullPtr (no more crls in store) + while (true) + { + crlContext = PInvokeHelper.CertEnumCRLsInStore((HCERTSTORE)storeHandle.ToPointer(), crlContext); + + if (crlContext != null) + { + byte[] crl = ReadCrlFromCrlContext(crlContext); + + if (crl != null) + { + crls.Add(crl); + } + } + else + { + int error = Marshal.GetLastWin32Error(); + if (error == -2146885628) + { + //No more crls found in store + } + else if (error != 0) + { + Utils.LogError("Error while enumerating Crls from X509Store, Win32Error-Code: {0}", error); + } + break; + } + } + } + catch (Exception ex) + { + Utils.LogError(ex, "Exception while enumerating Crls from X509Store"); + } + return crls.ToArray(); + } + + /// + /// gets the crl as byte array from the provided crlcontext + /// + /// crl context as pointer + /// crl as byte array + private static byte[] ReadCrlFromCrlContext(CRL_CONTEXT* crlContext) + { + uint length = crlContext->cbCrlEncoded; + byte[] crl = new byte[length]; + + Marshal.Copy((IntPtr)crlContext->pbCrlEncoded, crl, 0, (int)length); + + return crl; + } + + + /// + /// add a crl to to the provided store + /// + /// HCERTSTORE Handle to X509 Store + /// the crl as Asn1 or PKCS7 encoded byte array + public static void AddCrl(IntPtr storeHandle, byte[] crl) + { + + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + IntPtr crlPointer = Marshal.AllocHGlobal(crl.Length); + + Marshal.Copy(crl, 0, crlPointer, crl.Length);//copy from managed array to unmanaged memory + + try + { + /////+------------------------------------------------------------------------- + // Add certificate/CRL, encoded, context or element disposition values. + //-------------------------------------------------------------------------- + //#define CERT_STORE_ADD_NEW 1 + //#define CERT_STORE_ADD_USE_EXISTING 2 + //#define CERT_STORE_ADD_REPLACE_EXISTING 3 + //#define CERT_STORE_ADD_ALWAYS 4 + //#define CERT_STORE_ADD_REPLACE_EXISTING_INHERIT_PROPERTIES 5 + //#define CERT_STORE_ADD_NEWER 6 + //#define CERT_STORE_ADD_NEWER_INHERIT_PROPERTIES 7 + if (PInvokeHelper.CertAddEncodedCRLToStore( + (HCERTSTORE)storeHandle.ToPointer(), + CERT_QUERY_ENCODING_TYPE.X509_ASN_ENCODING | CERT_QUERY_ENCODING_TYPE.PKCS_7_ASN_ENCODING, + (byte*)crlPointer, + (uint)crl.Length, + 3, + null)) + { + //success + return; + } + else + { + int error = Marshal.GetLastWin32Error(); + if (error == -2147024809) + { + Utils.LogError("Error while adding Crl to X509Store, Win32Error-Code: {0}: ERROR_INVALID_PARAMETER, The parameter is incorrect. ", error); + } + if (error == -2146881269) + { + Utils.LogError("Error while adding Crl to X509Store, Win32Error-Code: {0}: CRYPT_E_ASN1_BADTAG, ASN1 bad tag value met. ", error); + } + if (error == -2147024891) + { + Utils.LogError("Error while adding Crl to X509Store, Win32Error-Code: {0}: ERROR_ACCESS_DENIED, Access is denied. ", error); + } + if (error != 0) + { + Utils.LogError("Error while adding Crl to X509Store, Win32Error-Code: {0}: ", error); + } + return; + } + } + catch (Exception ex) + { + Utils.LogError(ex, "Exception while adding Crl to X509Store"); + } + finally + { + if (crlPointer != IntPtr.Zero) + { + Marshal.FreeHGlobal(crlPointer); + } + } + } + + /// + /// deletes a crl from the provided store + /// + /// HCERTSTORE Handle to X509 Store + /// asn1 encoded crl to delete from the store + /// true if delete sucessfully, false if failure + public static bool DeleteCrl(IntPtr storeHandle, byte[] crl) + { + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + CRL_CONTEXT* crlContext = (CRL_CONTEXT*)IntPtr.Zero; + try + { + //read until Pointer to crlContext is NullPtr (no more crls in store) + while (true) + { + crlContext = PInvokeHelper.CertEnumCRLsInStore((HCERTSTORE)storeHandle.ToPointer(), crlContext); + + if (crlContext != null) + { + byte[] storeCrl = ReadCrlFromCrlContext(crlContext); + + + if (crl != null && crl.SequenceEqual(storeCrl)) + { + if (!PInvokeHelper.CertDeleteCRLFromStore(crlContext)) + { + var error = Marshal.GetLastWin32Error(); + if (error != 0) + { + Utils.LogError("Error while deleting Crl from X509Store, Win32Error-Code: {0}", error); + } + } + else + { + //was freed by CertDeleteCRLFromStore + crlContext = (CRL_CONTEXT*)IntPtr.Zero; + return true; + } + break; + } + } + else + { + var error = Marshal.GetLastWin32Error(); + if (error == -2146885628) + { + //No more crls found in store + } + else if (error != 0) + { + Utils.LogError("Error while deleting Crl from X509Store, Win32Error-Code: {0}", error); + } + break; + } + } + } + catch (Exception ex) + { + Utils.LogError(ex, "Exception while deleting Crl from X509Store"); + } + return false; + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/PlatformHelper.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/PlatformHelper.cs new file mode 100644 index 000000000..1971c6573 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/PlatformHelper.cs @@ -0,0 +1,59 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 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; + +namespace Opc.Ua.X509StoreExtensions +{ + /// + /// Static Helper class to retrieve if executed on a specific operating system + /// + internal static class PlatformHelper + { + private static bool? _isWindowsWithCrlSupport = null; + /// + /// True if OS Windows and Version >= Windows XP + /// + /// True if Crl Support is given in the system X509 Store + public static bool IsWindowsWithCrlSupport() + { + if (_isWindowsWithCrlSupport != null) + { + return _isWindowsWithCrlSupport.Value; + } + OperatingSystem version = Environment.OSVersion; + _isWindowsWithCrlSupport = version.Platform == PlatformID.Win32NT + && ( + (version.Version.Major > 5) + || (version.Version.Major == 5 && version.Version.Minor >= 1) + ); + return _isWindowsWithCrlSupport.Value; + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/X509StoreExtensions.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/X509StoreExtensions.cs new file mode 100644 index 000000000..125b47f74 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/Extensions/X509StoreExtensions.cs @@ -0,0 +1,114 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 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.Security.Cryptography.X509Certificates; +using Opc.Ua.X509StoreExtensions.Internal; + +namespace Opc.Ua.X509StoreExtensions +{ + /// + /// Extension Methods for the Windows X509 Store to add CRL Support to .NET + /// + internal static class X509StoreExtensions + { + /// + /// Enumerate all Crls in a Windows X509 Store + /// + /// the Windows X509 Store to retrieve the crls from + /// the crls as a byte array + /// if not called on a supported OS (Windows >= XP) + /// if store is null + public static byte[][] EnumerateCrls(this X509Store store) + { + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + if (store == null) + { + throw new ArgumentNullException(nameof(store)); + } + + IntPtr handle = store.StoreHandle; + + return X509CrlHelper.GetCrls(handle); + } + + /// + /// Adds a ASN1 encoded crl to the provided Windows X509 Store + /// + /// the Windows X509 Store to add the crl to + /// the ASN1 encoded crl as byte array + /// if not called on a supported OS (Windows >= XP) + /// if store is null + public static void AddCrl(this X509Store store, byte[] crl) + { + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + + if (store == null) + { + throw new ArgumentNullException(nameof(store)); + } + if (crl == null || crl.Length == 0) + { + throw new ArgumentNullException(nameof(crl)); + } + + IntPtr handle = store.StoreHandle; + + X509CrlHelper.AddCrl(handle, crl); + } + /// + /// Deletes the specified CRL from the provided X509 store + /// + /// the Windows X509 Store to delete the crl from + /// the ASN1 encoded crl as byte array + /// if not called on a supported OS (Windows >= XP) + /// if store is null + public static bool DeleteCrl(this X509Store store, byte[] crl) + { + if (!PlatformHelper.IsWindowsWithCrlSupport()) + { + throw new PlatformNotSupportedException(); + } + if (store == null) + { + throw new ArgumentNullException(nameof(store)); + } + + IntPtr handle = store.StoreHandle; + + return X509CrlHelper.DeleteCrl(handle, crl); + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs similarity index 58% rename from Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore.cs rename to Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs index db3f4654a..89886df64 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs @@ -15,10 +15,12 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.IO; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Opc.Ua.Security.Certificates; +using Opc.Ua.X509StoreExtensions; namespace Opc.Ua { @@ -217,42 +219,183 @@ public Task LoadPrivateKey(string thumbprint, string subjectNa } /// - /// CRLs are not supported here. - public bool SupportsCRLs => false; + /// CRLs are only supported on Windows Platform. + public bool SupportsCRLs => PlatformHelper.IsWindowsWithCrlSupport(); /// - /// CRLs are not supported here. - public Task IsRevoked(X509Certificate2 issuer, X509Certificate2 certificate) + /// CRLs are only supported on Windows Platform. + public async Task IsRevoked(X509Certificate2 issuer, X509Certificate2 certificate) { - return Task.FromResult((StatusCode)StatusCodes.BadNotSupported); + if (!SupportsCRLs) + { + throw new ServiceResultException(StatusCodes.BadNotSupported); + } + + if (issuer == null) + { + throw new ArgumentNullException(nameof(issuer)); + } + + if (certificate == null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + X509CRLCollection crls = await EnumerateCRLs().ConfigureAwait(false); + // check for CRL. + + bool crlExpired = true; + + foreach (X509CRL crl in crls) + { + + if (!X509Utils.CompareDistinguishedName(crl.IssuerName, issuer.SubjectName)) + { + continue; + } + + if (!crl.VerifySignature(issuer, false)) + { + continue; + } + + if (crl.IsRevoked(certificate)) + { + return (StatusCode)StatusCodes.BadCertificateRevoked; + } + + if (crl.ThisUpdate <= DateTime.UtcNow && (crl.NextUpdate == DateTime.MinValue || crl.NextUpdate >= DateTime.UtcNow)) + { + crlExpired = false; + } + } + + // certificate is fine. + if (!crlExpired) + { + return (StatusCode)StatusCodes.Good; + } + + // can't find a valid CRL. + return (StatusCode)StatusCodes.BadCertificateRevocationUnknown; } /// - /// CRLs are not supported here. + /// CRLs are only supported on Windows Platform. public Task EnumerateCRLs() { - throw new ServiceResultException(StatusCodes.BadNotSupported); + if (!SupportsCRLs) + { + throw new ServiceResultException(StatusCodes.BadNotSupported); + } + var crls = new X509CRLCollection(); + using (var store = new X509Store(m_storeName, m_storeLocation)) + { + store.Open(OpenFlags.ReadOnly); + + byte[][] rawCrls = store.EnumerateCrls(); + foreach (byte[] rawCrl in rawCrls) + { + var crl = new X509CRL(rawCrl); + crls.Add(crl); + } + } + return Task.FromResult(crls); } /// - /// CRLs are not supported here. - public Task EnumerateCRLs(X509Certificate2 issuer, bool validateUpdateTime = true) + /// CRLs are only supported on Windows Platform. + public async Task EnumerateCRLs(X509Certificate2 issuer, bool validateUpdateTime = true) { - throw new ServiceResultException(StatusCodes.BadNotSupported); + if (!SupportsCRLs) + { + throw new ServiceResultException(StatusCodes.BadNotSupported); + } + if (issuer == null) + { + throw new ArgumentNullException(nameof(issuer)); + } + var crls = new X509CRLCollection(); + foreach (X509CRL crl in await EnumerateCRLs().ConfigureAwait(false)) + { + if (!X509Utils.CompareDistinguishedName(crl.IssuerName, issuer.SubjectName)) + { + continue; + } + + if (!crl.VerifySignature(issuer, false)) + { + continue; + } + + if (!validateUpdateTime || + crl.ThisUpdate <= DateTime.UtcNow && (crl.NextUpdate == DateTime.MinValue || crl.NextUpdate >= DateTime.UtcNow)) + { + crls.Add(crl); + } + } + + return crls; } /// - /// CRLs are not supported here. - public Task AddCRL(X509CRL crl) + /// CRLs are only supported on Windows Platform. + public async Task AddCRL(X509CRL crl) { - throw new ServiceResultException(StatusCodes.BadNotSupported); + if (!SupportsCRLs) + { + throw new ServiceResultException(StatusCodes.BadNotSupported); + } + if (crl == null) + { + throw new ArgumentNullException(nameof(crl)); + } + + X509Certificate2 issuer = null; + X509Certificate2Collection certificates = null; + certificates = await Enumerate().ConfigureAwait(false); + foreach (X509Certificate2 certificate in certificates) + { + if (X509Utils.CompareDistinguishedName(certificate.SubjectName, crl.IssuerName)) + { + if (crl.VerifySignature(certificate, false)) + { + issuer = certificate; + break; + } + } + } + + if (issuer == null) + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Could not find issuer of the CRL."); + } + using (var store = new X509Store(m_storeName, m_storeLocation)) + { + store.Open(OpenFlags.ReadWrite); + + store.AddCrl(crl.RawData); + } } /// - /// CRLs are not supported here. + /// CRLs are only supported on Windows Platform. public Task DeleteCRL(X509CRL crl) { - throw new ServiceResultException(StatusCodes.BadNotSupported); + if (!SupportsCRLs) + { + throw new ServiceResultException(StatusCodes.BadNotSupported); + } + if (crl == null) + { + throw new ArgumentNullException(nameof(crl)); + } + using (var store = new X509Store(m_storeName, m_storeLocation)) + { + store.Open(OpenFlags.ReadWrite); + + return Task.FromResult(store.DeleteCrl(crl.RawData)); + } } private bool m_noPrivateKeys; diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs index 82e8fe2fd..fdc220e0f 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs @@ -30,11 +30,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using NUnit.Framework; +using Opc.Ua.Security.Certificates; +using Opc.Ua.X509StoreExtensions; using Assert = NUnit.Framework.Legacy.ClassicAssert; @@ -44,11 +47,13 @@ namespace Opc.Ua.Core.Tests.Security.Certificates /// Tests for the CertificateFactory class. /// [TestFixture, Category("CertificateStore")] + [NonParallelizable] [SetCulture("en-us")] public class CertificateStoreTest { #region DataPointSources public const string X509StoreSubject = "CN=Opc.Ua.Core.Tests, O=OPC Foundation, OU=X509Store, C=US"; + public const string X509StoreSubject2 = "CN=Opc.Ua.Core.Tests, O=OPC Foundation, OU=X509Store2, C=US"; [DatapointSource] public string[] CertStores = GetCertStores(); @@ -59,19 +64,38 @@ public class CertificateStoreTest /// Clean up the test cert store folder. /// [OneTimeTearDown] - protected void OneTimeTearDown() + protected async Task OneTimeTearDown() { foreach (var certStore in CertStores) { using (var x509Store = new X509CertificateStore()) { x509Store.Open(certStore); - var collection = x509Store.Enumerate().Result; + var collection = await x509Store.Enumerate().ConfigureAwait(false); foreach (var cert in collection) { if (X509Utils.CompareDistinguishedName(X509StoreSubject, cert.Subject)) { - x509Store.Delete(cert.Thumbprint).Wait(); + await x509Store.Delete(cert.Thumbprint).ConfigureAwait(false); + } + if (X509Utils.CompareDistinguishedName(X509StoreSubject2, cert.Issuer)) + { + await x509Store.Delete(cert.Thumbprint).ConfigureAwait(false); + } + } + if (x509Store.SupportsCRLs) + { + X509CRLCollection crls = x509Store.EnumerateCRLs().Result; + foreach (X509CRL crl in crls) + { + if (X509Utils.CompareDistinguishedName(X509StoreSubject, crl.Issuer)) + { + await x509Store.DeleteCRL(crl).ConfigureAwait(false); + } + if (X509Utils.CompareDistinguishedName(X509StoreSubject2, crl.Issuer)) + { + await x509Store.DeleteCRL(crl).ConfigureAwait(false); + } } } } @@ -84,7 +108,7 @@ protected void OneTimeTearDown() /// /// Verify new app certificate is stored in X509Store with private key. /// - [Theory] + [Theory, Order(10)] public async Task VerifyAppCertX509Store(string storePath) { var appCertificate = GetTestCert(); @@ -122,7 +146,7 @@ public async Task VerifyAppCertX509Store(string storePath) /// Verify new app certificate is stored in Directory Store /// with password for private key (PFX). /// - [Test] + [Test, Order(20)] public async Task VerifyAppCertDirectoryStore() { var appCertificate = GetTestCert(); @@ -186,7 +210,7 @@ public async Task VerifyAppCertDirectoryStore() /// /// Verify that old invalid cert stores throw. /// - [Test] + [Test, Order(30)] public void VerifyInvalidAppCertX509Store() { var appCertificate = GetTestCert(); @@ -199,6 +223,214 @@ public void VerifyInvalidAppCertX509Store() CertificateStoreType.X509Store, "System\\UA_MachineDefault")); } + + /// + /// Verify X509 Store supports no Crls on Linux or MacOs + /// + /// + [Theory, Order(40)] + public void VerifyNoCrlSupportOnLinuxOrMacOsX509Store(string storePath) + { + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.False(x509Store.SupportsCRLs); + Assert.Throws(() => x509Store.EnumerateCRLs()); + } + else + { + Assert.True(x509Store.SupportsCRLs); + } + } + } + + /// + /// Add an new crl to the X509 store + /// + /// + [Theory, Order(50)] + public async Task AddAndEnumerateNewCrlInX509StoreOnWindows(string storePath) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("Crls in an X509Store are only supported on Windows"); + } + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + + Assert.True(x509Store.SupportsCRLs); + + //add issuer to store + await x509Store.Add(GetTestCert()).ConfigureAwait(false); + //add Crl + var crlBuilder = CrlBuilder.Create(GetTestCert().SubjectName); + crlBuilder.AddRevokedCertificate(GetTestCert()); + var crl = new X509CRL(crlBuilder.CreateForRSA(GetTestCert())); + await x509Store.AddCRL(crl).ConfigureAwait(false); + + //enumerate Crls + X509CRLCollection crls = await x509Store.EnumerateCRLs().ConfigureAwait(false); + + Assert.AreEqual(1, crls.Count); + Assert.AreEqual(crl.RawData, crls[0].RawData); + Assert.AreEqual(GetTestCert().SerialNumber, + crls[0].RevokedCertificates.First().SerialNumber); + + //TestRevocation + var statusCode = await x509Store.IsRevoked(GetTestCert(), GetTestCert()).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.BadCertificateRevoked, statusCode); + + + } + } + + /// + /// Enumerate and update an existing crl to the X509 store + /// + /// + [Theory, Order(60)] + public async Task UpdateExistingCrlInX509StoreOnWindows(string storePath) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("Crls in an X509Store are only supported on Windows"); + } + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + //enumerate Crls + X509CRL crl = (await x509Store.EnumerateCRLs().ConfigureAwait(false)).First(); + + //Test Revocation before adding cert + var statusCode = await x509Store.IsRevoked(GetTestCert(), GetTestCert2()).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.Good, statusCode); + + var crlBuilder = CrlBuilder.Create(crl); + + crlBuilder.AddRevokedCertificate(GetTestCert2()); + var updatedCrl = new X509CRL(crlBuilder.CreateForRSA(GetTestCert())); + await x509Store.AddCRL(updatedCrl).ConfigureAwait(false); + + X509CRLCollection crls = await x509Store.EnumerateCRLs().ConfigureAwait(false); + + Assert.AreEqual(1, crls.Count); + + Assert.AreEqual(2, crls[0].RevokedCertificates.Count); + //Test Revocation after adding cert + var statusCode2 = await x509Store.IsRevoked(GetTestCert(), GetTestCert2()).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.BadCertificateRevoked, statusCode2); + } + } + + /// + /// Add a second crl to the X509 store + /// + /// + [Theory, Order(70)] + public async Task AddSecondCrlToX509StoreOnWindows(string storePath) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("Crls in an X509Store are only supported on Windows"); + } + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + //add issuer to store + await x509Store.Add(GetTestCert2()).ConfigureAwait(false); + //add Crl + var crlBuilder = CrlBuilder.Create(GetTestCert2().SubjectName); + crlBuilder.AddRevokedCertificate(GetTestCert2()); + var crl = new X509CRL(crlBuilder.CreateForRSA(GetTestCert2())); + await x509Store.AddCRL(crl).ConfigureAwait(false); + + //enumerate Crls + X509CRLCollection crls = await x509Store.EnumerateCRLs().ConfigureAwait(false); + + Assert.AreEqual(2, crls.Count); + Assert.NotNull(crls.SingleOrDefault(c => c.Issuer == crl.Issuer)); + } + } + + /// + /// Delete both crls from the X509 store + /// + /// + [Theory, Order(80)] + public async Task DeleteCrlsFromX509StoreOnWindows(string storePath) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("Crls in an X509Store are only supported on Windows"); + } + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + + //Delete first crl with revoked certificates + X509CRL crl = (await x509Store.EnumerateCRLs().ConfigureAwait(false)).Single(c => c.Issuer == X509StoreSubject); + await x509Store.DeleteCRL(crl).ConfigureAwait(false); + + X509CRLCollection crlsAfterFirstDelete = await x509Store.EnumerateCRLs().ConfigureAwait(false); + + //check the right crl was deleted + Assert.AreEqual(1, crlsAfterFirstDelete.Count); + Assert.Null(crlsAfterFirstDelete.FirstOrDefault(c => c == crl)); + + //make shure IsRevoked cant find crl anymore + var statusCode = await x509Store.IsRevoked(GetTestCert(), GetTestCert()).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.BadCertificateRevocationUnknown, statusCode); + + //Delete second (empty) crl from store + await x509Store.DeleteCRL(crlsAfterFirstDelete.First()).ConfigureAwait(false); + + X509CRLCollection crlsAfterSecondDelete = await x509Store.EnumerateCRLs().ConfigureAwait(false); + + //make shure no crls remain in store + Assert.AreEqual(0, crlsAfterSecondDelete.Count); + } + } + + + /// + /// Verify X509 Store Extension methods throw on Linux or MacOs + /// + /// + [Theory, Order(90)] + public void X509StoreExtensionsThrowException(string storePath) + { + using (var x509Store = new X509Store(storePath)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Throws(() => x509Store.AddCrl(new byte[0])); + Assert.Throws(() => x509Store.EnumerateCrls()); + Assert.Throws(() => x509Store.DeleteCrl(new byte[0])); + } + else + { + Assert.Ignore("Test only relevant on MacOS/Linux"); + } + } + using (var x509Store = new X509CertificateStore()) + { + x509Store.Open(storePath); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.ThrowsAsync(() => x509Store.AddCRL(new X509CRL())); + Assert.ThrowsAsync(() => x509Store.EnumerateCRLs()); + Assert.ThrowsAsync(() => x509Store.DeleteCRL(new X509CRL())); + } + else + { + Assert.Ignore("Test only relevant on MacOS/Linux"); + } + } + } + #endregion #region Private Methods @@ -208,6 +440,12 @@ private X509Certificate2 GetTestCert() (m_testCertificate = CertificateFactory.CreateCertificate(X509StoreSubject).CreateForRSA()); } + private X509Certificate2 GetTestCert2() + { + return m_testCertificate2 ?? + (m_testCertificate2 = CertificateFactory.CreateCertificate(X509StoreSubject2).CreateForRSA()); + } + private static string[] GetCertStores() { var result = new List { @@ -223,6 +461,7 @@ private static string[] GetCertStores() #region Private Fields private X509Certificate2 m_testCertificate; + private X509Certificate2 m_testCertificate2; #endregion } } diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs index 60a1eb23f..75984661d 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs @@ -30,6 +30,7 @@ using System; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Opc.Ua.Core.Tests { @@ -79,7 +80,7 @@ public void Dispose() { if (Interlocked.CompareExchange(ref m_disposed, 1, 0) == 0) { - CleanupValidatorAndStores(true); + CleanupValidatorAndStoresAsync(true).GetAwaiter().GetResult(); m_issuerStore = null; m_trustedStore = null; m_rejectedStore = null; @@ -138,13 +139,14 @@ public CertificateValidator Update() /// /// Clean up (delete) the content of the issuer and trusted store. /// - public void CleanupValidatorAndStores(bool dispose = false) + public async Task CleanupValidatorAndStoresAsync(bool dispose = false) { - TestUtils.CleanupTrustList(m_issuerStore, dispose); - TestUtils.CleanupTrustList(m_trustedStore, dispose); - TestUtils.CleanupTrustList(m_rejectedStore, dispose); + await TestUtils.CleanupTrustListAsync(m_issuerStore, dispose).ConfigureAwait(false); + await TestUtils.CleanupTrustListAsync(m_trustedStore, dispose).ConfigureAwait(false); + await TestUtils.CleanupTrustListAsync(m_rejectedStore, dispose).ConfigureAwait(false); } + #region Private Fields private int m_disposed; private CertificateValidator m_certificateValidator; diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TestUtils.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TestUtils.cs index b9f971919..6cd9da7e7 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TestUtils.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TestUtils.cs @@ -27,6 +27,8 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +using System.Threading.Tasks; + namespace Opc.Ua.Core.Tests { /// @@ -39,19 +41,22 @@ public static class TestUtils /// /// /// - public static void CleanupTrustList(ICertificateStore store, bool dispose = true) + public static async Task CleanupTrustListAsync(ICertificateStore store, bool dispose = true) { if (store != null) { - var certs = store.Enumerate().GetAwaiter().GetResult(); + var certs = await store.Enumerate().ConfigureAwait(false); foreach (var cert in certs) { - store.Delete(cert.Thumbprint); + await store.Delete(cert.Thumbprint).ConfigureAwait(false); } - var crls = store.EnumerateCRLs().GetAwaiter().GetResult(); - foreach (var crl in crls) + if (store.SupportsCRLs) { - store.DeleteCRL(crl); + var crls = await store.EnumerateCRLs().ConfigureAwait(false); + foreach (var crl in crls) + { + await store.DeleteCRL(crl).ConfigureAwait(false); + } } if (dispose) { diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index cbbe57d99..bff7f8ddd 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -31,6 +31,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -45,6 +46,7 @@ namespace Opc.Ua.Gds.Tests /// Test GDS Registration and Client Pull. /// [TestFixture, Category("GDSRegistrationAndPull"), Category("GDS")] + [TestFixtureSource(nameof(FixtureArgs))] [SetCulture("en-us"), SetUICulture("en-us")] [NonParallelizable] public class ClientTest @@ -67,6 +69,23 @@ public string ToString(string format, IFormatProvider formatProvider) } } + /// + /// store types to run the tests with + /// + public static readonly object[] FixtureArgs = { + new object [] { CertificateStoreType.Directory}, + new object [] { CertificateStoreType.X509Store} + }; + + public ClientTest(string storeType = CertificateStoreType.Directory) + { + if (storeType == CertificateStoreType.X509Store && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("X509 Store with crls is only supported on Windows, skipping test run"); + } + m_storeType = storeType; + } + /// /// Set up a Global Discovery Server and Client instance and connect the session /// @@ -74,10 +93,10 @@ public string ToString(string format, IFormatProvider formatProvider) protected async Task OneTimeSetUp() { // start GDS - m_server = await TestUtils.StartGDS(true).ConfigureAwait(false); + m_server = await TestUtils.StartGDS(true, m_storeType).ConfigureAwait(false); // load client - m_gdsClient = new GlobalDiscoveryTestClient(true); + m_gdsClient = new GlobalDiscoveryTestClient(true, m_storeType); await m_gdsClient.LoadClientConfiguration(m_server.BasePort).ConfigureAwait(false); // good applications test set @@ -1512,6 +1531,7 @@ private int GoodServersOnNetworkCount() private bool m_invalidRegistrationOk; private bool m_gdsRegisteredTestClient; private bool m_goodNewKeyPairRequestOk; + private string m_storeType; #endregion } diff --git a/Tests/Opc.Ua.Gds.Tests/Common.cs b/Tests/Opc.Ua.Gds.Tests/Common.cs index 6b3d45770..f3e810668 100644 --- a/Tests/Opc.Ua.Gds.Tests/Common.cs +++ b/Tests/Opc.Ua.Gds.Tests/Common.cs @@ -299,10 +299,13 @@ public static async Task CleanupTrustList(ICertificateStore store, bool dispose { await store.Delete(cert.Thumbprint).ConfigureAwait(false); } - var crls = await store.EnumerateCRLs().ConfigureAwait(false); - foreach (var crl in crls) + if (store.SupportsCRLs) { - await store.DeleteCRL(crl).ConfigureAwait(false); + var crls = await store.EnumerateCRLs().ConfigureAwait(false); + foreach (var crl in crls) + { + await store.DeleteCRL(crl).ConfigureAwait(false); + } } if (dispose) { @@ -349,7 +352,7 @@ public static string PatchOnlyGDSEndpointUrlPort(string url, int port) return url; } - public static async Task StartGDS(bool clean) + public static async Task StartGDS(bool clean, string storeType = CertificateStoreType.Directory) { GlobalDiscoveryTestServer server = null; int testPort = ServerFixtureUtils.GetNextFreeIPPort(); @@ -360,7 +363,7 @@ public static async Task StartGDS(bool clean) try { server = new GlobalDiscoveryTestServer(true); - await server.StartServer(clean, testPort).ConfigureAwait(false); + await server.StartServer(clean, testPort, storeType).ConfigureAwait(false); } catch (ServiceResultException sre) { diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs index 3a8fe5a36..28754d6a6 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs @@ -29,6 +29,7 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -43,9 +44,10 @@ public class GlobalDiscoveryTestClient public GlobalDiscoveryServerClient GDSClient => m_client; public static bool AutoAccept = false; - public GlobalDiscoveryTestClient(bool autoAccept) + public GlobalDiscoveryTestClient(bool autoAccept, string storeType = CertificateStoreType.Directory) { AutoAccept = autoAccept; + m_storeType = storeType; } public IUserIdentity AppUser { get; private set; } @@ -57,12 +59,24 @@ public GlobalDiscoveryTestClient(bool autoAccept) public async Task LoadClientConfiguration(int port = -1) { ApplicationInstance.MessageDlg = new ApplicationMessageDlg(); + + string configSectionName = "Opc.Ua.GlobalDiscoveryTestClient"; + if (m_storeType == CertificateStoreType.X509Store) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException("X509 Store with crls is only supported on Windows"); + } + configSectionName = "Opc.Ua.GlobalDiscoveryTestClientX509Stores"; + } + m_application = new ApplicationInstance { ApplicationName = "Global Discovery Client", ApplicationType = ApplicationType.Client, - ConfigSectionName = "Opc.Ua.GlobalDiscoveryTestClient" + ConfigSectionName = configSectionName }; + #if USE_FILE_CONFIG // load the application configuration. Configuration = await m_application.LoadApplicationConfiguration(false).ConfigureAwait(false); @@ -276,6 +290,7 @@ private ApplicationTestData GetOwnApplicationData() private GlobalDiscoveryServerClient m_client; private ApplicationInstance m_application; + private string m_storeType; } diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs index af862722c..1c0d0058a 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs @@ -29,6 +29,7 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Opc.Ua.Configuration; using Opc.Ua.Gds.Server; @@ -49,13 +50,23 @@ public GlobalDiscoveryTestServer(bool autoAccept) s_autoAccept = autoAccept; } - public async Task StartServer(bool clean, int basePort = -1) + public async Task StartServer(bool clean, int basePort = -1, string storeType = CertificateStoreType.Directory) { ApplicationInstance.MessageDlg = new ApplicationMessageDlg(); + + string configSectionName = "Opc.Ua.GlobalDiscoveryTestServer"; + if (storeType == CertificateStoreType.X509Store) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException("X509 Store with crls is only supported on Windows"); + } + configSectionName = "Opc.Ua.GlobalDiscoveryTestServerX509Stores"; + } Application = new ApplicationInstance { ApplicationName = "Global Discovery Server", ApplicationType = ApplicationType.Server, - ConfigSectionName = "Opc.Ua.GlobalDiscoveryTestServer" + ConfigSectionName = configSectionName }; BasePort = basePort; diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index f3b905127..2cb9e312f 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -1,4 +1,4 @@ - + Exe @@ -26,7 +26,7 @@ - + @@ -41,6 +41,12 @@ + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestClientX509Stores.Config.xml b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestClientX509Stores.Config.xml new file mode 100644 index 000000000..a7b50721c --- /dev/null +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestClientX509Stores.Config.xml @@ -0,0 +1,112 @@ + + + UA Global Discovery Test Client + urn:localhost:opcfoundation.org:GlobalDiscoveryTestClient + http://opcfoundation.org/UA/GlobalDiscoveryTestClient + Client_1 + + + + X509Store + CurrentUser\UA_Test_GDS_Client_own + CN=Global Discovery Test Client, O=OPC Foundation, DC=localhost + + + + X509Store + CurrentUser\UA_Test_GDS_Client_issuers + + + + X509Store + CurrentUser\UA_Test_GDS_Client_trusted + + + + X509Store + CurrentUser\UA_Test_GDS_Client_rejected + + + + true + + + false + 1024 + + false + false + + + + + + + 600000 + 1048576 + 1048576 + 65535 + 4194304 + 65535 + 300000 + 3600000 + + + + + 600000 + + + + opc.tcp://{0}:4840/UADiscovery + http://{0}:52601/UADiscovery + http://{0}/UADiscovery/Default.svc + + + + + + + + 10000 + + + + + + + opc.tcp://localhost:58810/GlobalDiscoveryTestServer + appuser + demo + appadmin + demo + + + + + + %LocalApplicationData%/OPC/Logs/Opc.Ua.Gds.Tests.log.txt + false + + + + + + + + + + + + 519 + + + \ No newline at end of file diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServerX509Stores.Config.xml b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServerX509Stores.Config.xml new file mode 100644 index 000000000..ea28075a0 --- /dev/null +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServerX509Stores.Config.xml @@ -0,0 +1,174 @@ + + + UA Global Discovery Test Server + urn:localhost:opcfoundation.org:GlobalDiscoveryTestServer + http://opcfoundation.org/UA/GlobalDiscoveryTestServer + Server_0 + + + + X509Store + CurrentUser\UA_Test_GDS_Server_own + CN=Global Discovery Test Server, O=OPC Foundation, DC=localhost + + + + X509Store + CurrentUser\UA_Test_GDS_Server_issuers + + + + X509Store + CurrentUser\UA_Test_GDS_Server_trusted + + + + X509Store + CurrentUser\UA_Test_GDS_Server_rejected + + + + true + + + false + 1024 + false + true + + + + + + + 600000 + 1048576 + 1048576 + 65535 + 4194304 + 65535 + 300000 + 3600000 + + + + + opc.tcp://localhost:58810/GlobalDiscoveryTestServer + + + + SignAndEncrypt_3 + + + + + + Anonymous_0 + http://opcfoundation.org/UA/SecurityPolicy#None + + + UserName_1 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + + true + 100 + 10000 + 3600000 + 10 + 10 + 100 + 600000 + 100 + 3600000 + 100 + 3600000 + 100 + 100 + 1000 + 1000 + + + + opc.tcp://localhost:4840 + + opc.tcp://localhost:4840 + DiscoveryServer_3 + + opc.tcp://localhost:4840 + + + SignAndEncrypt_3 + + + + + 0 + + + + http://opcfoundation.org/UA-Profile/Server/GlobalDiscoveryAndCertificateManagement2017 + + + 0 + + GDS + + + PFX + PEM + + 0 + false + + + + + + %LocalApplicationData%/OPC/GDS/authorities + %LocalApplicationData%/OPC/GDS/applications + O=OPC Foundation + + + Default + RsaSha256ApplicationCertificateType + CN=GDS Test CA, O=OPC Foundation + %LocalApplicationData%/OPC/GDS/CA/default + 12 + 2048 + 256 + 60 + 4096 + 512 + + + + %LocalApplicationData%/OPC/GDS/gdsdb.json + %LocalApplicationData%/OPC/GDS/gdsusersdb.json + + + + + + %LocalApplicationData%/OPC/Logs/Opc.Ua.Gds.Tests.log.txt + true + + + + + + + + + + + + 519 + + + \ No newline at end of file From 240312486b346463cc5c158d9e912282a8b05a55 Mon Sep 17 00:00:00 2001 From: romanett Date: Fri, 14 Jun 2024 16:39:34 +0200 Subject: [PATCH 12/16] Add null check in SelectEndpoints to avoid warning messages (#2646) --- Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index bcae7b622..086b54c22 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -378,7 +378,9 @@ public static ServerSecurityPolicyCollection FromListOfSecurityProfiles(ListOfSe /// public static byte CalculateSecurityLevel(MessageSecurityMode mode, string policyUri) { - if ((mode != MessageSecurityMode.Sign) && (mode != MessageSecurityMode.SignAndEncrypt)) + if ((mode != MessageSecurityMode.Sign && + mode != MessageSecurityMode.SignAndEncrypt) || + policyUri == null) { return 0; } From 5cd59b4289ca51203fbff1f028458d5d93c3c954 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:18:43 +0200 Subject: [PATCH 13/16] Bump Microsoft.IO.RecyclableMemoryStream from 3.0.0 to 3.0.1 (#2649) Bumps [Microsoft.IO.RecyclableMemoryStream](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/releases) - [Changelog](https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream/blob/master/CHANGES.md) - [Commits](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/compare/3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: Microsoft.IO.RecyclableMemoryStream dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 0c000195e..9fd3b5811 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -9,7 +9,7 @@ - + From fc07b04798523aefeeebd71dff5106f3a7301ec1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:19:51 +0200 Subject: [PATCH 14/16] Bump Serilog.Sinks.Debug and System.Diagnostics.DiagnosticSource (#2650) Bumps [Serilog.Sinks.Debug](https://github.com/serilog/serilog-sinks-debug) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog.Sinks.Debug` from 2.0.0 to 3.0.0 - [Release notes](https://github.com/serilog/serilog-sinks-debug/releases) - [Commits](https://github.com/serilog/serilog-sinks-debug/compare/v2.0.0...v3.0.0) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog.Sinks.Debug dependency-type: direct:production update-type: version-update:semver-major - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Applications/ReferenceServer/Reference Server.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index bfe763ec2..c9ffe2392 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -29,7 +29,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 3ff6ef968..257fd2c52 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -38,7 +38,7 @@ - + diff --git a/Applications/ReferenceServer/Reference Server.csproj b/Applications/ReferenceServer/Reference Server.csproj index 7a9acf87c..f8c0263f0 100644 --- a/Applications/ReferenceServer/Reference Server.csproj +++ b/Applications/ReferenceServer/Reference Server.csproj @@ -162,7 +162,7 @@ 4.0.0 - 2.0.0 + 3.0.0 5.0.0 diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index d8ee8e3f0..631393e7d 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -27,7 +27,7 @@ - + From 516bad32eb94960fabb9f535104b56465126bb4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:20:19 +0200 Subject: [PATCH 15/16] Bump docker/build-push-action from 5 to 6 (#2647) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 12fbe6ead..66c937b5b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -104,7 +104,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . build-args: | From f2815bb592b06769676440042ee088adb4ec87d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:21:05 +0200 Subject: [PATCH 16/16] Bump Serilog.Expressions and System.Diagnostics.DiagnosticSource (#2648) Bumps [Serilog.Expressions](https://github.com/serilog/serilog-expressions) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog.Expressions` from 4.0.0 to 5.0.0 - [Release notes](https://github.com/serilog/serilog-expressions/releases) - [Commits](https://github.com/serilog/serilog-expressions/compare/v4.0.0...v5.0.0) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog.Expressions dependency-type: direct:production update-type: version-update:semver-major - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index c9ffe2392..037838457 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -25,7 +25,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 257fd2c52..718d44d21 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -34,7 +34,7 @@ - + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 631393e7d..6a9689c10 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -23,7 +23,7 @@ - +