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