From 4bee42cef678e297e00c971fdcfa6539aff621bd Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Wed, 23 Oct 2024 16:57:42 +0200 Subject: [PATCH 01/27] ValidateRolePermissions for MIs montioring the Value of a Node --- Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs b/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs index 640052e78a..6c4676904b 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 reading the value MonitoredItem without permissions + continue; + } + QueueValue(context, node, monitoredItem); continue; } From dfa4c9a122e84c13cfd1f6793f54db592f430547 Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Wed, 23 Oct 2024 17:47:11 +0200 Subject: [PATCH 02/27] fix typo --- Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs b/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs index 6c4676904b..53b4511078 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/MonitoredNode.cs @@ -299,7 +299,7 @@ public void OnMonitoredNodeChanged(ISystemContext context, NodeState node, NodeS if (ServiceResult.IsBad(validationResult)) { - // skip reading the value MonitoredItem without permissions + // skip if the monitored item does not have permission to read continue; } From 16b9aeff4d23efc01f7a80cfbd8ae0004d7dad3b Mon Sep 17 00:00:00 2001 From: romanett Date: Thu, 24 Oct 2024 05:49:26 +0200 Subject: [PATCH 03/27] Add ReturnDiagnostics to Session Constructor (#2810) returndiagnostics was not propagated to recreated and reconnected sessions --- Libraries/Opc.Ua.Client/Session/Session.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 3de3e6573b..caf4cda25c 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -136,6 +136,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; From d60adc197a522bde36d7f81f83639472d5cd303e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 24 Oct 2024 12:55:40 +0200 Subject: [PATCH 04/27] IOP: Fix FetchOperationLimits for some use cases (#2807) * an exception in FetchOperationLimits may have skipped applying the configured operation limits on the server --- Libraries/Opc.Ua.Client/Session/Session.cs | 16 ++++++++-------- .../Opc.Ua.Client/Session/SessionAsync.cs | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index caf4cda25c..e9f7052bf9 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1687,19 +1687,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) { diff --git a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs index 2c38ffcfba..d569a8aa7e 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs @@ -639,8 +639,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; @@ -670,17 +669,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) From b3b2f9154e7bcb8383620aeacd437284381f2df6 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 26 Oct 2024 09:39:04 +0200 Subject: [PATCH 05/27] Update version.json to allow preview builds from develop (#2813) --- version.json | 1 + 1 file changed, 1 insertion(+) diff --git a/version.json b/version.json index 2868ff5f72..e8b1cc07dd 100644 --- a/version.json +++ b/version.json @@ -8,6 +8,7 @@ "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/master$", + "^refs/heads/develop/", "^refs/heads/release/\\d+\\.\\d+\\.\\d+" ], "cloudBuild": { From ce9850c491b9f4093bcd505e5c5709840448ee8e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 31 Oct 2024 08:02:32 +0100 Subject: [PATCH 06/27] Fix bugs in JSON decoder (#2828) JSON ReadEnumeratedString does not decode the number in e.g. "Red_0". JSON reencode of JSON content in an extension object runs into an encoder error. --- Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs | 6 ++++++ Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) 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/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; } From 787844c011d0b109c3e7a51f40be49e3f1183cf2 Mon Sep 17 00:00:00 2001 From: Suciu Mircea Adrian Date: Thu, 31 Oct 2024 22:03:55 +0200 Subject: [PATCH 07/27] Update brokerHostName before MqttClientOptionsBuilder uses it's value (#2830) --- .../Transport/MqttPubSubConnection.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs index 937c751dcc..61fcaf46cd 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) { @@ -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, From ff3674fe58112afb1999a8f564abe2c3a45aa5c4 Mon Sep 17 00:00:00 2001 From: romanett Date: Sat, 2 Nov 2024 11:44:51 +0100 Subject: [PATCH 08/27] Improve crl handling in certificate stores (#2829) * improve crl handling in certificate stores by not loading CRL with invalid or unsupported content. Hence the revocation check for such certificates may fail. --- .../X509Crl/X509Crl.cs | 9 +++------ .../Opc.Ua.Server/Configuration/TrustList.cs | 4 ++-- .../Certificates/DirectoryCertificateStore.cs | 15 +++++++++++---- .../X509CertificateStore/X509CertificateStore.cs | 13 +++++++++---- 4 files changed, 25 insertions(+), 16 deletions(-) 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/TrustList.cs b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs index c33e77a217..685b395b05 100644 --- a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs +++ b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs @@ -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/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index 5d61a0d9a0..abea61dde9 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 { @@ -620,7 +619,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; } @@ -670,8 +669,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); + } + } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs index 8cb54b9aae..9be4daf454 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; @@ -251,7 +250,6 @@ public async Task IsRevoked(X509Certificate2 issuer, X509Certificate foreach (X509CRL crl in crls) { - if (!X509Utils.CompareDistinguishedName(crl.IssuerName, issuer.SubjectName)) { continue; @@ -299,8 +297,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); From 56dd06d97fc282f6d4a64b6e6b8ed344677ce8ee Mon Sep 17 00:00:00 2001 From: KircMax Date: Thu, 7 Nov 2024 10:31:18 +0100 Subject: [PATCH 09/27] Using Uri.TryCreate causes regression with namespace uri that use mixed lower/uppercase letters in the of the Uri.(#2837) Uri.TryCreate lower cases the hostnames. Switch always back to the legacy implementation which maintains the casing. --- Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 61 ++++++++++++-------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index e6236c51e7..9cb66fda65 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -13,21 +13,21 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections; using System.Collections.Generic; -using System.Text; -using System.Xml; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; -using System.Security.Cryptography.X509Certificates; -using System.Reflection; -using System.Runtime.Serialization; using System.IO; using System.Linq; -using System.Threading.Tasks; -using System.Diagnostics; -using System.Security.Cryptography; using System.Net; -using System.Collections.ObjectModel; -using Opc.Ua.Security.Certificates; +using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Opc.Ua.Security.Certificates; namespace Opc.Ua { @@ -1267,37 +1267,30 @@ public static string EscapeUri(string uri) { if (!string.IsNullOrWhiteSpace(uri)) { - // back compat: for not well formed Uri, fall back to legacy formatting behavior - see #2793 - if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute) || - !Uri.TryCreate(uri.Replace(";", "%3b"), UriKind.Absolute, out Uri validUri)) + // always use back compat: for not well formed Uri, fall back to legacy formatting behavior - see #2793, #2826 + // problem with Uri.TryCreate(uri.Replace(";", "%3b"), UriKind.Absolute, out Uri validUri); + // -> uppercase letters will later be lowercase (and therefore the uri will later be non-matching) + var buffer = new StringBuilder(); + foreach (char ch in uri) { - var buffer = new StringBuilder(); - foreach (char ch in uri) + switch (ch) { - switch (ch) + case ';': + case '%': { - case ';': - case '%': - { - buffer.AppendFormat(CultureInfo.InvariantCulture, "%{0:X2}", Convert.ToInt16(ch)); - break; - } + buffer.AppendFormat(CultureInfo.InvariantCulture, "%{0:X2}", Convert.ToInt16(ch)); + break; + } - default: - { - buffer.Append(ch); - break; - } + default: + { + buffer.Append(ch); + break; } } - return buffer.ToString(); - } - else - { - return validUri.AbsoluteUri; } + return buffer.ToString(); } - return String.Empty; } @@ -2604,7 +2597,7 @@ public static void UpdateExtension(ref XmlElementCollection extensions, XmlQu extensions.Add(document.DocumentElement); } } -#endregion + #endregion #region Reflection Helper Functions /// From 76b53184de0a34016db1c223834899d4a3576425 Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:16:13 +0100 Subject: [PATCH 10/27] ChannelToken: Dispose HMAC and improve lifetime calculations. (#2846) - Channels are not disposed (client and server) - During long running tests an HMAC object leak was observed. - The HMAC and SymmetricSign objects were not disposed after use, leading to a small incremental memory leak per key renewal. - The channel token lifetime is calculated by the system clock, which can change. Instead use the continous HiResClock.TickCount to calculate the lifetimes. - Token renewal and connection requests are not removed from the request dictionary Improvements: - Make ChannelToken disposable. Dispose channels. - Dispose HMAC and SymmetricSign objects after use. - Use TickCount to calculate key renewal and key expiry. - Introduce a 5% jitter to the token renewal timer, to avoid token renewal storms. - Reduce the allocation per use of the HMAC objects by using ReadOnlySpan and avoid MemoryStream. --- .editorconfig | 3 + .../Common/AsnUtils.cs | 37 ++- Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs | 74 ++++- Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs | 5 + .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 22 +- .../Stack/Tcp/TcpTransportListener.cs | 17 +- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 294 +++++++++++------- .../Stack/Tcp/UaSCBinaryChannel.cs | 2 +- .../Stack/Tcp/UaSCBinaryClientChannel.cs | 100 +++--- .../Types/Encoders/BinaryEncoder.cs | 10 + Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs | 10 + Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 55 +++- 12 files changed, 438 insertions(+), 191 deletions(-) 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/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/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/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 8ce1377c47..869a907f6f 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -67,7 +67,10 @@ public TcpServerChannel( /// protected override void Dispose(bool disposing) { - base.Dispose(disposing); + lock (DataLock) + { + base.Dispose(disposing); + } } #endregion @@ -549,6 +552,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); @@ -1134,7 +1143,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 38bcd7616a..2753bdd72a 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -241,8 +241,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 +309,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 @@ -533,6 +541,7 @@ private void OnAccept(object sender, SocketAsyncEventArgs e) // check if the accept socket has been created. if (serveChannel && e.AcceptSocket != null && e.SocketError == SocketError.Success) { + channel = null; try { if (m_reverseConnectListener) @@ -578,11 +587,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); + } } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 8d91e253af..c94ec114d1 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 { @@ -41,15 +42,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; } @@ -62,20 +64,25 @@ 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; - 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); } /// @@ -83,8 +90,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; } #endregion @@ -176,25 +187,37 @@ protected void ComputeKeys(ChannelToken token) return; } - if (SecurityPolicyUri == SecurityPolicies.Basic256Sha256 || - SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep || - SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss) + bool useSHA256 = SecurityPolicyUri != SecurityPolicies.Basic128Rsa15 && SecurityPolicyUri != SecurityPolicies.Basic256; + + if (useSHA256) { - token.ClientSigningKey = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, 0, m_signatureKeySize); - token.ClientEncryptingKey = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); - token.ClientInitializationVector = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); - token.ServerSigningKey = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, 0, m_signatureKeySize); - token.ServerEncryptingKey = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); - token.ServerInitializationVector = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + using (HMACSHA256 hmac = new HMACSHA256(token.ServerNonce)) + { + token.ClientSigningKey = Utils.PSHA256(hmac, null, token.ClientNonce, 0, m_signatureKeySize); + token.ClientEncryptingKey = Utils.PSHA256(hmac, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); + token.ClientInitializationVector = Utils.PSHA256(hmac, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + } + using (HMACSHA256 hmac = new HMACSHA256(token.ClientNonce)) + { + token.ServerSigningKey = Utils.PSHA256(hmac, null, token.ServerNonce, 0, m_signatureKeySize); + token.ServerEncryptingKey = Utils.PSHA256(hmac, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); + token.ServerInitializationVector = Utils.PSHA256(hmac, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + } } else { - token.ClientSigningKey = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, 0, m_signatureKeySize); - token.ClientEncryptingKey = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); - token.ClientInitializationVector = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); - token.ServerSigningKey = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, 0, m_signatureKeySize); - token.ServerEncryptingKey = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); - token.ServerInitializationVector = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + using (HMACSHA1 hmac = new HMACSHA1(token.ServerNonce)) + { + token.ClientSigningKey = Utils.PSHA1(hmac, null, token.ClientNonce, 0, m_signatureKeySize); + token.ClientEncryptingKey = Utils.PSHA1(hmac, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); + token.ClientInitializationVector = Utils.PSHA1(hmac, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + } + using (HMACSHA1 hmac = new HMACSHA1(token.ClientNonce)) + { + token.ServerSigningKey = Utils.PSHA1(hmac, null, token.ServerNonce, 0, m_signatureKeySize); + token.ServerEncryptingKey = Utils.PSHA1(hmac, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); + token.ServerInitializationVector = Utils.PSHA1(hmac, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + } } switch (SecurityPolicyUri) @@ -206,24 +229,22 @@ protected void ComputeKeys(ChannelToken token) case SecurityPolicies.Aes256_Sha256_RsaPss: { // 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; - - // create HMACs. - if (SecurityPolicyUri == SecurityPolicies.Basic256Sha256 || - SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep || - SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss) + 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; + + // create HMACs. Must be disposed after use. + if (useSHA256) { // SHA256 token.ServerHmac = new HMACSHA256(token.ServerSigningKey); @@ -277,7 +298,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. @@ -311,6 +331,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++) @@ -325,28 +348,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; } @@ -377,9 +396,11 @@ protected BufferCollection WriteSymmetricMessage( // 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; @@ -405,9 +426,20 @@ protected BufferCollection WriteSymmetricMessage( // write padding. if (SecurityMode == MessageSecurityMode.SignAndEncrypt) { - 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); + } } } @@ -432,10 +464,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. @@ -486,10 +514,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); } @@ -502,7 +529,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, @@ -524,8 +551,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; @@ -536,15 +563,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)) @@ -552,26 +578,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. @@ -693,15 +717,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); @@ -710,7 +756,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. /// @@ -723,31 +796,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); - - return false; - } + 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 + + 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; @@ -779,7 +848,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); } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index f69fc55d99..0bb3d2fc6f 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -150,7 +150,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // nothing to do. + DiscardTokens(); } } #endregion diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 9c6915cece..84508c5142 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -81,6 +81,7 @@ public UaSCUaBinaryClientChannel( } m_requests = new ConcurrentDictionary(); + m_random = new Random(); m_lastRequestId = 0; m_ConnectCallback = new EventHandler(OnConnectComplete); m_startHandshake = new TimerCallback(OnScheduledHandshake); @@ -106,6 +107,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); @@ -161,8 +166,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; } /// @@ -504,7 +510,7 @@ private bool ProcessAcknowledgeMessage(ArraySegment messageChunk) decoder.Close(); } - + // ready to open the channel. State = TcpChannelState.Opening; @@ -958,11 +964,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. @@ -997,6 +1009,7 @@ private void OnHandshakeComplete(IAsyncResult result) { lock (DataLock) { + ServiceResult error = null; try { if (m_handshakeOperation == null) @@ -1007,17 +1020,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) @@ -1026,9 +1036,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); } } @@ -1134,13 +1149,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; @@ -1154,14 +1166,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. @@ -1192,17 +1206,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; @@ -1220,6 +1231,7 @@ private void ForceReconnect(ServiceResult reason) // clear the handshake state. m_handshakeOperation = null; + Utils.SilentDispose(m_requestedToken); m_requestedToken = null; m_reconnecting = true; @@ -1265,19 +1277,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); } /// @@ -1289,7 +1302,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; } @@ -1304,12 +1317,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.LogError("Could not remove requestId {0} from list of pending operations.", operation.RequestId); + } } /// @@ -1356,6 +1372,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) @@ -1390,9 +1410,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); @@ -1437,6 +1458,7 @@ protected bool ProcessErrorMessage(uint messageType, ArraySegment messageC if (m_handshakeOperation != null) { m_handshakeOperation.Fault(error); + OperationCompleted(m_handshakeOperation); return false; } @@ -1517,7 +1539,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; @@ -1605,6 +1626,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/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/Utils/HiResClock.cs b/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs index d4cf87edc8..b2a0b1c2d0 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/HiResClock.cs @@ -81,6 +81,16 @@ public static long TickCount64 /// public static int TickCount => Environment.TickCount; + /// + /// Returns the Utc time with respect to the current tick count. + /// + public static DateTime UtcTickCount(int tickCount) + { + DateTime utcNow = DateTime.UtcNow; + int delta = tickCount - TickCount; + return utcNow.AddMilliseconds(delta); + } + /// /// Disables the hires clock. /// diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 9cb66fda65..54269fe179 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -1544,6 +1544,7 @@ public static Array FlattenArray(Array array) /// /// Converts a buffer to a hexadecimal string. /// +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER public static string ToHexString(byte[] buffer, bool invertEndian = false) { if (buffer == null || buffer.Length == 0) @@ -1551,6 +1552,27 @@ public static string ToHexString(byte[] buffer, bool invertEndian = false) return String.Empty; } + return ToHexString(new ReadOnlySpan(buffer), invertEndian); + } + + /// + /// Converts a buffer to a hexadecimal string. + /// + public static string ToHexString(ReadOnlySpan buffer, bool invertEndian = false) + { + if (buffer.Length == 0) + { + return String.Empty; + } +#else + public static string ToHexString(byte[] buffer, bool invertEndian = false) + { + if (buffer == null || buffer.Length == 0) + { + return String.Empty; + } +#endif + #if NET6_0_OR_GREATER if (!invertEndian) { @@ -2597,7 +2619,7 @@ public static void UpdateExtension(ref XmlElementCollection extensions, XmlQu extensions.Add(document.DocumentElement); } } - #endregion +#endregion #region Reflection Helper Functions /// @@ -3011,8 +3033,10 @@ public static byte[] PSHA1(byte[] secret, string label, byte[] data, int offset, { if (secret == null) throw new ArgumentNullException(nameof(secret)); // create the hmac. - HMACSHA1 hmac = new HMACSHA1(secret); - return PSHA(hmac, label, data, offset, length); + using (HMACSHA1 hmac = new HMACSHA1(secret)) + { + return PSHA(hmac, label, data, offset, length); + } } /// @@ -3022,10 +3046,32 @@ public static byte[] PSHA256(byte[] secret, string label, byte[] data, int offse { if (secret == null) throw new ArgumentNullException(nameof(secret)); // create the hmac. - HMACSHA256 hmac = new HMACSHA256(secret); + using (HMACSHA256 hmac = new HMACSHA256(secret)) + { + return PSHA(hmac, label, data, offset, length); + } + } + + /// + /// Generates a Pseudo random sequence of bits using the P_SHA1 alhorithm. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", + Justification = "SHA1 is needed for deprecated security profiles.")] + public static byte[] PSHA1(HMACSHA1 hmac, string label, byte[] data, int offset, int length) + { + return PSHA(hmac, label, data, offset, length); + } + + /// + /// Generates a Pseudo random sequence of bits using the P_SHA256 alhorithm. + /// + public static byte[] PSHA256(HMACSHA256 hmac, string label, byte[] data, int offset, int length) + { return PSHA(hmac, label, data, offset, length); } + /// /// Generates a Pseudo random sequence of bits using the HMAC algorithm. /// @@ -3074,7 +3120,6 @@ private static byte[] PSHA(HMAC hmac, string label, byte[] data, int offset, int byte[] output = new byte[length]; int position = 0; - do { byte[] hash = hmac.ComputeHash(prfSeed); From 3543d0292556691f681e39145e2de4526b90487d Mon Sep 17 00:00:00 2001 From: Suciu Mircea Adrian Date: Fri, 22 Nov 2024 11:01:30 +0200 Subject: [PATCH 11/27] Added a minimal rogue client detection mechanism at the transport level (#2850) Clients that are failing too often to pass the security validationin a certain interval of time with the Basic128 security profile are now tracked and blocked. --- .../Stack/Tcp/TcpListenerChannel.cs | 13 + .../Stack/Tcp/TcpTransportListener.cs | 266 +++++++++++++++++- 2 files changed, 278 insertions(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index 763b71f1a6..2aeb52f1cd 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; @@ -267,6 +268,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/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index 2753bdd72a..27c51eb717 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; @@ -37,6 +38,233 @@ 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. /// @@ -331,6 +559,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; @@ -505,16 +739,44 @@ public void CertificateUpdate( } #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) { @@ -526,7 +788,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; @@ -821,6 +1083,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 } From 2f21ca885356e775aec06d81a768eac0cc5c6375 Mon Sep 17 00:00:00 2001 From: romanett Date: Thu, 28 Nov 2024 09:52:02 +0100 Subject: [PATCH 12/27] [Server] ValidateRolePermissions of MonitoredItems based of the saved user identity to allow validation when no session is present (#2832) * ValidateRolePermissions for MIs montioring the Value of a Node * allow validation of user identity also in case of disconnected session --- .../DataChangeMonitoredItem.cs | 12 +++++++++ .../Diagnostics/CustomNodeManager.cs | 2 +- .../NodeManager/MasterNodeManager.cs | 4 +-- .../Opc.Ua.Server/Server/OperationContext.cs | 1 + .../Subscription/IMonitoredItem.cs | 5 ++++ .../Subscription/MonitoredItem.cs | 13 ++++++++++ .../Subscription/Subscription.cs | 25 +++++++++++-------- .../Subscription/SubscriptionManager.cs | 4 +-- 8 files changed, 51 insertions(+), 15 deletions(-) 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/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/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 5e0275441f..ff01fe178c 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 From f76c457c72af1b3a3772a82f9bb9ea49d7f86f07 Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:57:10 +0100 Subject: [PATCH 13/27] Support .NET 9.0 build (#2865) * support .NET 9.0 build * provide a helper to use the X509CertificateLoader also on older .NET versions --- .azurepipelines/ci.yml | 5 + .azurepipelines/preview.yml | 4 +- .azurepipelines/signlistDebug.txt | 10 ++ .azurepipelines/signlistRelease.txt | 10 ++ .azurepipelines/test.yml | 4 +- .azurepipelines/testcc.yml | 4 +- .github/workflows/buildandtest.yml | 8 +- .../ConsoleReferenceClient.csproj | 6 +- .../ConsoleReferenceServer.csproj | 6 +- Libraries/Opc.Ua.Client/Session/Session.cs | 4 +- .../ApplicationInstance.cs | 7 +- .../ServerPushConfigurationClient.cs | 2 +- .../ApplicationsNodeManager.cs | 8 +- .../CertificateGroup.cs | 8 +- .../ICertificateGroup.cs | 2 +- .../MqttClientProtocolConfiguration.cs | 12 +-- .../Transport/MqttPubSubConnection.cs | 6 +- .../X509Certificate/X509CertificateLoader.cs | 91 +++++++++++++++++++ .../X509Certificate/X509PfxUtils.cs | 3 +- .../Configuration/ConfigurationNodeManager.cs | 6 +- .../Opc.Ua.Server/Configuration/TrustList.cs | 6 +- Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 10 +- .../Certificates/CertificateFactory.cs | 31 ++++--- .../Certificates/CertificateValidator.cs | 8 +- .../Certificates/DirectoryCertificateStore.cs | 7 +- .../X509CertificateStore.cs | 2 +- .../Security/Certificates/X509Utils.cs | 2 +- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 2 +- .../ApplicationInstanceTests.cs | 8 +- .../Certificates/CertificateFactoryTest.cs | 10 +- .../Certificates/CertificateStoreTest.cs | 4 +- .../Certificates/CertificateValidatorTest.cs | 60 ++++++------ Tests/Opc.Ua.Gds.Tests/ClientTest.cs | 4 +- .../GlobalDiscoveryTestClient.cs | 2 +- Tests/Opc.Ua.Gds.Tests/PushTest.cs | 14 +-- Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs | 8 +- .../CRLTests.cs | 4 +- .../CertificateTestUtils.cs | 2 +- .../CertificateTestsForECDsa.cs | 6 +- .../CertificateTestsForRSA.cs | 10 +- .../ExtensionTests.cs | 2 +- Tests/customtest.bat | 8 +- azure-pipelines.yml | 10 ++ targets.props | 64 +++++++++---- 44 files changed, 326 insertions(+), 164 deletions(-) create mode 100644 Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509CertificateLoader.cs 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 12f7449ad2..7d926e294f 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/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 00533e2587..001359d2b4 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 bdfd44f97a..04bf1da453 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 0fb89a57ea..0c9ea93ce1 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -29,9 +29,9 @@ - - - + + + diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index e9f7052bf9..6efed4ab8b 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1076,7 +1076,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; } @@ -1440,7 +1440,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; diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 1734790b1a..6ee1b5497a 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -631,7 +631,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) { @@ -995,7 +997,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.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.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 61fcaf46cd..bde6ae01a7 100644 --- a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs +++ b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs @@ -749,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)); } } @@ -852,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/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 03ad62b1f7..19909507f1 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs @@ -153,10 +153,11 @@ public static X509Certificate2 CreateCertificateFromPKCS12( try { // merge first cert with private key into X509Certificate2 - certificate = new X509Certificate2( + certificate = X509CertificateLoader.LoadPkcs12( rawData, password ?? string.Empty, flag); + // can we really access the private key? if (VerifyRSAKeyPair(certificate, certificate, true)) { diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index b63675147d..a41431b66f 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -385,12 +385,12 @@ private ServiceResult UpdateCertificate( { foreach (byte[] issuerRawCert in issuerCertificates) { - var newIssuerCert = new X509Certificate2(issuerRawCert); + var newIssuerCert = X509CertificateLoader.LoadCertificate(issuerRawCert); newIssuerCollection.Add(newIssuerCert); } } - newCert = new X509Certificate2(certificate); + newCert = X509CertificateLoader.LoadCertificate(certificate); } catch { @@ -499,7 +499,7 @@ private ServiceResult UpdateCertificate( var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; appStore.Add(updateCertificate.CertificateWithPrivateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)).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 685b395b05..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 { diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 280460251c..64ef8e948e 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -42,9 +42,10 @@ - + - + + @@ -60,8 +61,7 @@ - - + @@ -71,7 +71,7 @@ - + diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index 2f3acaf631..9ae5457077 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -58,10 +58,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); @@ -368,7 +369,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 /// @@ -453,18 +454,18 @@ public static X509Certificate2 CreateCertificateWithPEMPrivateKey( /// The certificate with a private key. [Obsolete("Use the new CreateCertificate methods with CertificateBuilder.")] internal static X509Certificate2 CreateCertificate( - string applicationUri, - string applicationName, - string subjectName, - IList domainNames, - ushort keySize, - DateTime startTime, - ushort lifetimeInMonths, - ushort hashSizeInBits, - bool isCA = false, - X509Certificate2 issuerCAKeyCert = null, - byte[] publicKey = null, - int pathLengthConstraint = 0) + string applicationUri, + string applicationName, + string subjectName, + IList domainNames, + ushort keySize, + DateTime startTime, + ushort lifetimeInMonths, + ushort hashSizeInBits, + bool isCA = false, + X509Certificate2 issuerCAKeyCert = null, + byte[] publicKey = null, + int pathLengthConstraint = 0) { ICertificateBuilder builder = null; if (isCA) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 4e30f5d1a6..2c13adf880 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -510,7 +510,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; } @@ -529,7 +529,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 { @@ -554,7 +554,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; } @@ -574,7 +574,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 abea61dde9..3aec185c37 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -449,7 +449,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)) { @@ -512,10 +512,11 @@ public async Task LoadPrivateKey(string thumbprint, string sub { try { - certificate = new X509Certificate2( + certificate = X509CertificateLoader.LoadPkcs12FromFile( privateKeyFilePfx.FullName, password, flag); + if (X509Utils.VerifyRSAKeyPair(certificate, certificate, true)) { Utils.LogInfo(Utils.TraceMasks.Security, "Imported the PFX private key for [{0}].", certificate.Thumbprint); @@ -831,7 +832,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 9be4daf454..a65fd0c205 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs @@ -153,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); } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index e0698029ff..6188d59d97 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -511,7 +511,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/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 36463b8816..1e3c060a32 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -156,7 +156,7 @@ public async Task GetEndpointsAsync() if (endpoint.ServerCertificate != null) { - using (var cert = new X509Certificate2(endpoint.ServerCertificate)) + using (var cert = X509CertificateLoader.LoadCertificate(endpoint.ServerCertificate)) { TestContext.Out.WriteLine(" [{0}]", cert.Thumbprint); } diff --git a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs index 95f0130fe8..b756318158 100644 --- a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs +++ b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs @@ -330,7 +330,7 @@ public async Task TestNoFileConfigAsServerX509Store() { // store public key in trusted store var rawData = applicationCertificate.Certificate.RawData; - await store.Add(new X509Certificate2(rawData)).ConfigureAwait(false); + await store.Add(X509CertificateLoader.LoadCertificate(rawData)).ConfigureAwait(false); } if (deleteAfterUse) @@ -427,7 +427,7 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -514,7 +514,7 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -715,7 +715,7 @@ private X509Certificate2Collection CreateInvalidCertChain(InvalidCertType certTy var result = new X509Certificate2Collection { appCert, - new X509Certificate2(rootCA.RawData) + X509CertificateLoader.LoadCertificate(rootCA.RawData) }; return result; diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs index 93637490d7..37f36749b0 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs @@ -111,7 +111,7 @@ KeyHashPair keyHashPair { rsa.ExportParameters(false); } - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -143,7 +143,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - using (var plainCert = new X509Certificate2(cert.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData)) { Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert, issuerCertificate); @@ -171,7 +171,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyCACert(plainCert, subject, pathLengthConstraint); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -220,7 +220,7 @@ KeyHashPair keyHashPair Assert.NotNull(rsa); } - using (var plainCert = new X509Certificate2(issuerCertificate.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(issuerCertificate.RawData)) { Assert.NotNull(plainCert); VerifyCACert(plainCert, issuerCertificate.Subject, pathLengthConstraint); @@ -285,7 +285,7 @@ public void ParseCertificateBlob() byte[] singleBlob = AsnUtils.ParseX509Blob(certBlob).ToArray(); Assert.NotNull(singleBlob); - var certX = new X509Certificate2(singleBlob); + var certX = X509CertificateLoader.LoadCertificate(singleBlob); Assert.NotNull(certX); Assert.AreEqual(certArray[0].RawData, singleBlob); Assert.AreEqual(singleBlob, certX.RawData); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs index d77170d59a..012ba43a0e 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs @@ -118,7 +118,7 @@ public async Task VerifyAppCertX509Store(string storePath) CertificateStoreType.X509Store, storePath ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); @@ -163,7 +163,7 @@ public async Task VerifyAppCertDirectoryStore() certificateStoreIdentifier, password ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs index 71466fbced..23a3f03f50 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs @@ -479,7 +479,7 @@ public async Task VerifyAppChainsOneTrusted() TestContext.Out.WriteLine($"InitValidator: {stopWatch.ElapsedMilliseconds - start}"); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } TestContext.Out.WriteLine($"Validation: {stopWatch.ElapsedMilliseconds - start}"); } @@ -507,7 +507,7 @@ public async Task VerifyAppChainsAllButOneTrusted() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -535,7 +535,7 @@ public async Task VerifyAppChainsIncompleteChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -569,7 +569,7 @@ public async Task VerifyAppChainsInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -598,7 +598,7 @@ public async Task VerifyAppChainsWithGoodAndInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -631,7 +631,7 @@ public async Task VerifyRevokedTrustedStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -665,7 +665,7 @@ public async Task VerifyRevokedIssuerStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -698,12 +698,12 @@ public async Task VerifyRevokedIssuerStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -736,7 +736,7 @@ public async Task VerifyRevokedTrustedStoreNotTrustedAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -768,12 +768,12 @@ public async Task VerifyRevokedTrustedStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -799,13 +799,13 @@ public async Task VerifyIssuerChainIncompleteTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -834,13 +834,13 @@ public async Task VerifyIssuerChainTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -859,9 +859,9 @@ public void VerifyPemWriterPrivateKeys() var pemDataBlob = PEMWriter.ExportPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -897,10 +897,10 @@ public void VerifyPemWriterRSAPrivateKeys() var pemDataBlob = PEMWriter.ExportRSAPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); Assert.NotNull(cert); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -1378,7 +1378,7 @@ public async Task VerifySomeMissingCRLRevokedTrustedStoreAppChains(bool rejectUn foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); @@ -1421,7 +1421,7 @@ public async Task VerifyAllMissingCRLRevokedTrustedStoreAppChains() foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.IsTrue(StatusCodes.BadCertificateRevocationUnknown == serviceResultException.StatusCode, serviceResultException.Message); @@ -1483,7 +1483,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe { if (rejectUnknownRevocationStatus) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevocationUnknown : StatusCodes.BadCertificateIssuerRevocationUnknown, serviceResultException.StatusCode, @@ -1491,7 +1491,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe } else { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -1523,7 +1523,7 @@ public async Task VerifyMissingCRLANDAppChainsIncompleteChain(bool rejectUnknown foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // no need to check for inner exceptions, since an incomplete chain error cannot be suppressed. } @@ -1547,12 +1547,12 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno { if (i != v || kCaChainCount == 1) { - await validator.TrustedStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.TrustedStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } else { - await validator.IssuerStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.IssuerStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.IssuerStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } } @@ -1563,7 +1563,7 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -1602,7 +1602,7 @@ public async Task VerifyMissingCRLAppChainsExpiredCertificates(bool rejectUnknow foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // BadCertificateTimeInvalid can be suppressed. Ensure the other issues are caught, as well: @@ -1663,7 +1663,7 @@ public async Task VerifyMissingCRLNoTrust(bool rejectUnknownRevocationStatus) foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateUntrusted, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index bff7f8ddd8..4624402337 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -794,7 +794,7 @@ public void StartGoodSigningRequests() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); @@ -1089,7 +1089,7 @@ public void GoodSigningRequestAsSelfAdmin() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs index e7ea4a97e5..316312c47a 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs @@ -206,7 +206,7 @@ public string ReadLogFile() #region Private Methods private async Task ApplyNewApplicationInstanceCertificateAsync(byte[] certificate, byte[] privateKey) { - using (var x509 = new X509Certificate2(certificate)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate)) { var certWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(x509, privateKey); m_client.Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(certWithPrivateKey); diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 6b9cfb2362..4c9f272106 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -83,7 +83,7 @@ protected async Task OneTimeSetUp() ConnectGDSClient(true); RegisterPushServerApplication(m_pushClient.PushClient.EndpointUrl); - m_selfSignedServerCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); + m_selfSignedServerCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); m_domainNames = X509Utils.GetDomainsFromCertificate(m_selfSignedServerCert).ToArray(); await CreateCATestCerts(m_pushClient.TempStorePath).ConfigureAwait(false); @@ -328,7 +328,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() { ConnectPushClient(true); using (X509Certificate2 invalidCert = CertificateFactory.CreateCertificate("uri:x:y:z", "TestApp", "CN=Push Server Test", null).CreateForRSA()) - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -358,7 +358,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() public void UpdateCertificateSelfSignedNoPrivateKey() { ConnectPushClient(true); - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -627,7 +627,7 @@ public void GetCertificates() Assert.That(certificateTypeIds.Length == 1); Assert.NotNull(certificates[0]); - using (var x509 = new X509Certificate2(certificates[0])) + using (var x509 = X509CertificateLoader.LoadCertificate(certificates[0])) { Assert.NotNull(x509); } @@ -687,7 +687,7 @@ private X509Certificate2Collection CreateCertCollection(ByteStringCollection cer var result = new X509Certificate2Collection(); foreach (var rawCert in certList) { - result.Add(new X509Certificate2(rawCert)); + result.Add(X509CertificateLoader.LoadCertificate(rawCert)); } return result; } @@ -757,7 +757,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust issuerCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.IssuerCertificates) { - issuerCertificates.Add(new X509Certificate2(cert)); + issuerCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.IssuerCrls) != 0) @@ -773,7 +773,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust trustedCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.TrustedCertificates) { - trustedCertificates.Add(new X509Certificate2(cert)); + trustedCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.TrustedCrls) != 0) diff --git a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs index e61ad47a1c..8bfa910deb 100644 --- a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs +++ b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs @@ -42,7 +42,7 @@ public static class X509TestUtils { public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] privateKey, string privateKeyPassword, string privateKeyFormat, byte[][] issuerCertificates) { - X509Certificate2 newCert = new X509Certificate2(certificate); + X509Certificate2 newCert = X509CertificateLoader.LoadCertificate(certificate); Assert.IsNotNull(newCert); X509Certificate2 newPrivateKeyCert = null; if (privateKeyFormat == "PFX") @@ -64,7 +64,7 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri CertificateIdentifierCollection issuerCertIdCollection = new CertificateIdentifierCollection(); foreach (var issuer in issuerCertificates) { - var issuerCert = new X509Certificate2(issuer); + var issuerCert = X509CertificateLoader.LoadCertificate(issuer); Assert.IsNotNull(issuerCert); issuerCertIdCollection.Add(new CertificateIdentifier(issuerCert)); } @@ -85,8 +85,8 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri public static void VerifySignedApplicationCert(ApplicationTestData testApp, byte[] rawSignedCert, byte[][] rawIssuerCerts) { - X509Certificate2 signedCert = new X509Certificate2(rawSignedCert); - X509Certificate2 issuerCert = new X509Certificate2(rawIssuerCerts[0]); + X509Certificate2 signedCert = X509CertificateLoader.LoadCertificate(rawSignedCert); + X509Certificate2 issuerCert = X509CertificateLoader.LoadCertificate(rawIssuerCerts[0]); TestContext.Out.WriteLine($"Signed cert: {signedCert}"); TestContext.Out.WriteLine($"Issuer cert: {issuerCert}"); diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index 536f78fd0b..a6302d9055 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -174,7 +174,7 @@ public void CrlBuilderTest(bool empty, bool noExtensions, KeyHashPair keyHashPai Assert.AreEqual(2, x509Crl.CrlExtensions.Count); } - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } @@ -219,7 +219,7 @@ public void CrlBuilderTestWithSignatureGenerator(KeyHashPair keyHashPair) Assert.AreEqual(serial, x509Crl.RevokedCertificates[0].UserCertificate); Assert.AreEqual(serstring, x509Crl.RevokedCertificates[1].SerialNumber); Assert.AreEqual(2, x509Crl.CrlExtensions.Count); - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs index 2f2cc4a85a..feb037b27b 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs @@ -193,7 +193,7 @@ public void Initialize(byte[] blob, string path) Cert = blob; try { - X509Certificate = new X509Certificate2(path); + X509Certificate = X509CertificateLoader.LoadCertificateFromFile(path); } catch { } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs index 2c2d24ab7b..767f96a233 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs @@ -304,7 +304,7 @@ ECCurveHashPair ecCurveHashPair { var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .CreateForRSA(generator); Assert.NotNull(cert); WriteCertificate(cert, "Default signed ECDsa cert"); @@ -316,7 +316,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECDsaPublicKey(ecdsaPublicKey) .CreateForECDsa(generator); Assert.NotNull(cert); @@ -328,7 +328,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECCurve(ecCurveHashPair.Curve) .CreateForECDsa(generator); Assert.NotNull(cert); diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs index e9563031e6..7abef4c8f9 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs @@ -350,7 +350,7 @@ public void CreateIssuerRSAWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -388,7 +388,7 @@ public void CreateIssuerRSACngWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -425,7 +425,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetIssuer(issuer) .CreateForRSA(generator)) @@ -442,7 +442,7 @@ KeyHashPair keyHashPair using (RSA rsaPublicKey = signingCert.GetRSAPublicKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) @@ -457,7 +457,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs index 5078a02647..a574e1282d 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs @@ -56,7 +56,7 @@ public void DecodeExtensions( CertificateAsset certAsset ) { - using (var x509Cert = new X509Certificate2(certAsset.Cert)) + using (var x509Cert = X509CertificateLoader.LoadCertificate(certAsset.Cert)) { Assert.NotNull(x509Cert); TestContext.Out.WriteLine("CertificateAsset:"); diff --git a/Tests/customtest.bat b/Tests/customtest.bat index 7ae1bbecb3..fef271ed2a 100644 --- a/Tests/customtest.bat +++ b/Tests/customtest.bat @@ -2,19 +2,19 @@ setlocal enabledelayedexpansion echo This script is used to run custom platform tests for the UA Core Library -echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 +echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net 9.0 REM Check if the target framework parameter is provided if "%1"=="" ( echo Usage: %0 [TargetFramework] - echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, default + echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0, default goto :eof ) REM Check if the provided TargetFramework is valid -set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 " +set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 net9.0 " if "!validFrameworks: %1 =!"=="%validFrameworks%" ( - echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0 + echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0, net9.0 goto :eof ) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2ede8b5c..0be491de2c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,6 +102,16 @@ stages: framework: net8.0 configuration: Release jobnamesuffix: net80 +- stage: testnet90 + dependsOn: [build] + displayName: 'Test .NET 9.0' + condition: and(succeeded(), ne(variables.ScheduledBuild, 'False')) + jobs: + - template: .azurepipelines/test.yml + parameters: + framework: net9.0 + configuration: Release + jobnamesuffix: net90 - stage: testnet462 dependsOn: [build] displayName: 'Test .NET 4.6.2' diff --git a/targets.props b/targets.props index 17b539d7c1..b1cf79a211 100644 --- a/targets.props +++ b/targets.props @@ -12,7 +12,7 @@ A build with all custom targets which are not part of a regular build is scheduled once a week in the DevOps build pipeline. Uncomment the following lines to test a custom test target - supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 + supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0 --> - + - preview-all - net8.0;net6.0;net48 - net8.0 - net48;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net462;net472;net48;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 + preview + all + default + net9.0;net6.0;net48 + net9.0 + net48;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net462;net472;net48;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 From 0a4804f4c5bc65890c678d826709219290c93490 Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:32:55 +0100 Subject: [PATCH 14/27] Client perf and memory improvements for JSON encoding and subscriptions (#2864) - reduce EscapeString overhead in JSON encoder - client monitored items: allocate queues only if needed - replace SortedDictionary with Dictionary for perf reasons - revert a change in padding and reduce a log level --- .azurepipelines/get-version.ps1 | 4 +- .../Opc.Ua.Encoders.Fuzz.Tests.csproj | 4 +- Libraries/Opc.Ua.Client/Session/Session.cs | 6 +- .../Subscription/MonitoredItem.cs | 90 ++++++--- .../Subscription/Subscription.cs | 6 +- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 2 +- .../Stack/Tcp/UaSCBinaryClientChannel.cs | 2 +- .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 178 +++++++++++------- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 4 +- .../Opc.Ua.Client.Tests.csproj | 4 +- .../Opc.Ua.Configuration.Tests.csproj | 4 +- .../Opc.Ua.Core.Tests.csproj | 4 +- .../JsonEncoderEscapeStringBenchmarks.cs | 78 +++++++- .../Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 4 +- .../Opc.Ua.PubSub.Tests.csproj | 4 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 11 +- .../Opc.Ua.Server.Tests.csproj | 4 +- version.props | 4 +- 18 files changed, 294 insertions(+), 119 deletions(-) diff --git a/.azurepipelines/get-version.ps1 b/.azurepipelines/get-version.ps1 index 02ef3e8b9a..c775f3d878 100644 --- a/.azurepipelines/get-version.ps1 +++ b/.azurepipelines/get-version.ps1 @@ -9,8 +9,8 @@ try { # Try install tool - # Note: Keep Version 3.6.143, it is known working for 4 digit versioning - & dotnet @("tool", "install", "--tool-path", "./tools", "--version", "3.6.143", "--framework", "net60", "nbgv") 2>&1 + # Note: Keep Version 3.6.146, it is known working for 4 digit versioning + & dotnet @("tool", "install", "--tool-path", "./tools", "--version", "3.6.146", "--framework", "net60", "nbgv") 2>&1 $props = (& ./tools/nbgv @("get-version", "-f", "json")) | ConvertFrom-Json if ($LastExitCode -ne 0) { 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 c740e2d375..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,10 +18,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 6efed4ab8b..18885a0ccc 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -4883,10 +4883,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 }, @@ -4944,7 +4944,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/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 febf7508be..b90b4dd2ca 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; @@ -2579,7 +2579,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); @@ -2803,7 +2803,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/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index c94ec114d1..3dd581531a 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -585,7 +585,7 @@ protected ArraySegment ReadSymmetricMessage( int paddingStart = signatureStart - 1; paddingCount = buffer.Array[paddingStart]; - for (int ii = paddingStart - paddingCount; ii <= paddingStart; ii++) + for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) { if (buffer.Array[ii] != paddingCount) { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 84508c5142..d5bcd78194 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -1324,7 +1324,7 @@ private void OperationCompleted(WriteOperation operation) if (!m_requests.TryRemove(operation.RequestId, out _)) { - Utils.LogError("Could not remove requestId {0} from list of pending operations.", operation.RequestId); + Utils.LogWarning("Could not remove requestId {0} from list of pending operations.", operation.RequestId); } } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index ca25ebd363..15555f4160 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -407,7 +407,6 @@ public void PushStructure(string fieldName) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -436,7 +435,6 @@ public void PushArray(string fieldName) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -594,21 +592,22 @@ public void PopNamespace() /// Using a span to escape the string, write strings to stream writer if possible. /// /// - private void EscapeString(string value) + private void EscapeString(ReadOnlySpan value) { - ReadOnlySpan charSpan = value.AsSpan(); int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) + m_writer.Write(s_quotation); + + for (int i = 0; i < value.Length; i++) { bool found = false; - char ch = charSpan[i]; + char ch = value[i]; for (int ii = 0; ii < m_specialChars.Length; ii++) { if (m_specialChars[ii] == ch) { - WriteSpan(ref lastOffset, charSpan, i); + WriteSpan(ref lastOffset, value, i); m_writer.Write('\\'); m_writer.Write(m_substitution[ii]); found = true; @@ -618,7 +617,7 @@ private void EscapeString(string value) if (!found && ch < 32) { - WriteSpan(ref lastOffset, charSpan, i); + WriteSpan(ref lastOffset, value, i); m_writer.Write('\\'); m_writer.Write('u'); m_writer.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); @@ -631,7 +630,7 @@ private void EscapeString(string value) } else { - WriteSpan(ref lastOffset, charSpan, charSpan.Length); + WriteSpan(ref lastOffset, value, value.Length); } } @@ -658,6 +657,8 @@ private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int ind /// private void EscapeString(string value) { + m_writer.Write(s_quotation); + foreach (char ch in value) { bool found = false; @@ -717,7 +718,6 @@ private void WriteSimpleField(string fieldName, string value) m_writer.Write(s_comma); } - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -755,9 +755,9 @@ private void WriteSimpleField(string fieldName, string value, EscapeOptions opti m_writer.Write(s_comma); } - m_writer.Write(s_quotation); if ((options & EscapeOptions.NoFieldNameEscape) == EscapeOptions.NoFieldNameEscape) { + m_writer.Write(s_quotation); m_writer.Write(fieldName); } else @@ -778,9 +778,9 @@ private void WriteSimpleField(string fieldName, string value, EscapeOptions opti { if ((options & EscapeOptions.Quotes) == EscapeOptions.Quotes) { - m_writer.Write(s_quotation); if ((options & EscapeOptions.NoValueEscape) == EscapeOptions.NoValueEscape) { + m_writer.Write(s_quotation); m_writer.Write(value); } else @@ -1011,26 +1011,7 @@ public void WriteString(string fieldName, string value) /// Writes a UTC date/time to the stream. /// public void WriteDateTime(string fieldName, DateTime value) - { - if (fieldName != null && !IncludeDefaultValues && value == DateTime.MinValue) - { - WriteSimpleFieldNull(fieldName); - return; - } - - if (value <= DateTime.MinValue) - { - WriteSimpleField(fieldName, "\"0001-01-01T00:00:00Z\""); - } - else if (value >= DateTime.MaxValue) - { - WriteSimpleField(fieldName, "\"9999-12-31T23:59:59Z\""); - } - else - { - WriteSimpleField(fieldName, ConvertUniversalTimeToString(value), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); - } - } + => WriteDateTime(fieldName, value, EscapeOptions.None); /// /// Writes a GUID to the stream. @@ -1332,32 +1313,7 @@ public void WriteExpandedNodeId(string fieldName, ExpandedNodeId value) /// Writes an StatusCode to the stream. /// public void WriteStatusCode(string fieldName, StatusCode value) - { - if (fieldName != null && !IncludeDefaultValues && value == StatusCodes.Good) - { - WriteSimpleFieldNull(fieldName); - return; - } - - if (EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact) - { - WriteUInt32(fieldName, value.Code); - return; - } - - // Verbose and NonReversible - PushStructure(fieldName); - if (value != StatusCodes.Good) - { - WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape); - string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); - if (!string.IsNullOrEmpty(symbolicId)) - { - WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); - } - } - PopStructure(); - } + => WriteStatusCode(fieldName, value, EscapeOptions.None); /// /// Writes a DiagnosticInfo to the stream. @@ -1461,7 +1417,6 @@ public void WriteVariant(string fieldName, Variant value) if (!string.IsNullOrEmpty(fieldName)) { - m_writer.Write(s_quotation); EscapeString(fieldName); m_writer.Write(s_quotationColon); } @@ -1506,12 +1461,12 @@ public void WriteDataValue(string fieldName, DataValue value) if (value.StatusCode != StatusCodes.Good) { - WriteStatusCode("StatusCode", value.StatusCode); + WriteStatusCode("StatusCode", value.StatusCode, EscapeOptions.NoFieldNameEscape); } if (value.SourceTimestamp != DateTime.MinValue) { - WriteDateTime("SourceTimestamp", value.SourceTimestamp); + WriteDateTime("SourceTimestamp", value.SourceTimestamp, EscapeOptions.NoFieldNameEscape); if (value.SourcePicoseconds != 0) { @@ -1521,7 +1476,7 @@ public void WriteDataValue(string fieldName, DataValue value) if (value.ServerTimestamp != DateTime.MinValue) { - WriteDateTime("ServerTimestamp", value.ServerTimestamp); + WriteDateTime("ServerTimestamp", value.ServerTimestamp, EscapeOptions.NoFieldNameEscape); if (value.ServerPicoseconds != 0) { @@ -2567,7 +2522,7 @@ public void WriteVariantContents(object value, TypeInfo typeInfo) } /// - /// Writes an Variant array to the stream. + /// Writes a Variant array to the stream. /// public void WriteObjectArray(string fieldName, IList values) { @@ -2725,6 +2680,101 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #endregion #region Private Methods + /// + /// Push structure with an option to not escape a known fieldname. + /// + private void PushStructure(string fieldName, EscapeOptions escapeOptions = EscapeOptions.None) + { + m_nestingLevel++; + + if (m_commaRequired) + { + m_writer.Write(s_comma); + } + + if (!string.IsNullOrEmpty(fieldName)) + { + if ((escapeOptions & EscapeOptions.NoFieldNameEscape) != 0) + { + m_writer.Write(s_quotation); + m_writer.Write(fieldName); + } + else + { + EscapeString(fieldName); + } + m_writer.Write(s_quotationColon); + } + else if (!m_commaRequired) + { + if (m_nestingLevel == 1 && !m_topLevelIsArray) + { + m_levelOneSkipped = true; + return; + } + } + + m_commaRequired = false; + m_writer.Write(s_leftCurlyBrace); + } + + /// + /// Writes an StatusCode to the stream. + /// + private void WriteStatusCode(string fieldName, StatusCode value, EscapeOptions escapeOptions) + { + if (fieldName != null && !IncludeDefaultValues && value == StatusCodes.Good) + { + WriteSimpleFieldNull(fieldName); + return; + } + + if (EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact) + { + WriteUInt32(fieldName, value.Code); + return; + } + + // Verbose and NonReversible + PushStructure(fieldName, escapeOptions); + if (value != StatusCodes.Good) + { + WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape | EscapeOptions.NoValueEscape); + string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); + if (!string.IsNullOrEmpty(symbolicId)) + { + WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape | EscapeOptions.NoValueEscape); + } + } + PopStructure(); + } + + /// + /// Writes a UTC date/time to the stream. Reduce escape overhead for fieldname. + /// + private void WriteDateTime(string fieldName, DateTime value, EscapeOptions escapeOptions) + { + if (fieldName != null && !IncludeDefaultValues && value == DateTime.MinValue) + { + WriteSimpleFieldNull(fieldName); + return; + } + + escapeOptions |= EscapeOptions.NoValueEscape; + if (value <= DateTime.MinValue) + { + WriteSimpleField(fieldName, "\"0001-01-01T00:00:00Z\"", escapeOptions); + } + else if (value >= DateTime.MaxValue) + { + WriteSimpleField(fieldName, "\"9999-12-31T23:59:59Z\"", escapeOptions); + } + else + { + WriteSimpleField(fieldName, ConvertUniversalTimeToString(value), escapeOptions | EscapeOptions.Quotes); + } + } + /// /// Returns true if a simple field can be written. /// @@ -2825,7 +2875,7 @@ private void WriteDiagnosticInfo(string fieldName, DiagnosticInfo value, int dep if (value.InnerStatusCode != StatusCodes.Good) { - WriteStatusCode("InnerStatusCode", value.InnerStatusCode); + WriteStatusCode("InnerStatusCode", value.InnerStatusCode, EscapeOptions.NoFieldNameEscape); } if (value.InnerDiagnosticInfo != null) diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index 9987b1a1d5..c0a3b76193 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -7,10 +7,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index 76bcf04a3f..e0f5439bca 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -9,10 +9,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index a7c617a427..d1d93a62a5 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -8,10 +8,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 5a44bb0964..3a1bea5ff6 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -10,11 +10,11 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs index 50da763274..9d8f407bd7 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs @@ -36,9 +36,15 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using Microsoft.IO; +using Newtonsoft.Json; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; +#if NET6_0_OR_GREATER +using System.Text.Encodings.Web; +using System.Text.Json; +#endif + namespace Opc.Ua.Core.Tests.Types.Encoders { [TestFixture, Category("JsonEncoder")] @@ -214,6 +220,37 @@ public void EscapeStringSpanDict() m_streamWriter.Flush(); } + /// + /// Using NewtonSoft, which first converts to string. + /// + [Benchmark] + public void EscapeStringNewtonSoft() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringNewtonSoft(m_testString); + } + m_streamWriter.Flush(); + } + +#if NET6_0_OR_GREATER + /// + /// A new implementation using ReadOnlySpan and Dictionary. + /// + [Benchmark] + public void EscapeStringSystemTextJson() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSystemTextJson(m_testString); + } + } +#endif + [Theory] [TestCase("No Escape chars", 0)] [TestCase("control chars escaped, 1 char space", 1)] @@ -225,26 +262,30 @@ public void EscapeStringSpanDict() [TestCase("binary chars escaped, 3 char spaces", 7)] [TestCase("binary chars escaped, 5 char spaces", 8)] [TestCase("all escape chars and long string", 9)] + [NonParallelizable] public void EscapeStringValidation(string name, int index) { + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + m_testString = EscapeTestStrings[index]; TestContext.Out.WriteLine(m_testString); var testArray = m_testString.ToCharArray(); m_memoryStream.Position = 0; - EscapeStringLegacy(); + EscapedStringLegacy(m_testString); m_streamWriter.Flush(); byte[] resultLegacy = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); m_memoryStream.Position = 0; - EscapeStringLegacyPlus(); + EscapedStringLegacyPlus(m_testString); m_streamWriter.Flush(); byte[] resultLegacyPlus = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); m_memoryStream.Position = 0; - EscapeStringStringBuilder(); + EscapeString(m_testString); m_streamWriter.Flush(); byte[] result = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); @@ -285,6 +326,21 @@ public void EscapeStringValidation(string name, int index) byte[] resultSpanDict = m_memoryStream.ToArray(); TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + EscapeStringNewtonSoft(m_testString); + m_streamWriter.Flush(); + byte[] resultNewtonSoft = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultNewtonSoft)); + +#if NET6_0_OR_GREATER + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + EscapeStringSystemTextJson(m_testString); + byte[] resultSystemTextJson = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSystemTextJson)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSystemTextJson)); +#endif + Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); @@ -293,6 +349,7 @@ public void EscapeStringValidation(string name, int index) Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultNewtonSoft)); } #region Test Setup @@ -771,6 +828,21 @@ private void EscapeStringSpanDict(string value) } } + private void EscapeStringNewtonSoft(string value) + { + string newtonSoftConvertedText = JsonConvert.ToString(value); + newtonSoftConvertedText = newtonSoftConvertedText.Substring(1, newtonSoftConvertedText.Length - 2); + m_streamWriter.Write(newtonSoftConvertedText); + } + +#if NET6_0_OR_GREATER + private void EscapeStringSystemTextJson(string value) + { + var jsonEncodedText = JsonEncodedText.Encode(m_testString, JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + m_memoryStream.Write(jsonEncodedText.EncodedUtf8Bytes); + } +#endif + private void EscapeString(string value) { StringBuilder stringBuilder = new StringBuilder(value.Length * 2); diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 350f9b10a8..33621c44d0 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -12,10 +12,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index 2f81b8cf91..bd017a0919 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -8,10 +8,10 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index b407213078..3eb757da87 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -23,10 +23,10 @@ - + - + all runtime; build; native; contentfiles; analyzers @@ -53,4 +53,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 1741621781..7fead9e0e0 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -8,11 +8,11 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/version.props b/version.props index f4038e5902..e9b9d796cf 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 1fa0b7f26eaf07744211720f1f77abfc813073bc Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:21:07 +0100 Subject: [PATCH 15/27] Register callback to notify about new channel token activation (#2872) (#2873) For better client diagnostic it is important to have access to the channel token. Today tooling must monitor the channel token change to be aware of the current token, it is more efficient for the tooling to be called back when the token changes. This can be now accomplished in that the owner of a client (Discovery, Session) can retrieve the Transport channel after connect, read the current token and register a change callback. * Add OnTokenActivated event for token change notifications Added a new event `OnTokenActivated` to the `UaChannelBase` class in the `Opc.Ua` namespace to notify when a channel token is activated. Implemented this event in the `HttpsTransportChannel` class within the `Opc.Ua.Bindings` namespace. Introduced a protected internal `OnTokenActivated` event handler in the `UaSCBinaryChannel.Symmetric` class, invoked during token changes in the `ActivateToken` and `DiscardTokens` methods. Cleaned up the event handler in the `Dispose` method of the `UaSCBinaryClientChannel` class. Registered the event handler with the internal channel in the `UaSCBinaryTransportChannel` class. Added a new delegate `ChannelTokenActivatedEventHandler` in the `ITransportChannel` interface and updated the interface to include the `OnTokenActivated` event. Made minor formatting and comment adjustments for improved readability and consistency. * Include channel in callback Co-authored-by: Marc Schier --- .../Opc.Ua.Core/Stack/Client/UaChannelBase.cs | 9 ++++++++- .../Stack/Https/HttpsTransportChannel.cs | 7 +++++++ .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 9 +++++++++ .../Stack/Tcp/UaSCBinaryClientChannel.cs | 14 +++++++------ .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 14 +++++++++++++ .../Stack/Transport/ITransportChannel.cs | 20 +++++++++++++++++-- 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs b/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs index 1442a4738f..4d7e888c14 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs @@ -198,6 +198,13 @@ public IServiceMessageContext MessageContext /// public ChannelToken CurrentToken => null; + /// + public event ChannelTokenActivatedEventHandler OnTokenActivated + { + add { } + remove { } + } + /// /// Gets or sets the default timeout for requests send via the channel. /// @@ -685,7 +692,7 @@ private void OnSendRequest(object state) #endregion } #endregion - + /// /// Processes the request. /// diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 387f224532..1d82412788 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -108,6 +108,13 @@ public void Dispose() /// public ChannelToken CurrentToken => null; + /// + public event ChannelTokenActivatedEventHandler OnTokenActivated + { + add { } + remove { } + } + /// public int OperationTimeout { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 3dd581531a..ad118a2f3b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -37,6 +37,11 @@ public partial class UaSCUaBinaryChannel /// protected ChannelToken RenewedToken => m_renewedToken; + /// + /// Called when the token changes + /// + protected internal Action OnTokenActivated { get; set; } + /// /// Creates a new token. /// @@ -70,6 +75,8 @@ protected void ActivateToken(ChannelToken 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}-{3}. Lifetime={4}.", Id, token.TokenId, token.CreatedAt, token.CreatedAtTickCount, token.Lifetime); } @@ -96,6 +103,8 @@ protected void DiscardTokens() m_currentToken = null; Utils.SilentDispose(m_renewedToken); m_renewedToken = null; + + OnTokenActivated?.Invoke(null, null); } #endregion diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index d5bcd78194..e883dde5e1 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -105,6 +105,8 @@ protected override void Dispose(bool disposing) if (disposing) { + OnTokenActivated = null; + Utils.SilentDispose(m_handshakeTimer); m_handshakeTimer = null; Utils.SilentDispose(m_requestedToken); @@ -589,7 +591,7 @@ private bool ProcessOpenSecureChannelResponse(uint messageType, ArraySegment - /// Creates an object to manage the state of an asynchronous operation. + /// Creates an object to manage the state of an asynchronous operation. /// private WriteOperation BeginOperation(int timeout, AsyncCallback callback, object state) { @@ -1442,7 +1444,7 @@ protected bool ProcessErrorMessage(uint messageType, ArraySegment messageC { ServiceResult error; - // read request buffer sizes. + // read request buffer sizes. using (var decoder = new BinaryDecoder(messageChunk, Quotas.MessageContext)) { ReadAndVerifyMessageTypeAndSize(decoder, TcpMessageType.Error, messageChunk.Count); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index b7a76f64f1..a56152a94b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -70,6 +70,16 @@ public IMessageSocket Socket #endregion #region ITransportChannel Members + + /// + /// Called when the token changes + /// + public event ChannelTokenActivatedEventHandler OnTokenActivated + { + add => m_OnTokenActivated += value; + remove => m_OnTokenActivated -= value; + } + /// /// A masking indicating which features are implemented. /// @@ -482,6 +492,9 @@ private UaSCUaBinaryClientChannel CreateChannel(ITransportWaitingConnection conn channel.ReverseSocket = true; } + // Register the token changed event handler with the internal channel + channel.OnTokenActivated = + (current, previous) => m_OnTokenActivated?.Invoke(this, current, previous); return channel; } #endregion @@ -494,6 +507,7 @@ private UaSCUaBinaryClientChannel CreateChannel(ITransportWaitingConnection conn private ChannelQuotas m_quotas; private BufferManager m_bufferManager; private UaSCUaBinaryClientChannel m_channel; + private event ChannelTokenActivatedEventHandler m_OnTokenActivated; private IMessageSocketFactory m_messageSocketFactory; #endregion } diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index cc02a3503a..ad7ae429f9 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -18,7 +18,18 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. namespace Opc.Ua { /// - /// This is an interface to a channel which supports + /// Callback when the token is activated + /// + /// + /// + /// + public delegate void ChannelTokenActivatedEventHandler( + ITransportChannel channel, + ChannelToken currentToken, + ChannelToken previousToken); + + /// + /// This is an interface to a channel which supports /// public interface ITransportChannel : IDisposable { @@ -47,6 +58,11 @@ public interface ITransportChannel : IDisposable /// ChannelToken CurrentToken { get; } + /// + /// Register for token change events + /// + event ChannelTokenActivatedEventHandler OnTokenActivated; + /// /// Gets or sets the default timeout for requests send via the channel. /// @@ -200,7 +216,7 @@ IAsyncResult BeginOpen( /// /// Completes an asynchronous operation to send a request over the secure channel. - /// Awaitable version + /// Awaitable version /// /// The result returned from the BeginSendRequest call. /// The cancellation token. From ab364b9e98c42955f9a8ab03b3012aea590361b3 Mon Sep 17 00:00:00 2001 From: Archie Miller <62433534+Archie-Miller@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:03:29 -0700 Subject: [PATCH 16/27] Update CauseMappings to reflect issue (#2877) SyspendedToRead caused by Reset remains as per Part 10 Table 7 --- Stack/Opc.Ua.Core/Stack/State/ProgramStateMachineState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/State/ProgramStateMachineState.cs b/Stack/Opc.Ua.Core/Stack/State/ProgramStateMachineState.cs index 460a735069..20668bc4e7 100644 --- a/Stack/Opc.Ua.Core/Stack/State/ProgramStateMachineState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/ProgramStateMachineState.cs @@ -118,11 +118,11 @@ protected override ElementInfo[] TransitionTable { Methods.ProgramStateMachineType_Reset, Objects.ProgramStateMachineType_Halted, Objects.ProgramStateMachineType_HaltedToReady }, { Methods.ProgramStateMachineType_Start, Objects.ProgramStateMachineType_Ready, Objects.ProgramStateMachineType_ReadyToRunning }, { Methods.ProgramStateMachineType_Suspend,Objects.ProgramStateMachineType_Running, Objects.ProgramStateMachineType_RunningToSuspended }, - { Methods.ProgramStateMachineType_Reset, Objects.ProgramStateMachineType_Running, Objects.ProgramStateMachineType_RunningToReady }, { Methods.ProgramStateMachineType_Halt, Objects.ProgramStateMachineType_Running, Objects.ProgramStateMachineType_RunningToHalted }, { Methods.ProgramStateMachineType_Resume, Objects.ProgramStateMachineType_Suspended, Objects.ProgramStateMachineType_SuspendedToRunning }, { Methods.ProgramStateMachineType_Reset, Objects.ProgramStateMachineType_Suspended, Objects.ProgramStateMachineType_SuspendedToReady }, - { Methods.ProgramStateMachineType_Halt, Objects.ProgramStateMachineType_Suspended, Objects.ProgramStateMachineType_SuspendedToHalted } + { Methods.ProgramStateMachineType_Halt, Objects.ProgramStateMachineType_Suspended, Objects.ProgramStateMachineType_SuspendedToHalted }, + { Methods.ProgramStateMachineType_Halt, Objects.ProgramStateMachineType_Ready, Objects.ProgramStateMachineType_ReadyToHalted } }; From f98c8a9e23a07e6d8f50624dd7ff664b764872e8 Mon Sep 17 00:00:00 2001 From: Archie Miller <62433534+Archie-Miller@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:11:23 -0700 Subject: [PATCH 17/27] Client fix for ConditionRefreshAsync always returns NodIdUnknown, add ConditionRefresh2Async support(#2876) Fixes #2854 --- .../Subscription/Subscription.cs | 27 +++++++++++++++++++ .../Subscription/SubscriptionAsync.cs | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs index b90b4dd2ca..fb7db1108c 100644 --- a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs +++ b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs @@ -1663,6 +1663,33 @@ public bool ConditionRefresh() return false; } + /// + /// Tells the server to refresh all conditions being monitored by the subscription for a specific + /// monitoredItem for events. + /// + public bool ConditionRefresh2(uint monitoredItemId) + { + VerifySubscriptionState(true); + + try + { + object[] inputArguments = new object[] { m_id, monitoredItemId }; + + m_session.Call( + ObjectTypeIds.ConditionType, + MethodIds.ConditionType_ConditionRefresh2, + inputArguments); + + return true; + } + catch (ServiceResultException sre) + { + Utils.LogError(sre, "SubscriptionId {0}: Item {1} Failed to call ConditionRefresh2 on server", + m_id, monitoredItemId); + } + return false; + } + /// /// Call the ResendData method on the server for this subscription. /// diff --git a/Libraries/Opc.Ua.Client/Subscription/SubscriptionAsync.cs b/Libraries/Opc.Ua.Client/Subscription/SubscriptionAsync.cs index 7b54124af0..c8381e34f2 100644 --- a/Libraries/Opc.Ua.Client/Subscription/SubscriptionAsync.cs +++ b/Libraries/Opc.Ua.Client/Subscription/SubscriptionAsync.cs @@ -454,6 +454,7 @@ public async Task ConditionRefreshAsync(CancellationToken ct = default) var methodsToCall = new CallMethodRequestCollection(); methodsToCall.Add(new CallMethodRequest() { + ObjectId = ObjectTypeIds.ConditionType, MethodId = MethodIds.ConditionType_ConditionRefresh, InputArguments = new VariantCollection() { new Variant(m_id) } }); @@ -464,6 +465,29 @@ public async Task ConditionRefreshAsync(CancellationToken ct = default) ct).ConfigureAwait(false); } + /// + /// Tells the server to refresh all conditions being monitored by the subscription for a specific + /// monitoredItem for events. + /// + public async Task ConditionRefresh2Async(uint monitoredItemId, CancellationToken ct = default) + { + VerifySubscriptionState(true); + + var methodsToCall = new CallMethodRequestCollection(); + methodsToCall.Add(new CallMethodRequest() { + ObjectId = ObjectTypeIds.ConditionType, + MethodId = MethodIds.ConditionType_ConditionRefresh2, + InputArguments = new VariantCollection() { + new Variant(m_id), + new Variant( monitoredItemId ) } + }); + + var response = await m_session.CallAsync( + null, + methodsToCall, + ct).ConfigureAwait(false); + } + #endregion } } From 6f7b8e7cf72567bcfc66aa1136c58074a1a1d838 Mon Sep 17 00:00:00 2001 From: romanett Date: Tue, 3 Dec 2024 10:47:33 +0100 Subject: [PATCH 18/27] [Client] Fix: KeepAliveInterval was not updated on ModifySubscription (#2871) * Bug fix for: keep alive timer is not updated if the timing of a subscription is changed #2863 * Restart KeepAlive Timer with correct KeepAliveInterval on ModifySubscription, if the interval changed --- .../Subscription/Subscription.cs | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs index b90b4dd2ca..953f1fe857 100644 --- a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs +++ b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs @@ -1822,17 +1822,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)); @@ -2020,10 +2025,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}", @@ -2053,6 +2059,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. From 71a6e69d621d52ec948242a57436128499283b04 Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:25:58 +0100 Subject: [PATCH 19/27] Bump BouncyCastle.Cryptography from 2.4.0 to 2.5.0, Serilog and Codecov (#2875) * bump bouncy castle to 2.50 * Bump codecov/codecov-action from 4 to 5 (#2847) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump Serilog and System.Diagnostics.DiagnosticSource (#2818) Bumps [Serilog](https://github.com/serilog/serilog) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog` from 4.0.2 to 4.1.0 - [Release notes](https://github.com/serilog/serilog/releases) - [Commits](https://github.com/serilog/serilog/compare/v4.0.2...v4.1.0) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/buildandtest.yml | 2 +- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 4 ++-- .../Opc.Ua.Gds.Server.Common.csproj | 2 +- .../Opc.Ua.Security.Certificates.csproj | 8 ++++---- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 001359d2b4..c9e382c330 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -65,7 +65,7 @@ jobs: if: ${{ always() }} - name: Upload to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: name: codecov-umbrella token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index 04bf1da453..abf04a59ff 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -24,7 +24,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 0c9ea93ce1..4a51d6cc53 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 3873af9a7c..1c16b233a4 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -22,8 +22,8 @@ - - + + 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.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 @@ - + - + From a887f909f1d314cfa7c32989628afa754984c4f1 Mon Sep 17 00:00:00 2001 From: Martin Regen <7962757+mregen@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:06:11 +0100 Subject: [PATCH 20/27] Allow ExtensionObjects which are encoded with an unknown size (#2869) An older OPC UA server returns well known extension object(811) which cannot be decoded by the client because the server doesn't set the length of the extension object. Recently, based on fuzz testing, the length check has been changed to enforce an exact match, according to spec. Workaround: Allow a value of -1 for the length, then allow to decode well known extension objects. Also improve some internal calls to Safe Read functions by calling not via the interface. --- .../Types/Encoders/BinaryDecoder.cs | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) 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; } From d0e89a3bf140efced967ad3482d8dd9c9c936c9f Mon Sep 17 00:00:00 2001 From: Suciu Mircea Adrian Date: Tue, 3 Dec 2024 13:43:39 +0200 Subject: [PATCH 21/27] Support mutual TLS on server https endpoints (#2849) A new configuration variable true enables the mutual TLS authentication support. The behavior of the TLS endpoint changes as the following: HttpsMutualTls is true: The server checks the trust on the certificate which is used by the client for TLS authentication. It must be a valid OPC UA application certificate which is trusted. A client can still connect without providing a client certificate, but then it is only able to call discovery services. In order to create a session, the client must use the same application certificate that was used for the TLS channel. HttpsMutualTls is false: - There is no application authentication. The server endpoint uses security None and there is no client application authentication. Instead, only user authentication is used to secure the server, anonymous user authentication is disabled. Discovery service calls are supported. Co-authored-by: Martin Regen --- .../Quickstarts.ReferenceServer.Config.xml | 2 + .../ReferenceServer/ReferenceServer.cs | 5 +- Libraries/Opc.Ua.Client/CoreClientUtils.cs | 2 +- .../ApplicationConfigurationBuilder.cs | 7 +++ .../IApplicationConfigurationBuilder.cs | 3 + .../Stack/Https/HttpsServiceHost.cs | 55 +++++++++++++------ .../Stack/Https/HttpsTransportListener.cs | 55 ++++++++++++++++++- .../Schema/ApplicationConfiguration.cs | 13 +++++ .../Schema/ApplicationConfiguration.xsd | 1 + .../Configuration/ConfiguredEndpoints.cs | 9 +-- .../Configuration/EndpointDescription.cs | 2 +- .../Stack/Https/HttpsTransportChannel.cs | 19 ++++++- Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs | 8 ++- .../Transport/TransportListenerSettings.cs | 12 ++++ Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 10 ++++ Tests/Opc.Ua.Client.Tests/ClientFixture.cs | 11 +++- .../ClientTestFramework.cs | 11 +++- 17 files changed, 194 insertions(+), 31 deletions(-) diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 6255b65dba..60a6448daf 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -154,6 +154,7 @@ + true 75 @@ -265,6 +266,7 @@ true + true diff --git a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs index 1abdb47d06..039417eda7 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/Libraries/Opc.Ua.Client/CoreClientUtils.cs b/Libraries/Opc.Ua.Client/CoreClientUtils.cs index b73f01abad..958caa85ca 100644 --- a/Libraries/Opc.Ua.Client/CoreClientUtils.cs +++ b/Libraries/Opc.Ua.Client/CoreClientUtils.cs @@ -307,7 +307,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.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs index a5240c95b8..015d886d31 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs @@ -779,6 +779,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/IApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs index 753c2666ad..4d89e47a6d 100644 --- a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs @@ -265,6 +265,9 @@ public interface IApplicationConfigurationBuilderServerOptions : /// IApplicationConfigurationBuilderServerOptions SetAuditingEnabled(bool auditingEnabled); + + /// + IApplicationConfigurationBuilderServerOptions SetHttpsMutualTls(bool mTlsEnabled); } /// diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs index 036002851b..47f5ccdd55 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; @@ -92,30 +93,44 @@ X509Certificate2Collection instanceCertificateChain 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 (instanceCertificate != null) { @@ -142,6 +157,12 @@ X509Certificate2Collection instanceCertificateChain 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 49a8bbc328..7149e0483d 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; @@ -24,6 +25,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.Hosting; +using Opc.Ua.Security.Certificates; namespace Opc.Ua.Bindings @@ -174,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 +208,7 @@ public void Open( m_serverCertificate = settings.ServerCertificate; m_serverCertificateChain = settings.ServerCertificateChain; + m_mutualTlsEnabled = settings.HttpsMutualTls; // start the listener Start(); } @@ -283,9 +287,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, + ClientCertificateValidation = ValidateClientCertificate, }; #if NET462 @@ -370,6 +375,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 +483,8 @@ public async Task SendAsync(HttpContext context) await WriteResponseAsync(context.Response, message, HttpStatusCode.InternalServerError).ConfigureAwait(false); } + + /// /// Called when a UpdateCertificate event occured. /// @@ -524,11 +546,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; @@ -537,6 +589,7 @@ private static async Task ReadBodyAsync(HttpRequest req) private IWebHost m_host; private X509Certificate2 m_serverCertificate; private X509Certificate2Collection m_serverCertificateChain; + private bool m_mutualTlsEnabled; #endregion } } diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 43ac29014b..bbf1a6af84 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -1509,6 +1509,7 @@ private void Initialize() m_maxTrustListSize = 0; m_multicastDnsEnabled = false; m_auditingEnabled = false; + m_httpsMutualTls = true; } /// @@ -1942,6 +1943,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 @@ -1980,6 +1992,7 @@ public bool AuditingEnabled private ReverseConnectServerConfiguration m_reverseConnect; private OperationLimits m_operationLimits; private bool m_auditingEnabled; + private bool m_httpsMutualTls; #endregion } #endregion 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/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 8de37f2696..20c2d319d6 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 c29d980f33..3b9b0f8e73 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -884,6 +884,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( @@ -1140,8 +1144,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/Transport/TransportListenerSettings.cs b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs index a67ce0f4d9..c4b79666e7 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs @@ -127,6 +127,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 @@ -139,6 +150,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/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 54269fe179..1b49fd94be 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -162,6 +162,16 @@ public static bool IsUriHttpsScheme(string url) return url.StartsWith(Utils.UriSchemeHttps, StringComparison.Ordinal) || url.StartsWith(Utils.UriSchemeOpcHttps, StringComparison.Ordinal); } + + /// + /// Returns true if the url starts with http, opc.https or https. + /// + /// The url + public static bool IsUriHttpRelatedScheme(string url) + { + return url.StartsWith(Utils.UriSchemeHttps, StringComparison.Ordinal) || + IsUriHttpsScheme(url); + } #endregion #region Trace Support diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index f0e6337b82..0d7b963f04 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -30,6 +30,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Security.Policy; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NUnit.Framework; @@ -208,7 +209,15 @@ public async Task Connect(string endpointUrl) /// public async Task ConnectAsync(Uri url, string securityProfile, EndpointDescriptionCollection endpoints = null, IUserIdentity userIdentity = null) { - return await ConnectAsync(await GetEndpointAsync(url, securityProfile, endpoints).ConfigureAwait(false), userIdentity).ConfigureAwait(false); + string uri = url.AbsoluteUri; + Uri getEndpointsUrl = url; + if (uri.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) || + Utils.IsUriHttpsScheme(uri)) + { + getEndpointsUrl = CoreClientUtils.GetDiscoveryUrl(uri); + } + + return await ConnectAsync(await GetEndpointAsync(getEndpointsUrl, securityProfile, endpoints).ConfigureAwait(false), userIdentity).ConfigureAwait(false); } /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index a93c6e5126..207f739b49 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -158,7 +158,16 @@ public async Task OneTimeSetUpAsync(TextWriter writer = null, } else { - ServerUrl = new Uri(UriScheme + "://localhost:" + ServerFixturePort.ToString(CultureInfo.InvariantCulture)); + string url = UriScheme + "://localhost:" + ServerFixturePort.ToString(CultureInfo.InvariantCulture); + + if (UriScheme.StartsWith(Utils.UriSchemeHttp, StringComparison.Ordinal) || + Utils.IsUriHttpsScheme(UriScheme)) + { + url = url + ConfiguredEndpoint.DiscoverySuffix; + } + + ServerUrl = new Uri(url); + } if (SingleSession) From 6c62eb26fd88bfe4b200320ce50b76570dc5aba4 Mon Sep 17 00:00:00 2001 From: romanett Date: Wed, 4 Dec 2024 09:42:19 +0100 Subject: [PATCH 22/27] Fix serialization of CertificateTrustList (#2879) --- Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index bbf1a6af84..23b2c99e9d 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -2878,6 +2878,7 @@ private int XmlEncodedValidationOptions #region CertificateTrustList Class [DataContract(Namespace = Namespaces.OpcUaConfig)] + [KnownType(typeof(CertificateTrustList))] public partial class CertificateTrustList : CertificateStoreIdentifier { #region Constructors From 67a0df1960624147d45aefcad3c8cbcf4a30a69a Mon Sep 17 00:00:00 2001 From: romanett Date: Thu, 5 Dec 2024 08:39:09 +0100 Subject: [PATCH 23/27] Fix read of NodeId attribute AccessRestrictions by casting return type from Enum to ushort (#2883) In the read NodeId servcie call an enum is by default cast to Int32, so the cast to ushort is needed here to return the expected type for the AccessRestrictions. --- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From c87eca918673e9f29bd770a0264a6a93f60b91b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:48:48 +0100 Subject: [PATCH 24/27] Bump Serilog from 4.1.0 to 4.2.0 (#2891) Bumps [Serilog](https://github.com/serilog/serilog) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/serilog/serilog/releases) - [Commits](https://github.com/serilog/serilog/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: Serilog dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index abf04a59ff..3e257890b8 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -24,7 +24,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 4a51d6cc53..597a9f85a5 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 1c16b233a4..9b112eff3d 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -23,7 +23,7 @@ - + From 69e83748ac4f6d946ed0a55b417fead734bd5241 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:38:31 +0200 Subject: [PATCH 25/27] Bump Nerdbank.GitVersioning from 3.6.146 to 3.7.112 (#2910) Bumps [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) from 3.6.146 to 3.7.112. - [Release notes](https://github.com/dotnet/Nerdbank.GitVersioning/releases) - [Commits](https://github.com/dotnet/Nerdbank.GitVersioning/compare/v3.6.146...v3.7.112) --- updated-dependencies: - dependency-name: Nerdbank.GitVersioning dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- version.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index 3eb757da87..caa60134af 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -54,7 +54,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/version.props b/version.props index e9b9d796cf..82418c4b55 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 8b83dbffbd7f7caf2bef0169db1cc55c097cec19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:41:59 +0200 Subject: [PATCH 26/27] Bump NUnit from 4.2.2 to 4.3.0 (#2911) Bumps [NUnit](https://github.com/nunit/nunit) from 4.2.2 to 4.3.0. - [Release notes](https://github.com/nunit/nunit/releases) - [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md) - [Commits](https://github.com/nunit/nunit/compare/4.2.2...4.3.0) --- updated-dependencies: - dependency-name: NUnit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj | 2 +- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) 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 07f10d1f21..ca7eb77c44 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -20,7 +20,7 @@ - + all diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index 1da96a2c36..d9d5309386 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -9,7 +9,7 @@ - + all diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index c1916c3608..64fdc94854 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -23,7 +23,7 @@ - + all diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index fd58f9d418..98893da620 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -23,7 +23,7 @@ - + all diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 71a4a42b19..7d5ecaf24c 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -13,7 +13,7 @@ - + all diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index e1a99a28c6..82e9d200fb 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -26,7 +26,7 @@ - + all diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index 34dd6373bf..16abd92cab 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -22,7 +22,7 @@ - + all diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index caa60134af..f479c4ec41 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -25,7 +25,7 @@ - + all diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 660d8dc02f..dcd8568844 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -23,7 +23,7 @@ - + all From 5b3f91d9dd3f1608d7cbe6ce5626196e96b863e7 Mon Sep 17 00:00:00 2001 From: Stephan Larws <146172346+larws@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:38:51 +0100 Subject: [PATCH 27/27] Handle status code Uncertain according to the specification (#2898) * #2896: Write output arguments for good and uncertain status code When a method state's call method is invoked the output arguments should be written in case the status code is good or uncertain. This behavior would be conform with the current specification. * #2896: The service result corresponds the method call result The result of the Call method in the CustomNodeManager2 class represents the status of the CallMethodResult. It does not correspond to the ServiceResult of the CallResponse, thus returning Good as a general response is incorrect behavior. --- .../Opc.Ua.Server/Diagnostics/CustomNodeManager.cs | 9 +++++---- Stack/Opc.Ua.Core/Stack/State/MethodState.cs | 2 +- Stack/Opc.Ua.Core/Types/Utils/ServiceResult.cs | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs index fa25395da6..217d38e620 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs @@ -3021,16 +3021,16 @@ protected virtual ServiceResult Call( List argumentErrors = new List(); VariantCollection outputArguments = new VariantCollection(); - ServiceResult error = method.Call( + ServiceResult callResult = method.Call( context, methodToCall.ObjectId, methodToCall.InputArguments, argumentErrors, outputArguments); - if (ServiceResult.IsBad(error)) + if (ServiceResult.IsBad(callResult)) { - return error; + return callResult; } // check for argument errors. @@ -3085,7 +3085,8 @@ protected virtual ServiceResult Call( // return output arguments. result.OutputArguments = outputArguments; - return ServiceResult.Good; + // return the actual result of the original call + return callResult; } diff --git a/Stack/Opc.Ua.Core/Stack/State/MethodState.cs b/Stack/Opc.Ua.Core/Stack/State/MethodState.cs index f3b79abb03..e327a82b4a 100644 --- a/Stack/Opc.Ua.Core/Stack/State/MethodState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/MethodState.cs @@ -706,7 +706,7 @@ public virtual ServiceResult Call( } // copy out arguments. - if (ServiceResult.IsGood(result)) + if (ServiceResult.IsGoodOrUncertain(result)) { for (int ii = 0; ii < outputs.Count; ii++) { diff --git a/Stack/Opc.Ua.Core/Types/Utils/ServiceResult.cs b/Stack/Opc.Ua.Core/Types/Utils/ServiceResult.cs index 1d04c49081..40d72b66fc 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ServiceResult.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ServiceResult.cs @@ -524,6 +524,19 @@ public static bool IsUncertain(ServiceResult status) return false; } + /// + /// Returns true if the status code is good or uncertain. + /// + public static bool IsGoodOrUncertain(ServiceResult status) + { + if (status != null) + { + return StatusCode.IsGood(status.m_code) || StatusCode.IsUncertain(status.m_code); + } + + return false; + } + /// /// Returns true if the status is good or uncertain. ///