diff --git a/.azurepipelines/ci.yml b/.azurepipelines/ci.yml index d6dccac880..7229e8aac2 100644 --- a/.azurepipelines/ci.yml +++ b/.azurepipelines/ci.yml @@ -33,6 +33,11 @@ jobs: - task: NuGetToolInstaller@1 inputs: versionSpec: '>=5.8.x' + - task: UseDotNet@2 + displayName: 'Install .NET 9.0' + inputs: + packageType: 'sdk' + version: '9.0.x' - task: PowerShell@2 displayName: Versioning inputs: diff --git a/.azurepipelines/preview.yml b/.azurepipelines/preview.yml index ac06f4d705..cb31bbb9b0 100644 --- a/.azurepipelines/preview.yml +++ b/.azurepipelines/preview.yml @@ -34,10 +34,10 @@ jobs: value: '.azurepipelines/signlist${{parameters.config}}.txt' steps: - task: UseDotNet@2 - displayName: 'Install .NET 8.0' + displayName: 'Install .NET 9.0' inputs: packageType: 'sdk' - version: '8.0.x' + version: '9.0.x' includePreviewVersions: false - task: DownloadSecureFile@1 name: strongnamefile diff --git a/.azurepipelines/signlistDebug.txt b/.azurepipelines/signlistDebug.txt index 077e6c4704..9e75cc3a4c 100644 --- a/.azurepipelines/signlistDebug.txt +++ b/.azurepipelines/signlistDebug.txt @@ -4,56 +4,66 @@ Stack\Opc.Ua.Core\bin\Debug\net472\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net6.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net8.0\Opc.Ua.Core.dll +Stack\Opc.Ua.Core\bin\Debug\net9.0\Opc.Ua.Core.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\netstandard2.0\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\netstandard2.1\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net472\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net48\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net6.0\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net8.0\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Debug\net9.0\Opc.Ua.Bindings.Https.dll Libraries\Opc.Ua.Server\bin\Debug\netstandard2.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Debug\netstandard2.1\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Debug\net472\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Debug\net48\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Debug\net6.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Debug\net8.0\Opc.Ua.Server.dll +Libraries\Opc.Ua.Server\bin\Debug\net9.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Client\bin\Debug\netstandard2.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\netstandard2.1\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net472\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net48\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net6.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net8.0\Opc.Ua.Client.dll +Libraries\Opc.Ua.Client\bin\Debug\net9.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\netstandard2.1\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net462\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net472\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net48\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net6.0\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net8.0\Opc.Ua.Client.ComplexTypes.dll +Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net9.0\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Configuration\bin\Debug\netstandard2.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Debug\netstandard2.1\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Debug\net472\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Debug\net48\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Debug\net6.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Debug\net8.0\Opc.Ua.Configuration.dll +Libraries\Opc.Ua.Configuration\bin\Debug\net9.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\netstandard2.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\netstandard2.1\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\net472\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\net48\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\net6.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\net8.0\Opc.Ua.Gds.Client.Common.dll +Libraries\Opc.Ua.Gds.Client.Common\bin\Debug\net9.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\netstandard2.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\netstandard2.1\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net472\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net48\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net6.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net8.0\Opc.Ua.Gds.Server.Common.dll +Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net9.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.1\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net472\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net48\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net6.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net8.0\Opc.Ua.Security.Certificates.dll +Libraries\Opc.Ua.Security.Certificates\bin\Debug\net9.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.PubSub\bin\Debug\netstandard2.1\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Debug\net472\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Debug\net48\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Debug\net6.0\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Debug\net8.0\Opc.Ua.PubSub.dll +Libraries\Opc.Ua.PubSub\bin\Debug\net9.0\Opc.Ua.PubSub.dll diff --git a/.azurepipelines/signlistRelease.txt b/.azurepipelines/signlistRelease.txt index c42668f930..2766d26986 100644 --- a/.azurepipelines/signlistRelease.txt +++ b/.azurepipelines/signlistRelease.txt @@ -4,56 +4,66 @@ Stack\Opc.Ua.Core\bin\Release\net472\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net6.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net8.0\Opc.Ua.Core.dll +Stack\Opc.Ua.Core\bin\Release\net9.0\Opc.Ua.Core.dll Stack\Opc.Ua.Bindings.Https\bin\Release\netstandard2.0\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\netstandard2.1\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net472\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net48\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net6.0\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net8.0\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Release\net9.0\Opc.Ua.Bindings.Https.dll Libraries\Opc.Ua.Server\bin\Release\netstandard2.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Release\netstandard2.1\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Release\net472\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Release\net48\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Release\net6.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Server\bin\Release\net8.0\Opc.Ua.Server.dll +Libraries\Opc.Ua.Server\bin\Release\net9.0\Opc.Ua.Server.dll Libraries\Opc.Ua.Client\bin\Release\netstandard2.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\netstandard2.1\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net472\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net48\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net6.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net8.0\Opc.Ua.Client.dll +Libraries\Opc.Ua.Client\bin\Release\net9.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\netstandard2.1\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net462\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net472\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net48\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net6.0\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net8.0\Opc.Ua.Client.ComplexTypes.dll +Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net9.0\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Configuration\bin\Release\netstandard2.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Release\netstandard2.1\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Release\net472\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Release\net48\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Release\net6.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Configuration\bin\Release\net8.0\Opc.Ua.Configuration.dll +Libraries\Opc.Ua.Configuration\bin\Release\net9.0\Opc.Ua.Configuration.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\netstandard2.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\netstandard2.1\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\net472\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\net48\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\net6.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Client.Common\bin\Release\net8.0\Opc.Ua.Gds.Client.Common.dll +Libraries\Opc.Ua.Gds.Client.Common\bin\Release\net9.0\Opc.Ua.Gds.Client.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\netstandard2.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\netstandard2.1\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net472\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net48\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net6.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net8.0\Opc.Ua.Gds.Server.Common.dll +Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net9.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.1\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net472\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net48\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net6.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net8.0\Opc.Ua.Security.Certificates.dll +Libraries\Opc.Ua.Security.Certificates\bin\Release\net9.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.PubSub\bin\Release\netstandard2.1\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Release\net472\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Release\net48\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Release\net6.0\Opc.Ua.PubSub.dll Libraries\Opc.Ua.PubSub\bin\Release\net8.0\Opc.Ua.PubSub.dll +Libraries\Opc.Ua.PubSub\bin\Release\net9.0\Opc.Ua.PubSub.dll diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 23f90ab7ef..eae5a222ea 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -47,10 +47,10 @@ jobs: packageType: 'sdk' version: '6.0.x' - task: UseDotNet@2 - displayName: 'Install .NET 8.0' + displayName: 'Install .NET 9.0' inputs: packageType: 'sdk' - version: '8.0.x' + version: '9.0.x' - task: NuGetToolInstaller@1 inputs: versionSpec: '>=5.8.x' diff --git a/.azurepipelines/testcc.yml b/.azurepipelines/testcc.yml index 4f2fdb8ada..67d7861e6f 100644 --- a/.azurepipelines/testcc.yml +++ b/.azurepipelines/testcc.yml @@ -30,10 +30,10 @@ jobs: packageType: 'sdk' version: '6.0.x' - task: UseDotNet@2 - displayName: 'Install .NET 8.0' + displayName: 'Install .NET 9.0' inputs: packageType: 'sdk' - version: '8.0.x' + version: '9.0.x' - task: NuGetToolInstaller@1 inputs: versionSpec: '>=5.8.x' diff --git a/.editorconfig b/.editorconfig index 1db323f25b..5b57ac1d05 100644 --- a/.editorconfig +++ b/.editorconfig @@ -342,6 +342,9 @@ dotnet_diagnostic.CA1819.severity = # CA1721: The property name is confusing given the existence of another method with the same name. dotnet_diagnostic.CA1721.severity = silent +# CA2014: Do not use stackalloc in loops +dotnet_diagnostic.CA2014.severity = error + # exclude generated code [**/Generated/*.cs] generated_code = true diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index ecbd7dd6aa..c9e382c330 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -1,4 +1,4 @@ -name: Build and Test .NET 8.0 +name: Build and Test .NET 9.0 on: push: @@ -22,10 +22,10 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] csproj: [Security.Certificates, Core, Server, Client, Client.ComplexTypes, PubSub, Configuration, Gds] include: - - framework: 'net8.0' - dotnet-version: '8.0.x' + - framework: 'net9.0' + dotnet-version: '9.0.x' configuration: 'Release' - customtesttarget: net8.0 + customtesttarget: net9.0 env: OS: ${{ matrix.os }} diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index 33a5c0630e..abf04a59ff 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -20,9 +20,9 @@ - - - + + + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 5c02e4fe19..4a51d6cc53 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -29,9 +29,9 @@ - - - + + + diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 0ced41fbb3..2364379e06 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -220,6 +220,7 @@ + true 75 @@ -331,6 +332,7 @@ true + true diff --git a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs index 151fce4036..842c75914d 100644 --- a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs +++ b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs @@ -53,6 +53,7 @@ public partial class ReferenceServer : ReverseConnectServer public ITokenValidator TokenValidator { get; set; } #endregion + #region Overridden Methods /// /// Creates the node managers for the server. @@ -247,7 +248,7 @@ private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArg { VerifyUserTokenCertificate(x509Token.Certificate); // set AuthenticatedUser role for accepted certificate authentication - args.Identity = new RoleBasedIdentity(new UserIdentity(x509Token), + args.Identity = new RoleBasedIdentity(new UserIdentity(x509Token), new List() { Role.AuthenticatedUser }); Utils.LogInfo(Utils.TraceMasks.Security, "X509 Token Accepted: {0}", args.Identity?.DisplayName); @@ -325,7 +326,7 @@ private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken) new LocalizedText(info))); } return new RoleBasedIdentity(new UserIdentity(userNameToken), - new List() { Role.AuthenticatedUser}); + new List() { Role.AuthenticatedUser }); } /// diff --git a/Applications/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs b/Applications/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs index c036616394..ab52ac6ffe 100644 --- a/Applications/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs +++ b/Applications/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs @@ -340,6 +340,18 @@ public Session Session } } + /// + /// The monitored items owner identity. + /// + public IUserIdentity EffectiveIdentity + { + get + { + ISubscription subscription = m_subscription; + return subscription?.EffectiveIdentity; + } + } + /// /// The identifier for the subscription that the monitored item belongs to. /// diff --git a/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs index 6b76fe466e..f92d9ff0e6 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs +++ b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs @@ -65,6 +65,12 @@ public void FuzzGoodTestcases( FuzzTarget(fuzzableCode, messageEncoder.Testcase); } + [Theory] + public void FuzzEmptyByteArray(FuzzTargetFunction fuzzableCode) + { + FuzzTarget(fuzzableCode, Array.Empty()); + } + [Theory] public void FuzzCrashAssets(FuzzTargetFunction fuzzableCode) { diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj index c442d0e383..07f10d1f21 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/Libraries/Opc.Ua.Client/CoreClientUtils.cs b/Libraries/Opc.Ua.Client/CoreClientUtils.cs index 780a63fcf6..8909f4f8b6 100644 --- a/Libraries/Opc.Ua.Client/CoreClientUtils.cs +++ b/Libraries/Opc.Ua.Client/CoreClientUtils.cs @@ -339,7 +339,7 @@ public static EndpointDescription SelectEndpoint( public static Uri GetDiscoveryUrl(string discoveryUrl) { // needs to add the '/discovery' back onto non-UA TCP URLs. - if (discoveryUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal)) + if (Utils.IsUriHttpRelatedScheme(discoveryUrl)) { if (!discoveryUrl.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)) { diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 32ee0d612c..6dda231707 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -138,6 +138,7 @@ public Session(ITransportChannel channel, Session template, bool copyEventHandle m_keepAliveInterval = template.KeepAliveInterval; m_checkDomain = template.m_checkDomain; m_continuationPointPolicy = template.m_continuationPointPolicy; + ReturnDiagnostics = template.ReturnDiagnostics; if (template.OperationTimeout > 0) { OperationTimeout = template.OperationTimeout; @@ -1027,7 +1028,7 @@ public static async Task CreateChannelAsync( endpoint.Description.ServerCertificate.Length > 0) { configuration.CertificateValidator?.ValidateDomains( - new X509Certificate2(endpoint.Description.ServerCertificate), + X509CertificateLoader.LoadCertificate(endpoint.Description.ServerCertificate), endpoint); checkDomain = false; } @@ -1391,7 +1392,7 @@ public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) byte[] serverCertificate = m_endpoint.Description?.ServerCertificate; m_sessionName = sessionConfiguration.SessionName; - m_serverCertificate = serverCertificate != null ? new X509Certificate2(serverCertificate) : null; + m_serverCertificate = serverCertificate != null ? X509CertificateLoader.LoadCertificate(serverCertificate) : null; m_identity = sessionConfiguration.Identity; m_checkDomain = sessionConfiguration.CheckDomain; m_serverNonce = sessionConfiguration.ServerNonce.Data; @@ -1646,19 +1647,19 @@ public void FetchOperationLimits() } property.SetValue(operationLimits, value); } - OperationLimits = operationLimits; - if (values[maxBrowseContinuationPointIndex] != null - && ServiceResult.IsNotBad(errors[maxBrowseContinuationPointIndex])) + + if (values[maxBrowseContinuationPointIndex] is UInt16 serverMaxContinuationPointsPerBrowse && + ServiceResult.IsNotBad(errors[maxBrowseContinuationPointIndex])) { - ServerMaxContinuationPointsPerBrowse = (UInt16)values[maxBrowseContinuationPointIndex]; + ServerMaxContinuationPointsPerBrowse = serverMaxContinuationPointsPerBrowse; } - if (values[maxByteStringLengthIndex] != null - && ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) + + if (values[maxByteStringLengthIndex] is UInt32 serverMaxByteStringLength && + ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) { - ServerMaxByteStringLength = (UInt32)values[maxByteStringLengthIndex]; + ServerMaxByteStringLength = serverMaxByteStringLength; } - } catch (Exception ex) { @@ -4868,10 +4869,10 @@ private Node ProcessReadResponse( /// /// Create a dictionary of attributes to read for a nodeclass. /// - private SortedDictionary CreateAttributes(NodeClass nodeclass = NodeClass.Unspecified, bool optionalAttributes = true) + private Dictionary CreateAttributes(NodeClass nodeclass = NodeClass.Unspecified, bool optionalAttributes = true) { // Attributes to read for all types of nodes - var attributes = new SortedDictionary() { + var attributes = new Dictionary() { { Attributes.NodeId, null }, { Attributes.NodeClass, null }, { Attributes.BrowseName, null }, @@ -4929,7 +4930,7 @@ private SortedDictionary CreateAttributes(NodeClass nodeclass = default: // build complete list of attributes. - attributes = new SortedDictionary { + attributes = new Dictionary { { Attributes.NodeId, null }, { Attributes.NodeClass, null }, { Attributes.BrowseName, null }, diff --git a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs index 427b9eb9fb..43d9cca5a4 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs @@ -655,8 +655,7 @@ public async Task FetchOperationLimitsAsync(CancellationToken ct) .GetValue(null)) ); - // add the server capability MaxContinuationPointPerBrowse. Add further capabilities - // later (when support form them will be implemented and in a more generic fashion) + // add the server capability MaxContinuationPointPerBrowse and MaxByteStringLength nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints); int maxBrowseContinuationPointIndex = nodeIds.Count - 1; @@ -686,17 +685,18 @@ public async Task FetchOperationLimitsAsync(CancellationToken ct) } property.SetValue(operationLimits, value); } - OperationLimits = operationLimits; - if (values[maxBrowseContinuationPointIndex].Value != null - && ServiceResult.IsNotBad(errors[maxBrowseContinuationPointIndex])) + + if (values[maxBrowseContinuationPointIndex].Value is UInt16 serverMaxContinuationPointsPerBrowse && + ServiceResult.IsNotBad(errors[maxBrowseContinuationPointIndex])) { - ServerMaxContinuationPointsPerBrowse = (UInt16)values[maxBrowseContinuationPointIndex].Value; + ServerMaxContinuationPointsPerBrowse = serverMaxContinuationPointsPerBrowse; } - if (values[maxByteStringLengthIndex] != null - && ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) + + if (values[maxByteStringLengthIndex].Value is UInt32 serverMaxByteStringLength && + ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) { - ServerMaxByteStringLength = (UInt32)values[maxByteStringLengthIndex].Value; + ServerMaxByteStringLength = serverMaxByteStringLength; } } catch (Exception ex) diff --git a/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs b/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs index d68f21b12d..288565959a 100644 --- a/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs +++ b/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs @@ -251,7 +251,6 @@ public NodeClass NodeClass QueueSize = Int32.MaxValue; } - m_eventCache = new MonitoredItemEventCache(100); m_attributeId = Attributes.EventNotifier; } else @@ -268,8 +267,11 @@ public NodeClass NodeClass QueueSize = 1; } - m_dataCache = new MonitoredItemDataCache(1); + m_attributeId = Attributes.Value; } + + m_dataCache = null; + m_eventCache = null; } m_nodeClass = value; @@ -489,6 +491,8 @@ public int CacheQueueSize { lock (m_cache) { + EnsureCacheIsInitialized(); + if (m_dataCache != null) { m_dataCache.SetQueueSize(value); @@ -613,6 +617,8 @@ public void SaveValueInCache(IEncodeable newValue) { lock (m_cache) { + EnsureCacheIsInitialized(); + // only validate timestamp on first sample bool validateTimestamp = m_lastNotification == null; @@ -620,9 +626,7 @@ public void SaveValueInCache(IEncodeable newValue) if (m_dataCache != null) { - MonitoredItemNotification datachange = newValue as MonitoredItemNotification; - - if (datachange != null) + if (newValue is MonitoredItemNotification datachange) { if (datachange.Value != null) { @@ -658,9 +662,7 @@ public void SaveValueInCache(IEncodeable newValue) if (m_eventCache != null) { - EventFieldList eventchange = newValue as EventFieldList; - - if (eventchange != null) + if (newValue is EventFieldList eventchange) { m_eventCache?.OnNotification(eventchange); } @@ -1034,6 +1036,26 @@ public static ServiceResult GetServiceResult(IEncodeable notification, int index #endregion #region Private Methods + /// + /// To save memory the cache is by default not initialized + /// until is called. + /// + /// + private void EnsureCacheIsInitialized() + { + if (m_dataCache == null && m_eventCache == null) + { + if ((m_nodeClass & (NodeClass.Object | NodeClass.View)) != 0) + { + m_eventCache = new MonitoredItemEventCache(100); + } + else + { + m_dataCache = new MonitoredItemDataCache(1); + } + } + } + /// /// Throws an exception if the flter cannot be used with the node class. /// @@ -1160,18 +1182,28 @@ internal MonitoredItemNotificationEventArgs(IEncodeable notificationValue) #endregion /// - /// An item in the cache + /// A client cache which can hold the last monitored items in a queue. + /// By default (1) only the last value is cached. /// public class MonitoredItemDataCache { + private const int kDefaultMaxCapacity = 100; + #region Constructors /// /// Constructs a cache for a monitored item. /// - public MonitoredItemDataCache(int queueSize) + public MonitoredItemDataCache(int queueSize = 1) { m_queueSize = queueSize; - m_values = new Queue(); + if (queueSize > 1) + { + m_values = new Queue(Math.Min(queueSize + 1, kDefaultMaxCapacity)); + } + else + { + m_queueSize = 1; + } } #endregion @@ -1191,13 +1223,20 @@ public MonitoredItemDataCache(int queueSize) /// public IList Publish() { - DataValue[] values = new DataValue[m_values.Count]; - - for (int ii = 0; ii < values.Length; ii++) + DataValue[] values; + if (m_values != null) { - values[ii] = m_values.Dequeue(); + values = new DataValue[m_values.Count]; + for (int ii = 0; ii < values.Length; ii++) + { + values[ii] = m_values.Dequeue(); + } + } + else + { + values = new DataValue[1]; + values[0] = m_lastValue; } - return values; } @@ -1206,14 +1245,16 @@ public IList Publish() /// public void OnNotification(MonitoredItemNotification notification) { - m_values.Enqueue(notification.Value); m_lastValue = notification.Value; - CoreClientUtils.EventLog.NotificationValue(notification.ClientHandle, m_lastValue.WrappedValue); - while (m_values.Count > m_queueSize) + if (m_values != null) { - m_values.Dequeue(); + m_values.Enqueue(notification.Value); + while (m_values.Count > m_queueSize) + { + m_values.Dequeue(); + } } } @@ -1227,9 +1268,14 @@ public void SetQueueSize(int queueSize) return; } - if (queueSize < 1) + if (queueSize <= 1) { queueSize = 1; + m_values = null; + } + else if (m_values == null) + { + m_values = new Queue(Math.Min(queueSize + 1, kDefaultMaxCapacity)); } m_queueSize = queueSize; @@ -1244,7 +1290,7 @@ public void SetQueueSize(int queueSize) #region Private Fields private int m_queueSize; private DataValue m_lastValue; - private readonly Queue m_values; + private Queue m_values; #endregion } diff --git a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs index d8fabd0421..7b449aa394 100644 --- a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs +++ b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs @@ -161,7 +161,7 @@ private void Initialize() m_sequentialPublishing = false; m_lastSequenceNumberProcessed = 0; m_messageCache = new LinkedList(); - m_monitoredItems = new SortedDictionary(); + m_monitoredItems = new Dictionary(); m_deletedItems = new List(); m_messageWorkerEvent = new AsyncAutoResetEvent(); m_messageWorkerCts = null; @@ -1849,17 +1849,22 @@ private void StartKeepAliveTimer() // stop the publish timer. lock (m_cache) { + int oldKeepAliveInterval = m_keepAliveInterval; + m_keepAliveInterval = CalculateKeepAliveInterval(); + + //don`t create new KeepAliveTimer if interval did not change and timers are still running + if (oldKeepAliveInterval == m_keepAliveInterval + && m_publishTimer != null + && m_messageWorkerTask != null + && !m_messageWorkerTask.IsCompleted) + { + return; + } + Utils.SilentDispose(m_publishTimer); m_publishTimer = null; - Interlocked.Exchange(ref m_lastNotificationTime, DateTime.UtcNow.Ticks); m_lastNotificationTickCount = HiResClock.TickCount; - m_keepAliveInterval = (int)(Math.Min(m_currentPublishingInterval * (m_currentKeepAliveCount + 1), Int32.MaxValue)); - if (m_keepAliveInterval < kMinKeepAliveTimerInterval) - { - m_keepAliveInterval = (int)(Math.Min(m_publishingInterval * (m_keepAliveCount + 1), Int32.MaxValue)); - m_keepAliveInterval = Math.Max(kMinKeepAliveTimerInterval, m_keepAliveInterval); - } #if NET6_0_OR_GREATER var publishTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(m_keepAliveInterval)); _ = Task.Run(() => OnKeepAliveAsync(publishTimer)); @@ -2047,10 +2052,11 @@ uint revisedLifetimeCounter { m_currentPublishingEnabled = m_publishingEnabled; m_transferId = m_id = subscriptionId; - StartKeepAliveTimer(); m_changeMask |= SubscriptionChangeMask.Created; } + StartKeepAliveTimer(); + if (m_keepAliveCount != revisedKeepAliveCount) { Utils.LogInfo("For subscription {0}, Keep alive count was revised from {1} to {2}", @@ -2080,6 +2086,21 @@ uint revisedLifetimeCounter } } + /// + /// Calculate the KeepAliveInterval based on and + /// + /// + private int CalculateKeepAliveInterval() + { + int keepAliveInterval = (int)(Math.Min(m_currentPublishingInterval * (m_currentKeepAliveCount + 1), Int32.MaxValue)); + if (keepAliveInterval < kMinKeepAliveTimerInterval) + { + keepAliveInterval = (int)(Math.Min(m_publishingInterval * (m_keepAliveCount + 1), Int32.MaxValue)); + keepAliveInterval = Math.Max(kMinKeepAliveTimerInterval, keepAliveInterval); + } + return keepAliveInterval; + } + /// /// Delete the subscription. /// Ignore errors, always reset all parameter. @@ -2606,7 +2627,7 @@ private void TransferItems( lock (m_cache) { itemsToModify = new List(); - var updatedMonitoredItems = new SortedDictionary(); + var updatedMonitoredItems = new Dictionary(); foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) { var index = serverHandles.FindIndex(handle => handle == monitoredItem.Status.Id); @@ -2830,7 +2851,7 @@ private IncomingMessage FindOrCreateEntry(DateTime utcNow, int tickCount, uint s private IList m_availableSequenceNumbers; private int m_maxMessageCount; private bool m_republishAfterTransfer; - private SortedDictionary m_monitoredItems; + private Dictionary m_monitoredItems; private bool m_disableMonitoredItemCache; private FastDataChangeNotificationEventHandler m_fastDataChangeCallback; private FastEventNotificationEventHandler m_fastEventCallback; diff --git a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs index 0e9f99f960..b68723c937 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs @@ -855,6 +855,13 @@ public IApplicationConfigurationBuilderServerOptions SetAuditingEnabled(bool aud return this; } + /// + public IApplicationConfigurationBuilderServerOptions SetHttpsMutualTls(bool mutualTlsEnabeld) + { + ApplicationConfiguration.ServerConfiguration.HttpsMutualTls = mutualTlsEnabeld; + return this; + } + /// public IApplicationConfigurationBuilderClientOptions SetDefaultSessionTimeout(int defaultSessionTimeout) { diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index e9cca94007..5333741ee0 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -697,7 +697,9 @@ private async Task CheckApplicationInstanceCertificateAsync( { // validate certificate. configuration.CertificateValidator.CertificateValidation += certValidator.OnCertificateValidation; - await configuration.CertificateValidator.ValidateAsync(certificate.HasPrivateKey ? new X509Certificate2(certificate.RawData) : certificate, ct).ConfigureAwait(false); + await configuration.CertificateValidator.ValidateAsync( + certificate.HasPrivateKey ? + X509CertificateLoader.LoadCertificate(certificate.RawData) : certificate, ct).ConfigureAwait(false); } catch (Exception ex) { @@ -1125,7 +1127,8 @@ private static async Task AddToTrustedStoreAsync(ApplicationConfiguration config } // add new certificate. - X509Certificate2 publicKey = new X509Certificate2(certificate.RawData); + X509Certificate2 publicKey = X509CertificateLoader.LoadCertificate(certificate.RawData); + await store.Add(publicKey).ConfigureAwait(false); Utils.LogInfo("Added application certificate to trusted peer store."); diff --git a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs index 3ecad157ec..4b13b08fef 100644 --- a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs @@ -266,6 +266,9 @@ public interface IApplicationConfigurationBuilderServerOptions : /// IApplicationConfigurationBuilderServerOptions SetAuditingEnabled(bool auditingEnabled); + + /// + IApplicationConfigurationBuilderServerOptions SetHttpsMutualTls(bool mTlsEnabled); } /// diff --git a/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs b/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs index 40940834f3..11e3d973b4 100644 --- a/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs +++ b/Libraries/Opc.Ua.Gds.Client.Common/ServerPushConfigurationClient.cs @@ -721,7 +721,7 @@ public X509Certificate2Collection GetRejectedList() X509Certificate2Collection collection = new X509Certificate2Collection(); foreach (var rawCertificate in rawCertificates) { - collection.Add(new X509Certificate2(rawCertificate)); + collection.Add(X509CertificateLoader.LoadCertificate(rawCertificate)); } return collection; } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs index bd3ca1332b..fb4238de97 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs @@ -234,7 +234,7 @@ private ICertificateGroup GetGroupForCertificate(byte[] certificate) { if (certificate != null && certificate.Length > 0) { - using (var x509 = new X509Certificate2(certificate)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate)) { foreach (var certificateGroup in m_certificateGroups.Values) { @@ -258,7 +258,7 @@ private async Task RevokeCertificateAsync(byte[] certificate) if (certificateGroup != null) { - using (X509Certificate2 x509 = new X509Certificate2(certificate)) + using (X509Certificate2 x509 = X509CertificateLoader.LoadCertificate(certificate)) { try { @@ -689,7 +689,7 @@ private ServiceResult OnCheckRevocationStatus( } } - using (var x509 = new X509Certificate2(certificate)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate)) { if (chain.Build(x509)) { @@ -1305,7 +1305,7 @@ out privateKeyPassword } else { - certificate = new X509Certificate2(signedCertificate); + certificate = X509CertificateLoader.LoadCertificate(signedCertificate); } // TODO: return chain, verify issuer chain cert is up to date, otherwise update local chain diff --git a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs index 1f1b6553c8..8ce0249f2d 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs @@ -119,7 +119,7 @@ public virtual async Task Init() Configuration.CACertificateLifetime ); X509Certificate2 newCertificate = await CreateCACertificateAsync(SubjectName).ConfigureAwait(false); - Certificate = new X509Certificate2(newCertificate.RawData); + Certificate = X509CertificateLoader.LoadCertificate(newCertificate.RawData); Utils.LogCertificate(Utils.TraceMasks.Security, "Created CA certificate: ", Certificate); } } @@ -173,7 +173,7 @@ public virtual async Task NewKeyPairRequestAsync( { throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Invalid private key format"); } - return new X509Certificate2KeyPair(new X509Certificate2(certificate.RawData), privateKeyFormat, privateKey); + return new X509Certificate2KeyPair(X509CertificateLoader.LoadCertificate(certificate.RawData), privateKeyFormat, privateKey); } } @@ -336,7 +336,7 @@ string subjectName { // save only public key - Certificate = new X509Certificate2(newCertificate.RawData); + Certificate = X509CertificateLoader.LoadCertificate(newCertificate.RawData); // initialize revocation list X509CRL crl = await RevokeCertificateAsync(AuthoritiesStore, newCertificate, null).ConfigureAwait(false); @@ -494,7 +494,7 @@ protected async Task UpdateAuthorityCertInCertificateStore(CertificateStoreIdent X509Certificate2Collection certs = await trustedOrIssuerStore.FindByThumbprint(certificate.Thumbprint).ConfigureAwait(false); if (certs.Count == 0) { - using (var x509 = new X509Certificate2(certificate.RawData)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate.RawData)) { await trustedOrIssuerStore.Add(x509).ConfigureAwait(false); } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ICertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/ICertificateGroup.cs index d1eac19265..787906ad62 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ICertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ICertificateGroup.cs @@ -44,7 +44,7 @@ public X509Certificate2KeyPair(X509Certificate2 certificate, string privateKeyFo { if (certificate.HasPrivateKey) { - certificate = new X509Certificate2(certificate.RawData); + certificate = X509CertificateLoader.LoadCertificate(certificate.RawData); } Certificate = certificate; PrivateKeyFormat = privateKeyFormat; 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 7750c96a6e..d46cc04559 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.PubSub/Transport/MqttClientProtocolConfiguration.cs b/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs index a5ea6a7292..ba75b7b042 100644 --- a/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs +++ b/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs @@ -43,8 +43,8 @@ public class MqttTlsCertificates { #region Private menbers - private X509Certificate m_caCertificate; - private X509Certificate m_clientCertificate; + private X509Certificate2 m_caCertificate; + private X509Certificate2 m_clientCertificate; #endregion Private menbers @@ -64,11 +64,11 @@ public MqttTlsCertificates(string caCertificatePath = null, if (!string.IsNullOrEmpty(CaCertificatePath)) { - m_caCertificate = X509Certificate.CreateFromCertFile(CaCertificatePath); + m_caCertificate = X509CertificateLoader.LoadCertificateFromFile(CaCertificatePath); } if (!string.IsNullOrEmpty(clientCertificatePath)) { - m_clientCertificate = new X509Certificate2(clientCertificatePath, ClientCertificatePassword); + m_clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, ClientCertificatePassword); } KeyValuePairs = new KeyValuePairCollection(); @@ -105,11 +105,11 @@ public MqttTlsCertificates(KeyValuePairCollection keyValuePairs) if (!string.IsNullOrEmpty(CaCertificatePath)) { - m_caCertificate = X509Certificate.CreateFromCertFile(CaCertificatePath); + m_caCertificate = X509CertificateLoader.LoadCertificateFromFile(CaCertificatePath); } if (!string.IsNullOrEmpty(ClientCertificatePath)) { - m_clientCertificate = new X509Certificate2(ClientCertificatePath, ClientCertificatePassword); + m_clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(ClientCertificatePath, ClientCertificatePassword); } } diff --git a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs index 17c9deaf67..15155eb4a9 100644 --- a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs +++ b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs @@ -717,6 +717,17 @@ private MqttClientOptions GetMqttClientOptions() return null; } + // Setup data needed also in mqttClientOptionsBuilder + if ((connectionUri.Scheme == Utils.UriSchemeMqtt) || (connectionUri.Scheme == Utils.UriSchemeMqtts)) + { + if (!String.IsNullOrEmpty(connectionUri.Host)) + { + m_brokerHostName = connectionUri.Host; + m_brokerPort = (connectionUri.Port > 0) ? connectionUri.Port : ((connectionUri.Scheme == Utils.UriSchemeMqtt) ? 1883 : 8883); + m_urlScheme = connectionUri.Scheme; + } + } + ITransportProtocolConfiguration transportProtocolConfiguration = new MqttClientProtocolConfiguration(PubSubConnectionConfiguration.ConnectionProperties); @@ -728,6 +739,7 @@ private MqttClientOptions GetMqttClientOptions() .ProtocolVersion; // create uniques client id string clientId = $"ClientId_{new Random().Next():D10}"; + // MQTTS mqttConnection. if (connectionUri.Scheme == Utils.UriSchemeMqtts) { @@ -737,9 +749,9 @@ private MqttClientOptions GetMqttClientOptions() var x509Certificate2s = new List(); if (mqttTlsOptions?.Certificates != null) { - foreach (X509Certificate x509cert in mqttTlsOptions?.Certificates.X509Certificates) + foreach (X509Certificate2 x509cert in mqttTlsOptions?.Certificates.X509Certificates) { - x509Certificate2s.Add(new X509Certificate2(x509cert.Handle)); + x509Certificate2s.Add(X509CertificateLoader.LoadCertificate(x509cert.RawData)); } } @@ -794,6 +806,8 @@ private MqttClientOptions GetMqttClientOptions() // Set user credentials. if (mqttProtocolConfiguration.UseCredentials) { + // Following Password usage in both cases is correct since it is the Password position + // to be taken into account for the UserName to be read properly mqttClientOptionsBuilder.WithCredentials( new System.Net.NetworkCredential(string.Empty, mqttProtocolConfiguration.UserName) .Password, @@ -838,7 +852,7 @@ private static CertificateValidator CreateCertificateValidator(MqttTlsOptions mq /// The context of the validation private bool ValidateBrokerCertificate(MqttClientCertificateValidationEventArgs context) { - var brokerCertificate = new X509Certificate2(context.Certificate.GetRawCertData()); + var brokerCertificate = X509CertificateLoader.LoadCertificate(context.Certificate.GetRawCertData()); try { diff --git a/Libraries/Opc.Ua.Security.Certificates/Common/AsnUtils.cs b/Libraries/Opc.Ua.Security.Certificates/Common/AsnUtils.cs index f4e3ff0e05..c2a4d02048 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Common/AsnUtils.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Common/AsnUtils.cs @@ -50,24 +50,35 @@ internal static string ToHexString(this byte[] buffer, bool invertEndian = false return String.Empty; } - var builder = new StringBuilder(buffer.Length * 2); - - if (invertEndian) +#if NET6_0_OR_GREATER + if (!invertEndian) { - for (int ii = buffer.Length - 1; ii >= 0; ii--) - { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); - } + return Convert.ToHexString(buffer); } else +#endif { - for (int ii = 0; ii < buffer.Length; ii++) + StringBuilder builder = new StringBuilder(buffer.Length * 2); + +#if !NET6_0_OR_GREATER + if (!invertEndian) + { + for (int ii = 0; ii < buffer.Length; ii++) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + } + } + else +#endif { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + for (int ii = buffer.Length - 1; ii >= 0; ii--) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + } } - } - return builder.ToString(); + return builder.ToString(); + } } /// @@ -85,6 +96,9 @@ internal static byte[] FromHexString(this string buffer) return Array.Empty(); } +#if NET6_0_OR_GREATER + return Convert.FromHexString(buffer); +#else const string digits = "0123456789ABCDEF"; byte[] bytes = new byte[(buffer.Length / 2) + (buffer.Length % 2)]; @@ -120,6 +134,7 @@ internal static byte[] FromHexString(this string buffer) } return bytes; +#endif } /// 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 6e9b9995a3..67aa710321 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj +++ b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj @@ -18,12 +18,12 @@ - + - + @@ -46,11 +46,11 @@ - + - + diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509CertificateLoader.cs b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509CertificateLoader.cs new file mode 100644 index 0000000000..84c163ca3f --- /dev/null +++ b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509CertificateLoader.cs @@ -0,0 +1,91 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +#if !NET9_0_OR_GREATER + +using System; +using System.Security.Cryptography.X509Certificates; + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// A helper to support the .NET 9 certificate loader primitives on older .NET versions. + /// + public static class X509CertificateLoader + { + /// + /// Initializes a new instance of the class from certificate data. + /// + public static X509Certificate2 LoadCertificate(byte[] data) + { + return new X509Certificate2(data); + } + +#if NET6_0_OR_GREATER + /// + /// Initializes a new instance of the class from certificate data. + /// + public static X509Certificate2 LoadCertificate(ReadOnlySpan rawData) + { + return new X509Certificate2(rawData); + } +#endif + + /// + /// Initializes a new instance of the class from certificate file. + /// + public static X509Certificate2 LoadCertificateFromFile(string path) + { + return new X509Certificate2(path); + } + + /// + /// Initializes a new instance of the class from Pfx data. + /// + public static X509Certificate2 LoadPkcs12( + byte[] data, + string password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet) + { + return new X509Certificate2(data, password, keyStorageFlags); + } + + /// + /// Initializes a new instance of the class from Pfx file. + /// + public static X509Certificate2 LoadPkcs12FromFile( + string filename, + string password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet) + { + return new X509Certificate2(filename, password, keyStorageFlags); + } + } +} +#endif diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs index 4edf156f48..353162f7c3 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs @@ -175,7 +175,7 @@ public static X509Certificate2 CreateCertificateFromPKCS12( try { // merge first cert with private key into X509Certificate2 - certificate = new X509Certificate2( + certificate = X509CertificateLoader.LoadPkcs12( rawData, password ?? string.Empty, flag); diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Crl.cs b/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Crl.cs index a60a421a31..9473aa6b48 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Crl.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Crl/X509Crl.cs @@ -50,6 +50,7 @@ public class X509CRL : IX509CRL public X509CRL(string filePath) : this() { RawData = File.ReadAllBytes(filePath); + EnsureDecoded(); } /// @@ -58,6 +59,7 @@ public X509CRL(string filePath) : this() public X509CRL(byte[] crl) : this() { RawData = crl; + EnsureDecoded(); } /// @@ -78,6 +80,7 @@ public X509CRL(IX509CRL crl) m_crlExtensions.Add(extension); } RawData = crl.RawData; + EnsureDecoded(); } /// @@ -99,7 +102,6 @@ public X500DistinguishedName IssuerName { get { - EnsureDecoded(); return m_issuerName; } } @@ -112,7 +114,6 @@ public DateTime ThisUpdate { get { - EnsureDecoded(); return m_thisUpdate; } } @@ -122,7 +123,6 @@ public DateTime NextUpdate { get { - EnsureDecoded(); return m_nextUpdate; } } @@ -132,7 +132,6 @@ public HashAlgorithmName HashAlgorithmName { get { - EnsureDecoded(); return m_hashAlgorithmName; } } @@ -142,7 +141,6 @@ public IList RevokedCertificates { get { - EnsureDecoded(); return m_revokedCertificates.AsReadOnly(); } } @@ -152,7 +150,6 @@ public X509ExtensionCollection CrlExtensions { get { - EnsureDecoded(); return m_crlExtensions; } } diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 860756bb54..f07117057b 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -80,7 +80,7 @@ ApplicationConfiguration configuration ServerCertificateGroup defaultApplicationGroup = new ServerCertificateGroup { NodeId = Opc.Ua.ObjectIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup, BrowseName = Opc.Ua.BrowseNames.DefaultApplicationGroup, - CertificateTypes = new NodeId[]{}, + CertificateTypes = new NodeId[] { }, ApplicationCertificates = new CertificateIdentifierCollection(), IssuerStore = new CertificateStoreIdentifier(configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath), TrustedStore = new CertificateStoreIdentifier(configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath) @@ -388,7 +388,7 @@ private ServiceResult UpdateCertificate( try { - newCert = new X509Certificate2(certificate); + newCert = X509CertificateLoader.LoadCertificate(certificate); } catch { @@ -397,7 +397,7 @@ private ServiceResult UpdateCertificate( // identify the existing certificate to be updated // it should be of the same type and same subject name as the new certificate - CertificateIdentifier existingCertIdentifier = certificateGroup.ApplicationCertificates.FirstOrDefault(cert => + CertificateIdentifier existingCertIdentifier = certificateGroup.ApplicationCertificates.FirstOrDefault(cert => X509Utils.CompareDistinguishedName(cert.Certificate.Subject, newCert.Subject) && cert.CertificateType == certificateTypeId); @@ -417,7 +417,7 @@ private ServiceResult UpdateCertificate( { foreach (byte[] issuerRawCert in issuerCertificates) { - var newIssuerCert = new X509Certificate2(issuerRawCert); + var newIssuerCert = X509CertificateLoader.LoadCertificate(issuerRawCert); newIssuerCollection.Add(newIssuerCert); } } @@ -451,8 +451,6 @@ private ServiceResult UpdateCertificate( { // verify cert with issuer chain CertificateValidator certValidator = new CertificateValidator(); -// TODO: why? -// certValidator.MinimumCertificateKeySize = 1024; CertificateTrustList issuerStore = new CertificateTrustList(); CertificateIdentifierCollection issuerCollection = new CertificateIdentifierCollection(); foreach (var issuerCert in newIssuerCollection) @@ -532,7 +530,7 @@ private ServiceResult UpdateCertificate( var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; appStore.Add(updateCertificate.CertificateWithPrivateKey, passwordProvider?.GetPassword(existingCertIdentifier)).Wait(); // keep only track of cert without private key - var certOnly = new X509Certificate2(updateCertificate.CertificateWithPrivateKey.RawData); + var certOnly = X509CertificateLoader.LoadCertificate(updateCertificate.CertificateWithPrivateKey.RawData); updateCertificate.CertificateWithPrivateKey.Dispose(); updateCertificate.CertificateWithPrivateKey = certOnly; } diff --git a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs index c33e77a217..1930c6b56b 100644 --- a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs +++ b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs @@ -347,7 +347,7 @@ private ServiceResult CloseAndUpdate( issuerCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.IssuerCertificates) { - issuerCertificates.Add(new X509Certificate2(cert)); + issuerCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.IssuerCrls) != 0) @@ -363,7 +363,7 @@ private ServiceResult CloseAndUpdate( trustedCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.TrustedCertificates) { - trustedCertificates.Add(new X509Certificate2(cert)); + trustedCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.TrustedCrls) != 0) @@ -461,7 +461,7 @@ private ServiceResult AddCertificate( X509Certificate2 cert = null; try { - cert = new X509Certificate2(certificate); + cert = X509CertificateLoader.LoadCertificate(certificate); } catch { @@ -471,7 +471,7 @@ private ServiceResult AddCertificate( result = StatusCodes.BadCertificateInvalid; } - var storeIdentifier = isTrustedCertificate? m_trustedStore : m_issuerStore; + var storeIdentifier = isTrustedCertificate ? m_trustedStore : m_issuerStore; ICertificateStore store = storeIdentifier.OpenStore(); try { @@ -539,7 +539,7 @@ private ServiceResult RemoveCertificate( foreach (var cert in certCollection) { if (X509Utils.CompareDistinguishedName(cert.SubjectName, crl.IssuerName) && - crl.VerifySignature(cert, false)) + crl.VerifySignature(cert, false)) { crlsToDelete.Add(crl); break; diff --git a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs index 5581456b36..fa25395da6 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs @@ -3752,7 +3752,7 @@ protected virtual void OnMonitoredItemCreated( /// public ServiceResult ValidateRolePermissions(OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission) { - if (operationContext.Session == null || requestedPermission == PermissionType.None) + if (requestedPermission == PermissionType.None) { // no permission is required hence the validation passes. return StatusCodes.Good; diff --git a/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs b/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs index 640052e78a..53b4511078 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs @@ -294,6 +294,15 @@ public void OnMonitoredNodeChanged(ISystemContext context, NodeState node, NodeS if (monitoredItem.AttributeId == Attributes.Value && (changes & NodeStateChangeMasks.Value) != 0) { + // validate if the monitored item has the required role permissions to read the value + ServiceResult validationResult = NodeManager.ValidateRolePermissions(new OperationContext(monitoredItem), node.NodeId, PermissionType.Read); + + if (ServiceResult.IsBad(validationResult)) + { + // skip if the monitored item does not have permission to read + continue; + } + QueueValue(context, node, monitoredItem); continue; } diff --git a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs index 90046cefc1..6792c19c8f 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs @@ -3233,7 +3233,7 @@ protected static ServiceResult ValidateAccessRestrictions(OperationContext conte /// protected internal static ServiceResult ValidateRolePermissions(OperationContext context, NodeMetadata nodeMetadata, PermissionType requestedPermission) { - if (context.Session == null || nodeMetadata == null || requestedPermission == PermissionType.None) + if (nodeMetadata == null || requestedPermission == PermissionType.None) { // no permission is required hence the validation passes return StatusCodes.Good; @@ -3323,7 +3323,7 @@ protected internal static ServiceResult ValidateRolePermissions(OperationContext } } - var currentRoleIds = context.Session.Identity.GrantedRoleIds; + var currentRoleIds = context.UserIdentity.GrantedRoleIds; if (currentRoleIds == null || currentRoleIds.Count == 0) { return ServiceResult.Create(StatusCodes.BadUserAccessDenied, "Current user has no granted role."); diff --git a/Libraries/Opc.Ua.Server/Server/OperationContext.cs b/Libraries/Opc.Ua.Server/Server/OperationContext.cs index d31a996cc1..7d7ec7f41a 100644 --- a/Libraries/Opc.Ua.Server/Server/OperationContext.cs +++ b/Libraries/Opc.Ua.Server/Server/OperationContext.cs @@ -127,6 +127,7 @@ public OperationContext(IMonitoredItem monitoredItem) if (monitoredItem == null) throw new ArgumentNullException(nameof(monitoredItem)); m_channelContext = null; + m_identity = monitoredItem.EffectiveIdentity; m_session = monitoredItem.Session; if (m_session != null) diff --git a/Libraries/Opc.Ua.Server/Subscription/IMonitoredItem.cs b/Libraries/Opc.Ua.Server/Subscription/IMonitoredItem.cs index 6f87302948..7b05a0d646 100644 --- a/Libraries/Opc.Ua.Server/Subscription/IMonitoredItem.cs +++ b/Libraries/Opc.Ua.Server/Subscription/IMonitoredItem.cs @@ -52,6 +52,11 @@ public interface IMonitoredItem /// Session Session { get; } + /// + /// The monitored items owner identity. + /// + IUserIdentity EffectiveIdentity { get; } + /// /// The identifier for the item that is unique within the server. /// diff --git a/Libraries/Opc.Ua.Server/Subscription/MonitoredItem.cs b/Libraries/Opc.Ua.Server/Subscription/MonitoredItem.cs index cefd14eef9..1c74cdf50e 100644 --- a/Libraries/Opc.Ua.Server/Subscription/MonitoredItem.cs +++ b/Libraries/Opc.Ua.Server/Subscription/MonitoredItem.cs @@ -398,6 +398,19 @@ public Session Session } } } + /// + /// The monitored items owner identity. + /// + public IUserIdentity EffectiveIdentity + { + get + { + lock (m_lock) + { + return m_subscription?.EffectiveIdentity; + } + } + } /// /// The identifier for the item that is unique within the server. diff --git a/Libraries/Opc.Ua.Server/Subscription/Subscription.cs b/Libraries/Opc.Ua.Server/Subscription/Subscription.cs index 34557b2370..3ace961e96 100644 --- a/Libraries/Opc.Ua.Server/Subscription/Subscription.cs +++ b/Libraries/Opc.Ua.Server/Subscription/Subscription.cs @@ -49,6 +49,11 @@ public interface ISubscription /// Session Session { get; } + /// + /// The subscriptions owner identity. + /// + IUserIdentity EffectiveIdentity { get; } + /// /// The identifier for the item that is unique within the server. /// @@ -208,6 +213,14 @@ public uint Id get { return m_id; } } + /// + /// The subscriptions owner identity. + /// + public IUserIdentity EffectiveIdentity + { + get { return (m_session != null) ? m_session.EffectiveIdentity : m_savedOwnerIdentity; } + } + /// /// Queues an item that is ready to publish. /// @@ -255,14 +268,6 @@ public NodeId SessionId } } - /// - /// The owner identity. - /// - public UserIdentityToken OwnerIdentity - { - get { return (m_session != null) ? m_session.IdentityToken : m_savedOwnerIdentity; } - } - /// /// Gets the lock that must be acquired before accessing the contents of the Diagnostics property. /// @@ -594,7 +599,7 @@ public void SessionClosed() { if (m_session != null) { - m_savedOwnerIdentity = m_session.IdentityToken; + m_savedOwnerIdentity = m_session.EffectiveIdentity; m_session = null; } } @@ -2414,7 +2419,7 @@ private void TraceState(LogLevel logLevel, TraceStateId id, string context) private IServerInternal m_server; private Session m_session; private uint m_id; - private UserIdentityToken m_savedOwnerIdentity; + private IUserIdentity m_savedOwnerIdentity; private double m_publishingInterval; private uint m_maxLifetimeCount; private uint m_maxKeepAliveCount; diff --git a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs index 87d91efd86..b512960684 100644 --- a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs +++ b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs @@ -1223,11 +1223,11 @@ public void TransferSubscriptions( } // get the identity of the current or last owner - UserIdentityToken ownerIdentity = subscription.OwnerIdentity; + UserIdentityToken ownerIdentity = subscription.EffectiveIdentity.GetIdentityToken(); // Validate the identity of the user who owns/owned the subscription // is the same as the new owner. - bool validIdentity = Utils.IsEqualUserIdentity(ownerIdentity, context.Session.IdentityToken); + bool validIdentity = Utils.IsEqualUserIdentity(ownerIdentity, context.Session.EffectiveIdentity.GetIdentityToken()); // Test if anonymous user is using a // secure session using Sign or SignAndEncrypt diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs index 1ef99083af..c5662dac76 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography.X509Certificates; using Opc.Ua.Security.Certificates; @@ -92,30 +93,44 @@ CertificateTypesProvider certificateTypesProvider uris.Add(uri.Uri); - // Only support one policy with HTTPS - // So pick the first policy with security mode sign and encrypt ServerSecurityPolicy bestPolicy = null; - foreach (ServerSecurityPolicy policy in securityPolicies) + bool httpsMutualTls = configuration.ServerConfiguration.HttpsMutualTls; + if (!httpsMutualTls) { - if (policy.SecurityMode != MessageSecurityMode.SignAndEncrypt) + // Only use security None without mutual TLS authentication! + // When the mutual TLS authentication is not used, anonymous access is disabled + // Then the only protection against unauthorized access is user authorization + bestPolicy = new ServerSecurityPolicy() { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }; + } + else + { + // Only support one secure policy with HTTPS and mutual authentication + // So pick the first policy with security mode sign and encrypt + foreach (ServerSecurityPolicy policy in securityPolicies) { - continue; - } + if (policy.SecurityMode != MessageSecurityMode.SignAndEncrypt) + { + continue; + } - bestPolicy = policy; - break; - } + bestPolicy = policy; + break; + } - // Pick the first policy from the list if no policies with sign and encrypt defined - if (bestPolicy == null) - { - bestPolicy = securityPolicies[0]; + // Pick the first policy from the list if no policies with sign and encrypt defined + if (bestPolicy == null) + { + bestPolicy = securityPolicies[0]; + } } - EndpointDescription description = new EndpointDescription(); - - description.EndpointUrl = uri.ToString(); - description.Server = serverDescription; + var description = new EndpointDescription { + EndpointUrl = uri.ToString(), + Server = serverDescription + }; if (certificateTypesProvider != null) { @@ -135,6 +150,12 @@ CertificateTypesProvider certificateTypesProvider description.UserIdentityTokens = serverBase.GetUserTokenPolicies(configuration, description); description.TransportProfileUri = Profiles.HttpsBinaryTransport; + // if no mutual TLS authentication is used, anonymous user tokens are not allowed + if (!httpsMutualTls) + { + description.UserIdentityTokens = new UserTokenPolicyCollection(description.UserIdentityTokens.Where(token => token.TokenType != UserTokenType.Anonymous)); + } + ITransportListener listener = Create(); if (listener != null) { diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index 0f46ebdbca..1f2094d893 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -14,6 +14,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -175,6 +176,7 @@ public void Open( m_listenerId = Guid.NewGuid().ToString(); m_uri = baseAddress; + m_discovery = m_uri.AbsolutePath?.TrimEnd('/') + ConfiguredEndpoint.DiscoverySuffix; m_descriptions = settings.Descriptions; var configuration = settings.Configuration; @@ -205,6 +207,7 @@ public void Open( m_serverCertProvider = settings.ServerCertificateTypesProvider; + m_mutualTlsEnabled = settings.HttpsMutualTls; // start the listener Start(); } @@ -283,9 +286,10 @@ public void Start() var httpsOptions = new HttpsConnectionAdapterOptions() { CheckCertificateRevocation = false, - ClientCertificateMode = ClientCertificateMode.NoCertificate, + ClientCertificateMode = m_mutualTlsEnabled ? ClientCertificateMode.AllowCertificate : ClientCertificateMode.NoCertificate, // note: this is the TLS certificate! - ServerCertificate = serverCertificate + ServerCertificate = serverCertificate, + ClientCertificateValidation = ValidateClientCertificate, }; #if NET462 @@ -370,6 +374,21 @@ public async Task SendAsync(HttpContext context) IServiceRequest input = (IServiceRequest)BinaryDecoder.DecodeMessage(buffer, null, m_quotas.MessageContext); + if (m_mutualTlsEnabled && input.TypeId == DataTypeIds.CreateSessionRequest) + { + // Match tls client certificate against client application certificate provided in CreateSessionRequest + byte[] tlsClientCertificate = context.Connection.ClientCertificate?.RawData; + byte[] opcUaClientCertificate = ((CreateSessionRequest)input).ClientCertificate; + + if (tlsClientCertificate == null || !Utils.IsEqual(tlsClientCertificate, opcUaClientCertificate)) + { + message = "Client TLS certificate does not match with ClientCertificate provided in CreateSessionRequest"; + Utils.LogError(message); + await WriteResponseAsync(context.Response, message, HttpStatusCode.Unauthorized).ConfigureAwait(false); + return; + } + } + // extract the JWT token from the HTTP headers. if (input.RequestHeader == null) { @@ -463,6 +482,8 @@ public async Task SendAsync(HttpContext context) await WriteResponseAsync(context.Response, message, HttpStatusCode.InternalServerError).ConfigureAwait(false); } + + /// /// Called when a UpdateCertificate event occured. /// @@ -519,11 +540,41 @@ private static async Task ReadBodyAsync(HttpRequest req) return memory.ToArray(); } } + + /// + /// Validate TLS client certificate at TLS handshake. + /// + /// Client certificate + /// Certificate chain + /// SSl policy errors + private bool ValidateClientCertificate( + X509Certificate2 clientCertificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + // certificate is valid + return true; + } + + try + { + m_quotas.CertificateValidator.Validate(clientCertificate); + } + catch (Exception) + { + return false; + } + + return true; + } #endregion #region Private Fields private string m_listenerId; private Uri m_uri; + private string m_discovery; private readonly string m_uriScheme; private EndpointDescriptionCollection m_descriptions; private ChannelQuotas m_quotas; @@ -531,6 +582,7 @@ private static async Task ReadBodyAsync(HttpRequest req) private IWebHostBuilder m_hostBuilder; private IWebHost m_host; private CertificateTypesProvider m_serverCertProvider; + private bool m_mutualTlsEnabled; #endregion } } diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 84a713692d..90c115e82d 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -53,9 +53,10 @@ - + - + + @@ -71,8 +72,7 @@ - - + @@ -82,7 +82,7 @@ - + diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 527ee7741d..8ebced22f1 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -1596,6 +1596,7 @@ private void Initialize() m_maxTrustListSize = 0; m_multicastDnsEnabled = false; m_auditingEnabled = false; + m_httpsMutualTls = true; } /// @@ -2029,6 +2030,17 @@ public bool AuditingEnabled get { return m_auditingEnabled; } set { m_auditingEnabled = value; } } + + /// + /// Whether mTLS is required/enforced by the HttpsTransportListener + /// + /// true if mutual TLS is enabled; otherwise, false. + [DataMember(IsRequired = false, Order = 38)] + public bool HttpsMutualTls + { + get { return m_httpsMutualTls; } + set { m_httpsMutualTls = value; } + } #endregion #region Private Members @@ -2067,6 +2079,7 @@ public bool AuditingEnabled private ReverseConnectServerConfiguration m_reverseConnect; private OperationLimits m_operationLimits; private bool m_auditingEnabled; + private bool m_httpsMutualTls; #endregion } #endregion @@ -2952,6 +2965,7 @@ private int XmlEncodedValidationOptions #region CertificateTrustList Class [DataContract(Namespace = Namespaces.OpcUaConfig)] + [KnownType(typeof(CertificateTrustList))] public partial class CertificateTrustList : CertificateStoreIdentifier { #region Constructors diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd index d3d201f67e..f6e4aec920 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd @@ -168,6 +168,7 @@ + diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index ec7935ba4f..0ef967afdf 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -59,10 +59,11 @@ public static class CertificateFactory public static X509Certificate2 Create(ReadOnlyMemory encodedData, bool useCache) { #if NET6_0_OR_GREATER - var certificate = new X509Certificate2(encodedData.Span); + var certificate = X509CertificateLoader.LoadCertificate(encodedData.Span); #else - var certificate = new X509Certificate2(encodedData.ToArray()); + var certificate = X509CertificateLoader.LoadCertificate(encodedData.ToArray()); #endif + if (useCache) { return Load(certificate, false); @@ -414,7 +415,7 @@ public static X509Certificate2 CreateCertificateWithPEMPrivateKey( string password = null) { RSA rsaPrivateKey = PEMReader.ImportPrivateKeyFromPEM(pemDataBlob, password); - return new X509Certificate2(certificate.RawData).CopyWithPrivateKey(rsaPrivateKey); + return X509CertificateLoader.LoadCertificate(certificate.RawData).CopyWithPrivateKey(rsaPrivateKey); } #else /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 406dad299e..e029abbbc5 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -521,7 +521,7 @@ public virtual async Task ValidateAsync(X509Certificate2Collection chain, Config await InternalValidateAsync(chain, endpoint, ct).ConfigureAwait(false); // add to list of validated certificates. - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); return; } @@ -540,7 +540,7 @@ public virtual async Task ValidateAsync(X509Certificate2Collection chain, Config try { Utils.LogCertificate(LogLevel.Warning, "Validation errors suppressed: ", certificate); - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); } finally { @@ -565,7 +565,7 @@ public virtual void Validate(X509Certificate2Collection chain, ConfiguredEndpoin InternalValidateAsync(chain, endpoint).GetAwaiter().GetResult(); // add to list of validated certificates. - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); return; } @@ -585,7 +585,7 @@ public virtual void Validate(X509Certificate2Collection chain, ConfiguredEndpoin try { Utils.LogCertificate(LogLevel.Warning, "Validation errors suppressed: ", certificate); - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); } finally { diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index 8397f38889..f5059530a5 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -20,7 +20,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Threading.Tasks; using Opc.Ua.Security.Certificates; using Opc.Ua.Redaction; -using System.Threading; namespace Opc.Ua { @@ -459,7 +458,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub { try { - var certificate = new X509Certificate2(file.FullName); + var certificate = X509CertificateLoader.LoadCertificateFromFile(file.FullName); if (!String.IsNullOrEmpty(thumbprint)) { @@ -521,7 +520,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub { try { - certificate = new X509Certificate2( + certificate = X509CertificateLoader.LoadPkcs12FromFile( privateKeyFilePfx.FullName, password, flag); @@ -628,7 +627,7 @@ public Task IsRevoked(X509Certificate2 issuer, X509Certificate2 cert } catch (Exception e) { - Utils.LogError(e, "Could not parse CRL file."); + Utils.LogError(e, "Failed to parse CRL {0} in store {1}.", file.FullName, StorePath); continue; } @@ -678,8 +677,16 @@ public Task EnumerateCRLs() { foreach (FileInfo file in m_crlSubdir.GetFiles("*" + kCrlExtension)) { - var crl = new X509CRL(file.FullName); - crls.Add(crl); + try + { + var crl = new X509CRL(file.FullName); + crls.Add(crl); + } + catch (Exception e) + { + Utils.LogError(e, "Failed to parse CRL {0} in store {1}.", file.FullName, StorePath); + } + } } @@ -832,7 +839,7 @@ private IDictionary Load(string thumbprint) try { var entry = new Entry { - Certificate = new X509Certificate2(file.FullName), + Certificate = X509CertificateLoader.LoadCertificateFromFile(file.FullName), CertificateFile = file, PrivateKeyFile = null, CertificateWithPrivateKey = null, diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs index 686e93ad62..7e66e0d8e5 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs @@ -15,7 +15,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; -using System.IO; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; @@ -154,7 +153,7 @@ public Task Add(X509Certificate2 certificate, string password = null) else if (certificate.HasPrivateKey && m_noPrivateKeys) { // ensure no private key is added to store - using (var publicKey = new X509Certificate2(certificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(certificate.RawData)) { store.Add(publicKey); } @@ -261,7 +260,6 @@ public async Task IsRevoked(X509Certificate2 issuer, X509Certificate foreach (X509CRL crl in crls) { - if (!X509Utils.CompareDistinguishedName(crl.IssuerName, issuer.SubjectName)) { continue; @@ -309,8 +307,15 @@ public Task EnumerateCRLs() byte[][] rawCrls = store.EnumerateCrls(); foreach (byte[] rawCrl in rawCrls) { - var crl = new X509CRL(rawCrl); - crls.Add(crl); + try + { + var crl = new X509CRL(rawCrl); + crls.Add(crl); + } + catch (Exception e) + { + Utils.LogError(e, "Failed to parse CRL in store {0}.", store.Name); + } } } return Task.FromResult(crls); diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index 03508118eb..b7e566977a 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -580,7 +580,7 @@ public static X509Certificate2 CreateCopyWithPrivateKey(X509Certificate2 certifi // see https://github.com/dotnet/runtime/issues/29144 string passcode = GeneratePasscode(); X509KeyStorageFlags storageFlags = persisted ? X509KeyStorageFlags.PersistKeySet : X509KeyStorageFlags.Exportable; - return new X509Certificate2(certificate.Export(X509ContentType.Pfx, passcode), passcode, storageFlags); + return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx, passcode), passcode, storageFlags); } return certificate; } diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs index 29cf6b273c..9410289667 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Globalization; using System.IO; using System.Runtime.Serialization; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -130,7 +131,7 @@ public static ConfiguredEndpointCollection Load(string filePath) { string discoveryUrl = endpoint.Description.EndpointUrl; - if (discoveryUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal)) + if (Utils.IsUriHttpRelatedScheme(discoveryUrl)) { discoveryUrl += ConfiguredEndpoint.DiscoverySuffix; } @@ -530,7 +531,7 @@ public void SetApplicationDescription(string serverUri, ApplicationDescription s } if (endpointUrl != null && - endpointUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) && + Utils.IsUriHttpRelatedScheme(endpointUrl) && endpointUrl.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)) { endpointUrl = endpointUrl.Substring(0, endpointUrl.Length - ConfiguredEndpoint.DiscoverySuffix.Length); @@ -815,7 +816,7 @@ public ConfiguredEndpoint( if (baseUrl != null) { - if (baseUrl.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) && + if (Utils.IsUriHttpRelatedScheme(baseUrl) && baseUrl.EndsWith(DiscoverySuffix, StringComparison.Ordinal)) { baseUrl = baseUrl.Substring(0, baseUrl.Length - DiscoverySuffix.Length); @@ -1208,7 +1209,7 @@ public Uri GetDiscoveryUrl(Uri endpointUrl) // attempt to construct a discovery url by appending 'discovery' to the endpoint. if (discoveryUrls == null || discoveryUrls.Count == 0) { - if (endpointUrl.Scheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal)) + if (Utils.IsUriHttpRelatedScheme(endpointUrl.Scheme)) { return new Uri(Utils.Format("{0}{1}", endpointUrl, DiscoverySuffix)); } diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 57324aacf6..d49b417a16 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -32,7 +32,7 @@ public EndpointDescription(string url) UriBuilder parsedUrl = new UriBuilder(url); - if (parsedUrl.Scheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal)) + if (Utils.IsUriHttpRelatedScheme(parsedUrl.Scheme)) { if (!parsedUrl.Path.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)) { diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 1d82412788..7a38b955cf 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -17,6 +17,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Net.Http.Headers; using System.Net.Security; using System.Reflection; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -165,11 +166,27 @@ public void Open() // send client certificate for servers that require TLS client authentication if (m_settings.ClientCertificate != null) { + // prepare the server TLS certificate + var clientCertificate = m_settings.ClientCertificate; +#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1 || NET472_OR_GREATER || NET5_0_OR_GREATER + try + { + // Create a copy of the certificate with the private key on platforms + // which default to the ephemeral KeySet. Also a new certificate must be reloaded. + // If the key fails to copy, its probably a non exportable key from the X509Store. + // Then we can use the original certificate, the private key is already in the key store. + clientCertificate = X509Utils.CreateCopyWithPrivateKey(m_settings.ClientCertificate, false); + } + catch (CryptographicException ce) + { + Utils.LogTrace("Copy of the private key for https was denied: {0}", ce.Message); + } +#endif PropertyInfo certProperty = handler.GetType().GetProperty("ClientCertificates"); if (certProperty != null) { X509CertificateCollection clientCertificates = (X509CertificateCollection)certProperty.GetValue(handler); - _ = clientCertificates?.Add(m_settings.ClientCertificate); + _ = clientCertificates?.Add(clientCertificate); } } diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index eb4f7de894..f269c15e0f 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -835,6 +835,10 @@ ICertificateValidator certificateValidator if (m_configuration is ApplicationConfiguration applicationConfiguration) { settings.MaxChannelCount = applicationConfiguration.ServerConfiguration.MaxChannelCount; + if (Utils.IsUriHttpsScheme(endpointUri.AbsoluteUri)) + { + settings.HttpsMutualTls = applicationConfiguration.ServerConfiguration.HttpsMutualTls; + } } listener.Open( @@ -1091,8 +1095,8 @@ private static string GetBestDiscoveryUrl(Uri clientUrl, BaseAddress baseAddress string url = baseAddress.Url.ToString(); if ((baseAddress.ProfileUri == Profiles.HttpsBinaryTransport) && - url.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) && - (!(url.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)))) + Utils.IsUriHttpRelatedScheme(url) && + (!url.EndsWith(ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase))) { url += ConfiguredEndpoint.DiscoverySuffix; } diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index c3e81b8e1f..ef96146731 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -3669,7 +3669,7 @@ protected virtual ServiceResult ReadNonValueAttribute( if (ServiceResult.IsGood(result)) { - value = accessRestrictions; + value = (ushort)accessRestrictions; } if (value != null || result != null) diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs index 1cdb5b4f7a..a6cada4bbd 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Diagnostics; using System.Security.Cryptography; namespace Opc.Ua.Bindings @@ -18,7 +19,7 @@ namespace Opc.Ua.Bindings /// /// Represents a security token associate with a channel. /// - public class ChannelToken + public sealed class ChannelToken : IDisposable { #region Constructors /// @@ -29,6 +30,49 @@ public ChannelToken() } #endregion + #region IDisposable + /// + /// The private version of the Dispose. + /// + private void Dispose(bool disposing) + { + if (!m_disposed) + { + if (disposing) + { + Utils.SilentDispose(m_clientHmac); + Utils.SilentDispose(m_serverHmac); + Utils.SilentDispose(m_clientEncryptor); + Utils.SilentDispose(m_serverEncryptor); + } + m_clientHmac = null; + m_serverHmac = null; + m_clientEncryptor = null; + m_serverEncryptor = null; + m_disposed = true; + } + } + +#if DEBUG + /// + /// The finalizer is used to catch issues with the dispose. + /// + ~ChannelToken() + { + Dispose(disposing: false); + } +#endif + + /// + /// Disposes the channel tokens. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion + #region Public Properties /// /// The id assigned to the channel that the token belongs to. @@ -57,6 +101,16 @@ public DateTime CreatedAt set { m_createdAt = value; } } + /// + /// When the token was created (refers to the local tick count). + /// Used for calculation of renewals. Uses . + /// + public int CreatedAtTickCount + { + get { return m_createdAtTickCount; } + set { m_createdAtTickCount = value; } + } + /// /// The lifetime of the token in milliseconds. /// @@ -73,12 +127,7 @@ public bool Expired { get { - if (DateTime.UtcNow > m_createdAt.AddMilliseconds(m_lifetime)) - { - return true; - } - - return false; + return (HiResClock.TickCount - m_createdAtTickCount) > m_lifetime; } } @@ -89,12 +138,7 @@ public bool ActivationRequired { get { - if (DateTime.UtcNow > m_createdAt.AddMilliseconds(m_lifetime * TcpMessageLimits.TokenActivationPeriod)) - { - return true; - } - - return false; + return (HiResClock.TickCount - m_createdAtTickCount) > (int)Math.Round(m_lifetime * TcpMessageLimits.TokenActivationPeriod); } } @@ -213,12 +257,13 @@ public HMAC ServerHmac get { return m_serverHmac; } set { m_serverHmac = value; } } - #endregion + #endregion #region Private Fields private uint m_channelId; private uint m_tokenId; private DateTime m_createdAt; + private int m_createdAtTickCount; private int m_lifetime; private byte[] m_clientNonce; private byte[] m_serverNonce; @@ -232,6 +277,7 @@ public HMAC ServerHmac private HMAC m_serverHmac; private SymmetricAlgorithm m_clientEncryptor; private SymmetricAlgorithm m_serverEncryptor; + private bool m_disposed; #endregion } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index c6aa8b5210..868f1beb9f 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Net; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; @@ -252,6 +253,18 @@ protected void ForceChannelFault(ServiceResult reason) if (close) { + // mark the RemoteAddress as potential problematic if Basic128Rsa15 + if ((SecurityPolicyUri == SecurityPolicies.Basic128Rsa15) && + (reason.StatusCode == StatusCodes.BadSecurityChecksFailed || reason.StatusCode == StatusCodes.BadTcpMessageTypeInvalid)) + { + var tcpTransportListener = m_listener as TcpTransportListener; + if (tcpTransportListener != null) + { + tcpTransportListener.MarkAsPotentialProblematic + (((IPEndPoint)Socket.RemoteEndpoint).Address); + } + } + // close channel immediately. ChannelFaulted(); } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index 8ac08c03d1..8fd4b0f233 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -298,6 +298,11 @@ public static class TcpMessageLimits /// public const double TokenRenewalPeriod = 0.75; + /// + /// The fraction of the lifetime to jitter renewing a token. + /// + public const double TokenRenewalJitterPeriod = 0.05; + /// /// The fraction of the lifetime to wait before forcing the activation of the renewed token. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 62624cd3df..6439d4cd28 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -51,7 +51,10 @@ public TcpServerChannel( /// protected override void Dispose(bool disposing) { - base.Dispose(disposing); + lock (DataLock) + { + base.Dispose(disposing); + } } #endregion @@ -533,6 +536,7 @@ private bool ProcessOpenSecureChannelRequest(uint messageType, ArraySegment messageBody, out BufferCollection chunksToProcess) { chunksToProcess = null; - using (var decoder = new BinaryDecoder(messageBody.AsMemory().ToArray(), Quotas.MessageContext)) + using (var decoder = new BinaryDecoder(messageBody, Quotas.MessageContext)) { // read the type of the message before more chunks are processed. NodeId typeId = decoder.ReadNodeId(null); @@ -1118,7 +1126,6 @@ private bool ValidateDiscoveryServiceCall(ChannelToken token, uint requestId, Ar return true; } } - #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index f651ab3d30..c7135bc504 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; @@ -38,6 +39,235 @@ public override ITransportListener Create() } } + /// + /// Represents a potential problematic ActiveClient + /// + public class ActiveClient + { + #region Properties + /// + /// Time of the last recorded problematic action + /// + public int LastActionTicks + { + get + { + return m_lastActionTicks; + } + set + { + m_lastActionTicks = value; + } + } + + /// + /// Counter for number of recorded potential problematic actions + /// + public int ActiveActionCount + { + get + { + return m_actionCount; + } + set + { + m_actionCount = value; + } + } + + /// + /// Ticks until the client is Blocked + /// + public int BlockedUntilTicks + { + get + { + return m_blockedUntilTicks; + } + set + { + m_blockedUntilTicks = value; + } + } + #endregion + + #region Private members + int m_lastActionTicks; + int m_actionCount; + int m_blockedUntilTicks; + #endregion + } + + /// + /// Manages clients with potential problematic activities + /// + public class ActiveClientTracker : IDisposable + { + #region Public + /// + /// Constructor + /// + public ActiveClientTracker() + { + m_cleanupTimer = new Timer(CleanupExpiredEntries, null, m_kCleanupIntervalMs, m_kCleanupIntervalMs); + } + + /// + /// Checks if an IP address is currently blocked + /// + /// + /// + public bool IsBlocked(IPAddress ipAddress) + { + if (m_activeClients.TryGetValue(ipAddress, out ActiveClient client)) + { + int currentTicks = HiResClock.TickCount; + return IsBlockedTicks(client.BlockedUntilTicks, currentTicks); + } + return false; + } + + /// + /// Adds a potential problematic action entry for a client + /// + /// + public void AddClientAction(IPAddress ipAddress) + { + int currentTicks = HiResClock.TickCount; + + m_activeClients.AddOrUpdate(ipAddress, + // If client is new , create a new entry + key => new ActiveClient + { + LastActionTicks = currentTicks, + ActiveActionCount = 1, + BlockedUntilTicks = 0 + }, + // If the client exists, update its entry + (key, existingEntry) => + { + // If IP currently blocked simply do nothing + if (IsBlockedTicks(existingEntry.BlockedUntilTicks, currentTicks)) + { + return existingEntry; + } + + // Elapsed time since last recorded action + int elapsedSinceLastRecAction = currentTicks - existingEntry.LastActionTicks; + + if (elapsedSinceLastRecAction <= m_kActionsIntervalMs) + { + existingEntry.ActiveActionCount++; + + if (existingEntry.ActiveActionCount > m_kNrActionsTillBlock) + { + // Block the IP + existingEntry.BlockedUntilTicks = currentTicks + m_kBlockDurationMs; + Utils.LogError("RemoteClient IPAddress: {0} blocked for {1} ms due to exceeding {2} actions under {3} ms ", + ipAddress.ToString(), + m_kBlockDurationMs, + m_kNrActionsTillBlock, + m_kActionsIntervalMs); + + } + } + else + { + // Reset the count as the last action was outside the interval + existingEntry.ActiveActionCount = 1; + } + + existingEntry.LastActionTicks = currentTicks; + + return existingEntry; + } + ); + } + + /// + /// Dispose the cleanup timer + /// + public void Dispose() + { + m_cleanupTimer?.Dispose(); + } + + #endregion + #region Private methods + + /// + /// Periodically cleans up expired active client entries to avoid memory leak and unblock clients whose duration has expired. + /// + /// + private void CleanupExpiredEntries(object state) + { + int currentTicks = HiResClock.TickCount; + + foreach (var entry in m_activeClients) + { + IPAddress clientIp = entry.Key; + ActiveClient rClient = entry.Value; + + // Unblock client if blocking duration has been exceeded + if (rClient.BlockedUntilTicks != 0 && !IsBlockedTicks(rClient.BlockedUntilTicks, currentTicks)) + { + rClient.BlockedUntilTicks = 0; + rClient.ActiveActionCount = 0; + Utils.LogDebug("Active Client with IP {0} is now unblocked, blocking duration of {1} ms has been exceeded", + clientIp.ToString(), + m_kBlockDurationMs); + } + + // Remove clients that haven't had any potential problematic actions in the last m_kEntryExpirationMs interval + int elapsedSinceBadActionTicks = currentTicks - rClient.LastActionTicks; + if (elapsedSinceBadActionTicks > m_kEntryExpirationMs) + { + // Even if TryRemove fails it will most probably succeed at the next execution + if (m_activeClients.TryRemove(clientIp, out _)) + { + Utils.LogDebug("Active Client with IP {0} is not tracked any longer, hasn't had actions for more than {1} ms", + clientIp.ToString(), + m_kEntryExpirationMs); + } + } + } + } + + /// + /// Determines if the IP is currently blocked based on the block expiration ticks and current ticks + /// + /// + /// + /// + private bool IsBlockedTicks(int blockedUntilTicks, int currentTicks) + { + if (blockedUntilTicks == 0) + { + return false; + } + // C# signed arithmetic + int diff = blockedUntilTicks - currentTicks; + // If currentTicks < blockedUntilTicks then it is still blocked + // Works even if TickCount has wrapped around due to C# signed integer arithmetic + return diff > 0; + } + + + #endregion + #region Private members + private ConcurrentDictionary m_activeClients = new ConcurrentDictionary(); + + private const int m_kActionsIntervalMs = 10_000; + private const int m_kNrActionsTillBlock = 3; + + private const int m_kBlockDurationMs = 30_000; // 30 seconds + private const int m_kCleanupIntervalMs = 15_000; + private const int m_kEntryExpirationMs = 600_000; // 10 minutes + + private Timer m_cleanupTimer; + #endregion + } + /// /// Manages the transport for a UA TCP server. /// @@ -131,7 +361,8 @@ public void Open( // initialize the quotas. m_quotas = new ChannelQuotas(); - var messageContext = new ServiceMessageContext() { + var messageContext = new ServiceMessageContext() + { NamespaceUris = settings.NamespaceUris, ServerUris = new StringTable(), Factory = settings.Factory @@ -241,8 +472,10 @@ public bool ReconnectToExistingChannel( /// public void ChannelClosed(uint channelId) { - if (m_channels?.TryRemove(channelId, out _) == true) + TcpListenerChannel channel = null; + if (m_channels?.TryRemove(channelId, out channel) == true) { + Utils.SilentDispose(channel); Utils.LogInfo("ChannelId {0}: closed", channelId); } else @@ -307,11 +540,17 @@ private void OnReverseHelloComplete(IAsyncResult result) channel.SetReportCloseSecureChannelAuditCallback(new ReportAuditCloseSecureChannelEventHandler(OnReportAuditCloseSecureChannelEvent)); channel.SetReportCertificateAuditCallback(new ReportAuditCertificateEventHandler(OnReportAuditCertificateEvent)); } + + channel = null; } catch (Exception e) { ConnectionStatusChanged?.Invoke(this, new ConnectionStatusEventArgs(channel.ReverseConnectionUrl, new ServiceResult(e), true)); } + finally + { + Utils.SilentDispose(channel); + } } #endregion @@ -323,6 +562,12 @@ public void Start() { lock (m_lock) { + // Track potential problematic client behavior only if Basic128Rsa15 security policy is offered + if (m_descriptions != null && m_descriptions.Any(d => d.SecurityPolicyUri == SecurityPolicies.Basic128Rsa15)) + { + m_activeClientTracker = new ActiveClientTracker(); + } + // ensure a valid port. int port = m_uri.Port; @@ -502,16 +747,44 @@ CertificateTypesProvider certificateTypesProvider } #endregion + #region Internal + /// + /// Mark a remote endpoint as potential problematic + /// + /// + internal void MarkAsPotentialProblematic(IPAddress remoteEndpoint) + { + Utils.LogDebug("MarkClientAsPotentialProblematic address: {0} ", remoteEndpoint.ToString()); + m_activeClientTracker?.AddClientAction(remoteEndpoint); + } + #endregion + #region Socket Event Handler /// /// Handles a new connection. /// private void OnAccept(object sender, SocketAsyncEventArgs e) { + TcpListenerChannel channel = null; bool repeatAccept = false; do { + bool isBlocked = false; + + // Track potential problematic client behavior only if Basic128Rsa15 security policy is offered + if (m_activeClientTracker != null) + { + // Filter out the Remote IP addresses which are detected with potential problematic behavior + IPAddress ipAddress = ((IPEndPoint)e?.AcceptSocket?.RemoteEndPoint)?.Address; + if (ipAddress != null && m_activeClientTracker.IsBlocked(ipAddress)) + { + Utils.LogDebug("OnAccept: RemoteEndpoint address: {0} refused access for behaving as potential problematic ", + ((IPEndPoint)e.AcceptSocket.RemoteEndPoint).Address.ToString()); + isBlocked = true; + } + } + repeatAccept = false; lock (m_lock) { @@ -523,7 +796,7 @@ private void OnAccept(object sender, SocketAsyncEventArgs e) } var channels = m_channels; - if (channels != null) + if (channels != null && !isBlocked) { // TODO: .Count is flagged as hotpath, implement separate counter int channelCount = channels.Count; @@ -535,32 +808,33 @@ private void OnAccept(object sender, SocketAsyncEventArgs e) Utils.SilentDispose(e.AcceptSocket); } - // check if the accept socket has been created. - if (serveChannel && e.AcceptSocket != null && e.SocketError == SocketError.Success) - { - try + // check if the accept socket has been created. + if (serveChannel && e.AcceptSocket != null && e.SocketError == SocketError.Success) { - if (m_reverseConnectListener) - { - // create the channel to manage incoming reverse connections. - channel = new TcpReverseConnectChannel( - m_listenerId, - this, - m_bufferManager, - m_quotas, - m_descriptions); - } - else + channel = null; + try { - // create the channel to manage incoming connections. - channel = new TcpServerChannel( - m_listenerId, - this, - m_bufferManager, - m_quotas, - m_serverCertificateTypesProvider, - m_descriptions); - } + if (m_reverseConnectListener) + { + // create the channel to manage incoming reverse connections. + channel = new TcpReverseConnectChannel( + m_listenerId, + this, + m_bufferManager, + m_quotas, + m_descriptions); + } + else + { + // create the channel to manage incoming connections. + channel = new TcpServerChannel( + m_listenerId, + this, + m_bufferManager, + m_quotas, + m_serverCertificateTypesProvider, + m_descriptions); + } if (m_callback != null) { @@ -582,11 +856,17 @@ private void OnAccept(object sender, SocketAsyncEventArgs e) // start accepting messages on the channel. channel.Attach(channelId, e.AcceptSocket); + + channel = null; } catch (Exception ex) { Utils.LogError(ex, "Unexpected error accepting a new connection."); } + finally + { + Utils.SilentDispose(channel); + } } } @@ -809,6 +1089,8 @@ private void SetUri(Uri baseAddress, string relativeAddress) private int m_inactivityDetectPeriod; private Timer m_inactivityDetectionTimer; private int m_maxChannelCount; + + private ActiveClientTracker m_activeClientTracker; #endregion } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index aba52d5a22..b39ef7bd6d 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Security.Cryptography; using System.Text; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; namespace Opc.Ua.Bindings { @@ -46,15 +47,16 @@ public partial class UaSCUaBinaryChannel /// protected ChannelToken CreateToken() { - ChannelToken token = new ChannelToken(); + ChannelToken token = new ChannelToken { + ChannelId = m_channelId, + TokenId = 0, + CreatedAt = DateTime.UtcNow, + CreatedAtTickCount = HiResClock.TickCount, + Lifetime = Quotas.SecurityTokenLifetime + }; - token.ChannelId = m_channelId; - token.TokenId = 0; - token.CreatedAt = DateTime.UtcNow; - token.Lifetime = (int)Quotas.SecurityTokenLifetime; - - Utils.LogInfo("ChannelId {0}: Token #{1} created. CreatedAt={2:HH:mm:ss.fff}. Lifetime={3}.", - Id, token.TokenId, token.CreatedAt, token.Lifetime); + Utils.LogInfo("ChannelId {0}: New Token created. CreatedAt={1:HH:mm:ss.fff}-{2}. Lifetime={3}.", + Id, token.CreatedAt, token.CreatedAtTickCount, token.Lifetime); return token; } @@ -67,22 +69,27 @@ protected void ActivateToken(ChannelToken token) // compute the keys for the token. ComputeKeys(token); + Utils.SilentDispose(m_previousToken); m_previousToken = m_currentToken; m_currentToken = token; + Utils.SilentDispose(m_renewedToken); m_renewedToken = null; OnTokenActivated?.Invoke(token, m_previousToken); - Utils.LogInfo("ChannelId {0}: Token #{1} activated. CreatedAt={2:HH:mm:ss.fff}. Lifetime={3}.", Id, token.TokenId, token.CreatedAt, token.Lifetime); + Utils.LogInfo("ChannelId {0}: Token #{1} activated. CreatedAt={2:HH:mm:ss.fff}-{3}. Lifetime={4}.", + Id, token.TokenId, token.CreatedAt, token.CreatedAtTickCount, token.Lifetime); } /// - /// Sets the renewed token + /// Sets the renewed token. /// protected void SetRenewedToken(ChannelToken token) { + Utils.SilentDispose(m_renewedToken); m_renewedToken = token; - Utils.LogInfo("ChannelId {0}: Renewed Token #{1} set. CreatedAt={2:HH:mm:ss.fff}. Lifetime ={3}.", Id, token.TokenId, token.CreatedAt, token.Lifetime); + Utils.LogInfo("ChannelId {0}: Renewed Token #{1} set. CreatedAt={2:HH:mm:ss.fff}-{3}. Lifetime={4}.", + Id, token.TokenId, token.CreatedAt, token.CreatedAtTickCount, token.Lifetime); } /// @@ -90,8 +97,12 @@ protected void SetRenewedToken(ChannelToken token) /// protected void DiscardTokens() { + Utils.SilentDispose(m_previousToken); m_previousToken = null; + Utils.SilentDispose(m_currentToken); m_currentToken = null; + Utils.SilentDispose(m_renewedToken); + m_renewedToken = null; OnTokenActivated?.Invoke(null, null); } @@ -101,10 +112,7 @@ protected void DiscardTokens() /// /// Indicates that an explicit signature is not present. /// - private bool UseAuthenticatedEncryption - { - get; set; - } + private bool AuthenticatedEncryption => m_authenticatedEncryption; /// /// The byte length of the MAC (a.k.a signature) attached to each message. @@ -121,7 +129,7 @@ private bool UseAuthenticatedEncryption /// protected void CalculateSymmetricKeySizes() { - UseAuthenticatedEncryption = false; + m_authenticatedEncryption = false; switch (SecurityPolicyUri) { @@ -183,7 +191,7 @@ protected void CalculateSymmetricKeySizes() case SecurityPolicies.ECC_curve25519: case SecurityPolicies.ECC_curve448: { - UseAuthenticatedEncryption = true; + m_authenticatedEncryption = true; m_hmacHashSize = 16; m_signatureKeySize = 32; m_encryptionKeySize = 32; @@ -212,6 +220,7 @@ protected void CalculateSymmetricKeySizes() } } } + private void DeriveKeysWithPSHA(HashAlgorithmName algorithmName, byte[] secret, byte[] seed, ChannelToken token, bool isServer) { int length = m_signatureKeySize + m_encryptionKeySize + m_encryptionBlockSize; @@ -271,14 +280,6 @@ private void DeriveKeysWithHKDF(HashAlgorithmName algorithmName, byte[] salt, Ch token.ClientInitializationVector = iv; } } - - static readonly byte[] s_HkdfClientLabel = new UTF8Encoding().GetBytes("opcua-client"); - static readonly byte[] s_HkdfServerLabel = new UTF8Encoding().GetBytes("opcua-server"); - static readonly byte[] s_HkdfAes128SignOnlyKeyLength = BitConverter.GetBytes((ushort)32); - static readonly byte[] s_HkdfAes256SignOnlyKeyLength = BitConverter.GetBytes((ushort)48); - static readonly byte[] s_HkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)64); - static readonly byte[] s_HkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)96); - static readonly byte[] s_HkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes((ushort)76); #endif /// @@ -295,16 +296,10 @@ protected void ComputeKeys(ChannelToken token) byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; - HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; + HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; switch (SecurityPolicyUri) { - default: - { - DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); - DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); - break; - } #if ECC_SUPPORT case SecurityPolicies.ECC_nistP256: case SecurityPolicies.ECC_brainpoolP256r1: @@ -314,11 +309,13 @@ protected void ComputeKeys(ChannelToken token) var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); +#if DEBUG Utils.LogTrace("Length={0}", Utils.ToHexString(length)); Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); +#endif DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); @@ -333,11 +330,13 @@ protected void ComputeKeys(ChannelToken token) var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); +#if DEBUG Utils.LogTrace("Length={0}", Utils.ToHexString(length)); Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); +#endif DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); @@ -352,25 +351,29 @@ protected void ComputeKeys(ChannelToken token) var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); +#if DEBUG Utils.LogTrace("Length={0}", Utils.ToHexString(length)); Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); +#endif DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); break; } #endif + case SecurityPolicies.Basic128Rsa15: case SecurityPolicies.Basic256: - { algorithmName = HashAlgorithmName.SHA1; + goto default; + + default: DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); break; - } } switch (SecurityPolicyUri) @@ -386,19 +389,19 @@ protected void ComputeKeys(ChannelToken token) case SecurityPolicies.ECC_brainpoolP384r1: { // create encryptors. - SymmetricAlgorithm AesCbcEncryptorProvider = Aes.Create(); - AesCbcEncryptorProvider.Mode = CipherMode.CBC; - AesCbcEncryptorProvider.Padding = PaddingMode.None; - AesCbcEncryptorProvider.Key = token.ClientEncryptingKey; - AesCbcEncryptorProvider.IV = token.ClientInitializationVector; - token.ClientEncryptor = AesCbcEncryptorProvider; - - SymmetricAlgorithm AesCbcDecryptorProvider = Aes.Create(); - AesCbcDecryptorProvider.Mode = CipherMode.CBC; - AesCbcDecryptorProvider.Padding = PaddingMode.None; - AesCbcDecryptorProvider.Key = token.ServerEncryptingKey; - AesCbcDecryptorProvider.IV = token.ServerInitializationVector; - token.ServerEncryptor = AesCbcDecryptorProvider; + SymmetricAlgorithm aesCbcEncryptorProvider = Aes.Create(); + aesCbcEncryptorProvider.Mode = CipherMode.CBC; + aesCbcEncryptorProvider.Padding = PaddingMode.None; + aesCbcEncryptorProvider.Key = token.ClientEncryptingKey; + aesCbcEncryptorProvider.IV = token.ClientInitializationVector; + token.ClientEncryptor = aesCbcEncryptorProvider; + + SymmetricAlgorithm aesCbcDecryptorProvider = Aes.Create(); + aesCbcDecryptorProvider.Mode = CipherMode.CBC; + aesCbcDecryptorProvider.Padding = PaddingMode.None; + aesCbcDecryptorProvider.Key = token.ServerEncryptingKey; + aesCbcDecryptorProvider.IV = token.ServerInitializationVector; + token.ServerEncryptor = aesCbcDecryptorProvider; break; } @@ -456,20 +459,18 @@ protected void ComputeKeys(ChannelToken token) break; } } - - } /// /// Secures the message using the security token. /// protected BufferCollection WriteSymmetricMessage( - uint messageType, - uint requestId, - ChannelToken token, - object messageBody, - bool isRequest, - out bool limitsExceeded) + uint messageType, + uint requestId, + ChannelToken token, + object messageBody, + bool isRequest, + out bool limitsExceeded) { limitsExceeded = false; bool success = false; @@ -485,7 +486,7 @@ protected BufferCollection WriteSymmetricMessage( int headerSize = TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; // no padding byte. - if (UseAuthenticatedEncryption) + if (AuthenticatedEncryption) { maxPayloadSize++; } @@ -498,7 +499,6 @@ protected BufferCollection WriteSymmetricMessage( maxPayloadSize); // check for encodeable body. - if (messageBody is IEncodeable encodeable) { // debug code used to verify that message aborts are handled correctly. @@ -532,6 +532,9 @@ protected BufferCollection WriteSymmetricMessage( BufferCollection chunksToSend = new BufferCollection(chunksToProcess.Capacity); +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + Span paddingBuffer = stackalloc byte[EncryptionBlockSize]; +#endif int messageSize = 0; for (int ii = 0; ii < chunksToProcess.Count; ii++) @@ -546,28 +549,24 @@ protected BufferCollection WriteSymmetricMessage( } MemoryStream strm = new MemoryStream(chunkToProcess.Array, 0, SendBufferSize); - BinaryEncoder encoder = new BinaryEncoder(strm, Quotas.MessageContext, false); - - try + using (BinaryEncoder encoder = new BinaryEncoder(strm, Quotas.MessageContext, false)) { - // check if the message needs to be aborted. if (MessageLimitsExceeded(isRequest, messageSize + chunkToProcess.Count - headerSize, ii + 1)) { encoder.WriteUInt32(null, messageType | TcpMessageType.Abort); // replace the body in the chunk with an error message. - BinaryEncoder errorEncoder = new BinaryEncoder( + using (BinaryEncoder errorEncoder = new BinaryEncoder( chunkToProcess.Array, chunkToProcess.Offset, chunkToProcess.Count, - Quotas.MessageContext); - - WriteErrorMessageBody(errorEncoder, (isRequest) ? StatusCodes.BadRequestTooLarge : StatusCodes.BadResponseTooLarge); - - int size = errorEncoder.Close(); - errorEncoder.Dispose(); - chunkToProcess = new ArraySegment(chunkToProcess.Array, chunkToProcess.Offset, size); + Quotas.MessageContext)) + { + WriteErrorMessageBody(errorEncoder, (isRequest) ? StatusCodes.BadRequestTooLarge : StatusCodes.BadResponseTooLarge); + int size = errorEncoder.Close(); + chunkToProcess = new ArraySegment(chunkToProcess.Array, chunkToProcess.Offset, size); + } limitsExceeded = true; } @@ -593,14 +592,16 @@ protected BufferCollection WriteSymmetricMessage( // calculate the padding. int padding = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !AuthenticatedEncryption) { // reserve one byte for the padding size. count++; - if (count % EncryptionBlockSize != 0) + // use padding as helper to calc the real padding + padding = count % EncryptionBlockSize; + if (padding != 0) { - padding = EncryptionBlockSize - (count % EncryptionBlockSize); + padding = EncryptionBlockSize - padding; } count += padding; @@ -624,18 +625,29 @@ protected BufferCollection WriteSymmetricMessage( messageSize += chunkToProcess.Count; // write padding. - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !AuthenticatedEncryption) { - for (int jj = 0; jj <= padding; jj++) +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + if (padding > 1) { - encoder.WriteByte(null, (byte)padding); + Span buffer = paddingBuffer.Slice(0, padding + 1); + buffer.Fill((byte)padding); + encoder.WriteRawBytes(buffer); + } + else +#endif + { + for (int jj = 0; jj <= padding; jj++) + { + encoder.WriteByte(null, (byte)padding); + } } } // calculate and write signature. if (SecurityMode != MessageSecurityMode.None) { - if (UseAuthenticatedEncryption) + if (AuthenticatedEncryption) { strm.Seek(SymmetricSignatureSize, SeekOrigin.Current); } @@ -650,8 +662,8 @@ protected BufferCollection WriteSymmetricMessage( } } - if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) || - (SecurityMode != MessageSecurityMode.None && UseAuthenticatedEncryption)) + if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && !AuthenticatedEncryption) || + (SecurityMode != MessageSecurityMode.None && AuthenticatedEncryption)) { // encrypt the data. ArraySegment dataToEncrypt = new ArraySegment(chunkToProcess.Array, TcpMessageLimits.SymmetricHeaderSize, encoder.Position - TcpMessageLimits.SymmetricHeaderSize); @@ -661,10 +673,6 @@ protected BufferCollection WriteSymmetricMessage( // add the header into chunk. chunksToSend.Add(new ArraySegment(chunkToProcess.Array, 0, encoder.Position)); } - finally - { - encoder.Dispose(); - } } // ensure the buffers don't get cleaned up on exit. @@ -715,10 +723,9 @@ protected ArraySegment ReadSymmetricMessage( } // check if activation of the new token should be forced. - if (RenewedToken != null && CurrentToken.ActivationRequired) + else if (RenewedToken != null && CurrentToken.ActivationRequired) { ActivateToken(RenewedToken); - Utils.LogInfo("ChannelId {0}: Token #{1} activated forced.", Id, CurrentToken.TokenId); } @@ -731,7 +738,7 @@ protected ArraySegment ReadSymmetricMessage( } // find the token. - if (currentToken.TokenId != tokenId && PreviousToken != null && PreviousToken.TokenId != tokenId) + if (currentToken.TokenId != tokenId && (PreviousToken == null || PreviousToken.TokenId != tokenId)) { throw ServiceResultException.Create( StatusCodes.BadTcpSecureChannelUnknown, @@ -753,8 +760,8 @@ protected ArraySegment ReadSymmetricMessage( if (token.Expired) { throw ServiceResultException.Create(StatusCodes.BadTcpSecureChannelUnknown, - "Channel{0}: Token #{1} has expired. Lifetime={2:HH:mm:ss.fff}", - Id, token.TokenId, token.CreatedAt); + "Channel{0}: Token #{1} has expired. Lifetime={2:HH:mm:ss.fff}-{3}", + Id, token.TokenId, token.CreatedAt, token.CreatedAtTickCount); } int headerSize = decoder.Position; @@ -765,15 +772,14 @@ protected ArraySegment ReadSymmetricMessage( Decrypt(token, new ArraySegment(buffer.Array, buffer.Offset + headerSize, buffer.Count - headerSize), isRequest); } + int paddingCount = 0; if (SecurityMode != MessageSecurityMode.None) { + int signatureStart = buffer.Offset + buffer.Count - SymmetricSignatureSize; + // extract signature. byte[] signature = new byte[SymmetricSignatureSize]; - - for (int ii = 0; ii < SymmetricSignatureSize; ii++) - { - signature[ii] = buffer.Array[buffer.Offset + buffer.Count - SymmetricSignatureSize + ii]; - } + Array.Copy(buffer.Array, signatureStart, signature, 0, signature.Length); // verify the signature. if (!Verify(token, signature, new ArraySegment(buffer.Array, buffer.Offset, buffer.Count - SymmetricSignatureSize), isRequest)) @@ -781,26 +787,24 @@ protected ArraySegment ReadSymmetricMessage( Utils.LogError("ChannelId {0}: Could not verify signature on message.", Id); throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the signature on the message."); } - } - - int paddingCount = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // verify padding. - int paddingStart = buffer.Offset + buffer.Count - SymmetricSignatureSize - 1; - paddingCount = buffer.Array[paddingStart]; - - for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt) { - if (buffer.Array[ii] != paddingCount) + // verify padding. + int paddingStart = signatureStart - 1; + paddingCount = buffer.Array[paddingStart]; + + for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) { - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the padding in the message."); + if (buffer.Array[ii] != paddingCount) + { + throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the padding in the message."); + } } - } - // add byte for size. - paddingCount++; + // add byte for size. + paddingCount++; + } } // extract request id and sequence number. @@ -863,12 +867,12 @@ protected bool Verify( case SecurityPolicies.Basic128Rsa15: case SecurityPolicies.Basic256: case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: { return SymmetricVerify(token, signature, dataToVerify, useClientKeys); } @@ -894,16 +898,17 @@ protected void Encrypt(ChannelToken token, ArraySegment dataToEncrypt, boo case SecurityPolicies.Basic256: case SecurityPolicies.Basic256Sha256: case SecurityPolicies.Basic128Rsa15: + case SecurityPolicies.Aes128_Sha256_RsaOaep: + case SecurityPolicies.Aes256_Sha256_RsaPss: case SecurityPolicies.ECC_nistP256: case SecurityPolicies.ECC_nistP384: case SecurityPolicies.ECC_brainpoolP256r1: case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: { SymmetricEncrypt(token, dataToEncrypt, useClientKeys); break; } + #if CURVE25519 case SecurityPolicies.ECC_curve25519: case SecurityPolicies.ECC_curve448: @@ -919,7 +924,6 @@ protected void Encrypt(ChannelToken token, ArraySegment dataToEncrypt, boo break; } #endif - //case SecurityPolicies.Aes128_Gcm256_RsaOaep: default: { throw new NotSupportedException(SecurityPolicyUri); @@ -952,6 +956,7 @@ protected void Decrypt(ChannelToken token, ArraySegment dataToDecrypt, boo SymmetricDecrypt(token, dataToDecrypt, useClientKeys); break; } + #if CURVE25519 case SecurityPolicies.ECC_curve25519: case SecurityPolicies.ECC_curve448: @@ -966,7 +971,7 @@ protected void Decrypt(ChannelToken token, ArraySegment dataToDecrypt, boo break; } #endif - //case SecurityPolicies.Aes128_Gcm256_RsaOaep: + default: { throw new NotSupportedException(SecurityPolicyUri); @@ -974,15 +979,37 @@ protected void Decrypt(ChannelToken token, ArraySegment dataToDecrypt, boo } } +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + /// + /// Signs the message using HMAC. + /// + private static byte[] SymmetricSign(ChannelToken token, ReadOnlySpan dataToSign, bool useClientKeys) + { + // get HMAC object. + HMAC hmac = (useClientKeys) ? token.ClientHmac : token.ServerHmac; + + // compute hash. + int hashSizeInBytes = hmac.HashSize >> 3; + byte[] signature = new byte[hashSizeInBytes]; + bool result = hmac.TryComputeHash(dataToSign, signature, out int bytesWritten); + + // check result + if (!result || bytesWritten != hashSizeInBytes) + { + ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "The computed hash doesn't match the expected size."); + } + // return signature. + return signature; + } +#else /// - /// Signs the message using SHA1 HMAC + /// Signs the message using HMAC. /// private static byte[] SymmetricSign(ChannelToken token, ArraySegment dataToSign, bool useClientKeys) { // get HMAC object. HMAC hmac = (useClientKeys) ? token.ClientHmac : token.ServerHmac; - // compute hash. MemoryStream istrm = new MemoryStream(dataToSign.Array, dataToSign.Offset, dataToSign.Count, false); byte[] signature = hmac.ComputeHash(istrm); @@ -991,7 +1018,34 @@ private static byte[] SymmetricSign(ChannelToken token, ArraySegment dataT // return signature. return signature; } +#endif +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + /// + /// Verifies a HMAC for a message. + /// + private bool SymmetricVerify( + ChannelToken token, + ReadOnlySpan signature, + ReadOnlySpan dataToVerify, + bool useClientKeys) + { + // get HMAC object. + HMAC hmac = (useClientKeys) ? token.ClientHmac : token.ServerHmac; + + // compute hash. + int hashSizeInBytes = hmac.HashSize >> 3; + Span computedSignature = stackalloc byte[hashSizeInBytes]; + bool result = hmac.TryComputeHash(dataToVerify, computedSignature, out int bytesWritten); + Debug.Assert(bytesWritten == hashSizeInBytes); + // compare signatures. + if (!result || !computedSignature.SequenceEqual(signature)) + { + string expectedSignature = Utils.ToHexString(computedSignature.ToArray()); + string messageType = Encoding.UTF8.GetString(dataToVerify.Slice(0, 4)); + int messageLength = BitConverter.ToInt32(dataToVerify.Slice(4)); + string actualSignature = Utils.ToHexString(signature); +#else /// /// Verifies a HMAC for a message. /// @@ -1004,31 +1058,27 @@ private bool SymmetricVerify( // get HMAC object. HMAC hmac = (useClientKeys) ? token.ClientHmac : token.ServerHmac; - // compute hash. MemoryStream istrm = new MemoryStream(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count, false); byte[] computedSignature = hmac.ComputeHash(istrm); istrm.Dispose(); - // compare signatures. - for (int ii = 0; ii < signature.Length; ii++) + if (!Utils.IsEqual(computedSignature, signature)) { - if (computedSignature[ii] != signature[ii]) - { - string messageType = Encoding.UTF8.GetString(dataToVerify.Array, dataToVerify.Offset, 4); - int messageLength = BitConverter.ToInt32(dataToVerify.Array, dataToVerify.Offset + 4); - string expectedSignature = Utils.ToHexString(computedSignature); - string actualSignature = Utils.ToHexString(signature); - - var message = new StringBuilder(); - message.AppendLine("Channel{0}: Could not validate signature."); - message.AppendLine("ChannelId={1}, TokenId={2}, MessageType={3}, Length={4}"); - message.AppendLine("ExpectedSignature={5}"); - message.AppendLine("ActualSignature={6}"); - Utils.LogError(message.ToString(), Id, token.ChannelId, token.TokenId, - messageType, messageLength, expectedSignature, actualSignature); + string expectedSignature = Utils.ToHexString(computedSignature); + string messageType = Encoding.UTF8.GetString(dataToVerify.Array, dataToVerify.Offset, 4); + int messageLength = BitConverter.ToInt32(dataToVerify.Array, dataToVerify.Offset + 4); + string actualSignature = Utils.ToHexString(signature); +#endif - return false; - } + var message = new StringBuilder(); + message.AppendLine("Channel{0}: Could not validate signature."); + message.AppendLine("ChannelId={1}, TokenId={2}, MessageType={3}, Length={4}"); + message.AppendLine("ExpectedSignature={5}"); + message.AppendLine("ActualSignature={6}"); + Utils.LogError(message.ToString(), Id, token.ChannelId, token.TokenId, + messageType, messageLength, expectedSignature, actualSignature); + + return false; } return true; @@ -1060,7 +1110,6 @@ private static void SymmetricEncrypt( { throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Input data is not an even number of encryption blocks."); } - encryptor.TransformBlock(blockToEncrypt, start, count, blockToEncrypt, start); } } @@ -1097,18 +1146,6 @@ private static void SymmetricDecrypt( } } - private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) - { - iv[0] ^= (byte)((token.TokenId & 0x000000FF)); - iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); - iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); - iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); - iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); - iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); - iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); - iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); - } - #if CURVE25519 /// /// Encrypts a message using a symmetric algorithm. @@ -1353,9 +1390,31 @@ private static void SymmetricVerifyWithPoly1305( } } } + + private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) + { + iv[0] ^= (byte)((token.TokenId & 0x000000FF)); + iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); + iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); + iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); + iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); + iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); + iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); + iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + } #endif #endregion + #region Private Static Fields + private static readonly byte[] s_HkdfClientLabel = new UTF8Encoding().GetBytes("opcua-client"); + private static readonly byte[] s_HkdfServerLabel = new UTF8Encoding().GetBytes("opcua-server"); + private static readonly byte[] s_HkdfAes128SignOnlyKeyLength = BitConverter.GetBytes((ushort)32); + private static readonly byte[] s_HkdfAes256SignOnlyKeyLength = BitConverter.GetBytes((ushort)48); + private static readonly byte[] s_HkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)64); + private static readonly byte[] s_HkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)96); + private static readonly byte[] s_HkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes((ushort)76); + #endregion + #region Private Fields private ChannelToken m_currentToken; private ChannelToken m_previousToken; @@ -1364,6 +1423,7 @@ private static void SymmetricVerifyWithPoly1305( private int m_signatureKeySize; private int m_encryptionKeySize; private int m_encryptionBlockSize; + private bool m_authenticatedEncryption; #endregion } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 5f20ecbf3a..e2dc2a9432 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -174,6 +174,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { + DiscardTokens(); #if ECC_SUPPORT if (m_localNonce != null) { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index a4f67e65d9..100320a6b3 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -65,6 +65,7 @@ public UaSCUaBinaryClientChannel( } m_requests = new ConcurrentDictionary(); + m_random = new Random(); m_lastRequestId = 0; m_ConnectCallback = new EventHandler(OnConnectComplete); m_startHandshake = new TimerCallback(OnScheduledHandshake); @@ -92,6 +93,10 @@ protected override void Dispose(bool disposing) Utils.SilentDispose(m_handshakeTimer); m_handshakeTimer = null; + Utils.SilentDispose(m_requestedToken); + m_requestedToken = null; + m_requests?.Clear(); + m_handshakeOperation = null; } base.Dispose(disposing); @@ -147,8 +152,9 @@ public IAsyncResult BeginConnect(Uri url, int timeout, AsyncCallback callback, o Socket = m_socketFactory.Create(this, BufferManager, Quotas.MaxBufferSize); Socket.BeginConnect(m_via, m_ConnectCallback, operation); } + + return operation; } - return m_handshakeOperation; } /// @@ -949,11 +955,17 @@ private void OnScheduledHandshake(object state) // close the socket and reconnect. State = TcpChannelState.Closed; - if (Socket != null) + // dispose of the tokens. + uint channelId = ChannelId; + ChannelId = 0; + DiscardTokens(); + + var socket = Socket; + if (socket != null) { - Utils.LogInfo("ChannelId {0}: CLIENTCHANNEL SOCKET CLOSED: {1:X8}", ChannelId, Socket.Handle); - Socket.Close(); Socket = null; + Utils.LogInfo("ChannelId {0}: CLIENTCHANNEL SOCKET CLOSED ON SCHEDULED HANDSHAKE: {1:X8}", channelId, socket.Handle); + socket.Close(); } // set the state. @@ -988,6 +1000,7 @@ private void OnHandshakeComplete(IAsyncResult result) { lock (DataLock) { + ServiceResult error = null; try { if (m_handshakeOperation == null) @@ -998,17 +1011,14 @@ private void OnHandshakeComplete(IAsyncResult result) Utils.LogTrace("ChannelId {0}: OnHandshakeComplete", ChannelId); m_handshakeOperation.End(Int32.MaxValue); - m_handshakeOperation = null; - m_reconnecting = false; + + return; } catch (Exception e) { Utils.LogError(e, "ChannelId {0}: Handshake Failed {1}", ChannelId, e.Message); - m_handshakeOperation = null; - m_reconnecting = false; - - ServiceResult error = ServiceResult.Create(e, StatusCodes.BadUnexpectedError, "Unexpected error reconnecting or renewing a token."); + error = ServiceResult.Create(e, StatusCodes.BadUnexpectedError, "Unexpected error reconnecting or renewing a token."); // check for expired channel or token. if (error.Code == StatusCodes.BadTcpSecureChannelUnknown || error.Code == StatusCodes.BadSecurityChecksFailed) @@ -1017,9 +1027,14 @@ private void OnHandshakeComplete(IAsyncResult result) Shutdown(error); return; } - - ForceReconnect(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, "Unexpected error reconnecting or renewing a token.")); } + finally + { + OperationCompleted(m_handshakeOperation); + m_reconnecting = false; + } + + ForceReconnect(error); } } @@ -1125,13 +1140,10 @@ private void Shutdown(ServiceResult reason) } // cancel all requests. - List operations = new List(m_requests.Values); - - foreach (WriteOperation operation in operations) + foreach (var operation in m_requests.ToArray()) { - operation.Fault(new ServiceResult(StatusCodes.BadSecureChannelClosed, reason)); + operation.Value.Fault(new ServiceResult(StatusCodes.BadSecureChannelClosed, reason)); } - m_requests.Clear(); uint channelId = ChannelId; @@ -1145,14 +1157,16 @@ private void Shutdown(ServiceResult reason) // clear the handshake state. m_handshakeOperation = null; + Utils.SilentDispose(m_requestedToken); m_requestedToken = null; m_reconnecting = false; - if (Socket != null) + var socket = Socket; + if (socket != null) { - Utils.LogInfo("ChannelId {0}: CLIENTCHANNEL SOCKET CLOSED: {1:X8}", channelId, Socket.Handle); - Socket.Close(); Socket = null; + Utils.LogInfo("ChannelId {0}: CLIENTCHANNEL SOCKET CLOSED SHUTDOWN: {1:X8}", channelId, socket.Handle); + socket.Close(); } // set the state. @@ -1183,17 +1197,14 @@ private void ForceReconnect(ServiceResult reason) Utils.LogWarning("ChannelId {0}: Force reconnect reason={1}", Id, reason); // cancel all requests. - List operations = new List(m_requests.Values); - - foreach (WriteOperation operation in operations) + foreach (var operation in m_requests.ToArray()) { - operation.Fault(new ServiceResult(StatusCodes.BadSecureChannelClosed, reason)); + operation.Value.Fault(new ServiceResult(StatusCodes.BadSecureChannelClosed, reason)); } - m_requests.Clear(); // halt any existing handshake. - if (m_handshakeOperation != null && !m_handshakeOperation.IsCompleted) + if (m_handshakeOperation?.IsCompleted == false) { m_handshakeOperation.Fault(reason); return; @@ -1211,6 +1222,7 @@ private void ForceReconnect(ServiceResult reason) // clear the handshake state. m_handshakeOperation = null; + Utils.SilentDispose(m_requestedToken); m_requestedToken = null; m_reconnecting = true; @@ -1256,19 +1268,20 @@ private void ScheduleTokenRenewal(ChannelToken token) m_handshakeTimer = null; } - // calculate renewal timing based on token lifetime. - DateTime expiryTime = token.CreatedAt.AddMilliseconds(token.Lifetime); - - double timeToRenewal = ((expiryTime.Ticks - DateTime.UtcNow.Ticks) / TimeSpan.TicksPerMillisecond) * TcpMessageLimits.TokenRenewalPeriod; - + // calculate renewal timing based on token lifetime + jitter. Do not rely on the server time! + int jitterResolution = (int)Math.Round(token.Lifetime * TcpMessageLimits.TokenRenewalJitterPeriod); + int jitter = m_random.Next(-jitterResolution, jitterResolution); + int timeToRenewal = (int)Math.Round(token.Lifetime * TcpMessageLimits.TokenRenewalPeriod) + + jitter - (HiResClock.TickCount - token.CreatedAtTickCount); if (timeToRenewal < 0) { timeToRenewal = 0; } - Utils.LogInfo("ChannelId {0}: Token Expiry {1}, renewal scheduled in {2} ms.", ChannelId, expiryTime, (int)timeToRenewal); + Utils.LogInfo("ChannelId {0}: Token Expiry {1:HH:mm:ss.fff}, renewal scheduled at {2:HH:mm:ss.fff} in {3} ms.", + ChannelId, token.CreatedAt.AddMilliseconds(token.Lifetime), HiResClock.UtcTickCount(token.CreatedAtTickCount + timeToRenewal), timeToRenewal); - m_handshakeTimer = new Timer(m_startHandshake, token, (int)timeToRenewal, Timeout.Infinite); + m_handshakeTimer = new Timer(m_startHandshake, token, timeToRenewal, Timeout.Infinite); } /// @@ -1280,7 +1293,7 @@ private WriteOperation BeginOperation(int timeout, AsyncCallback callback, objec operation.RequestId = Utils.IncrementIdentifier(ref m_lastRequestId); if (!m_requests.TryAdd(operation.RequestId, operation)) { - throw new ServiceResultException(StatusCodes.BadUnexpectedError, "Could not add operation to list of pending operations."); + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Could not add request {0} to list of pending operations.", operation.RequestId); } return operation; } @@ -1295,12 +1308,15 @@ private void OperationCompleted(WriteOperation operation) return; } - if (m_handshakeOperation == operation) + if (Object.ReferenceEquals(m_handshakeOperation, operation)) { m_handshakeOperation = null; } - m_requests.TryRemove(operation.RequestId, out _); + if (!m_requests.TryRemove(operation.RequestId, out _)) + { + Utils.LogWarning("Could not remove requestId {0} from list of pending operations.", operation.RequestId); + } } /// @@ -1347,6 +1363,10 @@ private void OnConnectOnDemandComplete(object state) request.Operation.Fault(StatusCodes.BadNoCommunication, "Error establishing a connection: " + e.Message); continue; } + finally + { + OperationCompleted(operation); + } } if (this.CurrentToken == null) @@ -1381,9 +1401,10 @@ private WriteOperation InternalClose(int timeout) } // check if a handshake is in progress. - if (m_handshakeOperation != null && !m_handshakeOperation.IsCompleted) + if (m_handshakeOperation?.IsCompleted == false) { m_handshakeOperation.Fault(ServiceResult.Create(StatusCodes.BadConnectionClosed, "Channel was closed by the user.")); + OperationCompleted(m_handshakeOperation); } Utils.LogTrace("ChannelId {0}: Close", ChannelId); @@ -1428,6 +1449,7 @@ protected bool ProcessErrorMessage(uint messageType, ArraySegment messageC if (m_handshakeOperation != null) { m_handshakeOperation.Fault(error); + OperationCompleted(m_handshakeOperation); return false; } @@ -1508,7 +1530,6 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message // check if operation is still available. WriteOperation operation = null; - if (!m_requests.TryGetValue(requestId, out operation)) { return false; @@ -1596,6 +1617,7 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message private TimerCallback m_startHandshake; private AsyncCallback m_handshakeComplete; private List m_queuedOperations; + private Random m_random; private readonly string g_ImplementationString = "UA.NETStandard ClientChannel {0} " + Utils.GetAssemblyBuildNumber(); #endregion } diff --git a/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs index 69f4dc6868..86cb9cc460 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs @@ -117,6 +117,17 @@ public int MaxChannelCount get { return m_maxChannelCount; } set { m_maxChannelCount = value; } } + + /// + /// Indicates if Http listener requires mutual TLS + /// Handled only by HttpsTransportListner + /// In case true, the client should provide it's own valid TLS certificate to the TLS layer for the connection to succeed. + /// + public bool HttpsMutualTls + { + get { return m_httpMutualTls; } + set { m_httpMutualTls = value; } + } #endregion #region Private Fields @@ -128,6 +139,7 @@ public int MaxChannelCount private IEncodeableFactory m_channelFactory; private bool m_reverseConnectListener; private int m_maxChannelCount; + private bool m_httpMutualTls; #endregion } } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 6dd072593c..e517198c9f 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -901,7 +901,7 @@ public Int32Collection ReadInt32Array(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadInt32(null)); + values.Add(SafeReadInt32()); } return values; @@ -945,7 +945,7 @@ public Int64Collection ReadInt64Array(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadInt64(null)); + values.Add(SafeReadInt64()); } return values; @@ -967,7 +967,7 @@ public UInt64Collection ReadUInt64Array(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadUInt64(null)); + values.Add(SafeReadUInt64()); } return values; @@ -989,7 +989,7 @@ public FloatCollection ReadFloatArray(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadFloat(null)); + values.Add(SafeReadFloat()); } return values; @@ -1011,7 +1011,7 @@ public DoubleCollection ReadDoubleArray(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadDouble(null)); + values.Add(SafeReadDouble()); } return values; @@ -1547,22 +1547,22 @@ private DiagnosticInfo ReadDiagnosticInfo(string fieldName, int depth) // read the fields of the diagnostic info structure. if ((encodingByte & (byte)DiagnosticInfoEncodingBits.SymbolicId) != 0) { - value.SymbolicId = ReadInt32(null); + value.SymbolicId = SafeReadInt32(); } if ((encodingByte & (byte)DiagnosticInfoEncodingBits.NamespaceUri) != 0) { - value.NamespaceUri = ReadInt32(null); + value.NamespaceUri = SafeReadInt32(); } if ((encodingByte & (byte)DiagnosticInfoEncodingBits.Locale) != 0) { - value.Locale = ReadInt32(null); + value.Locale = SafeReadInt32(); } if ((encodingByte & (byte)DiagnosticInfoEncodingBits.LocalizedText) != 0) { - value.LocalizedText = ReadInt32(null); + value.LocalizedText = SafeReadInt32(); } if ((encodingByte & (byte)DiagnosticInfoEncodingBits.AdditionalInfo) != 0) @@ -1617,7 +1617,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadBoolean(null); + values[ii] = SafeReadBoolean(); } array = values; @@ -1656,7 +1656,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadInt16(null); + values[ii] = SafeReadInt16(); } array = values; @@ -1669,7 +1669,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadUInt16(null); + values[ii] = SafeReadUInt16(); } array = values; @@ -1683,7 +1683,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadInt32(null); + values[ii] = SafeReadInt32(); } array = values; break; @@ -1708,7 +1708,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadInt64(null); + values[ii] = SafeReadInt64(); } array = values; @@ -1721,7 +1721,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadUInt64(null); + values[ii] = SafeReadUInt64(); } array = values; @@ -1734,7 +1734,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadFloat(null); + values[ii] = SafeReadFloat(); } array = values; @@ -1747,7 +1747,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadDouble(null); + values[ii] = SafeReadDouble(); } array = values; @@ -2096,15 +2096,16 @@ private ExtensionObject ReadExtensionObject() return extension; } - // get the length. - int length = ReadInt32(null); + // Get the length. + // Allow a length of -1 to support legacy devices that don't fill the length correctly + int length = SafeReadInt32(); // save the current position. int start = Position; // create instance of type. IEncodeable encodeable = null; - if (systemType != null && length >= 0) + if (systemType != null && length >= -1) { encodeable = Activator.CreateInstance(systemType) as IEncodeable; @@ -2132,7 +2133,7 @@ private ExtensionObject ReadExtensionObject() // verify the decoder did not exceed the length of the encodeable object int used = Position - start; - if (length != used) + if (length >= 0 && length != used) { errorMessage = "Length mismatch"; exception = null; @@ -2211,11 +2212,14 @@ private ExtensionObject ReadExtensionObject() } // any unread data indicates a decoding error. - long unused = length - (Position - start); - if (unused > 0) + if (length >= 0) { - throw ServiceResultException.Create(StatusCodes.BadDecodingError, - "Cannot skip {0} bytes of unknown extension object body with type '{1}'.", unused, extension.TypeId); + long unused = length - (Position - start); + if (unused > 0) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot skip {0} bytes of unknown extension object body with type '{1}'.", unused, extension.TypeId); + } } if (encodeable != null) @@ -2307,7 +2311,7 @@ private Variant ReadVariantValue(string fieldName) case BuiltInType.Boolean: { - value.Set(ReadBoolean(null)); + value.Set(SafeReadBoolean()); break; } @@ -2325,20 +2329,20 @@ private Variant ReadVariantValue(string fieldName) case BuiltInType.Int16: { - value.Set(ReadInt16(null)); + value.Set(SafeReadInt16()); break; } case BuiltInType.UInt16: { - value.Set(ReadUInt16(null)); + value.Set(SafeReadUInt16()); break; } case BuiltInType.Int32: case BuiltInType.Enumeration: { - value.Set(ReadInt32(null)); + value.Set(SafeReadInt32()); break; } @@ -2350,25 +2354,25 @@ private Variant ReadVariantValue(string fieldName) case BuiltInType.Int64: { - value.Set(ReadInt64(null)); + value.Set(SafeReadInt64()); break; } case BuiltInType.UInt64: { - value.Set(ReadUInt64(null)); + value.Set(SafeReadUInt64()); break; } case BuiltInType.Float: { - value.Set(ReadFloat(null)); + value.Set(SafeReadFloat()); break; } case BuiltInType.Double: { - value.Set(ReadDouble(null)); + value.Set(SafeReadDouble()); break; } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs index 48522da10c..91e0a81f45 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs @@ -193,6 +193,16 @@ public void WriteRawBytes(byte[] buffer, int offset, int count) m_writer.Write(buffer, offset, count); } +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + /// + /// Writes raw bytes to the stream. + /// + public void WriteRawBytes(ReadOnlySpan buffer) + { + m_writer.Write(buffer); + } +#endif + /// /// Encodes a message in a buffer. /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 61a86f4397..54068ac40f 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -82,7 +82,7 @@ public JsonDecoder(string json, IServiceMessageContext context) /// /// Create a JSON decoder to decode a from a . /// - /// The system type of the encoded JSON stram. + /// The system type of the encoded JSON stream. /// The text reader. /// The service message context to use. public JsonDecoder(Type systemType, JsonTextReader reader, IServiceMessageContext context) @@ -2847,7 +2847,7 @@ private T ReadEnumeratedString(object token, TryParseHandler handler) wher if (handler?.Invoke(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) == false) { int lastIndex = text.LastIndexOf('_'); - if (lastIndex == -1) + if (lastIndex != -1) { text = text.Substring(lastIndex + 1); retry = true; @@ -3280,7 +3280,7 @@ private void EncodeAsJson(JsonTextWriter writer, object value) EncodeAsJson(writer, element); } - writer.WriteStartArray(); + writer.WriteEndArray(); return; } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index ca25ebd363..15555f4160 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -407,7 +407,6 @@ public void PushStructure(string fieldName) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -436,7 +435,6 @@ public void PushArray(string fieldName) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -594,21 +592,22 @@ public void PopNamespace() /// Using a span to escape the string, write strings to stream writer if possible. /// /// - private void EscapeString(string value) + private void EscapeString(ReadOnlySpan value) { - ReadOnlySpan charSpan = value.AsSpan(); int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) + m_writer.Write(s_quotation); + + for (int i = 0; i < value.Length; i++) { bool found = false; - char ch = charSpan[i]; + char ch = value[i]; for (int ii = 0; ii < m_specialChars.Length; ii++) { if (m_specialChars[ii] == ch) { - WriteSpan(ref lastOffset, charSpan, i); + WriteSpan(ref lastOffset, value, i); m_writer.Write('\\'); m_writer.Write(m_substitution[ii]); found = true; @@ -618,7 +617,7 @@ private void EscapeString(string value) if (!found && ch < 32) { - WriteSpan(ref lastOffset, charSpan, i); + WriteSpan(ref lastOffset, value, i); m_writer.Write('\\'); m_writer.Write('u'); m_writer.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); @@ -631,7 +630,7 @@ private void EscapeString(string value) } else { - WriteSpan(ref lastOffset, charSpan, charSpan.Length); + WriteSpan(ref lastOffset, value, value.Length); } } @@ -658,6 +657,8 @@ private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int ind /// private void EscapeString(string value) { + m_writer.Write(s_quotation); + foreach (char ch in value) { bool found = false; @@ -717,7 +718,6 @@ private void WriteSimpleField(string fieldName, string value) m_writer.Write(s_comma); } - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -755,9 +755,9 @@ private void WriteSimpleField(string fieldName, string value, EscapeOptions opti m_writer.Write(s_comma); } - m_writer.Write(s_quotation); if ((options & EscapeOptions.NoFieldNameEscape) == EscapeOptions.NoFieldNameEscape) { + m_writer.Write(s_quotation); m_writer.Write(fieldName); } else @@ -778,9 +778,9 @@ private void WriteSimpleField(string fieldName, string value, EscapeOptions opti { if ((options & EscapeOptions.Quotes) == EscapeOptions.Quotes) { - m_writer.Write(s_quotation); if ((options & EscapeOptions.NoValueEscape) == EscapeOptions.NoValueEscape) { + m_writer.Write(s_quotation); m_writer.Write(value); } else @@ -1011,26 +1011,7 @@ public void WriteString(string fieldName, string value) /// Writes a UTC date/time to the stream. /// public void WriteDateTime(string fieldName, DateTime value) - { - if (fieldName != null && !IncludeDefaultValues && value == DateTime.MinValue) - { - WriteSimpleFieldNull(fieldName); - return; - } - - if (value <= DateTime.MinValue) - { - WriteSimpleField(fieldName, "\"0001-01-01T00:00:00Z\""); - } - else if (value >= DateTime.MaxValue) - { - WriteSimpleField(fieldName, "\"9999-12-31T23:59:59Z\""); - } - else - { - WriteSimpleField(fieldName, ConvertUniversalTimeToString(value), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); - } - } + => WriteDateTime(fieldName, value, EscapeOptions.None); /// /// Writes a GUID to the stream. @@ -1332,32 +1313,7 @@ public void WriteExpandedNodeId(string fieldName, ExpandedNodeId value) /// Writes an StatusCode to the stream. /// public void WriteStatusCode(string fieldName, StatusCode value) - { - if (fieldName != null && !IncludeDefaultValues && value == StatusCodes.Good) - { - WriteSimpleFieldNull(fieldName); - return; - } - - if (EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact) - { - WriteUInt32(fieldName, value.Code); - return; - } - - // Verbose and NonReversible - PushStructure(fieldName); - if (value != StatusCodes.Good) - { - WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape); - string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); - if (!string.IsNullOrEmpty(symbolicId)) - { - WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); - } - } - PopStructure(); - } + => WriteStatusCode(fieldName, value, EscapeOptions.None); /// /// Writes a DiagnosticInfo to the stream. @@ -1461,7 +1417,6 @@ public void WriteVariant(string fieldName, Variant value) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -1506,12 +1461,12 @@ public void WriteDataValue(string fieldName, DataValue value) if (value.StatusCode != StatusCodes.Good) { - WriteStatusCode("StatusCode", value.StatusCode); + WriteStatusCode("StatusCode", value.StatusCode, EscapeOptions.NoFieldNameEscape); } if (value.SourceTimestamp != DateTime.MinValue) { - WriteDateTime("SourceTimestamp", value.SourceTimestamp); + WriteDateTime("SourceTimestamp", value.SourceTimestamp, EscapeOptions.NoFieldNameEscape); if (value.SourcePicoseconds != 0) { @@ -1521,7 +1476,7 @@ public void WriteDataValue(string fieldName, DataValue value) if (value.ServerTimestamp != DateTime.MinValue) { - WriteDateTime("ServerTimestamp", value.ServerTimestamp); + WriteDateTime("ServerTimestamp", value.ServerTimestamp, EscapeOptions.NoFieldNameEscape); if (value.ServerPicoseconds != 0) { @@ -2567,7 +2522,7 @@ public void WriteVariantContents(object value, TypeInfo typeInfo) } /// - /// Writes an Variant array to the stream. + /// Writes a Variant array to the stream. /// public void WriteObjectArray(string fieldName, IList values) { @@ -2725,6 +2680,101 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #endregion #region Private Methods + /// + /// Push structure with an option to not escape a known fieldname. + /// + private void PushStructure(string fieldName, EscapeOptions escapeOptions = EscapeOptions.None) + { + m_nestingLevel++; + + if (m_commaRequired) + { + m_writer.Write(s_comma); + } + + if (!string.IsNullOrEmpty(fieldName)) + { + if ((escapeOptions & EscapeOptions.NoFieldNameEscape) != 0) + { + m_writer.Write(s_quotation); + m_writer.Write(fieldName); + } + else + { + EscapeString(fieldName); + } + m_writer.Write(s_quotationColon); + } + else if (!m_commaRequired) + { + if (m_nestingLevel == 1 && !m_topLevelIsArray) + { + m_levelOneSkipped = true; + return; + } + } + + m_commaRequired = false; + m_writer.Write(s_leftCurlyBrace); + } + + /// + /// Writes an StatusCode to the stream. + /// + private void WriteStatusCode(string fieldName, StatusCode value, EscapeOptions escapeOptions) + { + if (fieldName != null && !IncludeDefaultValues && value == StatusCodes.Good) + { + WriteSimpleFieldNull(fieldName); + return; + } + + if (EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact) + { + WriteUInt32(fieldName, value.Code); + return; + } + + // Verbose and NonReversible + PushStructure(fieldName, escapeOptions); + if (value != StatusCodes.Good) + { + WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape | EscapeOptions.NoValueEscape); + string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); + if (!string.IsNullOrEmpty(symbolicId)) + { + WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape | EscapeOptions.NoValueEscape); + } + } + PopStructure(); + } + + /// + /// Writes a UTC date/time to the stream. Reduce escape overhead for fieldname. + /// + private void WriteDateTime(string fieldName, DateTime value, EscapeOptions escapeOptions) + { + if (fieldName != null && !IncludeDefaultValues && value == DateTime.MinValue) + { + WriteSimpleFieldNull(fieldName); + return; + } + + escapeOptions |= EscapeOptions.NoValueEscape; + if (value <= DateTime.MinValue) + { + WriteSimpleField(fieldName, "\"0001-01-01T00:00:00Z\"", escapeOptions); + } + else if (value >= DateTime.MaxValue) + { + WriteSimpleField(fieldName, "\"9999-12-31T23:59:59Z\"", escapeOptions); + } + else + { + WriteSimpleField(fieldName, ConvertUniversalTimeToString(value), escapeOptions | EscapeOptions.Quotes); + } + } + /// /// Returns true if a simple field can be written. /// @@ -2825,7 +2875,7 @@ private void WriteDiagnosticInfo(string fieldName, DiagnosticInfo value, int dep if (value.InnerStatusCode != StatusCodes.Good) { - WriteStatusCode("InnerStatusCode", value.InnerStatusCode); + WriteStatusCode("InnerStatusCode", value.InnerStatusCode, EscapeOptions.NoFieldNameEscape); } if (value.InnerDiagnosticInfo != null) diff --git a/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs b/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs index d4cf87edc8..b2a0b1c2d0 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs @@ -81,6 +81,16 @@ public static long TickCount64 /// public static int TickCount => Environment.TickCount; + /// + /// Returns the Utc time with respect to the current tick count. + /// + public static DateTime UtcTickCount(int tickCount) + { + DateTime utcNow = DateTime.UtcNow; + int delta = tickCount - TickCount; + return utcNow.AddMilliseconds(delta); + } + /// /// Disables the hires clock. /// diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 371038461d..c89e3cb9fb 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -13,21 +13,21 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections; using System.Collections.Generic; -using System.Text; -using System.Xml; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; -using System.Security.Cryptography.X509Certificates; -using System.Reflection; -using System.Runtime.Serialization; using System.IO; using System.Linq; -using System.Threading.Tasks; -using System.Diagnostics; -using System.Security.Cryptography; using System.Net; -using System.Collections.ObjectModel; -using Opc.Ua.Security.Certificates; +using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Opc.Ua.Security.Certificates; using NewNonceImplementation = Opc.Ua.Nonce; @@ -164,6 +164,16 @@ public static bool IsUriHttpsScheme(string url) return url.StartsWith(Utils.UriSchemeHttps, StringComparison.Ordinal) || url.StartsWith(Utils.UriSchemeOpcHttps, StringComparison.Ordinal); } + + /// + /// Returns true if the url starts with http, opc.https or https. + /// + /// The url + public static bool IsUriHttpRelatedScheme(string url) + { + return url.StartsWith(Utils.UriSchemeHttps, StringComparison.Ordinal) || + IsUriHttpsScheme(url); + } #endregion #region Trace Support @@ -1269,37 +1279,30 @@ public static string EscapeUri(string uri) { if (!string.IsNullOrWhiteSpace(uri)) { - // back compat: for not well formed Uri, fall back to legacy formatting behavior - see #2793 - if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute) || - !Uri.TryCreate(uri.Replace(";", "%3b"), UriKind.Absolute, out Uri validUri)) + // always use back compat: for not well formed Uri, fall back to legacy formatting behavior - see #2793, #2826 + // problem with Uri.TryCreate(uri.Replace(";", "%3b"), UriKind.Absolute, out Uri validUri); + // -> uppercase letters will later be lowercase (and therefore the uri will later be non-matching) + var buffer = new StringBuilder(); + foreach (char ch in uri) { - var buffer = new StringBuilder(); - foreach (char ch in uri) + switch (ch) { - switch (ch) + case ';': + case '%': { - case ';': - case '%': - { - buffer.AppendFormat(CultureInfo.InvariantCulture, "%{0:X2}", Convert.ToInt16(ch)); - break; - } + buffer.AppendFormat(CultureInfo.InvariantCulture, "%{0:X2}", Convert.ToInt16(ch)); + break; + } - default: - { - buffer.Append(ch); - break; - } + default: + { + buffer.Append(ch); + break; } } - return buffer.ToString(); - } - else - { - return validUri.AbsoluteUri; } + return buffer.ToString(); } - return String.Empty; } @@ -1553,6 +1556,7 @@ public static Array FlattenArray(Array array) /// /// Converts a buffer to a hexadecimal string. /// +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER public static string ToHexString(byte[] buffer, bool invertEndian = false) { if (buffer == null || buffer.Length == 0) @@ -1560,6 +1564,27 @@ public static string ToHexString(byte[] buffer, bool invertEndian = false) return String.Empty; } + return ToHexString(new ReadOnlySpan(buffer), invertEndian); + } + + /// + /// Converts a buffer to a hexadecimal string. + /// + public static string ToHexString(ReadOnlySpan buffer, bool invertEndian = false) + { + if (buffer.Length == 0) + { + return String.Empty; + } +#else + public static string ToHexString(byte[] buffer, bool invertEndian = false) + { + if (buffer == null || buffer.Length == 0) + { + return String.Empty; + } +#endif + #if NET6_0_OR_GREATER if (!invertEndian) { @@ -2974,8 +2999,10 @@ public static byte[] PSHA1(byte[] secret, string label, byte[] data, int offset, { if (secret == null) throw new ArgumentNullException(nameof(secret)); // create the hmac. - HMACSHA1 hmac = new HMACSHA1(secret); - return PSHA(hmac, label, data, offset, length); + using (HMACSHA1 hmac = new HMACSHA1(secret)) + { + return PSHA(hmac, label, data, offset, length); + } } /// @@ -2985,10 +3012,32 @@ public static byte[] PSHA256(byte[] secret, string label, byte[] data, int offse { if (secret == null) throw new ArgumentNullException(nameof(secret)); // create the hmac. - HMACSHA256 hmac = new HMACSHA256(secret); + using (HMACSHA256 hmac = new HMACSHA256(secret)) + { + return PSHA(hmac, label, data, offset, length); + } + } + + /// + /// Generates a Pseudo random sequence of bits using the P_SHA1 alhorithm. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", + Justification = "SHA1 is needed for deprecated security profiles.")] + public static byte[] PSHA1(HMACSHA1 hmac, string label, byte[] data, int offset, int length) + { + return PSHA(hmac, label, data, offset, length); + } + + /// + /// Generates a Pseudo random sequence of bits using the P_SHA256 alhorithm. + /// + public static byte[] PSHA256(HMACSHA256 hmac, string label, byte[] data, int offset, int length) + { return PSHA(hmac, label, data, offset, length); } + /// /// Generates a Pseudo random sequence of bits using the HMAC algorithm. /// @@ -3037,7 +3086,6 @@ public static byte[] PSHA(HMAC hmac, string label, byte[] data, int offset, int byte[] output = new byte[length]; int position = 0; - do { byte[] hash = hmac.ComputeHash(prfSeed); 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 1672657e9b..1da96a2c36 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/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index a59848e734..68e508324e 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -218,7 +218,15 @@ public async Task Connect(string endpointUrl) /// public async Task ConnectAsync(Uri url, string securityProfile, EndpointDescriptionCollection endpoints = null, IUserIdentity userIdentity = null) { - return await ConnectAsync(await GetEndpointAsync(url, securityProfile, endpoints).ConfigureAwait(false), userIdentity).ConfigureAwait(false); + string uri = url.AbsoluteUri; + Uri getEndpointsUrl = url; + if (uri.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) || + Utils.IsUriHttpsScheme(uri)) + { + getEndpointsUrl = CoreClientUtils.GetDiscoveryUrl(uri); + } + + return await ConnectAsync(await GetEndpointAsync(getEndpointsUrl, securityProfile, endpoints).ConfigureAwait(false), userIdentity).ConfigureAwait(false); } /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 7b30df3118..e4fb6b6721 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -161,7 +161,7 @@ public async Task GetEndpointsAsync() if (endpoint.ServerCertificate != null) { - using (var cert = new X509Certificate2(endpoint.ServerCertificate)) + using (var cert = X509CertificateLoader.LoadCertificate(endpoint.ServerCertificate)) { TestContext.Out.WriteLine(" [{0}]", cert.Thumbprint); } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index 5b28e56d22..ccadc6f49e 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -158,7 +158,16 @@ public async Task OneTimeSetUpAsync(TextWriter writer = null, } else { - ServerUrl = new Uri(UriScheme + "://localhost:" + ServerFixturePort.ToString(CultureInfo.InvariantCulture)); + string url = UriScheme + "://localhost:" + ServerFixturePort.ToString(CultureInfo.InvariantCulture); + + if (UriScheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) || + Utils.IsUriHttpsScheme(UriScheme)) + { + url = url + ConfiguredEndpoint.DiscoverySuffix; + } + + ServerUrl = new Uri(url); + } if (SingleSession) 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 f43c0d631c..c1916c3608 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index 70805f3b6d..12fe6cb81b 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -479,34 +479,55 @@ public void SequentialPublishingSubscription(bool enabled) [Test, Combinatorial, Order(350), Explicit] public Task ReconnectWithSavedSessionSecretsSync( [Values(SecurityPolicies.None, - SecurityPolicies.Basic256Sha256, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_nistP384)] string securityPolicy, +#if ECC_SUPPORT + SecurityPolicies.ECC_nistP256, + SecurityPolicies.ECC_nistP384, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1, +#endif + SecurityPolicies.Basic256Sha256)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) => ReconnectWithSavedSessionSecretsAsync(securityPolicy, anonymous, sequentialPublishing, sendInitialValues, false); +#if ECC_SUPPORT + /// + /// Open a session on a channel, then reconnect (activate) + /// the same session on a new channel with saved session secrets. + /// Use only asnc methods. Test the ECC profiles. + /// + [Test, Combinatorial, Order(351), Explicit] + public Task ReconnectWithSavedSessionSecretsOnlyECCAsync( + [Values( + SecurityPolicies.ECC_nistP256, + SecurityPolicies.ECC_nistP384, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1)] string securityPolicy, + [Values(true, false)] bool anonymous, + [Values(true, false)] bool sequentialPublishing, + [Values(true, false)] bool sendInitialValues) + => ReconnectWithSavedSessionSecretsAsync(securityPolicy, anonymous, sequentialPublishing, sendInitialValues, true); +#endif + /// /// Open a session on a channel, then reconnect (activate) /// the same session on a new channel with saved session secrets. /// Use only asnc methods. /// - [Test, Combinatorial, Order(351)] + [Test, Combinatorial, Order(352)] public Task ReconnectWithSavedSessionSecretsOnlyAsync( [Values(SecurityPolicies.None, - SecurityPolicies.Basic256Sha256, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_nistP384)] string securityPolicy, +#if ECC_SUPPORT + SecurityPolicies.ECC_nistP256, +#endif + SecurityPolicies.Basic256Sha256)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) => ReconnectWithSavedSessionSecretsAsync(securityPolicy, anonymous, sequentialPublishing, sendInitialValues, true); + public async Task ReconnectWithSavedSessionSecretsAsync(string securityPolicy, bool anonymous, bool sequentialPublishing, bool sendInitialValues, bool asyncTest) { const int kTestSubscriptions = 5; diff --git a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs index 8a26638531..9602e6db70 100644 --- a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs +++ b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs @@ -366,7 +366,7 @@ public async Task TestNoFileConfigAsServerX509Store() { // store public key in trusted store var rawData = applicationCertificate.Certificate.RawData; - await store.Add(new X509Certificate2(rawData)).ConfigureAwait(false); + await store.Add(X509CertificateLoader.LoadCertificate(rawData)).ConfigureAwait(false); } if (deleteAfterUse) @@ -475,7 +475,7 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -568,7 +568,7 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -774,7 +774,7 @@ private X509Certificate2Collection CreateInvalidCertChain(InvalidCertType certTy var result = new X509Certificate2Collection { appCert, - new X509Certificate2(rootCA.RawData) + X509CertificateLoader.LoadCertificate(rootCA.RawData) }; return result; 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 0c90649ccf..fd58f9d418 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -21,7 +21,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 ea041e1922..71a4a42b19 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.Core.Tests/Security/Certificates/CertificateFactoryTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs index 93637490d7..37f36749b0 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs @@ -111,7 +111,7 @@ KeyHashPair keyHashPair { rsa.ExportParameters(false); } - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -143,7 +143,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - using (var plainCert = new X509Certificate2(cert.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData)) { Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert, issuerCertificate); @@ -171,7 +171,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyCACert(plainCert, subject, pathLengthConstraint); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -220,7 +220,7 @@ KeyHashPair keyHashPair Assert.NotNull(rsa); } - using (var plainCert = new X509Certificate2(issuerCertificate.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(issuerCertificate.RawData)) { Assert.NotNull(plainCert); VerifyCACert(plainCert, issuerCertificate.Subject, pathLengthConstraint); @@ -285,7 +285,7 @@ public void ParseCertificateBlob() byte[] singleBlob = AsnUtils.ParseX509Blob(certBlob).ToArray(); Assert.NotNull(singleBlob); - var certX = new X509Certificate2(singleBlob); + var certX = X509CertificateLoader.LoadCertificate(singleBlob); Assert.NotNull(certX); Assert.AreEqual(certArray[0].RawData, singleBlob); Assert.AreEqual(singleBlob, certX.RawData); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs index d77170d59a..012ba43a0e 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs @@ -118,7 +118,7 @@ public async Task VerifyAppCertX509Store(string storePath) CertificateStoreType.X509Store, storePath ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); @@ -163,7 +163,7 @@ public async Task VerifyAppCertDirectoryStore() certificateStoreIdentifier, password ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs index c07a86ecdd..1068b783f4 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs @@ -479,7 +479,7 @@ public async Task VerifyAppChainsOneTrusted() TestContext.Out.WriteLine($"InitValidator: {stopWatch.ElapsedMilliseconds - start}"); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } TestContext.Out.WriteLine($"Validation: {stopWatch.ElapsedMilliseconds - start}"); } @@ -507,7 +507,7 @@ public async Task VerifyAppChainsAllButOneTrusted() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -535,7 +535,7 @@ public async Task VerifyAppChainsIncompleteChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -569,7 +569,7 @@ public async Task VerifyAppChainsInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -598,7 +598,7 @@ public async Task VerifyAppChainsWithGoodAndInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -631,7 +631,7 @@ public async Task VerifyRevokedTrustedStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -665,7 +665,7 @@ public async Task VerifyRevokedIssuerStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -698,12 +698,12 @@ public async Task VerifyRevokedIssuerStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -736,7 +736,7 @@ public async Task VerifyRevokedTrustedStoreNotTrustedAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -768,12 +768,12 @@ public async Task VerifyRevokedTrustedStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -799,13 +799,13 @@ public async Task VerifyIssuerChainIncompleteTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -834,13 +834,13 @@ public async Task VerifyIssuerChainTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -859,9 +859,9 @@ public void VerifyPemWriterPrivateKeys() var pemDataBlob = PEMWriter.ExportPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -897,10 +897,10 @@ public void VerifyPemWriterRSAPrivateKeys() var pemDataBlob = PEMWriter.ExportRSAPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); Assert.NotNull(cert); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -1378,7 +1378,7 @@ public async Task VerifySomeMissingCRLRevokedTrustedStoreAppChains(bool rejectUn foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); @@ -1421,7 +1421,7 @@ public async Task VerifyAllMissingCRLRevokedTrustedStoreAppChains() foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.IsTrue(StatusCodes.BadCertificateRevocationUnknown == serviceResultException.StatusCode, serviceResultException.Message); @@ -1483,7 +1483,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe { if (rejectUnknownRevocationStatus) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevocationUnknown : StatusCodes.BadCertificateIssuerRevocationUnknown, serviceResultException.StatusCode, @@ -1491,7 +1491,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe } else { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -1523,7 +1523,7 @@ public async Task VerifyMissingCRLANDAppChainsIncompleteChain(bool rejectUnknown foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // no need to check for inner exceptions, since an incomplete chain error cannot be suppressed. } @@ -1547,12 +1547,12 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno { if (i != v || kCaChainCount == 1) { - await validator.TrustedStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.TrustedStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } else { - await validator.IssuerStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.IssuerStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.IssuerStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } } @@ -1563,7 +1563,7 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -1602,7 +1602,7 @@ public async Task VerifyMissingCRLAppChainsExpiredCertificates(bool rejectUnknow foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // BadCertificateTimeInvalid can be suppressed. Ensure the other issues are caught, as well: @@ -1663,7 +1663,7 @@ public async Task VerifyMissingCRLNoTrust(bool rejectUnknownRevocationStatus) foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateUntrusted, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs index 50da763274..9d8f407bd7 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs @@ -36,9 +36,15 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using Microsoft.IO; +using Newtonsoft.Json; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; +#if NET6_0_OR_GREATER +using System.Text.Encodings.Web; +using System.Text.Json; +#endif + namespace Opc.Ua.Core.Tests.Types.Encoders { [TestFixture, Category("JsonEncoder")] @@ -214,6 +220,37 @@ public void EscapeStringSpanDict() m_streamWriter.Flush(); } + /// + /// Using NewtonSoft, which first converts to string. + /// + [Benchmark] + public void EscapeStringNewtonSoft() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringNewtonSoft(m_testString); + } + m_streamWriter.Flush(); + } + +#if NET6_0_OR_GREATER + /// + /// A new implementation using ReadOnlySpan and Dictionary. + /// + [Benchmark] + public void EscapeStringSystemTextJson() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSystemTextJson(m_testString); + } + } +#endif + [Theory] [TestCase("No Escape chars", 0)] [TestCase("control chars escaped, 1 char space", 1)] @@ -225,26 +262,30 @@ public void EscapeStringSpanDict() [TestCase("binary chars escaped, 3 char spaces", 7)] [TestCase("binary chars escaped, 5 char spaces", 8)] [TestCase("all escape chars and long string", 9)] + [NonParallelizable] public void EscapeStringValidation(string name, int index) { + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + m_testString = EscapeTestStrings[index]; TestContext.Out.WriteLine(m_testString); var testArray = m_testString.ToCharArray(); m_memoryStream.Position = 0; - EscapeStringLegacy(); + EscapedStringLegacy(m_testString); m_streamWriter.Flush(); byte[] resultLegacy = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); m_memoryStream.Position = 0; - EscapeStringLegacyPlus(); + EscapedStringLegacyPlus(m_testString); m_streamWriter.Flush(); byte[] resultLegacyPlus = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); m_memoryStream.Position = 0; - EscapeStringStringBuilder(); + EscapeString(m_testString); m_streamWriter.Flush(); byte[] result = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); @@ -285,6 +326,21 @@ public void EscapeStringValidation(string name, int index) byte[] resultSpanDict = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + EscapeStringNewtonSoft(m_testString); + m_streamWriter.Flush(); + byte[] resultNewtonSoft = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultNewtonSoft)); + +#if NET6_0_OR_GREATER + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + EscapeStringSystemTextJson(m_testString); + byte[] resultSystemTextJson = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSystemTextJson)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSystemTextJson)); +#endif + Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); @@ -293,6 +349,7 @@ public void EscapeStringValidation(string name, int index) Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultNewtonSoft)); } #region Test Setup @@ -771,6 +828,21 @@ private void EscapeStringSpanDict(string value) } } + private void EscapeStringNewtonSoft(string value) + { + string newtonSoftConvertedText = JsonConvert.ToString(value); + newtonSoftConvertedText = newtonSoftConvertedText.Substring(1, newtonSoftConvertedText.Length - 2); + m_streamWriter.Write(newtonSoftConvertedText); + } + +#if NET6_0_OR_GREATER + private void EscapeStringSystemTextJson(string value) + { + var jsonEncodedText = JsonEncodedText.Encode(m_testString, JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + m_memoryStream.Write(jsonEncodedText.EncodedUtf8Bytes); + } +#endif + private void EscapeString(string value) { StringBuilder stringBuilder = new StringBuilder(value.Length * 2); diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index c9df6309e5..0118a9b034 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -794,7 +794,7 @@ public void StartGoodSigningRequests() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); @@ -1089,7 +1089,7 @@ public void GoodSigningRequestAsSelfAdmin() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs index b1ec5aa5fa..9c8c64fe1c 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs @@ -211,7 +211,7 @@ public string ReadLogFile() #region Private Methods private async Task ApplyNewApplicationInstanceCertificateAsync(byte[] certificate, byte[] privateKey) { - using (var x509 = new X509Certificate2(certificate)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate)) { var certWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(x509, privateKey); m_client.Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(certWithPrivateKey); 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 e3fb13096d..e1a99a28c6 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -24,7 +24,7 @@ - + diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 8650d8f10b..880623d65d 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -83,7 +83,7 @@ protected async Task OneTimeSetUp() ConnectGDSClient(true); RegisterPushServerApplication(m_pushClient.PushClient.EndpointUrl); - m_selfSignedServerCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); + m_selfSignedServerCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); m_domainNames = X509Utils.GetDomainsFromCertificate(m_selfSignedServerCert).ToArray(); await CreateCATestCerts(m_pushClient.TempStorePath).ConfigureAwait(false); @@ -328,7 +328,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() { ConnectPushClient(true); using (X509Certificate2 invalidCert = CertificateFactory.CreateCertificate("uri:x:y:z", "TestApp", "CN=Push Server Test", null).CreateForRSA()) - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -358,7 +358,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() public void UpdateCertificateSelfSignedNoPrivateKey() { ConnectPushClient(true); - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -620,7 +620,7 @@ public void GetCertificates() Assert.That(certificateTypeIds.Length == 1); Assert.NotNull(certificates[0]); - using (var x509 = new X509Certificate2(certificates[0])) + using (var x509 = X509CertificateLoader.LoadCertificate(certificates[0])) { Assert.NotNull(x509); } @@ -680,7 +680,7 @@ private X509Certificate2Collection CreateCertCollection(ByteStringCollection cer var result = new X509Certificate2Collection(); foreach (var rawCert in certList) { - result.Add(new X509Certificate2(rawCert)); + result.Add(X509CertificateLoader.LoadCertificate(rawCert)); } return result; } @@ -750,7 +750,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust issuerCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.IssuerCertificates) { - issuerCertificates.Add(new X509Certificate2(cert)); + issuerCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.IssuerCrls) != 0) @@ -766,7 +766,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust trustedCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.TrustedCertificates) { - trustedCertificates.Add(new X509Certificate2(cert)); + trustedCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.TrustedCrls) != 0) diff --git a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs index e61ad47a1c..8bfa910deb 100644 --- a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs +++ b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs @@ -42,7 +42,7 @@ public static class X509TestUtils { public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] privateKey, string privateKeyPassword, string privateKeyFormat, byte[][] issuerCertificates) { - X509Certificate2 newCert = new X509Certificate2(certificate); + X509Certificate2 newCert = X509CertificateLoader.LoadCertificate(certificate); Assert.IsNotNull(newCert); X509Certificate2 newPrivateKeyCert = null; if (privateKeyFormat == "PFX") @@ -64,7 +64,7 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri CertificateIdentifierCollection issuerCertIdCollection = new CertificateIdentifierCollection(); foreach (var issuer in issuerCertificates) { - var issuerCert = new X509Certificate2(issuer); + var issuerCert = X509CertificateLoader.LoadCertificate(issuer); Assert.IsNotNull(issuerCert); issuerCertIdCollection.Add(new CertificateIdentifier(issuerCert)); } @@ -85,8 +85,8 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri public static void VerifySignedApplicationCert(ApplicationTestData testApp, byte[] rawSignedCert, byte[][] rawIssuerCerts) { - X509Certificate2 signedCert = new X509Certificate2(rawSignedCert); - X509Certificate2 issuerCert = new X509Certificate2(rawIssuerCerts[0]); + X509Certificate2 signedCert = X509CertificateLoader.LoadCertificate(rawSignedCert); + X509Certificate2 issuerCert = X509CertificateLoader.LoadCertificate(rawIssuerCerts[0]); TestContext.Out.WriteLine($"Signed cert: {signedCert}"); TestContext.Out.WriteLine($"Issuer cert: {issuerCert}"); 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 e94dd88109..34dd6373bf 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index 536f78fd0b..a6302d9055 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -174,7 +174,7 @@ public void CrlBuilderTest(bool empty, bool noExtensions, KeyHashPair keyHashPai Assert.AreEqual(2, x509Crl.CrlExtensions.Count); } - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } @@ -219,7 +219,7 @@ public void CrlBuilderTestWithSignatureGenerator(KeyHashPair keyHashPair) Assert.AreEqual(serial, x509Crl.RevokedCertificates[0].UserCertificate); Assert.AreEqual(serstring, x509Crl.RevokedCertificates[1].SerialNumber); Assert.AreEqual(2, x509Crl.CrlExtensions.Count); - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs index 2f2cc4a85a..feb037b27b 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs @@ -193,7 +193,7 @@ public void Initialize(byte[] blob, string path) Cert = blob; try { - X509Certificate = new X509Certificate2(path); + X509Certificate = X509CertificateLoader.LoadCertificateFromFile(path); } catch { } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs index 72939db302..dbb8f1cd5d 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs @@ -35,7 +35,7 @@ using System.Security.Cryptography.X509Certificates; using NUnit.Framework; using Opc.Ua.Tests; - using Assert = NUnit.Framework.Legacy.ClassicAssert; +using Assert = NUnit.Framework.Legacy.ClassicAssert; #if NETFRAMEWORK using Org.BouncyCastle.X509; #endif @@ -99,7 +99,7 @@ public void VerifyOneSelfSignedAppCertForAll() .SetECCurve(eCCurveHash.Curve) .CreateForECDsa()) { - + Assert.NotNull(cert); WriteCertificate(cert, $"Default cert with ECDsa {eCCurveHash.Curve.Oid.FriendlyName} {eCCurveHash.HashAlgorithmName} signature."); Assert.AreEqual(eCCurveHash.HashAlgorithmName, Oids.GetHashAlgorithmName(cert.SignatureAlgorithm.Value)); @@ -308,7 +308,7 @@ ECCurveHashPair ecCurveHashPair { var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .CreateForRSA(generator); Assert.NotNull(cert); WriteCertificate(cert, "Default signed ECDsa cert"); @@ -320,7 +320,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECDsaPublicKey(ecdsaPublicKey) .CreateForECDsa(generator); Assert.NotNull(cert); @@ -332,7 +332,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECCurve(ecCurveHashPair.Curve) .CreateForECDsa(generator); Assert.NotNull(cert); @@ -376,7 +376,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECDsaPublicKey(pubKeyBytes) .CreateForECDsa(generator); Assert.NotNull(cert); @@ -435,7 +435,7 @@ private static byte[] GetPublicKey(ECDsa ecdsa) return ecdsa.ExportSubjectPublicKeyInfo(); #endif } -#endregion + #endregion #region Private Fields #endregion diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs index 4e7000b683..7e3dfc296a 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs @@ -354,7 +354,7 @@ public void CreateIssuerRSAWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -392,7 +392,7 @@ public void CreateIssuerRSACngWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -429,7 +429,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetIssuer(issuer) .CreateForRSA(generator)) @@ -446,7 +446,7 @@ KeyHashPair keyHashPair using (RSA rsaPublicKey = signingCert.GetRSAPublicKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) @@ -464,7 +464,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs index 5078a02647..a574e1282d 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs @@ -56,7 +56,7 @@ public void DecodeExtensions( CertificateAsset certAsset ) { - using (var x509Cert = new X509Certificate2(certAsset.Cert)) + using (var x509Cert = X509CertificateLoader.LoadCertificate(certAsset.Cert)) { Assert.NotNull(x509Cert); TestContext.Out.WriteLine("CertificateAsset:"); 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 74deacf33f..3eb757da87 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 @@ - + @@ -53,4 +53,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + 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 6d535bd756..660d8dc02f 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/Tests/customtest.bat b/Tests/customtest.bat index 7ae1bbecb3..fef271ed2a 100644 --- a/Tests/customtest.bat +++ b/Tests/customtest.bat @@ -2,19 +2,19 @@ setlocal enabledelayedexpansion echo This script is used to run custom platform tests for the UA Core Library -echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 +echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net 9.0 REM Check if the target framework parameter is provided if "%1"=="" ( echo Usage: %0 [TargetFramework] - echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, default + echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0, default goto :eof ) REM Check if the provided TargetFramework is valid -set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 " +set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 net9.0 " if "!validFrameworks: %1 =!"=="%validFrameworks%" ( - echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0 + echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0, net9.0 goto :eof ) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2ede8b5c..0be491de2c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,6 +102,16 @@ stages: framework: net8.0 configuration: Release jobnamesuffix: net80 +- stage: testnet90 + dependsOn: [build] + displayName: 'Test .NET 9.0' + condition: and(succeeded(), ne(variables.ScheduledBuild, 'False')) + jobs: + - template: .azurepipelines/test.yml + parameters: + framework: net9.0 + configuration: Release + jobnamesuffix: net90 - stage: testnet462 dependsOn: [build] displayName: 'Test .NET 4.6.2' diff --git a/targets.props b/targets.props index 17b539d7c1..b1cf79a211 100644 --- a/targets.props +++ b/targets.props @@ -12,7 +12,7 @@ A build with all custom targets which are not part of a regular build is scheduled once a week in the DevOps build pipeline. Uncomment the following lines to test a custom test target - supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 + supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0 --> - + - preview-all - net8.0;net6.0;net48 - net8.0 - net48;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net462;net472;net48;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 + preview + all + default + net9.0;net6.0;net48 + net9.0 + net48;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net462;net472;net48;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 diff --git a/version.json b/version.json index 2ee8aa01c2..ba9e3d2525 100644 --- a/version.json +++ b/version.json @@ -8,6 +8,7 @@ "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/master$", + "^refs/heads/develop/", "^refs/heads/release/\\d+\\.\\d+\\.\\d+" ], "cloudBuild": {