diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index 74faa9178..5bcf6e990 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -75,8 +75,8 @@ public static async Task Main(string[] args) byte[] userpassword = null; string userCertificateThumbprint = null; byte[] userCertificatePassword = null; - bool logConsole = false; - bool appLog = false; + bool logConsole = true; + bool appLog = true; bool fileLog = false; bool renewCertificate = false; bool loadTypes = false; @@ -94,6 +94,7 @@ public static async Task Main(string[] args) bool leakChannels = false; bool forever = false; bool enableDurableSubscriptions = false; + bool connectAllEndpointDescriptions = true; var options = new Mono.Options.OptionSet { @@ -264,6 +265,17 @@ public static async Task Main(string[] args) enableDurableSubscriptions = true; } } + }, + { + "ca|connectall", + "Connects using all published EndpointDescriptions.", + ca => + { + if (ca != null) + { + connectAllEndpointDescriptions = true; + } + } } }; @@ -333,7 +345,7 @@ public static async Task Main(string[] args) logConsole, fileLog, appLog, - LogLevel.Information); + LogLevel.Warning); // delete old certificate if (renewCertificate) @@ -368,6 +380,17 @@ await application.DeleteApplicationInstanceCertificateAsync() CancellationToken ct = quitCTS.Token; ManualResetEvent quitEvent = ConsoleUtils.CtrlCHandler(quitCTS); + // handle connect all endpoints test. + if (connectAllEndpointDescriptions) + { + var tester = new SecurityTestClient.RunConnectAll(config, telemetry); + + if (await tester.RunAsync(quitEvent, ct).ConfigureAwait(false)) + { + return; + } + } + var userIdentity = new UserIdentity(); // set user identity of type username/pw diff --git a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml index aa3a8ae26..f067984d1 100644 --- a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml +++ b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml @@ -13,35 +13,35 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -49,17 +49,17 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/issuer + ../../pki/issuer Directory - %LocalApplicationData%/OPC Foundation/pki/trusted + ../../pki/trusted Directory - %LocalApplicationData%/OPC Foundation/pki/rejected + ../../pki/rejected 5 Directory - %LocalApplicationData%/OPC Foundation/pki/userIssuer + ../../pki/userIssuer Directory - %LocalApplicationData%/OPC Foundation/pki/trustedUser + ../../pki/trustedUser @@ -92,7 +92,7 @@ 4194304 65535 300000 - 3600000 + 30000 60000 @@ -120,7 +120,7 @@ - %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceClient.log.txt + ./Logs/Quickstarts.ReferenceClient.log.txt true diff --git a/Applications/ConsoleReferenceClient/RunConnectAll.cs b/Applications/ConsoleReferenceClient/RunConnectAll.cs new file mode 100644 index 000000000..0d0c22281 --- /dev/null +++ b/Applications/ConsoleReferenceClient/RunConnectAll.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace SecurityTestClient +{ + internal sealed class RunConnectAll + { + private readonly Lock m_lock = new(); + private SessionReconnectHandler m_reconnectHandler; + private ILogger m_logger; + private ITelemetryContext m_context; + private ApplicationConfiguration m_configuration; + private ISession m_session; + + const string ServerUrl = "opc.tcp://localhost:62541"; + //const string ServerUrl = "opc.tcp://WhiteCat:4880/Softing/OpcUa/TestServer"; + const int kMaxSearchDepth = 128; + const int ReconnectPeriod = 1000; + const int ReconnectPeriodExponentialBackoff = 15000; + + public RunConnectAll(ApplicationConfiguration configuration, ITelemetryContext context) + { + m_context = context; + m_configuration = configuration; + m_logger = context.CreateLogger("Test"); + + m_reconnectHandler = new SessionReconnectHandler( + context, + true, + ReconnectPeriodExponentialBackoff); + } + + private string GetUserCertificateFile(string securityPolicyUri) + { + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + switch (securityPolicy.CertificateKeyAlgorithm) + { + default: + case CertificateKeyAlgorithm.RSA: + case CertificateKeyAlgorithm.RSADH: + return $"iama.tester.rsa.der"; + case CertificateKeyAlgorithm.BrainpoolP256r1: + return $"iama.tester.brainpoolP256r1.der"; + case CertificateKeyAlgorithm.BrainpoolP384r1: + return $"iama.tester.brainpoolP384r1.der"; + case CertificateKeyAlgorithm.NistP256: + return $"iama.tester.nistP256.der"; + case CertificateKeyAlgorithm.NistP384: + return $"iama.tester.nistP384.der"; + } + } + + public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken ct) + { + try + { + m_logger.LogInformation("OPC UA Security Test Client"); + + // The application name and config file names + const string applicationName = "ConsoleReferenceClient"; + const string configSectionName = "Quickstarts.ReferenceClient"; + + // Define the UA Client application + var passwordProvider = new CertificatePasswordProvider([]); + + var application = new ApplicationInstance(m_context) + { + ApplicationName = applicationName, + ApplicationType = ApplicationType.Client, + ConfigSectionName = configSectionName, + CertificatePasswordProvider = passwordProvider + }; + + // load the application configuration. + var configuration = m_configuration = await application + .LoadApplicationConfigurationAsync(silent: false, ct: ct) + .ConfigureAwait(false); + + m_configuration.CertificateValidator.CertificateValidation += CertificateValidation; + + // check the application certificate. + bool haveAppCertificate = await application + .CheckApplicationInstanceCertificatesAsync(false, ct: ct) + .ConfigureAwait(false); + + if (!haveAppCertificate) + { + throw new InvalidOperationException("Application instance certificate invalid!"); + } + + m_logger.LogInformation("Connecting to... {ServerUrl}", ServerUrl); + + var endpoints = await GetEndpoints( + m_configuration, + ServerUrl, + ct).ConfigureAwait(false); + + //endpoints = endpoints.Where(x => x.SecurityPolicyUri == SecurityPolicies.RSA_DH_ChaChaPoly).ToList(); + + var endpointConfiguration = EndpointConfiguration.Create(m_configuration); + var sessionFactory = new DefaultSessionFactory(m_context); + var userNameidentity = new UserIdentity("sysadmin", new UTF8Encoding(false).GetBytes("demo")); + //var userNameidentity = new UserIdentity("usr", new UTF8Encoding(false).GetBytes("pwd")); + + foreach (var ii in endpoints) + { + var userCertificateFile = GetUserCertificateFile(ii.SecurityPolicyUri); + var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine("..\\..\\pki\\trustedUser\\certs", userCertificateFile)); + var thumbprint = x509.Thumbprint; + + var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false); + + foreach (var identity in new UserIdentity[] { userNameidentity, certificateIdentity }) + { + try + { + m_logger.LogWarning("{Line}", new string('=', 80)); + + m_logger.LogWarning( + "SECURITY-POLICY={SecurityPolicyUri} {SecurityMode}", + SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), + ii.SecurityMode); + + m_logger.LogWarning( + "IDENTITY={DisplayName} {TokenType}", + identity.DisplayName, + identity.TokenType); + + ISession session = await RunTestAsync( + endpointConfiguration, + sessionFactory, + ii, + identity, + ct).ConfigureAwait(false); + + m_logger.LogWarning("Waiting for SecureChannel renew"); + await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false); + + for (int count = 0; count < 8; count++) + { + var result = await session.ReadAsync( + null, + 0, + TimestampsToReturn.Neither, + new ReadValueIdCollection() + { + new ReadValueId() + { + NodeId = Opc.Ua.VariableIds.Server_ServerStatus_CurrentTime, + AttributeId = Attributes.Value + } + }, + ct).ConfigureAwait(false); + + m_logger.LogWarning( + "CurrentTime: {CurrentTime}", + result.Results[0].GetValueOrDefault().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture)); + + await Task.Delay(5000, ct).ConfigureAwait(false); + } + + await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false); + + await session.CloseAsync(true, ct: ct).ConfigureAwait(false); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + Console.WriteLine("StackTrace: {0}", e.StackTrace); + quitEvent.WaitOne(20000); + } + + m_logger.LogWarning( + "TEST COMPLETE: {SecurityPolicyUri} {SecurityMode}", + SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), + ii.SecurityMode); + + m_logger.LogWarning("{Line}", new string('=', 80)); + //break; + } + + //break; + } + + Console.WriteLine("Ctrl-C to stop."); + quitEvent.WaitOne(); + } + catch (Exception e) + { + m_logger.LogError("Exception: {Message}", e.Message); + m_logger.LogTrace("StackTrace: {StackTrace}", e.StackTrace); + } + + return true; + } + + private async Task LoadUserCertificateAsync(string thumbprint, string password, CancellationToken ct) + { +#if NET8_0_OR_GREATER + var store = m_configuration.SecurityConfiguration.TrustedUserCertificates; + + // get user certificate with matching thumbprint + var hit = ( + await store.GetCertificatesAsync(m_context, ct).ConfigureAwait(false) + ).Find(X509FindType.FindByThumbprint, thumbprint, false).FirstOrDefault(); + + // create Certificate Identifier + var cid = new CertificateIdentifier(hit) + { + StorePath = store.StorePath, + StoreType = store.StoreType + }; + + return await UserIdentity.CreateAsync( + cid, + new CertificatePasswordProvider(new UTF8Encoding(false).GetBytes(password)), + m_context, + ct + ).ConfigureAwait(false); +#else + await Task.Delay(1, ct).ConfigureAwait(false); + throw new NotSupportedException("User certificate identity is only supported on .NET 8 or greater."); +#endif + } + + public async Task RunTestAsync( + EndpointConfiguration endpointConfiguration, + DefaultSessionFactory sessionFactory, + EndpointDescription endpointDescription, + UserIdentity identity, + CancellationToken ct) + { + var endpoint = new ConfiguredEndpoint( + null, + endpointDescription, + endpointConfiguration); + + // Create the session + ISession session = await sessionFactory + .CreateAsync( + m_configuration, + endpoint, + false, + false, + m_configuration.ApplicationName, + 600000, + //new UserIdentity(), + (endpointDescription.SecurityMode != MessageSecurityMode.None) ? identity : new UserIdentity(), + null, + ct + ) + .ConfigureAwait(false); + + // Assign the created session + if (session == null || !session.Connected) + { + throw new InvalidOperationException("Could not connect to server at " + ServerUrl); + } + + session.KeepAliveInterval = 10000; + session.KeepAlive += Session_KeepAlive; + + var nodes = await BrowseFullAddressSpaceAsync( + session, + ObjectIds.ObjectsFolder, + null, + ct).ConfigureAwait(false); + + return session; + } + private async ValueTask> GetEndpoints( + ApplicationConfiguration application, + string discoveryUrl, + CancellationToken ct = default) + { + var endpointConfiguration = EndpointConfiguration.Create(application); + + using var client = await DiscoveryClient.CreateAsync( + application, + new Uri(discoveryUrl), + endpointConfiguration, + ct: ct).ConfigureAwait(false); + + return await client.GetEndpointsAsync(null, ct).ConfigureAwait(false); + } + + private void CertificateValidation( + CertificateValidator sender, + CertificateValidationEventArgs e) + { + bool certificateAccepted = false; + + // **** + // Implement a custom logic to decide if the certificate should be + // accepted or not and set certificateAccepted flag accordingly. + // The certificate can be retrieved from the e.Certificate field + // *** + + ServiceResult error = e.Error; + m_logger.LogInformation("{ServiceResult}", error); + if (error.StatusCode == StatusCodes.BadCertificateUntrusted) + { + certificateAccepted = true; + } + + if (certificateAccepted) + { + m_logger.LogInformation( + "Untrusted Certificate accepted. Subject = {Subject}", + e.Certificate.Subject); + e.Accept = true; + } + else + { + m_logger.LogInformation( + "Untrusted Certificate rejected. Subject = {Subject}", + e.Certificate.Subject); + } + } + + private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) + { + try + { + // check for events from discarded sessions. + if (m_session == null || !m_session.Equals(session)) + { + return; + } + + // start reconnect sequence on communication error. + if (ServiceResult.IsBad(e.Status)) + { + SessionReconnectHandler.ReconnectState state = m_reconnectHandler + .BeginReconnect( + m_session, + null, + ReconnectPeriod, + Client_ReconnectComplete); + + if (state == SessionReconnectHandler.ReconnectState.Triggered) + { + m_logger.LogInformation( + "KeepAlive status {Status}, reconnect status {State}, reconnect period {ReconnectPeriod}ms.", + e.Status, + state, + ReconnectPeriod + ); + } + else + { + m_logger.LogInformation( + "KeepAlive status {Status}, reconnect status {State}.", + e.Status, + state); + } + + // cancel sending a new keep alive request, because reconnect is triggered. + e.CancelKeepAlive = true; + } + } + catch (Exception exception) + { + m_logger.LogError(exception, "Error in OnKeepAlive."); + } + } + private void Client_ReconnectComplete(object sender, EventArgs e) + { + // ignore callbacks from discarded objects. + if (!ReferenceEquals(sender, m_reconnectHandler)) + { + return; + } + + lock (m_lock) + { + // if session recovered, Session property is null + if (m_reconnectHandler.Session != null) + { + // ensure only a new instance is disposed + // after reactivate, the same session instance may be returned + if (!ReferenceEquals(m_session, m_reconnectHandler.Session)) + { + m_logger.LogInformation( + "--- RECONNECTED TO NEW SESSION --- {SessionId}", + m_reconnectHandler.Session.SessionId + ); + ISession session = m_session; + m_session = m_reconnectHandler.Session; + Utils.SilentDispose(session); + } + else + { + m_logger.LogInformation( + "--- REACTIVATED SESSION --- {SessionId}", + m_reconnectHandler.Session.SessionId); + } + } + else + { + m_logger.LogInformation("--- RECONNECT KeepAlive recovered ---"); + } + } + } + + private async Task BrowseFullAddressSpaceAsync( + ISession session, + NodeId startingNode = null, + BrowseDescription browseDescription = null, + CancellationToken ct = default) + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + // Browse template + const int kMaxReferencesPerNode = 1000; + BrowseDescription browseTemplate = + browseDescription + ?? new BrowseDescription + { + NodeId = startingNode ?? ObjectIds.RootFolder, + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, + IncludeSubtypes = true, + NodeClassMask = 0, + ResultMask = (uint)BrowseResultMask.All + }; + BrowseDescriptionCollection browseDescriptionCollection + = CreateBrowseDescriptionCollectionFromNodeId( + [.. new NodeId[] { startingNode ?? ObjectIds.RootFolder }], + browseTemplate); + + // Browse + var referenceDescriptions = new Dictionary(); + var random = new Random(11211); // use a fixed seed for test reproducibility + + int searchDepth = 0; + uint maxNodesPerBrowse = session.OperationLimits.MaxNodesPerBrowse; + while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth) + { + searchDepth++; + m_logger.LogInformation( + "{SearchDepth}: Browse {Count} nodes after {ElapsedMilliseconds}ms", + searchDepth, + browseDescriptionCollection.Count, + stopWatch.ElapsedMilliseconds); + + var allBrowseResults = new BrowseResultCollection(); + bool repeatBrowse; + var browseResultCollection = new BrowseResultCollection(); + var unprocessedOperations = new BrowseDescriptionCollection(); + DiagnosticInfoCollection diagnosticsInfoCollection; + do + { + BrowseDescriptionCollection browseCollection = + maxNodesPerBrowse == 0 + ? browseDescriptionCollection + : browseDescriptionCollection.Take((int)maxNodesPerBrowse).ToArray(); + repeatBrowse = false; + try + { + RequestHeader requestHeader = null; + + // a random pattern to obscure the message size + // (only useful for application running over untrusted networks). + if (session.ConfiguredEndpoint.Description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + // a real application needs to use secure randomness +#pragma warning disable CA5394 // Do not use insecure randomness + var padding = new byte[random.Next() % 128]; + random.NextBytes(padding); +#pragma warning restore CA5394 // Do not use insecure randomness + + m_logger.LogWarning("Sending Padding with {Length} Bytes", padding.Length); + + requestHeader = new RequestHeader + { + AdditionalHeader = new ExtensionObject(new Opc.Ua.AdditionalParametersType() + { + Parameters = new KeyValuePairCollection([ + new Opc.Ua.KeyValuePair() { + Key = AdditionalParameterNames.Padding, + Value = new Variant(padding) + } + ]) + }) + }; + } + + BrowseResponse browseResponse = await + session.BrowseAsync( + requestHeader, + null, + kMaxReferencesPerNode, + browseCollection, + ct) + .ConfigureAwait(false); + browseResultCollection = browseResponse.Results; + diagnosticsInfoCollection = browseResponse.DiagnosticInfos; + ClientBase.ValidateResponse(browseResultCollection, browseCollection); + ClientBase.ValidateDiagnosticInfos( + diagnosticsInfoCollection, + browseCollection); + + // separate unprocessed nodes for later + int ii = 0; + foreach (BrowseResult browseResult in browseResultCollection) + { + // check for error. + StatusCode statusCode = browseResult.StatusCode; + if (StatusCode.IsBad(statusCode)) + { + // this error indicates that the server does not have enough simultaneously active + // continuation points. This request will need to be resent after the other operations + // have been completed and their continuation points released. + if (statusCode == StatusCodes.BadNoContinuationPoints) + { + unprocessedOperations.Add(browseCollection[ii++]); + continue; + } + } + + // save results. + allBrowseResults.Add(browseResult); + ii++; + } + } + catch (ServiceResultException sre) + { + if (sre.StatusCode is StatusCodes.BadEncodingLimitsExceeded or StatusCodes + .BadResponseTooLarge) + { + // try to address by overriding operation limit + maxNodesPerBrowse = + maxNodesPerBrowse == 0 + ? (uint)browseCollection.Count / 2 + : maxNodesPerBrowse / 2; + repeatBrowse = true; + } + else + { + m_logger.LogError("Browse error: {Message}", sre.Message); + throw; + } + } + } while (repeatBrowse); + + if (maxNodesPerBrowse == 0) + { + browseDescriptionCollection.Clear(); + } + else + { + browseDescriptionCollection = browseDescriptionCollection + .Skip(browseResultCollection.Count) + .ToArray(); + } + + // Browse next + ByteStringCollection continuationPoints = PrepareBrowseNext(browseResultCollection); + while (continuationPoints.Count > 0) + { + m_logger.LogInformation("BrowseNext {Count} continuation points.", continuationPoints.Count); + BrowseNextResponse browseNextResult = await + session.BrowseNextAsync(null, false, continuationPoints, ct) + .ConfigureAwait(false); + BrowseResultCollection browseNextResultCollection = browseNextResult.Results; + diagnosticsInfoCollection = browseNextResult.DiagnosticInfos; + ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints); + ClientBase.ValidateDiagnosticInfos( + diagnosticsInfoCollection, + continuationPoints); + allBrowseResults.AddRange(browseNextResultCollection); + continuationPoints = PrepareBrowseNext(browseNextResultCollection); + } + + // Build browse request for next level + var browseTable = new NodeIdCollection(); + int duplicates = 0; + foreach (BrowseResult browseResult in allBrowseResults) + { + foreach (ReferenceDescription reference in browseResult.References) + { + if (!referenceDescriptions.ContainsKey(reference.NodeId)) + { + referenceDescriptions[reference.NodeId] = reference; + if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty) + { + browseTable.Add( + ExpandedNodeId.ToNodeId( + reference.NodeId, + session.NamespaceUris)); + } + } + else + { + duplicates++; + } + } + } + if (duplicates > 0) + { + m_logger.LogInformation("Browse Result {Duplicates} duplicate nodes were ignored.", duplicates); + } + browseDescriptionCollection.AddRange( + CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate)); + + // add unprocessed nodes if any + browseDescriptionCollection.AddRange(unprocessedOperations); + } + + stopWatch.Stop(); + + var result = new ReferenceDescriptionCollection(referenceDescriptions.Values); + result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); + + m_logger.LogWarning( + "BrowseFullAddressSpace found {Count} references on server in {ElapsedMilliseconds}ms.", + referenceDescriptions.Count, + stopWatch.ElapsedMilliseconds); + + return result; + } + + private static BrowseDescriptionCollection CreateBrowseDescriptionCollectionFromNodeId( + NodeIdCollection nodeIdCollection, + BrowseDescription template) + { + var browseDescriptionCollection = new BrowseDescriptionCollection(); + foreach (NodeId nodeId in nodeIdCollection) + { + var browseDescription = (BrowseDescription)template.MemberwiseClone(); + browseDescription.NodeId = nodeId; + browseDescriptionCollection.Add(browseDescription); + } + return browseDescriptionCollection; + } + + private static ByteStringCollection PrepareBrowseNext( + BrowseResultCollection browseResultCollection) + { + var continuationPoints = new ByteStringCollection(); + foreach (BrowseResult browseResult in browseResultCollection) + { + if (browseResult.ContinuationPoint != null) + { + continuationPoints.Add(browseResult.ContinuationPoint); + } + } + return continuationPoints; + } + } +} diff --git a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 new file mode 100644 index 000000000..3174d2485 --- /dev/null +++ b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 @@ -0,0 +1,83 @@ +# 1. Ensure directories exist +$certDir = "./pki/trustedUser/certs" +$privateDir = "./pki/trustedUser/private" + +$curves = @( + 'nistP256', + 'nistP384', + 'brainpoolP256r1', + 'brainpoolP384r1' +) + +foreach ($d in @($certDir, $privateDir)) { + if (-not (Test-Path $d)) { + New-Item -ItemType Directory -Path $d -Force | Out-Null + } +} + +# 2. Create a self-signed ECC certificate (NIST P-256) +# $cert = New-SelfSignedCertificate ` +# -Subject "CN=iama.tester@example.com" ` +# -CertStoreLocation "Cert:\CurrentUser\My" ` +# -KeyExportPolicy Exportable ` +# -KeySpec Signature ` +# -KeyAlgorithm ECDSA_nistP256 ` +# -Curve 'CurveName' ` +# -HashAlgorithm SHA256 ` +# -NotAfter (Get-Date).AddYears(1) + +foreach ($curve in $curves) { + + Write-Host "Generating certificate for curve: $curve" + + $signatureAlgorithm = if ($curve -match 'P384') { 'SHA384' } else { 'SHA256' } + + # Create certificate parameters and dynamically insert the curve + $params = @{ + Type = 'Custom' + Subject = 'CN=iama.tester@example.com' + TextExtension = @( + '2.5.29.37={text}1.3.6.1.5.5.7.3.2' + '2.5.29.17={text}upn=iama.tester@example.com' + ) + KeyUsage = @('DigitalSignature', 'NonRepudiation') + KeyAlgorithm = "ECDSA_$curve" # <-- dynamic! + CurveExport = 'CurveName' + HashAlgorithm = $signatureAlgorithm + CertStoreLocation = 'Cert:\CurrentUser\My' + } + + # 1. Create cert + $cert = New-SelfSignedCertificate @params + + # 2. Export DER + $derPath = Join-Path $certDir "iama.tester.$curve.der" + Export-Certificate -Cert $cert -FilePath $derPath -Type CERT + + # 3. Export PFX with password + $secret = ConvertTo-SecureString -String "password" -Force -AsPlainText + $pfxPath = Join-Path $privateDir "iama.tester.$curve.pfx" + Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $secret + + Write-Host "Finished: $curve`n" +} + +Write-Host "`n=== Generating RSA-2048 ===" + +$rsaParams = @{ + Type = 'Custom' + Subject = 'CN=iama.tester@example.com' + TextExtension = @( + '2.5.29.37={text}1.3.6.1.5.5.7.3.2' + '2.5.29.17={text}upn=iama.tester@example.com' + ) + KeyUsage = @('DigitalSignature','DataEncipherment','NonRepudiation','KeyEncipherment') + KeyAlgorithm = 'RSA' + KeyLength = 2048 + CertStoreLocation = 'Cert:\CurrentUser\My' +} + +$rsaCert = New-SelfSignedCertificate @rsaParams + +Export-Certificate -Cert $rsaCert -FilePath (Join-Path $certDir "iama.tester.rsa.der") -Type CERT +Export-PfxCertificate -Cert $rsaCert -FilePath (Join-Path $privateDir "iama.tester.rsa.pfx") -Password $secret diff --git a/Applications/ConsoleReferenceServer/Program.cs b/Applications/ConsoleReferenceServer/Program.cs index f6fcedf82..ce42ddbdc 100644 --- a/Applications/ConsoleReferenceServer/Program.cs +++ b/Applications/ConsoleReferenceServer/Program.cs @@ -61,9 +61,9 @@ public static async Task Main(string[] args) // command line options bool showHelp = false; - bool autoAccept = false; - bool logConsole = false; - bool appLog = false; + bool autoAccept = true; + bool logConsole = true; + bool appLog = true; bool fileLog = false; bool renewCertificate = false; bool shadowConfig = false; @@ -158,7 +158,13 @@ await server } // setup the logging - telemetry.ConfigureLogging(server.Configuration, applicationName, logConsole, fileLog, appLog, LogLevel.Information); + telemetry.ConfigureLogging( + server.Configuration, + applicationName, + logConsole, + fileLog, + appLog, + LogLevel.Warning); // check or renew the certificate Console.WriteLine("Check the certificate."); diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 16fd703f6..b612da440 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -14,35 +14,35 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -51,17 +51,17 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/issuer + ../../pki/issuer Directory - %LocalApplicationData%/OPC Foundation/pki/trusted + ../../pki/trusted Directory - %LocalApplicationData%/OPC Foundation/pki/rejected + ../../pki/rejected 5 Directory - %LocalApplicationData%/OPC Foundation/pki/issuerUser + ../../pki/issuerUser Directory - %LocalApplicationData%/OPC Foundation/pki/trustedUser + ../../pki/trustedUser @@ -94,11 +94,11 @@ 4194304 65535 30000 - 3600000 + 30000 - opc.https://localhost:62540/Quickstarts/ReferenceServer + opc.tcp://localhost:62541/Quickstarts/ReferenceServer @@ -122,6 +122,7 @@ --> + Sign_2 - http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None_1 + http://opcfoundation.org/UA/SecurityPolicy#None + 5 100 @@ -335,7 +385,7 @@ - %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceServer.log.txt + ./Logs/Quickstarts.ReferenceServer.log.txt true diff --git a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs index c98397494..8e25a2e7e 100644 --- a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs +++ b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs @@ -251,21 +251,21 @@ public override UserTokenPolicyCollection GetUserTokenPolicies( } // sample how to modify default user token policies - if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && - description.SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - return [.. policies.Where(u => u.TokenType != UserTokenType.Certificate)]; - } - else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && - description.SecurityMode == MessageSecurityMode.Sign) - { - return [.. policies.Where(u => u.TokenType != UserTokenType.Anonymous)]; - } - else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && - description.SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - return [.. policies.Where(u => u.TokenType != UserTokenType.UserName)]; - } + //if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && + // description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + //{ + // return [.. policies.Where(u => u.TokenType != UserTokenType.Certificate)]; + //} + //else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + // description.SecurityMode == MessageSecurityMode.Sign) + //{ + // return [.. policies.Where(u => u.TokenType != UserTokenType.Anonymous)]; + //} + //else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + // description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + //{ + // return [.. policies.Where(u => u.TokenType != UserTokenType.UserName)]; + //} return policies; } @@ -332,7 +332,7 @@ private void SessionManager_ImpersonateUser(ISession session, ImpersonateEventAr m_logger.LogInformation( Utils.TraceMasks.Security, "X509 Token Accepted: {Identity}", - args.Identity?.DisplayName); + args.Identity.DisplayName); return; } diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 73670373b..873635dcf 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -308,7 +308,7 @@ private void ValidateServerNonce( if (!Nonce.ValidateNonce( serverNonce, MessageSecurityMode.SignAndEncrypt, - (uint)m_configuration.SecurityConfiguration.NonceLength)) + m_configuration.SecurityConfiguration.NonceLength)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) @@ -875,7 +875,7 @@ public virtual void Restore(SessionState state) public void Snapshot(out SessionConfiguration sessionConfiguration) { var serverNonce = Nonce.CreateNonce( - m_endpoint.Description?.SecurityPolicyUri, + SecurityPolicies.GetInfo(m_endpoint.Description?.SecurityPolicyUri), m_serverNonce); sessionConfiguration = new SessionConfiguration { @@ -1115,8 +1115,8 @@ await m_configuration } // create a nonce. - uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; - byte[] clientNonce = Nonce.CreateRandomNonceData(length); + int length = m_configuration.SecurityConfiguration.NonceLength; + m_clientNonce = Nonce.CreateRandomNonceData(length); // send the application instance certificate for the client. BuildCertificateData( @@ -1144,10 +1144,10 @@ await m_configuration bool successCreateSession = false; CreateSessionResponse? response = null; - //if security none, first try to connect without certificate + // if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { - //first try to connect with client certificate NULL + // first try to connect with client certificate NULL try { response = await base.CreateSessionAsync( @@ -1156,7 +1156,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, null, sessionTimeout, maxMessageSize, @@ -1179,7 +1179,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, clientCertificateChainData ?? clientCertificateData, sessionTimeout, maxMessageSize, @@ -1196,8 +1196,6 @@ await m_configuration byte[] serverCertificateData = response.ServerCertificate; SignatureData serverSignature = response.ServerSignature; EndpointDescriptionCollection serverEndpoints = response.ServerEndpoints; - SignedSoftwareCertificateCollection serverSoftwareCertificates = response - .ServerSoftwareCertificates; m_sessionTimeout = response.RevisedSessionTimeout; m_maxRequestMessageSize = response.MaxRequestMessageSize; @@ -1230,18 +1228,27 @@ await m_configuration serverSignature, clientCertificateData, clientCertificateChainData, - clientNonce); - - HandleSignedSoftwareCertificates(serverSoftwareCertificates); + m_clientNonce, + serverNonce); // process additional header ProcessResponseAdditionalHeader(response.ResponseHeader, serverCertificate); // create the client signature. - byte[] dataToSign = Utils.Append(serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + // create the client signature. + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // select the security policy for the user token. @@ -1252,37 +1259,49 @@ await m_configuration tokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; } - // save previous nonce - byte[]? previousServerNonce = GetCurrentTokenServerNonce(); - // validate server nonce and security parameters for user identity. ValidateServerNonce( identity, serverNonce, tokenSecurityPolicyUri, - previousServerNonce, + m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri, - m_telemetry); + SignatureData? userTokenSignature = null; - // encrypt token. - identityToken.Encrypt( - serverCertificate, - serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + if (identityToken is X509IdentityToken) + { + // sign data with user token. + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri, + m_telemetry); + } + else + { + // encrypt token. + identityToken.Encrypt( + serverCertificate, + serverNonce, + m_userTokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates - = GetSoftwareCertificates(); + SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); // copy the preferred locales if provided. if (preferredLocales != null && preferredLocales.Count > 0) @@ -1290,16 +1309,18 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates m_preferredLocales = [.. preferredLocales]; } + var header = CreateRequestHeaderForActivateSession(securityPolicy, tokenSecurityPolicyUri); + // activate session. ActivateSessionResponse activateResponse = await ActivateSessionAsync( - null, - clientSignature, - clientSoftwareCertificates, - m_preferredLocales, - new ExtensionObject(identityToken), - userTokenSignature, - ct) - .ConfigureAwait(false); + header, + clientSignature, + clientSoftwareCertificates, + m_preferredLocales, + new ExtensionObject(identityToken), + userTokenSignature, + ct) + .ConfigureAwait(false); // process additional header ProcessResponseAdditionalHeader(activateResponse.ResponseHeader, serverCertificate); @@ -1320,12 +1341,6 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates } } - if (clientSoftwareCertificates?.Count > 0 && - (certificateResults == null || certificateResults.Count == 0)) - { - m_logger.LogInformation("Empty results were received for the ActivateSession call."); - } - // fetch namespaces. await FetchNamespaceTablesAsync(ct).ConfigureAwait(false); @@ -1334,7 +1349,7 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates // save nonces. m_sessionName = sessionName; m_identity = identity; - m_previousServerNonce = previousServerNonce; + m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; m_serverCertificate = serverCertificate; @@ -1419,12 +1434,20 @@ public async Task UpdateSessionAsync( // get the identity token. string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // choose a default token. @@ -1435,10 +1458,14 @@ public async Task UpdateSessionAsync( m_endpoint.Description.FindUserTokenPolicy( identity.TokenType, identity.IssuedTokenType, - securityPolicyUri) - ?? throw ServiceResultException.Create( - StatusCodes.BadIdentityTokenRejected, - "Endpoint does not support the user identity type provided."); + securityPolicyUri); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadIdentityTokenRejected, + "Endpoint does not support the user identity type provided."); + } // select the security policy for the user token. string tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; @@ -1469,6 +1496,16 @@ public async Task UpdateSessionAsync( // sign data with user token. UserIdentityToken identityToken = identity.GetIdentityToken(); identityToken.PolicyId = identityPolicy.PolicyId; + + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + SignatureData userTokenSignature = identityToken.Sign( dataToSign, tokenSecurityPolicyUri, @@ -1488,11 +1525,14 @@ public async Task UpdateSessionAsync( m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates - = GetSoftwareCertificates(); + SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); + + RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( + securityPolicy, + tokenSecurityPolicyUri); ActivateSessionResponse response = await ActivateSessionAsync( - null, + requestHeader, clientSignature, clientSoftwareCertificates, preferredLocales, @@ -2279,12 +2319,23 @@ public async Task ReconnectAsync( // await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, m_serverNonce); + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.ChannelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + EndpointDescription endpoint = m_endpoint.Description; - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( endpoint.SecurityPolicyUri, + m_instanceCertificate, dataToSign); // check that the user identity is supported by the endpoint. @@ -2323,6 +2374,16 @@ public async Task ReconnectAsync( // sign data with user token. UserIdentityToken identityToken = m_identity.GetIdentityToken(); identityToken.PolicyId = identityPolicy.PolicyId; + + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + SignatureData userTokenSignature = identityToken.Sign( dataToSign, tokenSecurityPolicyUri, @@ -2340,8 +2401,7 @@ public async Task ReconnectAsync( m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates - = GetSoftwareCertificates(); + SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); @@ -2409,7 +2469,16 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); - var header = new RequestHeader { TimeoutHint = kReconnectTimeout }; + var header = CreateRequestHeaderForActivateSession( + securityPolicy, + tokenSecurityPolicyUri); + + if (header == null) + { + header = new RequestHeader(); + } + + header.TimeoutHint = kReconnectTimeout; using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromMilliseconds(kReconnectTimeout / 2)); @@ -2640,14 +2709,6 @@ public bool RemoveTransferredSubscription(Subscription subscription) return true; } - /// - /// Returns the software certificates assigned to the application. - /// - protected virtual SignedSoftwareCertificateCollection GetSoftwareCertificates() - { - return []; - } - /// /// Handles an error when validating the application instance certificate provided by the server. /// @@ -2659,26 +2720,6 @@ protected virtual void OnApplicationCertificateError( throw new ServiceResultException(result); } - /// - /// Handles an error when validating software certificates provided by the server. - /// - /// - protected virtual void OnSoftwareCertificateError( - SignedSoftwareCertificate signedCertificate, - ServiceResult result) - { - throw new ServiceResultException(result); - } - - /// - /// Inspects the software certificates provided by the server. - /// - protected virtual void ValidateSoftwareCertificates( - List softwareCertificates) - { - // always accept valid certificates. - } - /// /// Starts a timer to check that the connection to the server is still available. /// @@ -3875,36 +3916,48 @@ private void ValidateServerSignature( SignatureData serverSignature, byte[]? clientCertificateData, byte[]? clientCertificateChainData, - byte[] clientNonce) + byte[] clientNonce, + byte[] serverNonce) { if (serverSignature == null || serverSignature.Signature == null) { m_logger.LogInformation("Server signature is null or empty."); - - //throw ServiceResultException.Create( - // StatusCodes.BadSecurityChecksFailed, - // "Server signature is null or empty."); + return; } // validate the server's signature. - byte[] dataToSign = Utils.Append(clientCertificateData, clientNonce); + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(m_endpoint.Description.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.ChannelThumbprint, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateData, + TransportChannel.ClientChannelCertificate, + serverNonce); - if (!SecurityPolicies.Verify( + if (!SecurityPolicies.VerifySignatureData( + serverSignature, + securityPolicy, serverCertificate, - m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + dataToSign)) { // validate the signature with complete chain if the check with leaf certificate failed. if (clientCertificateChainData != null) { - dataToSign = Utils.Append(clientCertificateChainData, clientNonce); - - if (!SecurityPolicies.Verify( - serverCertificate, - m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.ChannelThumbprint, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateChainData, + TransportChannel.ClientChannelCertificate, + serverNonce); + + if (!SecurityPolicies.VerifySignatureData( + serverSignature, + securityPolicy, + serverCertificate, + dataToSign)) { throw ServiceResultException.Create( StatusCodes.BadApplicationSignatureInvalid, @@ -4166,47 +4219,6 @@ private static void UpdateDescription( return (result, error); } - /// - /// If available, returns the current nonce or null. - /// - private byte[]? GetCurrentTokenServerNonce() - { - ChannelToken? currentToken = (NullableTransportChannel as ISecureChannel)?.CurrentToken; - return currentToken?.ServerNonce; - } - - /// - /// Handles the validation of server software certificates and application callback. - /// - private void HandleSignedSoftwareCertificates( - SignedSoftwareCertificateCollection serverSoftwareCertificates) - { - // get a validator to check certificates provided by server. - CertificateValidator validator = m_configuration.CertificateValidator; - - // validate software certificates. - var softwareCertificates = new List(); - - foreach (SignedSoftwareCertificate signedCertificate in serverSoftwareCertificates) - { - ServiceResult result = SoftwareCertificate.Validate( - validator, - signedCertificate.CertificateData, - m_telemetry, - out SoftwareCertificate softwareCertificate); - - if (ServiceResult.IsBad(result)) - { - OnSoftwareCertificateError(signedCertificate, result); - } - - softwareCertificates.Add(softwareCertificate); - } - - // check if software certificates meet application requirements. - ValidateSoftwareCertificates(softwareCertificates); - } - /// /// Processes the response from a publish request. /// @@ -4805,16 +4817,54 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( { userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; } + m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; - if (EccUtils.IsEccPolicy(userTokenSecurityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { var parameters = new AdditionalParametersType(); parameters.Parameters.Add( - new KeyValuePair { Key = "ECDHPolicyUri", Value = userTokenSecurityPolicyUri }); + new KeyValuePair { Key = AdditionalParameterNames.ECDHPolicyUri, Value = userTokenSecurityPolicyUri }); requestHeader.AdditionalHeader = new ExtensionObject(parameters); + + m_logger.LogWarning("Request EphemeralKey for {Policy}.", userTokenSecurityPolicyUri); + } + + return requestHeader; + } + + private RequestHeader? CreateRequestHeaderForActivateSession( + SecurityPolicyInfo securityPolicy, + string userTokenSecurityPolicyUri) + { + var requestHeader = new RequestHeader(); + var parameters = new AdditionalParametersType(); + + if (userTokenSecurityPolicyUri != null) + { + var userTokenSecurityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); + + if (userTokenSecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + parameters.Parameters.Add( + new KeyValuePair + { + Key = AdditionalParameterNames.ECDHPolicyUri, + Value = userTokenSecurityPolicyUri + }); + + m_logger.LogWarning("Requesting new EphmeralKey using {SecurityPolicyUri}.", userTokenSecurityPolicyUri); + } } + if (parameters.Parameters.Count == 0) + { + return null; + } + + requestHeader.AdditionalHeader = new ExtensionObject(parameters); return requestHeader; } @@ -4839,7 +4889,30 @@ protected virtual void ProcessResponseAdditionalHeader( { foreach (KeyValuePair ii in parameters.Parameters) { - if (ii.Key == "ECDHKey") + if (ii.Key == AdditionalParameterNames.Padding) + { + var padding = ii.Value.Value as byte[]; + + if (ii.Value.TypeInfo != TypeInfo.Scalars.ByteString || padding == null) + { + m_logger.LogWarning( + "Server returned invalid message padding. Ignored."); + } + else if (padding.Length > 128) + { + m_logger.LogWarning( + "Server returned a {Size}byte message padding that is too long. Ignored.", + padding.Length); + } + else + { + m_logger.LogWarning("Ignoring Padding with {Length} Bytes", padding.Length); + } + + continue; + } + + if (ii.Key == AdditionalParameterNames.ECDHKey) { if (ii.Value.TypeInfo == TypeInfo.Scalars.StatusCode) { @@ -4856,7 +4929,7 @@ protected virtual void ProcessResponseAdditionalHeader( "Server did not provide a valid ECDHKey. User authentication not possible."); } - if (!EccUtils.Verify( + if (!CryptoUtils.Verify( new ArraySegment(key.PublicKey), key.Signature, serverCertificate, @@ -4868,8 +4941,10 @@ protected virtual void ProcessResponseAdditionalHeader( } m_eccServerEphemeralKey = Nonce.CreateNonce( - m_userTokenSecurityPolicyUri, + SecurityPolicies.GetInfo(m_userTokenSecurityPolicyUri), key.PublicKey); + + m_logger.LogWarning("Updating ServerEphemeralKey: {Key}", CryptoTrace.KeyToString(m_eccServerEphemeralKey.Data)); } } } @@ -4951,6 +5026,7 @@ protected virtual void ProcessResponseAdditionalHeader( private readonly NodeCache m_nodeCache; private readonly List m_identityHistory = []; private byte[]? m_serverNonce; + private byte[]? m_clientNonce; private byte[]? m_previousServerNonce; private X509Certificate2? m_serverCertificate; private uint m_publishCounter; diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 19f9da779..5aade135f 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -943,7 +943,7 @@ await DeleteApplicationInstanceCertificateAsync(configuration, id, ct).Configure else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(id.CertificateType) + CryptoUtils.GetCurveFromCertificateTypeId(id.CertificateType) ?? throw new ServiceResultException( StatusCodes.BadConfigurationError, "The Ecc certificate type is not supported."); diff --git a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs index d23cd51d2..a9969e985 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs @@ -674,7 +674,7 @@ private static bool TryGetECCCurve(NodeId certificateType, out ECCurve curve) return false; } curve = - EccUtils.GetCurveFromCertificateTypeId(certificateType) + CryptoUtils.GetCurveFromCertificateTypeId(certificateType) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, $"The certificate type {certificateType} is not supported."); diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index fde4bcd1a..962496b33 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -980,7 +980,7 @@ private X509Certificate2 GenerateTemporaryApplicationCertificate( else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(certificateTypeId) + CryptoUtils.GetCurveFromCertificateTypeId(certificateTypeId) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, "The Ecc certificate type is not supported."); diff --git a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs index 73ff4e8a7..a43bcd872 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs @@ -1078,25 +1078,6 @@ public static void ReportAuditActivateSessionEvent( Utils.Clone(session?.IdentityToken), false); - if (softwareCertificates != null) - { - // build the list of SignedSoftwareCertificate - var signedSoftwareCertificates = new List(); - foreach (SoftwareCertificate softwareCertificate in softwareCertificates) - { - var item = new SignedSoftwareCertificate - { - CertificateData = softwareCertificate.SignedCertificate.RawData - }; - signedSoftwareCertificates.Add(item); - } - e.SetChildValue( - systemContext, - BrowseNames.ClientSoftwareCertificates, - signedSoftwareCertificates.ToArray(), - false); - } - server.ReportAuditEvent(systemContext, e); } catch (Exception e) diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 834e47ea4..17b6d0c58 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -529,7 +529,7 @@ X509Certificate2Collection clientCertificateChain serverEndpoints = GetEndpointDescriptions(endpointUrl, BaseAddresses, null); // return the software certificates assigned to the server. - serverSoftwareCertificates = [.. ServerProperties.SoftwareCertificates]; + serverSoftwareCertificates = new(); // sign the nonce provided by the client. serverSignature = null; @@ -537,10 +537,19 @@ X509Certificate2Collection clientCertificateChain // sign the client nonce (if provided). if (parsedClientCertificate != null && clientNonce != null) { - byte[] dataToSign = Utils.Append(parsedClientCertificate.RawData, clientNonce); - serverSignature = SecurityPolicies.Sign( - instanceCertificate, + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(context.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + context.ChannelContext.ChannelThumbprint, + clientNonce, + context.ChannelContext.ServerChannelCertificate, + parsedClientCertificate.RawData, + context.ChannelContext.ClientChannelCertificate, + serverNonce); + + serverSignature = SecurityPolicies.CreateSignatureData( context.SecurityPolicyUri, + instanceCertificate, dataToSign); } } @@ -640,29 +649,36 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet foreach (KeyValuePair ii in parameters.Parameters) { - if (ii.Key == "ECDHPolicyUri") + if (ii.Key == AdditionalParameterNames.ECDHPolicyUri) { string policyUri = ii.Value.ToString(); + m_logger.LogWarning("Received request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); + + var securityPolicy = SecurityPolicies.GetInfo(policyUri); - if (EccUtils.IsEccPolicy(policyUri)) + if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - session.SetEccUserTokenSecurityPolicy(policyUri); - EphemeralKeyType key = session.GetNewEccKey(); + session.SetUserTokenSecurityPolicy(policyUri); + EphemeralKeyType key = session.GetNewEphemeralKey(); response.Parameters.Add( new KeyValuePair { - Key = "ECDHKey", + Key = AdditionalParameterNames.ECDHKey, Value = new ExtensionObject(key) }); + + m_logger.LogWarning("Returning new EphmeralKey: {PublicKey}.", CryptoTrace.KeyToString(key.PublicKey)); } else { response.Parameters.Add( new KeyValuePair { - Key = "ECDHKey", + Key = AdditionalParameterNames.ECDHKey, Value = StatusCodes.BadSecurityPolicyRejected }); + + m_logger.LogWarning("Rejecting request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); } } } @@ -683,13 +699,15 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam { AdditionalParametersType response = null; - EphemeralKeyType key = session.GetNewEccKey(); + EphemeralKeyType key = session.GetNewEphemeralKey(); if (key != null) { response = new AdditionalParametersType(); response.Parameters - .Add(new KeyValuePair { Key = "ECDHKey", Value = new ExtensionObject(key) }); + .Add(new KeyValuePair { Key = AdditionalParameterNames.ECDHKey, Value = new ExtensionObject(key) }); + + m_logger.LogWarning("Returning new EphmeralKey: {PublicKey}.", CryptoTrace.KeyToString(key.PublicKey)); } return response; @@ -729,64 +747,6 @@ public override async Task ActivateSessionAsync( try { - if (context?.SecurityPolicyUri != SecurityPolicies.None) - { - bool diagnosticsExist = false; - - if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) - { - diagnosticInfos = []; - } - - results = []; - diagnosticInfos = []; - - foreach (SignedSoftwareCertificate signedCertificate in clientSoftwareCertificates) - { - ServiceResult result = SoftwareCertificate.Validate( - CertificateValidator, - signedCertificate.CertificateData, - m_serverInternal.Telemetry, - out SoftwareCertificate softwareCertificate); - - if (ServiceResult.IsBad(result)) - { - results.Add(result.Code); - - // add diagnostics if requested. - if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) - { - DiagnosticInfo diagnosticInfo = ServerUtils.CreateDiagnosticInfo( - ServerInternal, - context, - result, - m_logger); - diagnosticInfos.Add(diagnosticInfo); - diagnosticsExist = true; - } - } - else - { - softwareCertificates.Add(softwareCertificate); - results.Add(StatusCodes.Good); - - // add diagnostics if requested. - if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) - { - diagnosticInfos.Add(null); - } - } - } - - if (!diagnosticsExist && diagnosticInfos != null) - { - diagnosticInfos.Clear(); - } - } - - // check if certificates meet the server's requirements. - ValidateSoftwareCertificates(softwareCertificates); - // activate the session. (bool identityChanged, serverNonce) = await ServerInternal.SessionManager.ActivateSessionAsync( context, @@ -806,6 +766,7 @@ public override async Task ActivateSessionAsync( ISession session = ServerInternal.SessionManager .GetSession(requestHeader.AuthenticationToken); + var parameters = ExtensionObject.ToEncodeable( requestHeader.AdditionalHeader) as AdditionalParametersType; @@ -2728,16 +2689,6 @@ protected virtual void OnApplicationCertificateError( throw new ServiceResultException(result); } - /// - /// Inspects the software certificates provided by the server. - /// - /// The software certificates. - protected virtual void ValidateSoftwareCertificates( - List softwareCertificates) - { - // always accept valid certificates. - } - /// /// Verifies that the request header is valid. /// diff --git a/Libraries/Opc.Ua.Server/Session/ISession.cs b/Libraries/Opc.Ua.Server/Session/ISession.cs index 020ae48e6..3674cf459 100644 --- a/Libraries/Opc.Ua.Server/Session/ISession.cs +++ b/Libraries/Opc.Ua.Server/Session/ISession.cs @@ -43,6 +43,11 @@ public interface ISession : IDisposable /// bool Activated { get; } + /// + /// The application instance certificate associated with the server. + /// + X509Certificate2 ServerCertificate { get; } + /// /// The application instance certificate associated with the client. /// @@ -129,7 +134,7 @@ bool Activate( /// Create new ECC ephemeral key /// /// A new ephemeral key - EphemeralKeyType GetNewEccKey(); + EphemeralKeyType GetNewEphemeralKey(); /// /// Checks if the secure channel is currently valid. @@ -173,7 +178,7 @@ bool Activate( /// /// Set the ECC security policy URI /// - void SetEccUserTokenSecurityPolicy(string securityPolicyUri); + void SetUserTokenSecurityPolicy(string securityPolicyUri); /// /// Updates the requested locale ids. diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index fb348f74e..9fb31e855 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -95,6 +95,7 @@ public Session( m_clientIssuerCertificates = clientCertificateChain; SecureChannelId = context.ChannelContext.SecureChannelId; + m_channelThumbprint = context.ChannelContext.ChannelThumbprint; MaxBrowseContinuationPoints = maxBrowseContinuationPoints; m_maxHistoryContinuationPoints = maxHistoryContinuationPoints; EndpointDescription = context.ChannelContext.EndpointDescription; @@ -238,6 +239,11 @@ protected virtual void Dispose(bool disposing) /// public byte[] ClientNonce { get; } + /// + /// The application instance certificate associated with the server. + /// + public X509Certificate2 ServerCertificate => m_serverCertificate; + /// /// The application instance certificate associated with the client. /// @@ -286,12 +292,12 @@ public DateTime ClientLastContactTime /// /// Set the ECC security policy URI /// - public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) + public virtual void SetUserTokenSecurityPolicy(string securityPolicyUri) { lock (m_lock) { - m_eccUserTokenSecurityPolicyUri = securityPolicyUri; - m_eccUserTokenNonce = null; + m_userTokenSecurityPolicyUri = securityPolicyUri; + m_userTokenNonce = null; } } @@ -299,23 +305,23 @@ public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) /// Create new ECC ephemeral key /// /// A new ephemeral key - public virtual EphemeralKeyType GetNewEccKey() + public virtual EphemeralKeyType GetNewEphemeralKey() { lock (m_lock) { - if (m_eccUserTokenSecurityPolicyUri == null) + if (m_userTokenSecurityPolicyUri == null) { return null; } - m_eccUserTokenNonce = Nonce.CreateNonce(m_eccUserTokenSecurityPolicyUri); + m_userTokenNonce = Nonce.CreateNonce(m_userTokenSecurityPolicyUri); - var key = new EphemeralKeyType { PublicKey = m_eccUserTokenNonce.Data }; + var key = new EphemeralKeyType { PublicKey = m_userTokenNonce.Data }; - key.Signature = EccUtils.Sign( + key.Signature = CryptoUtils.Sign( new ArraySegment(key.PublicKey), m_serverCertificate, - m_eccUserTokenSecurityPolicyUri); + m_userTokenSecurityPolicyUri); return key; } @@ -472,15 +478,21 @@ public void ValidateBeforeActivate( StatusCodes.BadApplicationSignatureInvalid); } - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(EndpointDescription.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); - if (!SecurityPolicies.Verify( - ClientCertificate, + if (!SecurityPolicies.VerifySignatureData( + clientSignature, EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + ClientCertificate, + dataToSign)) { // verify for certificate chain in endpoint. // validate the signature with complete chain if the check with leaf certificate failed. @@ -501,15 +513,19 @@ public void ValidateBeforeActivate( byte[] serverCertificateChainData = [.. serverCertificateChainList]; - dataToSign = Utils.Append( + dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, serverCertificateChainData, - m_serverNonce.Data); - - if (!SecurityPolicies.Verify( - ClientCertificate, - EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); + + if (!SecurityPolicies.VerifySignatureData( + clientSignature, + EndpointDescription.SecurityPolicyUri, + ClientCertificate, + dataToSign)) { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); @@ -542,12 +558,14 @@ public void ValidateBeforeActivate( // validate the user identity token. identityToken = ValidateUserIdentityToken( + context, userIdentityToken, userTokenSignature, out userTokenPolicy); TraceState("VALIDATED"); } + } /// @@ -847,6 +865,7 @@ private ServiceResult OnUpdateSecurityDiagnostics( /// /// private UserIdentityToken ValidateUserIdentityToken( + OperationContext context, ExtensionObject identityToken, SignatureData userTokenSignature, out UserTokenPolicy policy) @@ -1024,7 +1043,7 @@ private UserIdentityToken ValidateUserIdentityToken( m_serverNonce, securityPolicyUri, m_server.MessageContext, - m_eccUserTokenNonce, + m_userTokenNonce, ClientCertificate, m_clientIssuerCertificates); } @@ -1039,9 +1058,16 @@ private UserIdentityToken ValidateUserIdentityToken( // verify the signature. if (securityPolicyUri != SecurityPolicies.None) { - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + byte[] dataToSign = securityPolicy.GetUserTokenSignatureData( + context.ChannelContext.ChannelThumbprint, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + ClientCertificate?.RawData, + context.ChannelContext.ClientChannelCertificate, + ClientNonce ?? []); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry)) { @@ -1283,8 +1309,9 @@ private void UpdateDiagnosticCounters( private readonly string m_sessionName; private X509Certificate2 m_serverCertificate; private Nonce m_serverNonce; - private string m_eccUserTokenSecurityPolicyUri; - private Nonce m_eccUserTokenNonce; + private byte[] m_channelThumbprint; + private string m_userTokenSecurityPolicyUri; + private Nonce m_userTokenNonce; private readonly X509Certificate2Collection m_clientIssuerCertificates; private readonly int m_maxHistoryContinuationPoints; private readonly SessionSecurityDiagnosticsDataType m_securityDiagnostics; diff --git a/Libraries/Opc.Ua.Server/Session/SessionManager.cs b/Libraries/Opc.Ua.Server/Session/SessionManager.cs index a087b3bf8..e07ccd152 100644 --- a/Libraries/Opc.Ua.Server/Session/SessionManager.cs +++ b/Libraries/Opc.Ua.Server/Session/SessionManager.cs @@ -219,8 +219,7 @@ public virtual async ValueTask CreateSessionAsync( } // create server nonce. - var serverNonceObject = Nonce.CreateNonce( - context.ChannelContext.EndpointDescription.SecurityPolicyUri); + var serverNonceObject = Nonce.CreateNonce(0); // assign client name. if (string.IsNullOrEmpty(sessionName)) diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index 60222fb08..1e3f5928c 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -174,6 +174,8 @@ protected virtual void Dispose(bool disposing) /// public string ListenerId { get; private set; } + internal byte[] ServerChannelCertificate { get; set; } + /// /// Opens the listener and starts accepting connection. /// @@ -304,6 +306,8 @@ private void ConfigureWebHost(IWebHostBuilder webHostBuilder) m_logger.LogTrace("Copy of the private key for https was denied: {Message}", ce.Message); } #endif + // save the server certificate so it can be used in the secure channel context. + ServerChannelCertificate = serverCertificate.RawData; var httpsOptions = new HttpsConnectionAdapterOptions { @@ -486,10 +490,13 @@ await WriteServiceResponseAsync(context, serviceResponse, ct) return; } } + var secureChannelContext = new SecureChannelContext( - ListenerId, - endpoint, - RequestEncoding.Binary); + ListenerId, + endpoint, + RequestEncoding.Binary, + context.Connection.ClientCertificate?.RawData, + ServerChannelCertificate); IServiceResponse output = await m_callback.ProcessRequestAsync( diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index 28ef021c2..61a5fe375 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -374,8 +374,17 @@ public static byte CalculateSecurityLevel( result = 2; break; case SecurityPolicies.Basic256: - logger.LogWarning( - "Deprecated Security Policy Basic256 requested - Not rcommended."); + logger.LogWarning("Deprecated Security Policy Basic256 requested - Not recommended."); + result = 4; + break; + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_nistP[256/384]_AES.", policyUri); + result = 4; + break; + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_brainpoolP[256/384]r1_AES.", policyUri); result = 4; break; case SecurityPolicies.Basic256Sha256: @@ -387,16 +396,20 @@ public static byte CalculateSecurityLevel( case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; - case SecurityPolicies.ECC_brainpoolP256r1: - result = 11; - break; - case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: result = 12; break; - case SecurityPolicies.ECC_brainpoolP384r1: - result = 13; + case SecurityPolicies.ECC_brainpoolP256r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: + case SecurityPolicies.ECC_nistP256_AesGcm: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: + result = 12; break; - case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AesGcm: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: + case SecurityPolicies.ECC_brainpoolP384r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result = 14; break; case SecurityPolicies.None: diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 1a8dad537..4048a56e7 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -708,7 +708,7 @@ public static NodeId GetCertificateType(X509Certificate2 certificate) case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - return EccUtils.GetEccCertificateTypeId(certificate); + return CryptoUtils.GetEccCertificateTypeId(certificate); case Oids.RsaPkcs1Sha256: case Oids.RsaPkcs1Sha384: case Oids.RsaPkcs1Sha512: @@ -739,7 +739,7 @@ public static bool ValidateCertificateType( case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - NodeId certType = EccUtils.GetEccCertificateTypeId(certificate); + NodeId certType = CryptoUtils.GetEccCertificateTypeId(certificate); if (certType.IsNullNodeId) { return false; @@ -795,32 +795,45 @@ public static IList MapSecurityPolicyToCertificateTypes(string securityP case SecurityPolicies.Basic256Sha256: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: result.Add(ObjectTypeIds.RsaSha256ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP256_AesGcm: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP256ApplicationCertificateType); goto case SecurityPolicies.ECC_nistP384; case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AesGcm: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP384ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP256r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); goto case SecurityPolicies.ECC_brainpoolP384r1; case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_brainpoolP384r1_AesGcm: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve25519_AesGcm: + case SecurityPolicies.ECC_curve25519_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve25519ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve448: + case SecurityPolicies.ECC_curve448_AesGcm: + case SecurityPolicies.ECC_curve448_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve448ApplicationCertificateType); - goto default; + break; case SecurityPolicies.Https: result.Add(ObjectTypeIds.HttpsCertificateType); - goto default; - default: - return result; + break; } + return result; } /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs new file mode 100644 index 000000000..cc6e55fa9 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -0,0 +1,1312 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.Globalization; +using System.Numerics; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Opc.Ua.Bindings; +using Opc.Ua.Security.Certificates; +#if CURVE25519 +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Digests; +#endif + +namespace Opc.Ua +{ + /// + /// Defines functions to implement ECC cryptography. + /// + public static class CryptoUtils + { + /// + /// The name of the NIST P-256 curve. + /// + public const string NistP256 = nameof(NistP256); + + /// + /// The name of the NIST P-384 curve. + /// + public const string NistP384 = nameof(NistP384); + + /// + /// The name of the BrainpoolP256r1 curve. + /// + public const string BrainpoolP256r1 = nameof(BrainpoolP256r1); + + /// + /// The name of the BrainpoolP384r1 curve. + /// + public const string BrainpoolP384r1 = nameof(BrainpoolP384r1); + + internal const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; + internal const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; + internal const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; + internal const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; + + /// + /// Returns true if the certificate is an ECC certificate. + /// + public static bool IsEccPolicy(string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info != null) + { + return info.CertificateKeyFamily == CertificateKeyFamily.ECC; + } + + return false; + } + + /// + /// Returns the NodeId for the certificate type for the specified certificate. + /// + public static NodeId GetEccCertificateTypeId(X509Certificate2 certificate) + { + string keyAlgorithm = certificate.GetKeyAlgorithm(); + if (keyAlgorithm != Oids.ECPublicKey) + { + return NodeId.Null; + } + + PublicKey encodedPublicKey = certificate.PublicKey; + switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) + { + // nistP256 + case NistP256KeyParameters: + return ObjectTypeIds.EccNistP256ApplicationCertificateType; + // nistP384 + case NistP384KeyParameters: + return ObjectTypeIds.EccNistP384ApplicationCertificateType; + // brainpoolP256r1 + case BrainpoolP256r1KeyParameters: + return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; + // brainpoolP384r1 + case BrainpoolP384r1KeyParameters: + return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; + default: + return NodeId.Null; + } + } + + /// + /// returns an ECCCurve if there is a matching supported curve for the provided + /// certificate type id. if no supported ECC curve is found null is returned. + /// + /// the application certificatate type node id + /// the ECCCurve, null if certificatate type id has no matching supported ECC curve + public static ECCurve? GetCurveFromCertificateTypeId(NodeId certificateType) + { + ECCurve? curve = null; + + if (certificateType == ObjectTypeIds.EccApplicationCertificateType || + certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP256; + } + else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP384; + } + else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP256r1; + } + else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP384r1; + } +#if CURVE25519 + else if (certificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) + { + curve = default(ECCurve); + } + else if (certificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) + { + curve = default(ECCurve); + } +#endif + return curve; + } + + /// + /// Returns the signature algorithm for the specified certificate. + /// + public static string GetECDsaQualifier(X509Certificate2 certificate) + { + if (X509Utils.IsECDsaSignature(certificate)) + { + const string signatureQualifier = "ECDsa"; + PublicKey encodedPublicKey = certificate.PublicKey; + + // New values can be determined by running the dotted-decimal OID value + // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); + + switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) + { + case NistP256KeyParameters: + return NistP256; + case NistP384KeyParameters: + return NistP384; + case BrainpoolP256r1KeyParameters: + return BrainpoolP256r1; + case BrainpoolP384r1KeyParameters: + return BrainpoolP384r1; + default: + return signatureQualifier; + } + } + return string.Empty; + } + + /// + /// Returns the public key for the specified certificate. + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate) + { + return GetPublicKey(certificate, out string[] _); + } + + /// + /// Returns the public key for the specified certificate and outputs the security policy uris. + /// + /// + /// + public static ECDsa GetPublicKey( + X509Certificate2 certificate, + out string[] securityPolicyUris) + { + securityPolicyUris = null; + + if (certificate == null) + { + return null; + } + + string keyAlgorithm = certificate.GetKeyAlgorithm(); + + if (keyAlgorithm != Oids.ECPublicKey) + { + return null; + } + + const X509KeyUsageFlags kSufficientFlags = + X509KeyUsageFlags.KeyAgreement | + X509KeyUsageFlags.DigitalSignature | + X509KeyUsageFlags.NonRepudiation | + X509KeyUsageFlags.CrlSign | + X509KeyUsageFlags.KeyCertSign; + + foreach (X509Extension extension in certificate.Extensions) + { + if (extension.Oid.Value == "2.5.29.15") + { + var kuExt = (X509KeyUsageExtension)extension; + + if ((kuExt.KeyUsages & kSufficientFlags) == 0) + { + return null; + } + } + } + + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString( + encodedPublicKey.EncodedParameters.RawData); + byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; + + var ecParameters = default(ECParameters); + + if (keyValue[0] != 0x04) + { + throw new InvalidOperationException("Only uncompressed points are supported"); + } + + byte[] x = new byte[(keyValue.Length - 1) / 2]; + byte[] y = new byte[x.Length]; + + Buffer.BlockCopy(keyValue, 1, x, 0, x.Length); + Buffer.BlockCopy(keyValue, 1 + x.Length, y, 0, y.Length); + + ecParameters.Q.X = x; + ecParameters.Q.Y = y; + + // New values can be determined by running the dotted-decimal OID value + // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); + + switch (keyParameters) + { + case NistP256KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.nistP256; + securityPolicyUris = [SecurityPolicies.ECC_nistP256]; + break; + case NistP384KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.nistP384; + securityPolicyUris = [SecurityPolicies.ECC_nistP384, SecurityPolicies + .ECC_nistP256]; + break; + case BrainpoolP256r1KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; + securityPolicyUris = [SecurityPolicies.ECC_brainpoolP256r1]; + break; + case BrainpoolP384r1KeyParameters: + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; + securityPolicyUris = [SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies + .ECC_brainpoolP256r1]; + break; + default: + throw new NotImplementedException(keyParameters); + } + + return ECDsa.Create(ecParameters); + } + + /// + /// Returns the length of a ECDsa signature of a digest. + /// + /// + public static int GetSignatureLength(X509Certificate2 signingCertificate) + { + if (signingCertificate == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "No public key for certificate."); + } + + if (signingCertificate.GetRSAPublicKey() != null) + { + return RsaUtils.GetSignatureLength(signingCertificate); + } + + using ECDsa publicKey = + GetPublicKey(signingCertificate) + ?? throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "No public key for certificate."); + + return publicKey.KeySize / 4; + } + + /// + /// Computes an ECDSA signature. + /// + public static byte[] Sign( + ArraySegment dataToSign, + X509Certificate2 signingCertificate, + string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return Sign(dataToSign, signingCertificate, info.AsymmetricSignatureAlgorithm); + } + + /// + /// Computes an signature. + /// + /// + public static byte[] Sign( + ArraySegment dataToSign, + X509Certificate2 signingCertificate, + AsymmetricSignatureAlgorithm algorithm) + { + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.None: + return null; + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA1, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPssSha256: + return RsaUtils.Rsa_Sign( + dataToSign, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + break; + default: + throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); + } + + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; + + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}"); + } + + ECDsa senderPrivateKey = + signingCertificate.GetECDsaPrivateKey() + ?? throw new ServiceResultException( + StatusCodes.BadCertificateInvalid, + "Missing private key needed for create a signature."); + + using (senderPrivateKey) + { + byte[] signature = senderPrivateKey.SignData( + dataToSign.Array, + dataToSign.Offset, + dataToSign.Count, + hashAlgorithm); + + return signature; + } + } + + /// + /// Verifies a ECDsa signature. + /// + public static bool Verify( + ArraySegment dataToVerify, + byte[] signature, + X509Certificate2 signingCertificate, + string securityPolicyUri) + { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info == null) + { + throw new ServiceResultException( + StatusCodes.BadSecurityChecksFailed, + $"Unknown security policy: {securityPolicyUri}"); + } + + return Verify( + dataToVerify, + signature, + signingCertificate, + info.AsymmetricSignatureAlgorithm); + } + + /// + /// Verifies a ECDsa signature. + /// + public static bool Verify( + ArraySegment dataToVerify, + byte[] signature, + X509Certificate2 signingCertificate, + AsymmetricSignatureAlgorithm algorithm) + { + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.None: + return true; + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA1, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + case AsymmetricSignatureAlgorithm.RsaPssSha256: + return RsaUtils.Rsa_Verify( + dataToVerify, + signature, + signingCertificate, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + break; + default: + return false; + } + + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; + + switch (algorithm) + { + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}."); + } + + using ECDsa ecdsa = GetPublicKey(signingCertificate); + + return ecdsa.VerifyData( + dataToVerify.Array, + dataToVerify.Offset, + dataToVerify.Count, + signature, + hashAlgorithm); + } + + /// + /// Adds padding to a buffer. Input: buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding. + /// + /// buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding. + /// + /// Output: buffer with unencrypted data starting at 0; plaintext data starting at offset; padding added. + private static ArraySegment AddPadding(ArraySegment data, int blockSize) + { + int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; + int paddingSize = blockSize - ((data.Count + paddingByteSize) % blockSize); + paddingSize %= blockSize; + + int endOfData = data.Offset + data.Count; + int endOfPaddedData = data.Offset + data.Count + paddingSize + paddingByteSize; + + for (int ii = endOfData; ii < endOfPaddedData - paddingByteSize && ii < data.Array.Length; ii++) + { + data.Array[ii] = (byte)(paddingSize & 0xFF); + } + + data.Array[endOfData + paddingSize] = (byte)(paddingSize & 0xFF); + + if (blockSize > byte.MaxValue) + { + data.Array[endOfData + paddingSize + 1] = (byte)((paddingSize & 0xFF) >> 8); + } + + return new ArraySegment(data.Array, data.Offset, data.Count + paddingSize + paddingByteSize); + } + + /// + /// Removes padding from a buffer. Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed. + /// + /// Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed. + /// + /// Output: buffer with unencrypted data starting at 0; plaintext starting at offset; padding excluded. + /// + private static ArraySegment RemovePadding(ArraySegment data, int blockSize) + { + int paddingSize = data.Array[data.Offset + data.Count - 1]; + int paddingByteSize = 1; + + if (blockSize > byte.MaxValue) + { + paddingSize <<= 8; + paddingSize += data.Array[data.Offset + data.Count - 2]; + paddingByteSize = 2; + } + + int notvalid = paddingSize < data.Count ? 0 : 1; + int start = data.Offset + data.Count - paddingSize - paddingByteSize; + + for (int ii = data.Offset; ii < data.Count - paddingByteSize && ii < paddingSize; ii++) + { + if (start < 0 || start + ii >= data.Count) + { + notvalid |= 1; + continue; + } + + notvalid |= data.Array[start + ii] ^ (paddingSize & 0xFF); + } + + if (notvalid != 0) + { + throw new CryptographicException("Invalid padding."); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - paddingSize - paddingByteSize); + } + + /// + /// Encrypts the buffer using the algorithm specified by the security policy. + /// + public static ArraySegment SymmetricEncryptAndSign( + ArraySegment data, + SecurityPolicyInfo securityPolicy, + byte[] encryptingKey, + byte[] iv, + byte[] signingKey = null, + HMAC hmac = null, + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) + { + SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; + + if (algorithm == SymmetricEncryptionAlgorithm.None) + { + return data; + } + + if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) + { +#if NET8_0_OR_GREATER + return EncryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); +#else + throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); +#endif + } + + if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) + { +#if NET8_0_OR_GREATER + return EncryptWithChaCha20Poly1305( + data, + encryptingKey, + iv, + signOnly, + tokenId, + lastSequenceNumber); +#else + throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); +#endif + } + + if (!signOnly) + { + data = AddPadding(data, iv.Length); + } + + if (signingKey != null) + { + byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count); + + Buffer.BlockCopy( + hash, + 0, + data.Array, + data.Offset + data.Count, + hash.Length); + + data = new ArraySegment( + data.Array, + data.Offset, + data.Count + hash.Length); + } + + if (!signOnly) + { +#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector + using var aes = Aes.Create(); + + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = encryptingKey; + aes.IV = iv; + + using ICryptoTransform encryptor = aes.CreateEncryptor(); +#pragma warning restore CA5401 + + encryptor.TransformBlock( + data.Array, + data.Offset, + data.Count, + data.Array, + data.Offset); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count); + } + +#if NET8_0_OR_GREATER + private static byte[] ApplyAeadMask(uint tokenId, uint lastSequenceNumber, byte[] iv) + { + var copy = new byte[iv.Length]; + Buffer.BlockCopy(iv, 0, copy, 0, iv.Length); + + copy[0] ^= (byte)((tokenId & 0x000000FF)); + copy[1] ^= (byte)((tokenId & 0x0000FF00) >> 8); + copy[2] ^= (byte)((tokenId & 0x00FF0000) >> 16); + copy[3] ^= (byte)((tokenId & 0xFF000000) >> 24); + copy[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); + copy[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); + copy[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); + copy[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + + return copy; + } + + private const int kChaChaPolyIvLength = 12; + private const int kChaChaPolyTagLength = 16; + + private static ArraySegment EncryptWithChaCha20Poly1305( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null || encryptingKey.Length != 32) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kChaChaPolyIvLength) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); + } + + byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; + byte[] tag = new byte[kChaChaPolyTagLength]; // ChaCha20-Poly1305/AES-GCM uses 128-bit authentication tag + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count : data.Offset); + + using var chacha = new ChaCha20Poly1305(encryptingKey); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + chacha.Encrypt( + iv, + signOnly ? Array.Empty() : data, + ciphertext, + tag, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "EncryptWithChaCha20Poly1305"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(ciphertext)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("EncryptWithChaCha20Poly1305"); + + // Return layout: [associated data | ciphertext | tag] + if (!signOnly) + { + Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length); + } + + Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length); + + return new ArraySegment( + data.Array, + 0, + data.Offset + data.Count + kChaChaPolyTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private static ArraySegment DecryptWithChaCha20Poly1305( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null || encryptingKey.Length != 32) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kChaChaPolyIvLength) + { + throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); + } + + if (data.Count < kChaChaPolyTagLength) // Must at least contain tag + { + throw new ArgumentException("Ciphertext too short.", nameof(data)); + } + + byte[] plaintext = new byte[data.Count - kChaChaPolyTagLength]; + + var encryptedData = new ArraySegment( + data.Array, + data.Offset, + signOnly ? 0 : data.Count - kChaChaPolyTagLength); + + var tag = new ArraySegment( + data.Array, + data.Offset + data.Count - kChaChaPolyTagLength, + kChaChaPolyTagLength); + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count - kChaChaPolyTagLength : data.Offset); + + using var chacha = new ChaCha20Poly1305(encryptingKey); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + chacha.Decrypt( + iv, + encryptedData, + tag, + signOnly ? [] : plaintext, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "DecryptWithChaCha20Poly1305"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count - kChaChaPolyTagLength}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(encryptedData)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithChaCha20Poly1305"); + + // Return layout: [associated data | plaintext] + if (!signOnly) + { + Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - kChaChaPolyTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private const int kAesGcmIvLength = 12; + private const int kAesGcmTagLength = 16; + + private static ArraySegment EncryptWithAesGcm( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null) + { + throw new ArgumentNullException(nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kAesGcmIvLength) + { + throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); + } + + byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; + byte[] tag = new byte[kAesGcmTagLength]; // AES-GCM uses 128-bit authentication tag + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count : data.Offset); + + using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + aesGcm.Encrypt( + iv, + signOnly ? Array.Empty() : data, + ciphertext, + tag, + extraData); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "EncryptWithAesGcm"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(ciphertext)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithAesGcm"); + + // Return layout: [associated data | ciphertext | tag] + if (!signOnly) + { + Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length); + } + + Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length); + + return new ArraySegment( + data.Array, + 0, + data.Offset + data.Count + kAesGcmTagLength); + } +#endif + +#if NET8_0_OR_GREATER + private static ArraySegment DecryptWithAesGcm( + ArraySegment data, + byte[] encryptingKey, + byte[] iv, + bool signOnly, + uint tokenId, + uint lastSequenceNumber) + { + if (encryptingKey == null) + { + throw new ArgumentNullException(nameof(encryptingKey)); + } + + if (iv == null || iv.Length != kAesGcmIvLength) + { + throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); + } + + if (data.Count < kAesGcmTagLength) // Must at least contain tag + { + throw new ArgumentException("Ciphertext too short.", nameof(data)); + } + + byte[] plaintext = new byte[data.Count - kAesGcmTagLength]; + + var encryptedData = new ArraySegment( + data.Array, + data.Offset, + signOnly ? 0 : data.Count - kAesGcmTagLength); + + var tag = new ArraySegment( + data.Array, + data.Offset + data.Count - kAesGcmTagLength, + kAesGcmTagLength); + + var extraData = new ReadOnlySpan( + data.Array, + 0, + signOnly ? data.Offset + data.Count - kAesGcmTagLength : data.Offset); + + using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + + CryptoTrace.Start(ConsoleColor.DarkCyan, "DecryptWithAesGcm"); + CryptoTrace.WriteLine($"Data Offset/Count={data.Offset}/{data.Count - kAesGcmTagLength}"); + CryptoTrace.WriteLine($"TokenId/LastSequenceNumber={tokenId}/{lastSequenceNumber}"); + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.WriteLine($"EncryptedData={CryptoTrace.KeyToString(encryptedData)}"); + CryptoTrace.WriteLine($"Tag={CryptoTrace.KeyToString(tag)}"); + CryptoTrace.WriteLine($"ExtraData={CryptoTrace.KeyToString(extraData.ToArray())}"); + CryptoTrace.Finish("DecryptWithAesGcm"); + + aesGcm.Decrypt( + iv, + encryptedData, + tag, + signOnly ? [] : plaintext, + extraData); + + // Return layout: [associated data | plaintext] + if (!signOnly) + { + Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count - kAesGcmTagLength); + } +#endif + + /// + /// Decrypts the buffer using the algorithm specified by the security policy. + /// + /// + /// + public static ArraySegment SymmetricDecryptAndVerify( + ArraySegment data, + SecurityPolicyInfo securityPolicy, + byte[] encryptingKey, + byte[] iv, + byte[] signingKey = null, + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) + { + SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; + + if (algorithm == SymmetricEncryptionAlgorithm.None) + { + return data; + } + + if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) + { +#if NET8_0_OR_GREATER + return DecryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); +#else + throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); +#endif + } + + if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) + { +#if NET8_0_OR_GREATER + return DecryptWithChaCha20Poly1305( + data, + encryptingKey, + iv, + signOnly, + tokenId, + lastSequenceNumber); +#else + throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); +#endif + } + + if (!signOnly) + { + using var aes = Aes.Create(); + + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = encryptingKey; + aes.IV = iv; + + using ICryptoTransform decryptor = aes.CreateDecryptor(); + + decryptor.TransformBlock( + data.Array, + data.Offset, + data.Count, + data.Array, + data.Offset); + } + + int isNotValid = 0; + + if (signingKey != null) + { + using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey); + byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count - (hmac.HashSize / 8)); + for (int ii = 0; ii < hash.Length; ii++) + { + int index = data.Offset + data.Count - hash.Length + ii; + isNotValid |= data.Array[index] != hash[ii] ? 1 : 0; + } + + data = new ArraySegment( + data.Array, + data.Offset, + data.Count - hash.Length); + } + + if (!signOnly) + { + data = RemovePadding(data, iv.Length); + } + + if (isNotValid != 0) + { + throw new CryptographicException("Invalid signature."); + } + + return new ArraySegment(data.Array, 0, data.Offset + data.Count); + } + } + +#if X + class FfdheDhWithRsaAuth + { + // ffdhe2048 prime from RFC 7919 (hex, without whitespace). + // (RFC 7919 Appendix A.3 — use this canonical modulus in production.) + const string FFDHE2048_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 61285C97 FFFFFFFF FFFFFFFF"; + + // Generator for FFDHE groups is 2 + static readonly BigInteger G = new BigInteger(2); + + static void Main() + { + // Parse the RFC hex prime into a positive BigInteger + BigInteger p = ParseHexBigInteger(FFDHE2048_HEX); + + // Recommended: use a short ephemeral exponent (e.g. 256-bit) for performance + // while still achieving ~128-bit security for typical scenarios. + // (RFC7919 and implementation guidance discuss exponent sizing; choose per your threat model.) + int privateBitLength = 256; + + Console.WriteLine("Simulating DH exchange (ffdhe2048) with RSA signing..."); + + // Simulate Alice and Bob + var alice = CreateDhParticipant(p, privateBitLength); + var bob = CreateDhParticipant(p, privateBitLength); + + // Each signs their public value with their own RSA key (DHE-RSA style authentication) + byte[] alicePub = ToBigEndian(alice.PublicValue); + byte[] bobPub = ToBigEndian(bob.PublicValue); + + byte[] aliceSig, bobSig; + using (RSA rsaAlice = RSA.Create(2048)) + using (RSA rsaBob = RSA.Create(2048)) + { + // In real use the RSA keys are persistent (server cert) and public keys/certificates exchanged + aliceSig = rsaAlice.SignData(alicePub, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + bobSig = rsaBob.SignData(bobPub, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Each verifies the other's signature (in a real protocol they'd have the peer's cert/public key) + bool verifyAlice = rsaBob.VerifyData(bobPub, bobSig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + bool verifyBob = rsaAlice.VerifyData(alicePub, aliceSig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + Console.WriteLine($"Alice verifies Bob's signature: {verifyAlice}"); + Console.WriteLine($"Bob verifies Alice's signature: {verifyBob}"); + } + + // Compute shared secrets + BigInteger aliceShared = BigInteger.ModPow(bob.PublicValue, alice.PrivateValue, p); + BigInteger bobShared = BigInteger.ModPow(alice.PublicValue, bob.PrivateValue, p); + + if (aliceShared != bobShared) + { + Console.WriteLine("Shared secrets do not match! Aborting."); + return; + } + + byte[] sharedBytes = ToBigEndian(aliceShared); // same as bobShared + + // Derive 32-byte key with HKDF-SHA256 (RFC 5869) + byte[] salt = RandomNumberGenerator.GetBytes(32); // optional but recommended + byte[] info = Encoding.UTF8.GetBytes("ffdhe2048-dhe-rsa-derived-key"); + byte[] aesKey = HkdfSha256(sharedBytes, salt, info, 32); + } + + // Creates a participant with ephemeral private/public values + static (BigInteger PrivateValue, BigInteger PublicValue) CreateDhParticipant(BigInteger p, int privateBitLen) + { + BigInteger priv = GenerateRandomBigInteger(privateBitLen); + BigInteger pub = BigInteger.ModPow(G, priv, p); + return (priv, pub); + } + + // Generate an unsigned positive BigInteger of bitLength bits (big-endian) + static BigInteger GenerateRandomBigInteger(int bitLength) + { + int byteCount = (bitLength + 7) / 8; + byte[] be = RandomNumberGenerator.GetBytes(byteCount); + + // Ensure top bit set so the value has the requested bit length (avoid tiny exponents) + int topBitIndex = (bitLength - 1) % 8; + be[0] |= (byte)(1 << topBitIndex); + + // Convert big-endian to little-endian + sign byte for BigInteger ctor + byte[] le = new byte[be.Length + 1]; // extra zero to force positive + for (int i = 0; i < be.Length; i++) + le[i] = be[be.Length - 1 - i]; + le[le.Length - 1] = 0; + return new BigInteger(le); + } + + // Convert BigInteger to big-endian unsigned byte[] (no leading zero) + static byte[] ToBigEndian(BigInteger value) + { + if (value.Sign < 0) + throw new ArgumentException("value must be non-negative"); + byte[] le = value.ToByteArray(); // little-endian two's complement + // Trim any trailing zero that indicates sign if present + int last = le.Length - 1; + if (le[last] == 0) + { + Array.Resize(ref le, last); + last--; + } + byte[] be = new byte[le.Length]; + for (int i = 0; i < le.Length; i++) + be[i] = le[le.Length - 1 - i]; + return be; + } + + // Parse hex into a positive BigInteger (handles odd-length and ensures positive) + static BigInteger ParseHexBigInteger(string hex) + { + hex = hex.Trim().Replace(" ", "").Replace("\n", "").Replace("\r", ""); + // Ensure even length + if ((hex.Length & 1) == 1) + hex = "0" + hex; + // Use Convert.FromHexString (available in .NET 5+) or implement equivalent + byte[] be = Convert.FromHexString(hex); + // If highest bit of first byte is set, BigInteger.Parse with HexNumber can treat it as negative; + // we therefore construct positive BigInteger from big-endian bytes: + byte[] le = new byte[be.Length + 1]; + for (int i = 0; i < be.Length; i++) + le[i] = be[be.Length - 1 - i]; + le[le.Length - 1] = 0; + return new BigInteger(le); + } + + // Simple HKDF-SHA256 implementation (RFC 5869) + static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) + { + // HKDF-Extract + byte[] prk; + using (var hmac = new HMACSHA256(salt ?? new byte[0])) + { + prk = hmac.ComputeHash(ikm); + } + + // HKDF-Expand + int hashLen = 32; + int n = (outLen + hashLen - 1) / hashLen; + byte[] okm = new byte[outLen]; + byte[] previous = Array.Empty(); + using (var hmac = new HMACSHA256(prk)) + { + int offset = 0; + for (byte i = 1; i <= n; i++) + { + // T(i) = HMAC-PRK( T(i-1) | info | i ) + hmac.Initialize(); + hmac.TransformBlock(previous, 0, previous.Length, null, 0); + if (info != null && info.Length > 0) + hmac.TransformBlock(info, 0, info.Length, null, 0); + byte[] counter = new byte[] { i }; + hmac.TransformFinalBlock(counter, 0, 1); + previous = hmac.Hash!; + int toCopy = Math.Min(hashLen, outLen - offset); + Array.Copy(previous, 0, okm, offset, toCopy); + offset += toCopy; + } + } + return okm; + } + } +#endif + + /// + /// A class to assist with tracing crypto operations. + /// + public static class CryptoTrace + { + /// + /// Starts a trace block. + /// + public static void Start(ConsoleColor color, string format, params object[] args) + { +#if DEBUG + Console.ForegroundColor = color; + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" ============"); +#endif + } + + /// + /// Finishes a trace block. + /// + public static void Finish(string format, params object[] args) + { +#if DEBUG + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" Finished ============"); + Console.ForegroundColor = ConsoleColor.White; +#endif + } + + /// + /// Writes a trace message. + /// + public static void Write(string format, params object[] args) + { +#if DEBUG + Console.Write(format, args); +#endif + } + + /// + /// Writes a trace message. + /// + public static void WriteLine(string format, params object[] args) + { +#if DEBUG + Console.WriteLine(format, args); +#endif + } + + /// + /// Returns a debug string for a key. + /// + public static string KeyToString(ArraySegment key) + { +#if DEBUG + byte[] bytes = new byte[key.Count]; + Buffer.BlockCopy(key.Array ?? [], key.Offset, bytes, 0, key.Count); + return KeyToString(bytes); +#else + return String.Empty; +#endif + } + + /// + /// Returns a debug string for a key. + /// + public static string KeyToString(byte[] key) + { +#if DEBUG + if (key == null || key.Length == 0) + { + return "Len=0:---"; + } + + byte checksum = 0; + + foreach (var item in key) + { + checksum ^= item; + } + + if (key.Length <= 16) + { + return "Len=" + key.Length.ToString(CultureInfo.InvariantCulture) + + ":" + + Utils.ToHexString(key) + + "=>XOR=" + + checksum.ToString(CultureInfo.InvariantCulture); + } + + var text = Utils.ToHexString(key); + return $"Len={key.Length}:{text.Substring(0, 8)}...{text.Substring(text.Length - 8, 8)}=>XOR={checksum}"; +#else + return String.Empty; +#endif + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs deleted file mode 100644 index 1aacf695c..000000000 --- a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs +++ /dev/null @@ -1,1280 +0,0 @@ -/* ======================================================================== - * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. - * - * OPC Foundation MIT License 1.00 - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * The complete license agreement can be found here: - * http://opcfoundation.org/License/MIT/1.00/ - * ======================================================================*/ - -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Opc.Ua.Security.Certificates; -#if CURVE25519 -using Org.BouncyCastle.Pkcs; -using Org.BouncyCastle.X509; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; -using Org.BouncyCastle.Crypto.Agreement; -using Org.BouncyCastle.Crypto.Generators; -using Org.BouncyCastle.Crypto.Modes; -using Org.BouncyCastle.Crypto.Digests; -#endif - -namespace Opc.Ua -{ - /// - /// Defines functions to implement ECC cryptography. - /// - public static class EccUtils - { - /// - /// The name of the NIST P-256 curve. - /// - public const string NistP256 = nameof(NistP256); - - /// - /// The name of the NIST P-384 curve. - /// - public const string NistP384 = nameof(NistP384); - - /// - /// The name of the BrainpoolP256r1 curve. - /// - public const string BrainpoolP256r1 = nameof(BrainpoolP256r1); - - /// - /// The name of the BrainpoolP384r1 curve. - /// - public const string BrainpoolP384r1 = nameof(BrainpoolP384r1); - - internal const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; - internal const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; - internal const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; - internal const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; - - /// - /// Returns true if the certificate is an ECC certificate. - /// - public static bool IsEccPolicy(string securityPolicyUri) - { - if (securityPolicyUri != null) - { - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return true; - default: - return false; - } - } - - return false; - } - - /// - /// Returns the NodeId for the certificate type for the specified certificate. - /// - public static NodeId GetEccCertificateTypeId(X509Certificate2 certificate) - { - string keyAlgorithm = certificate.GetKeyAlgorithm(); - if (keyAlgorithm != Oids.ECPublicKey) - { - return NodeId.Null; - } - - PublicKey encodedPublicKey = certificate.PublicKey; - switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) - { - // nistP256 - case NistP256KeyParameters: - return ObjectTypeIds.EccNistP256ApplicationCertificateType; - // nistP384 - case NistP384KeyParameters: - return ObjectTypeIds.EccNistP384ApplicationCertificateType; - // brainpoolP256r1 - case BrainpoolP256r1KeyParameters: - return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; - // brainpoolP384r1 - case BrainpoolP384r1KeyParameters: - return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; - default: - return NodeId.Null; - } - } - - /// - /// returns an ECCCurve if there is a matching supported curve for the provided - /// certificate type id. if no supported ECC curve is found null is returned. - /// - /// the application certificatate type node id - /// the ECCCurve, null if certificatate type id has no matching supported ECC curve - public static ECCurve? GetCurveFromCertificateTypeId(NodeId certificateType) - { - ECCurve? curve = null; - - if (certificateType == ObjectTypeIds.EccApplicationCertificateType || - certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.nistP256; - } - else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.nistP384; - } - else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.brainpoolP256r1; - } - else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) - { - curve = ECCurve.NamedCurves.brainpoolP384r1; - } -#if CURVE25519 - else if (certificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) - { - curve = default(ECCurve); - } - else if (certificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) - { - curve = default(ECCurve); - } -#endif - return curve; - } - - /// - /// Returns the signature algorithm for the specified certificate. - /// - public static string GetECDsaQualifier(X509Certificate2 certificate) - { - if (X509Utils.IsECDsaSignature(certificate)) - { - const string signatureQualifier = "ECDsa"; - PublicKey encodedPublicKey = certificate.PublicKey; - - // New values can be determined by running the dotted-decimal OID value - // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); - - switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) - { - case NistP256KeyParameters: - return NistP256; - case NistP384KeyParameters: - return NistP384; - case BrainpoolP256r1KeyParameters: - return BrainpoolP256r1; - case BrainpoolP384r1KeyParameters: - return BrainpoolP384r1; - default: - return signatureQualifier; - } - } - return string.Empty; - } - - /// - /// Returns the public key for the specified certificate. - /// - public static ECDsa GetPublicKey(X509Certificate2 certificate) - { - return GetPublicKey(certificate, out string[] _); - } - - /// - /// Returns the public key for the specified certificate and outputs the security policy uris. - /// - /// - /// - public static ECDsa GetPublicKey( - X509Certificate2 certificate, - out string[] securityPolicyUris) - { - securityPolicyUris = null; - - if (certificate == null) - { - return null; - } - - string keyAlgorithm = certificate.GetKeyAlgorithm(); - - if (keyAlgorithm != Oids.ECPublicKey) - { - return null; - } - - const X509KeyUsageFlags kSufficientFlags = - X509KeyUsageFlags.KeyAgreement | - X509KeyUsageFlags.DigitalSignature | - X509KeyUsageFlags.NonRepudiation | - X509KeyUsageFlags.CrlSign | - X509KeyUsageFlags.KeyCertSign; - - foreach (X509Extension extension in certificate.Extensions) - { - if (extension.Oid.Value == "2.5.29.15") - { - var kuExt = (X509KeyUsageExtension)extension; - - if ((kuExt.KeyUsages & kSufficientFlags) == 0) - { - return null; - } - } - } - - PublicKey encodedPublicKey = certificate.PublicKey; - string keyParameters = BitConverter.ToString( - encodedPublicKey.EncodedParameters.RawData); - byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; - - var ecParameters = default(ECParameters); - - if (keyValue[0] != 0x04) - { - throw new InvalidOperationException("Only uncompressed points are supported"); - } - - byte[] x = new byte[(keyValue.Length - 1) / 2]; - byte[] y = new byte[x.Length]; - - Buffer.BlockCopy(keyValue, 1, x, 0, x.Length); - Buffer.BlockCopy(keyValue, 1 + x.Length, y, 0, y.Length); - - ecParameters.Q.X = x; - ecParameters.Q.Y = y; - - // New values can be determined by running the dotted-decimal OID value - // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); - - switch (keyParameters) - { - case NistP256KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.nistP256; - securityPolicyUris = [SecurityPolicies.ECC_nistP256]; - break; - case NistP384KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.nistP384; - securityPolicyUris = [SecurityPolicies.ECC_nistP384, SecurityPolicies - .ECC_nistP256]; - break; - case BrainpoolP256r1KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; - securityPolicyUris = [SecurityPolicies.ECC_brainpoolP256r1]; - break; - case BrainpoolP384r1KeyParameters: - ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; - securityPolicyUris = [SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies - .ECC_brainpoolP256r1]; - break; - default: - throw new NotImplementedException(keyParameters); - } - - return ECDsa.Create(ecParameters); - } - - /// - /// Returns the length of a ECDsa signature of a digest. - /// - /// - public static int GetSignatureLength(X509Certificate2 signingCertificate) - { - if (signingCertificate == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "No public key for certificate."); - } - using ECDsa publicKey = - GetPublicKey(signingCertificate) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "No public key for certificate."); - - return publicKey.KeySize / 4; - } - - /// - /// Returns the hash algorithm for the specified security policy. - /// - /// is null. - /// - public static HashAlgorithmName GetSignatureAlgorithmName(string securityPolicyUri) - { - if (securityPolicyUri == null) - { - throw new ArgumentNullException(nameof(securityPolicyUri)); - } - - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return HashAlgorithmName.SHA256; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return HashAlgorithmName.SHA384; - default: - throw ServiceResultException.Unexpected( - "Unexpected security policy URI for ECC: {0}", securityPolicyUri); - } - } - - /// - /// Computes an ECDSA signature. - /// - public static byte[] Sign( - ArraySegment dataToSign, - X509Certificate2 signingCertificate, - string securityPolicyUri) - { - HashAlgorithmName algorithm = GetSignatureAlgorithmName(securityPolicyUri); - return Sign(dataToSign, signingCertificate, algorithm); - } - - /// - /// Computes an ECDSA signature. - /// - /// - public static byte[] Sign( - ArraySegment dataToSign, - X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) - { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); - - if (publicKey is Ed25519PublicKeyParameters) - { - var signer = new Ed25519Signer(); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; - } - - if (publicKey is Ed448PublicKeyParameters) - { - var signer = new Ed448Signer(new byte[32]); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; - } -#endif - ECDsa senderPrivateKey = - signingCertificate.GetECDsaPrivateKey() - ?? throw new ServiceResultException( - StatusCodes.BadCertificateInvalid, - "Missing private key needed for create a signature."); - - using (senderPrivateKey) - { - byte[] signature = senderPrivateKey.SignData( - dataToSign.Array, - dataToSign.Offset, - dataToSign.Count, - algorithm); - -#if DEBUGxxx - using (ECDsa ecdsa = EccUtils.GetPublicKey(new X509Certificate2(signingCertificate.RawData))) - { - if (!ecdsa.VerifyData(dataToSign.Array, dataToSign.Offset, dataToSign.Count, signature, algorithm)) - { - throw new ServiceResultException( - StatusCodes.BadSecurityChecksFailed, - "Could not verify signature."); - } - } -#endif - - return signature; - } - } - - /// - /// Verifies a ECDsa signature. - /// - public static bool Verify( - ArraySegment dataToVerify, - byte[] signature, - X509Certificate2 signingCertificate, - string securityPolicyUri) - { - return Verify( - dataToVerify, - signature, - signingCertificate, - GetSignatureAlgorithmName(securityPolicyUri)); - } - - /// - /// Verifies a ECDsa signature. - /// - public static bool Verify( - ArraySegment dataToVerify, - byte[] signature, - X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) - { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); - - if (publicKey is Ed25519PublicKeyParameters) - { - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; - } - - if (publicKey is Ed448PublicKeyParameters) - { - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; - } -#endif - using ECDsa ecdsa = GetPublicKey(signingCertificate); - return ecdsa.VerifyData( - dataToVerify.Array, - dataToVerify.Offset, - dataToVerify.Count, - signature, - algorithm); - } - } - - /// - /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC). - /// - public class EncryptedSecret - { - /// - /// Create secret - /// - public EncryptedSecret( - IServiceMessageContext context, - string securityPolicyUri, - X509Certificate2Collection senderIssuerCertificates, - X509Certificate2 receiverCertificate, - Nonce receiverNonce, - X509Certificate2 senderCertificate, - Nonce senderNonce, - CertificateValidator validator = null, - bool doNotEncodeSenderCertificate = false) - { - SenderCertificate = senderCertificate; - SenderIssuerCertificates = senderIssuerCertificates; - DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate; - SenderNonce = senderNonce; - ReceiverNonce = receiverNonce; - ReceiverCertificate = receiverCertificate; - Validator = validator; - SecurityPolicyUri = securityPolicyUri; - Context = context; - } - - /// - /// Gets or sets the X.509 certificate of the sender. - /// - public X509Certificate2 SenderCertificate { get; private set; } - - /// - /// Gets or sets the collection of X.509 certificates of the sender's issuer. - /// - public X509Certificate2Collection SenderIssuerCertificates { get; private set; } - - /// - /// Gets or sets a value indicating whether the sender's certificate should not be encoded. - /// - public bool DoNotEncodeSenderCertificate { get; } - - /// - /// Gets or sets the nonce of the sender. - /// - public Nonce SenderNonce { get; private set; } - - /// - /// Gets or sets the nonce of the receiver. - /// - public Nonce ReceiverNonce { get; } - - /// - /// Gets or sets the X.509 certificate of the receiver. - /// - public X509Certificate2 ReceiverCertificate { get; } - - /// - /// Gets or sets the certificate validator. - /// - public CertificateValidator Validator { get; } - - /// - /// Gets or sets the security policy URI. - /// - public string SecurityPolicyUri { get; private set; } - - /// - /// Service message context to use - /// - public IServiceMessageContext Context { get; } - - /// - /// Encrypts a secret using the specified nonce, encrypting key, and initialization vector (IV). - /// - /// The secret to encrypt. - /// The nonce to use for encryption. - /// The key to use for encryption. - /// The initialization vector to use for encryption. - /// The encrypted secret. - /// - private byte[] EncryptSecret( - byte[] secret, - byte[] nonce, - byte[] encryptingKey, - byte[] iv) - { -#if CURVE25519 - bool useAuthenticatedEncryption = false; - if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters - || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters) - { - useAuthenticatedEncryption = true; - } -#endif - byte[] dataToEncrypt = null; - - using (var encoder = new BinaryEncoder(Context)) - { - encoder.WriteByteString(null, nonce); - encoder.WriteByteString(null, secret); - - // add padding. - int paddingSize = iv.Length - ((encoder.Position + 2) % iv.Length); - paddingSize %= iv.Length; - - if (secret.Length + paddingSize < iv.Length) - { - paddingSize += iv.Length; - } - - for (int ii = 0; ii < paddingSize; ii++) - { - encoder.WriteByte(null, (byte)(paddingSize & 0xFF)); - } - - encoder.WriteUInt16(null, (ushort)paddingSize); - - dataToEncrypt = encoder.CloseAndReturnBuffer(); - } -#if CURVE25519 - if (useAuthenticatedEncryption) - { - return EncryptWithChaCha20Poly1305(encryptingKey, iv, dataToEncrypt); - } -#endif - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = encryptingKey; - aes.IV = iv; - -#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable - using ICryptoTransform encryptor = aes.CreateEncryptor(); -#pragma warning restore CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable - if (dataToEncrypt.Length % encryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - encryptor.TransformBlock(dataToEncrypt, 0, dataToEncrypt.Length, dataToEncrypt, 0); - } - - return dataToEncrypt; - } - -#if CURVE25519 - /// - /// Encrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV). - /// - /// The key used for encryption. - /// The initialization vector used for encryption. - /// The data to be encrypted. - /// The encrypted data. - private static byte[] EncryptWithChaCha20Poly1305(byte[] encryptingKey, byte[] iv, byte[] dataToEncrypt) - { - Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(); - encryptor.Init(true, parameters); - - byte[] ciphertext = new byte[encryptor.GetOutputSize(dataToEncrypt.Length)]; - int length = encryptor.ProcessBytes(dataToEncrypt, 0, dataToEncrypt.Length, ciphertext, 0); - length += encryptor.DoFinal(ciphertext, length); - - if (ciphertext.Length != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"CipherText not the expected size. [{ciphertext.Length} != {length}]"); - } - - return ciphertext; - } - - /// - /// Decrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV). - /// - /// The key used for encryption. - /// The initialization vector used for encryption. - /// The data to be decrypted. - /// The offset in the data to start decrypting from. - /// The number of bytes to decrypt. - /// An containing the decrypted data. - /// Thrown if the plaintext is not the expected size or too short, or if the nonce is invalid. - private ArraySegment DecryptWithChaCha20Poly1305( - byte[] encryptingKey, - byte[] iv, - byte[] dataToDecrypt, - int offset, - int count) - { - Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 decryptor = new ChaCha20Poly1305(); - decryptor.Init(false, parameters); - - byte[] plaintext = new byte[decryptor.GetOutputSize(count)]; - int length = decryptor.ProcessBytes(dataToDecrypt, offset, count, plaintext, 0); - length += decryptor.DoFinal(plaintext, length); - - if (plaintext.Length != length || plaintext.Length < iv.Length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"PlainText not the expected size or too short. [{count} != {length}]"); - } - - ushort paddingSize = plaintext[length - 1]; - paddingSize <<= 8; - paddingSize += plaintext[length - 2]; - - int notvalid = (paddingSize < length) ? 0 : 1; - int start = length - paddingSize - 2; - - for (int ii = 0; ii < length - 2 && ii < paddingSize; ii++) - { - if (start < 0 || start + ii >= plaintext.Length) - { - notvalid |= 1; - continue; - } - - notvalid |= plaintext[start + ii] ^ (paddingSize & 0xFF); - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - - return new ArraySegment(plaintext, 0, start); - } -#endif - - /// - /// Decrypts the specified data using the provided encrypting key and initialization vector (IV). - /// - /// The data to decrypt. - /// The offset in the data to start decrypting from. - /// The number of bytes to decrypt. - /// The key to use for decryption. - /// The initialization vector to use for decryption. - /// The decrypted data. - /// Thrown if the input data is not an even number of encryption blocks or if the nonce is invalid. - private static ArraySegment DecryptSecret( - byte[] dataToDecrypt, - int offset, - int count, - byte[] encryptingKey, - byte[] iv) - { -#if CURVE25519 - bool useAuthenticatedEncryption = false; - if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters - || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters) - { - useAuthenticatedEncryption = true; - } - if (useAuthenticatedEncryption) - { - return DecryptWithChaCha20Poly1305(encryptingKey, iv, dataToDecrypt, offset, count); - } -#endif - using (var aes = Aes.Create()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = encryptingKey; - aes.IV = iv; - - using ICryptoTransform decryptor = aes.CreateDecryptor(); - if (count % decryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - decryptor.TransformBlock(dataToDecrypt, offset, count, dataToDecrypt, offset); - } - - ushort paddingSize = dataToDecrypt[offset + count - 1]; - paddingSize <<= 8; - paddingSize += dataToDecrypt[offset + count - 2]; - - int notvalid = paddingSize < count ? 0 : 1; - int start = offset + count - paddingSize - 2; - - for (int ii = 0; ii < count - 2 && ii < paddingSize; ii++) - { - if (start < 0 || start + ii >= dataToDecrypt.Length) - { - notvalid |= 1; - continue; - } - - notvalid |= dataToDecrypt[start + ii] ^ (paddingSize & 0xFF); - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - - return new ArraySegment(dataToDecrypt, offset, count - paddingSize); - } - - private static readonly byte[] s_label = System.Text.Encoding.UTF8.GetBytes("opcua-secret"); - - /// - /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption. - /// - /// The security policy URI. - /// The sender nonce. - /// The receiver nonce. - /// if set to true, creates the keys for decryption; otherwise, creates the keys for encryption. - /// The encrypting key. - /// The initialization vector (IV). - private static void CreateKeysForEcc( - string securityPolicyUri, - Nonce senderNonce, - Nonce receiverNonce, - bool forDecryption, - out byte[] encryptingKey, - out byte[] iv) - { - int encryptingKeySize; - int blockSize; - HashAlgorithmName algorithmName; - - switch (securityPolicyUri) - { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - blockSize = 16; - encryptingKeySize = 16; - algorithmName = HashAlgorithmName.SHA256; - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - encryptingKeySize = 32; - blockSize = 16; - algorithmName = HashAlgorithmName.SHA384; - break; - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - encryptingKeySize = 32; - blockSize = 12; - algorithmName = HashAlgorithmName.SHA256; - break; - default: - encryptingKeySize = 32; - blockSize = 16; - algorithmName = HashAlgorithmName.SHA256; - break; - } - - encryptingKey = new byte[encryptingKeySize]; - iv = new byte[blockSize]; - - byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); - byte[] salt = Utils.Append(keyLength, s_label, senderNonce.Data, receiverNonce.Data); - - byte[] keyData; - if (forDecryption) - { - keyData = receiverNonce.DeriveKey( - senderNonce, - salt, - algorithmName, - encryptingKeySize + blockSize); - } - else - { - keyData = senderNonce.DeriveKey( - receiverNonce, - salt, - algorithmName, - encryptingKeySize + blockSize); - } - - Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length); - Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length); - } - - /// - /// Encrypts a secret using the specified nonce. - /// - /// The secret to encrypt. - /// The nonce to use for encryption. - /// The encrypted secret. - public byte[] Encrypt(byte[] secret, byte[] nonce) - { - byte[] encryptingKey = null; - byte[] iv = null; - byte[] message = null; - int lengthPosition = 0; - - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); - - using (var encoder = new BinaryEncoder(Context)) - { - // write header. - encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret); - encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary); - - lengthPosition = encoder.Position; - encoder.WriteUInt32(null, 0); - - encoder.WriteString(null, SecurityPolicyUri); - - byte[] senderCertificate = null; - - if (!DoNotEncodeSenderCertificate) - { - senderCertificate = SenderCertificate.RawData; - - if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0) - { - int blobSize = senderCertificate.Length; - - foreach (X509Certificate2 issuer in SenderIssuerCertificates) - { - blobSize += issuer.RawData.Length; - } - - byte[] blob = new byte[blobSize]; - Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length); - - int pos = senderCertificate.Length; - - foreach (X509Certificate2 issuer in SenderIssuerCertificates) - { - byte[] data = issuer.RawData; - Buffer.BlockCopy(data, 0, blob, pos, data.Length); - pos += data.Length; - } - - senderCertificate = blob; - } - } - - encoder.WriteByteString(null, senderCertificate); - encoder.WriteDateTime(null, DateTime.UtcNow); - - byte[] senderNonce = SenderNonce.Data; - byte[] receiverNonce = ReceiverNonce.Data; - - encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8)); - encoder.WriteByteString(null, senderNonce); - encoder.WriteByteString(null, receiverNonce); - - // create keys. - if (EccUtils.IsEccPolicy(SecurityPolicyUri)) - { - CreateKeysForEcc( - SecurityPolicyUri, - SenderNonce, - ReceiverNonce, - false, - out encryptingKey, - out iv); - } - - // encrypt secret, - byte[] encryptedData = EncryptSecret(secret, nonce, encryptingKey, iv); - - // append encrypted secret. - for (int ii = 0; ii < encryptedData.Length; ii++) - { - encoder.WriteByte(null, encryptedData[ii]); - } - - // save space for signature. - for (int ii = 0; ii < signatureLength; ii++) - { - encoder.WriteByte(null, 0); - } - - message = encoder.CloseAndReturnBuffer(); - } - - int length = message.Length - lengthPosition - 4; - - message[lengthPosition++] = (byte)(length & 0xFF); - message[lengthPosition++] = (byte)((length & 0xFF00) >> 8); - message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16); - message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24); - - // get the algorithm used for the signature. - HashAlgorithmName signatureAlgorithm; - switch (SecurityPolicyUri) - { - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - signatureAlgorithm = HashAlgorithmName.SHA384; - break; - default: - signatureAlgorithm = HashAlgorithmName.SHA256; - break; - } - - var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength); - byte[] signature = EccUtils.Sign(dataToSign, SenderCertificate, signatureAlgorithm); - Buffer.BlockCopy( - signature, - 0, - message, - message.Length - signatureLength, - signatureLength); - return message; - } - - /// - /// Verifies the header for an ECC encrypted message and returns the encrypted data. - /// - /// The data to decrypt. - /// The earliest time allowed for the message signing time. - /// The telemetry context to use to create obvservability instruments - /// The encrypted data. - /// - private ArraySegment VerifyHeaderForEcc( - ArraySegment dataToDecrypt, - DateTime earliestTime, - ITelemetryContext telemetry) - { - using var decoder = new BinaryDecoder( - dataToDecrypt.Array, - dataToDecrypt.Offset, - dataToDecrypt.Count, - Context); - NodeId typeId = decoder.ReadNodeId(null); - - if (typeId != DataTypeIds.EccEncryptedSecret) - { - throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown); - } - - var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null); - - if (encoding != ExtensionObjectEncoding.Binary) - { - throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported); - } - - uint length = decoder.ReadUInt32(null); - - // get the start of data. - int startOfData = decoder.Position + dataToDecrypt.Offset; - - SecurityPolicyUri = decoder.ReadString(null); - - if (!EccUtils.IsEccPolicy(SecurityPolicyUri)) - { - throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); - } - - // get the algorithm used for the signature. - HashAlgorithmName signatureAlgorithm; - - switch (SecurityPolicyUri) - { - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - signatureAlgorithm = HashAlgorithmName.SHA384; - break; - default: - signatureAlgorithm = HashAlgorithmName.SHA256; - break; - } - - // extract the send certificate and any chain. - byte[] senderCertificate = decoder.ReadByteString(null); - - if (senderCertificate == null || senderCertificate.Length == 0) - { - if (SenderCertificate == null) - { - throw new ServiceResultException(StatusCodes.BadCertificateInvalid); - } - } - else - { - X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob( - senderCertificate, - telemetry); - - SenderCertificate = senderCertificateChain[0]; - SenderIssuerCertificates = []; - - for (int ii = 1; ii < senderCertificateChain.Count; ii++) - { - SenderIssuerCertificates.Add(senderCertificateChain[ii]); - } - - // validate the sender. - Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult(); - } - - // extract the send certificate and any chain. - DateTime signingTime = decoder.ReadDateTime(null); - - if (signingTime < earliestTime) - { - throw new ServiceResultException(StatusCodes.BadInvalidTimestamp); - } - - // extract the policy header. - ushort headerLength = decoder.ReadUInt16(null); - - if (headerLength == 0 || headerLength > length) - { - throw new ServiceResultException(StatusCodes.BadDecodingError); - } - - // read the policy header. - byte[] senderPublicKey = decoder.ReadByteString(null); - byte[] receiverPublicKey = decoder.ReadByteString(null); - - if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8) - { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - "Unexpected policy header length"); - } - - int startOfEncryption = decoder.Position; - - SenderNonce = Nonce.CreateNonce(SecurityPolicyUri, senderPublicKey); - - if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data)) - { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - "Unexpected receiver nonce."); - } - - // check the signature. - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); - - if (signatureLength >= length) - { - throw new ServiceResultException(StatusCodes.BadDecodingError); - } - - byte[] signature = new byte[signatureLength]; - Buffer.BlockCopy( - dataToDecrypt.Array, - startOfData + (int)length - signatureLength, - signature, - 0, - signatureLength); - - var dataToSign = new ArraySegment( - dataToDecrypt.Array, - 0, - startOfData + (int)length - signatureLength); - - if (!EccUtils.Verify(dataToSign, signature, SenderCertificate, signatureAlgorithm)) - { - throw new ServiceResultException( - StatusCodes.BadSecurityChecksFailed, - "Could not verify signature."); - } - - // extract the encrypted data. - return new ArraySegment( - dataToDecrypt.Array, - startOfEncryption, - (int)length - (startOfEncryption - startOfData + signatureLength)); - } - - /// - /// Decrypts the specified data using the ECC algorithm. - /// - /// The earliest time allowed for the message. - /// The expected nonce value. - /// The data to decrypt. - /// The offset of the data to decrypt. - /// The number of bytes to decrypt. - /// The telemetry context to use to create obvservability instruments - /// The decrypted data. - /// - public byte[] Decrypt( - DateTime earliestTime, - byte[] expectedNonce, - byte[] data, - int offset, - int count, - ITelemetryContext telemetry) - { - ArraySegment dataToDecrypt = VerifyHeaderForEcc( - new ArraySegment(data, offset, count), - earliestTime, - telemetry); - - CreateKeysForEcc( - SecurityPolicyUri, - SenderNonce, - ReceiverNonce, - true, - out byte[] encryptingKey, - out byte[] iv); - - ArraySegment plainText = DecryptSecret( - dataToDecrypt.Array, - dataToDecrypt.Offset, - dataToDecrypt.Count, - encryptingKey, - iv); - - using var decoder = new BinaryDecoder( - plainText.Array, - plainText.Offset, - plainText.Count, - Context); - byte[] actualNonce = decoder.ReadByteString(null); - - if (expectedNonce != null && expectedNonce.Length > 0) - { - int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1; - - for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++) - { - notvalid |= expectedNonce[ii] ^ actualNonce[ii]; - } - - if (notvalid != 0) - { - throw new ServiceResultException(StatusCodes.BadNonceInvalid); - } - } - - return decoder.ReadByteString(null); - } - } -} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs new file mode 100644 index 000000000..21c86173f --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs @@ -0,0 +1,549 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Bindings; +#if CURVE25519 +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +#endif + +namespace Opc.Ua +{ + /// + /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC). + /// + public class EncryptedSecret + { + /// + /// Create secret + /// + public EncryptedSecret( + IServiceMessageContext context, + string securityPolicyUri, + X509Certificate2Collection senderIssuerCertificates, + X509Certificate2 receiverCertificate, + Nonce receiverNonce, + X509Certificate2 senderCertificate, + Nonce senderNonce, + CertificateValidator validator = null, + bool doNotEncodeSenderCertificate = false) + { + SenderCertificate = senderCertificate; + SenderIssuerCertificates = senderIssuerCertificates; + DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate; + SenderNonce = senderNonce; + ReceiverNonce = receiverNonce; + ReceiverCertificate = receiverCertificate; + Validator = validator; + SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + Context = context; + + if (SecurityPolicy == null) + { + throw new ArgumentException($"Cannot resolve SecurityPolicy '{securityPolicyUri}'.", nameof(securityPolicyUri)); + } + } + + /// + /// Gets or sets the X.509 certificate of the sender. + /// + public X509Certificate2 SenderCertificate { get; private set; } + + /// + /// Gets or sets the collection of X.509 certificates of the sender's issuer. + /// + public X509Certificate2Collection SenderIssuerCertificates { get; private set; } + + /// + /// Gets or sets a value indicating whether the sender's certificate should not be encoded. + /// + public bool DoNotEncodeSenderCertificate { get; } + + /// + /// Gets or sets the nonce of the sender. + /// + public Nonce SenderNonce { get; private set; } + + /// + /// Gets or sets the nonce of the receiver. + /// + public Nonce ReceiverNonce { get; } + + /// + /// Gets or sets the X.509 certificate of the receiver. + /// + public X509Certificate2 ReceiverCertificate { get; } + + /// + /// Gets or sets the certificate validator. + /// + public CertificateValidator Validator { get; } + + /// + /// Gets or sets the security policy. + /// + public SecurityPolicyInfo SecurityPolicy { get; private set; } + + /// + /// Service message context to use + /// + public IServiceMessageContext Context { get; } + + private static readonly byte[] s_secretLabel = System.Text.Encoding.UTF8.GetBytes("opcua-secret"); + + /// + /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption. + /// + private static void CreateKeysForEcc( + SecurityPolicyInfo securityPolicy, + Nonce localNonce, + Nonce remoteNonce, + bool forDecryption, + out byte[] encryptingKey, + out byte[] iv) + { + CryptoTrace.Start(ConsoleColor.Blue, $"EncryptedSecret {((forDecryption) ? "DECRYPT" : "ENCRYPT")}"); + CryptoTrace.WriteLine($"SecurityPolicy={securityPolicy.Name}"); + CryptoTrace.WriteLine($"LocalNonce={CryptoTrace.KeyToString(localNonce?.Data)}"); + CryptoTrace.WriteLine($"RemoteNonce={CryptoTrace.KeyToString(remoteNonce?.Data)}"); + + int encryptingKeySize = securityPolicy.SymmetricEncryptionKeyLength; + int blockSize = securityPolicy.InitializationVectorLength; + + encryptingKey = new byte[encryptingKeySize]; + iv = new byte[blockSize]; + + byte[] secret = localNonce.GenerateSecret(remoteNonce, null); + byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); + + byte[] salt = Utils.Append( + keyLength, + s_secretLabel, + forDecryption ? remoteNonce.Data : localNonce.Data, + forDecryption ? localNonce.Data : remoteNonce.Data); + + byte[] keyData = localNonce.DeriveKeyData( + secret, + salt, + securityPolicy.KeyDerivationAlgorithm, + encryptingKeySize + blockSize); + + Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length); + Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length); + + Console.ForegroundColor = ConsoleColor.Blue; + CryptoTrace.WriteLine($"EncryptingKey={CryptoTrace.KeyToString(encryptingKey)}"); + CryptoTrace.WriteLine($"IV={CryptoTrace.KeyToString(iv)}"); + CryptoTrace.Finish("EncryptedSecret"); + } + + /// + /// Encrypts a secret using the specified nonce. + /// + /// The secret to encrypt. + /// The nonce to use for encryption. + /// The encrypted secret. + public byte[] Encrypt(byte[] secret, byte[] nonce) + { + byte[] encryptingKey = null; + byte[] iv = null; + byte[] message = null; + int lengthPosition = 0; + + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); + + using var encoder = new BinaryEncoder(Context); + + // write header. + encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret); + encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary); + + lengthPosition = encoder.Position; + encoder.WriteUInt32(null, 0); + + encoder.WriteString(null, SecurityPolicy.Uri); + + byte[] senderCertificate = null; + + if (!DoNotEncodeSenderCertificate) + { + senderCertificate = SenderCertificate.RawData; + + if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0) + { + int blobSize = senderCertificate.Length; + + foreach (X509Certificate2 issuer in SenderIssuerCertificates) + { + blobSize += issuer.RawData.Length; + } + + byte[] blob = new byte[blobSize]; + Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length); + + int pos = senderCertificate.Length; + + foreach (X509Certificate2 issuer in SenderIssuerCertificates) + { + byte[] data = issuer.RawData; + Buffer.BlockCopy(data, 0, blob, pos, data.Length); + pos += data.Length; + } + + senderCertificate = blob; + } + } + + encoder.WriteByteString(null, senderCertificate); + encoder.WriteDateTime(null, DateTime.UtcNow); + + byte[] senderNonce = SenderNonce.Data; + byte[] receiverNonce = ReceiverNonce.Data; + + encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8)); + int senderNonceStart = encoder.Position; + encoder.WriteByteString(null, senderNonce); + int senderNonceEnd = encoder.Position; + encoder.WriteByteString(null, receiverNonce); + int receiverNonceEnd = encoder.Position; + + // create keys. + CreateKeysForEcc( + SecurityPolicy, + SenderNonce, + ReceiverNonce, + false, + out encryptingKey, + out iv); + + // reserves space for padding and tag that is added by SymmetricEncryptAndSign. + int startOfSecret = encoder.Position; + encoder.WriteByteString(null, nonce); + encoder.WriteByteString(null, secret); + + int paddingCount = 0; + int tagLength = 0; + + switch (SecurityPolicy.SymmetricEncryptionAlgorithm) + { + case SymmetricEncryptionAlgorithm.Aes128Cbc: + case SymmetricEncryptionAlgorithm.Aes256Cbc: + paddingCount = GetPaddingCount(SecurityPolicy.InitializationVectorLength, encoder.Position - startOfSecret); + tagLength = 0; + break; + case SymmetricEncryptionAlgorithm.Aes128Gcm: + case SymmetricEncryptionAlgorithm.Aes256Gcm: + case SymmetricEncryptionAlgorithm.ChaCha20Poly1305: + paddingCount = GetPaddingCount(encryptingKey.Length, encoder.Position - startOfSecret); + tagLength = SecurityPolicy.SymmetricSignatureLength; + break; + } + + for (int ii = 0; ii < paddingCount; ii++) + { + encoder.WriteByte(null, (byte)paddingCount); + } + + encoder.WriteByte(null, (byte)paddingCount); + encoder.WriteByte(null, 0); + + int endOfSecret = encoder.Position; + + // save space for tag. + for (int ii = 0; ii < tagLength; ii++) + { + encoder.WriteByte(null, 0xAB); + } + + // save space for signature. + for (int ii = 0; ii < signatureLength; ii++) + { + encoder.WriteByte(null, 0xDE); + } + + message = encoder.CloseAndReturnBuffer(); + + int length = message.Length - lengthPosition - 4; + + message[lengthPosition++] = (byte)(length & 0xFF); + message[lengthPosition++] = (byte)((length & 0xFF00) >> 8); + message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16); + message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24); + + _ = CryptoUtils.SymmetricEncryptAndSign( + new ArraySegment(message, startOfSecret, endOfSecret - startOfSecret), + SecurityPolicy, + encryptingKey, + iv); + + var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength); + + byte[] signature = CryptoUtils.Sign( + dataToSign, + SenderCertificate, + SecurityPolicy.AsymmetricSignatureAlgorithm); + + Buffer.BlockCopy( + signature, + 0, + message, + endOfSecret + tagLength, + signatureLength); + + return message; + } + + private int GetPaddingCount(int blockSize, int dataLength) + { + int paddingCount = blockSize - ((dataLength + 2) % blockSize); + + if (paddingCount == blockSize) + { + paddingCount = 0; + } + + return paddingCount; + } + + /// + /// Verifies the header for an ECC encrypted message and returns the encrypted data. + /// + /// The data to decrypt. + /// The earliest time allowed for the message signing time. + /// The telemetry context to use to create obvservability instruments + /// The encrypted data. + /// + private ArraySegment VerifyHeaderForEcc( + ArraySegment dataToDecrypt, + DateTime earliestTime, + ITelemetryContext telemetry) + { + using var decoder = new BinaryDecoder( + dataToDecrypt.Array, + dataToDecrypt.Offset, + dataToDecrypt.Count, + Context); + NodeId typeId = decoder.ReadNodeId(null); + + if (typeId != DataTypeIds.EccEncryptedSecret) + { + throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown); + } + + var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null); + + if (encoding != ExtensionObjectEncoding.Binary) + { + throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported); + } + + int length = (int)decoder.ReadUInt32(null) + decoder.Position; + + SecurityPolicy = SecurityPolicies.GetInfo(decoder.ReadString(null)); + + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) + { + throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); + } + + // extract the send certificate and any chain. + byte[] senderCertificate = decoder.ReadByteString(null); + + if (senderCertificate == null || senderCertificate.Length == 0) + { + if (SenderCertificate == null) + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid); + } + } + else + { + X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob( + senderCertificate, + telemetry); + + SenderCertificate = senderCertificateChain[0]; + SenderIssuerCertificates = []; + + for (int ii = 1; ii < senderCertificateChain.Count; ii++) + { + SenderIssuerCertificates.Add(senderCertificateChain[ii]); + } + + // validate the sender. + Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult(); + } + + // extract the send certificate and any chain. + DateTime signingTime = decoder.ReadDateTime(null); + + if (signingTime < earliestTime) + { + throw new ServiceResultException(StatusCodes.BadInvalidTimestamp); + } + + // extract the key data length. + ushort headerLength = decoder.ReadUInt16(null); + + if (headerLength == 0 || headerLength > length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + // read the key data. + int senderNonceStart = decoder.Position; + byte[] senderPublicKey = decoder.ReadByteString(null); + int senderNonceEnd = decoder.Position; + byte[] receiverPublicKey = decoder.ReadByteString(null); + int receiverNonceEnd = decoder.Position; + + if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Unexpected key data length"); + } + + int startOfEncryption = decoder.Position; + + SenderNonce = Nonce.CreateNonce(SecurityPolicy, senderPublicKey); + + if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data)) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Unexpected receiver nonce."); + } + + // check the signature. + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); + + if (signatureLength >= length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + byte[] signature = new byte[signatureLength]; + + Buffer.BlockCopy( + dataToDecrypt.Array, + dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength, + signature, + 0, + signatureLength); + + var dataToSign = new ArraySegment( + dataToDecrypt.Array, + dataToDecrypt.Offset, + dataToDecrypt.Count - signatureLength); + + if (!CryptoUtils.Verify(dataToSign, signature, SenderCertificate, SecurityPolicy.AsymmetricSignatureAlgorithm)) + { + throw new ServiceResultException( + StatusCodes.BadSecurityChecksFailed, + "Could not verify signature."); + } + + // extract the encrypted data. + return new ArraySegment( + dataToDecrypt.Array, + dataToDecrypt.Offset + startOfEncryption, + dataToDecrypt.Count - startOfEncryption - signatureLength); + } + + /// + /// Decrypts the specified data using the ECC algorithm. + /// + /// The earliest time allowed for the message. + /// The expected nonce value. + /// The data to decrypt. + /// The offset of the data to decrypt. + /// The number of bytes to decrypt. + /// The telemetry context to use to create obvservability instruments + /// The decrypted data. + /// + public byte[] Decrypt( + DateTime earliestTime, + byte[] expectedNonce, + byte[] data, + int offset, + int count, + ITelemetryContext telemetry) + { + ArraySegment dataToDecrypt = VerifyHeaderForEcc( + new ArraySegment(data, offset, count), + earliestTime, + telemetry); + + CreateKeysForEcc( + SecurityPolicy, + ReceiverNonce, + SenderNonce, + true, + out byte[] encryptingKey, + out byte[] iv); + + ArraySegment plainText = CryptoUtils.SymmetricDecryptAndVerify( + dataToDecrypt, + SecurityPolicy, + encryptingKey, + iv); + + using var decoder = new BinaryDecoder( + plainText.Array, + plainText.Offset + dataToDecrypt.Offset, + plainText.Count - dataToDecrypt.Offset, + Context); + + byte[] actualNonce = decoder.ReadByteString(null); + + if (expectedNonce != null && expectedNonce.Length > 0) + { + int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1; + + for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++) + { + notvalid |= expectedNonce[ii] ^ actualNonce[ii]; + } + + if (notvalid != 0) + { + throw new ServiceResultException(StatusCodes.BadNonceInvalid); + } + } + + var key = decoder.ReadByteString(null); + var paddingCount = decoder.ReadByte(null); + + int error = 0; + + for (int ii = 0; ii < paddingCount; ii++) + { + var padding = decoder.ReadByte(null); + error |= (padding & ~paddingCount); + } + + var highByte = decoder.ReadByte(null); + + if (error != 0 || highByte != 0) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + return key; + } + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index 37bad43a6..090028754 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -28,8 +28,11 @@ * ======================================================================*/ using System; +using System.Collections.Generic; +using System.Numerics; using System.Runtime.Serialization; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; #if CURVE25519 using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.X509; @@ -48,18 +51,20 @@ namespace Opc.Ua /// /// Represents a cryptographic nonce used for secure communication. /// - [Serializable] - public class Nonce : IDisposable, ISerializable + public class Nonce : IDisposable { + private ECDiffieHellman m_ecdh; + private RSADiffieHellman m_rsadh; + private static readonly RandomNumberGenerator s_rng = RandomNumberGenerator.Create(); + private static uint s_minNonceLength = 32; + /// /// Constructor /// private Nonce() { m_ecdh = null; -#if CURVE25519 - m_bcKeyPair = null; -#endif + m_rsadh = null; } /// @@ -67,162 +72,195 @@ private Nonce() /// public byte[] Data { get; private set; } + internal byte[] GenerateSecret( + Nonce remoteNonce, + byte[] previousSecret) + { + byte[] ikm = null; + CryptoTrace.Start(ConsoleColor.Cyan, $"GenerateSecret"); + +#if NET8_0_OR_GREATER +#if xDEBUG + Span privateKey = stackalloc char[2048]; + + if (m_ecdh.TryExportECPrivateKeyPem(privateKey, out int charsWritten)) + { + CryptoTrace.WriteLine($"Private Key PEM ({charsWritten} chars):"); + } +#endif + + if (m_ecdh != null) + { + ikm = m_ecdh.DeriveRawSecretAgreement(remoteNonce.m_ecdh.PublicKey); + + } + else if (m_rsadh != null) + { + ikm = m_rsadh.DeriveRawSecretAgreement(remoteNonce.m_rsadh); + } + + CryptoTrace.WriteLine($"IKM-Raw={CryptoTrace.KeyToString(ikm)}"); + CryptoTrace.WriteLine($"Previous-IKM={CryptoTrace.KeyToString(previousSecret)}"); + + if (previousSecret != null) + { + for (int ii = 0; ii < ikm.Length && ii < previousSecret.Length; ii++) + { + ikm[ii] ^= previousSecret[ii]; + } + } + + CryptoTrace.WriteLine($"IKM-XOR={CryptoTrace.KeyToString(ikm)}"); + CryptoTrace.Finish("GenerateSecret"); + +#endif + return ikm; + } + /// /// Derives a key from the remote nonce, using the specified salt, hash algorithm, and length. /// - /// The remote nonce to use in key derivation. + /// The secret to use in key derivation. /// The salt to use in key derivation. /// The hash algorithm to use in key derivation. /// The length of the derived key. /// The derived key. - public byte[] DeriveKey( - Nonce remoteNonce, + public byte[] DeriveKeyData( + byte[] secret, byte[] salt, - HashAlgorithmName algorithm, + KeyDerivationAlgorithm algorithm, int length) { -#if CURVE25519 - if (m_bcKeyPair != null) - { - var localPublicKey = m_bcKeyPair.Public; + CryptoTrace.Start(ConsoleColor.DarkCyan, $"DeriveKeyData"); + CryptoTrace.WriteLine($"Secret={CryptoTrace.KeyToString(secret)}"); + CryptoTrace.WriteLine($"Salt={CryptoTrace.KeyToString(salt)}"); - if (localPublicKey is X25519PublicKeyParameters) - { - X25519Agreement agreement = new X25519Agreement(); - agreement.Init(m_bcKeyPair.Private); - - var key = new X25519PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); + using HMAC extract = algorithm switch + { + KeyDerivationAlgorithm.HKDFSha256 => new HMACSHA256(salt), + KeyDerivationAlgorithm.HKDFSha384 => new HMACSHA384(salt), + _ => new HMACSHA256(salt) + }; - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); + byte[] prk = extract.ComputeHash(secret); + CryptoTrace.WriteLine($"PRK={CryptoTrace.KeyToString(prk)}"); - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } + using HMAC expand = algorithm switch + { + KeyDerivationAlgorithm.HKDFSha256 => new HMACSHA256(prk), + KeyDerivationAlgorithm.HKDFSha384 => new HMACSHA384(prk), + _ => new HMACSHA256(prk) + }; - if (localPublicKey is X448PublicKeyParameters) - { - X448Agreement agreement = new X448Agreement(); - agreement.Init(m_bcKeyPair.Private); + byte[] output = new byte[length]; + byte counter = 1; - var key = new X448PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); + byte[] info = new byte[(expand.HashSize / 8) + salt.Length + 1]; + Buffer.BlockCopy(salt, 0, info, 0, salt.Length); + info[salt.Length] = counter++; - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); + // computer T(1) + byte[] hash = expand.ComputeHash(info, 0, salt.Length + 1); + CryptoTrace.WriteLine($"T(1)={CryptoTrace.KeyToString(hash)}"); - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } + int pos = 0; - throw new NotSupportedException(); - } -#endif - if (m_ecdh != null) + for (int ii = 0; ii < hash.Length && pos < length; ii++) { - byte[] secret = m_ecdh.DeriveKeyFromHmac( - remoteNonce.m_ecdh.PublicKey, - algorithm, - salt, - null, - null); - - byte[] output = new byte[length]; - - HMAC hmac = algorithm.Name switch - { - "SHA256" => new HMACSHA256(secret), - "SHA384" => new HMACSHA384(secret), - _ => new HMACSHA256(secret) - }; - - byte counter = 1; - - byte[] info = new byte[(hmac.HashSize / 8) + salt.Length + 1]; - Buffer.BlockCopy(salt, 0, info, 0, salt.Length); - info[salt.Length] = counter++; + output[pos++] = hash[ii]; + } - byte[] hash = hmac.ComputeHash(info, 0, salt.Length + 1); + while (pos < length) + { + Buffer.BlockCopy(hash, 0, info, 0, hash.Length); + Buffer.BlockCopy(salt, 0, info, hash.Length, salt.Length); + info[^1] = counter++; - int pos = 0; + hash = expand.ComputeHash(info, 0, info.Length); + CryptoTrace.WriteLine($"T({counter - 1})={CryptoTrace.KeyToString(hash)}"); for (int ii = 0; ii < hash.Length && pos < length; ii++) { output[pos++] = hash[ii]; } + } - while (pos < length) - { - Buffer.BlockCopy(hash, 0, info, 0, hash.Length); - Buffer.BlockCopy(salt, 0, info, hash.Length, salt.Length); - info[^1] = counter++; - - hash = hmac.ComputeHash(info, 0, info.Length); - - for (int ii = 0; ii < hash.Length && pos < length; ii++) - { - output[pos++] = hash[ii]; - } - } + CryptoTrace.WriteLine($"KeyData={CryptoTrace.KeyToString(output)}"); + CryptoTrace.Finish("DeriveKeyData"); - return output; - } + return output; + } - return Data; + /// + /// Generates a Nonce for cryptographic functions of a given length. + /// + /// The requested Nonce as a + public static Nonce CreateNonce(int length) + { + return new Nonce { Data = CreateRandomNonceData(length) }; } /// /// Creates a nonce for the specified security policy URI and nonce length. /// - /// The security policy URI. - /// A object containing the generated nonce. - /// is null. public static Nonce CreateNonce(string securityPolicyUri) { - if (securityPolicyUri == null) + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return CreateNonce(info); + } + + /// + /// Creates a nonce for the specified security policy and nonce length. + /// + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy) + { + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } - switch (securityPolicyUri) + switch (securityPolicy.EphemeralKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.RSADH: + return securityPolicy.MinAsymmetricKeyLength switch + { + //2048 => CreateNonce(RSADiffieHellmanGroup.FFDHE2048), + //3072 => CreateNonce(RSADiffieHellmanGroup.FFDHE3072), + //4096 => CreateNonce(RSADiffieHellmanGroup.FFDHE4096), + _ => CreateNonce(RSADiffieHellmanGroup.FFDHE3072) + }; + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1); -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - return CreateNonceForCurve25519(); - case SecurityPolicies.ECC_curve448: - return CreateNonceForCurve448(); -#endif default: - uint rsaNonceLength = GetNonceLength(securityPolicyUri); - return new Nonce { Data = CreateRandomNonceData(rsaNonceLength) }; + return new Nonce { Data = CreateRandomNonceData(securityPolicy.SecureChannelNonceLength) }; } } + /// + /// Creates a new Nonce object for the specified RSA DiffieHellman group. + /// + public static Nonce CreateNonce(RSADiffieHellmanGroup group) + { + var nonce = new Nonce(); + nonce.m_rsadh = RSADiffieHellman.Create(group); + nonce.Data = nonce.m_rsadh.GetNonce(); + return nonce; + } + /// /// Creates a new Nonce object for the specified security policy URI and nonce data. /// - /// The security policy URI. - /// The nonce data. - /// A new Nonce object. - /// is null. - public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy, byte[] nonceData) { - if (securityPolicyUri == null) + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } if (nonceData == null) @@ -230,24 +268,30 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) throw new ArgumentNullException(nameof(nonceData)); } - var nonce = new Nonce { Data = nonceData }; + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) + { + var nonce = new Nonce(); + nonce.m_rsadh = RSADiffieHellman.Create(nonceData); + nonce.Data = nonceData; + return nonce; + } - switch (securityPolicyUri) + switch (securityPolicy.EphemeralKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256, nonceData); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384, nonceData); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1, nonceData); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1, nonceData); - case SecurityPolicies.ECC_curve25519: + case CertificateKeyAlgorithm.Curve25519: return CreateNonceForCurve25519(nonceData); - case SecurityPolicies.ECC_curve448: + case CertificateKeyAlgorithm.Curve448: return CreateNonceForCurve448(nonceData); default: - return nonce; + return new Nonce { Data = nonceData }; } } @@ -255,8 +299,13 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) /// Generates a Nonce for cryptographic functions of a given length. /// /// The requested Nonce as a - public static byte[] CreateRandomNonceData(uint length) + public static byte[] CreateRandomNonceData(int length) { + if (length < s_minNonceLength) + { + length = (int)s_minNonceLength; + } + byte[] randomBytes = new byte[length]; s_rng.GetBytes(randomBytes); return randomBytes; @@ -268,9 +317,9 @@ public static byte[] CreateRandomNonceData(uint length) public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - string securityPolicyUri) + SecurityPolicyInfo securityPolicy) { - return ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); + return ValidateNonce(nonce, securityMode, securityPolicy.SecureChannelNonceLength); } /// @@ -279,7 +328,7 @@ public static bool ValidateNonce( public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - uint minNonceLength) + int minNonceLength) { // no nonce needed for no security. if (securityMode == MessageSecurityMode.None) @@ -305,38 +354,6 @@ public static bool ValidateNonce( return false; } - /// - /// Returns the length of the symmetric encryption key for a security policy. - /// - public static uint GetNonceLength(string securityPolicyUri) - { - switch (securityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - return 16; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_curve25519: - return 32; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - // Q.X + Q.Y = 32 + 32 = 64 - return 64; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - // Q.X + Q.Y = 48 + 48 = 96 - return 96; - case SecurityPolicies.ECC_curve448: - // Q.X - return 56; - default: - // Minimum nonce length by default - return s_minNonceLength; - } - } - /// /// Compare Nonce for equality. /// @@ -446,119 +463,298 @@ private static Nonce CreateNonce(ECCurve curve) return new Nonce { Data = senderNonce, m_ecdh = ecdh }; } -#if CURVE25519 /// - /// Creates a new Nonce object to be used in Curve25519 cryptography. + /// Frees any unmanaged resources. /// - /// A new Nonce object. - private static Nonce CreateNonceForCurve25519() + public void Dispose() { - SecureRandom random = new SecureRandom(); - IAsymmetricCipherKeyPairGenerator generator = new X25519KeyPairGenerator(); - generator.Init(new X25519KeyGenerationParameters(random)); - - var keyPair = generator.GenerateKeyPair(); - - byte[] senderNonce = new byte[X25519PublicKeyParameters.KeySize]; - ((X25519PublicKeyParameters)(keyPair.Public)).Encode(senderNonce, 0); - - var nonce = new Nonce() { Data = senderNonce, m_bcKeyPair = keyPair }; - - return nonce; + Dispose(true); + GC.SuppressFinalize(this); } /// - /// Creates a Nonce object using the X448 elliptic curve algorithm. + /// An overrideable version of the Dispose. /// - /// A Nonce object containing the generated nonce data and key pair. - private static Nonce CreateNonceForCurve448() + protected virtual void Dispose(bool disposing) { - SecureRandom random = new SecureRandom(); - IAsymmetricCipherKeyPairGenerator generator = new X448KeyPairGenerator(); - generator.Init(new X448KeyGenerationParameters(random)); + if (disposing) + { + if (m_ecdh != null) + { + m_ecdh.Dispose(); + m_ecdh = null; + } + } + } + } - var keyPair = generator.GenerateKeyPair(); + /// + /// The known RSA Diffie-Hellman groups. + /// + public enum RSADiffieHellmanGroup + { + /// + /// The 2048-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE2048, - byte[] senderNonce = new byte[X448PublicKeyParameters.KeySize]; - ((X448PublicKeyParameters)(keyPair.Public)).Encode(senderNonce, 0); + /// + /// The 3072-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE3072, - var nonce = new Nonce() { Data = senderNonce, m_bcKeyPair = keyPair }; + /// + /// The 4096-bit finite field Diffie-Hellman ephemeral group defined in RFC 7919. + /// + FFDHE4096 + } - return nonce; - } -#endif + /// + /// A RSA Diffie-Hellman key exchange implementation. + /// + public class RSADiffieHellman + { + private BigInteger m_privateKey; + private BigInteger m_publicKey; + private int m_nonceLength; + + // ffdhe2048 prime from RFC 7919 (hex, without whitespace). + // (RFC 7919 Appendix A.3 — use this canonical modulus in production.) + const string FFDHE2048_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 61285C97 FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P2048 = new(() => RfcTextToBytes(FFDHE2048_HEX)); + + const int k_FFDHE2048_MinExponent = 224; + const int k_FFDHE2048_MaxExponent = 255; + + const string FFDHE3072_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 611FCFDC DE355B3B 6519035B BC34F4DE F99C0238 + 61B46FC9 D6E6C907 7AD91D26 91F7F7EE 598CB0FA C186D91C + AEFE1309 85139270 B4130C93 BC437944 F4FD4452 E2D74DD3 + 64F2E21E 71F54BFF 5CAE82AB 9C9DF69E E86D2BC5 22363A0D + ABC52197 9B0DEADA 1DBF9A42 D5C4484E 0ABCD06B FA53DDEF + 3C1B20EE 3FD59D7C 25E41D2B 66C62E37 FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P3072 = new(() => RfcTextToBytes(FFDHE3072_HEX)); + + const int k_FFDHE3072_MinExponent = 275; + const int k_FFDHE3072_MaxExponent = 383; + + const string FFDHE4096_HEX = @" + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 611FCFDC DE355B3B 6519035B BC34F4DE F99C0238 + 61B46FC9 D6E6C907 7AD91D26 91F7F7EE 598CB0FA C186D91C + AEFE1309 85139270 B4130C93 BC437944 F4FD4452 E2D74DD3 + 64F2E21E 71F54BFF 5CAE82AB 9C9DF69E E86D2BC5 22363A0D + ABC52197 9B0DEADA 1DBF9A42 D5C4484E 0ABCD06B FA53DDEF + 3C1B20EE 3FD59D7C 25E41D2B 669E1EF1 6E6F52C3 164DF4FB + 7930E9E4 E58857B6 AC7D5F42 D69F6D18 7763CF1D 55034004 + 87F55BA5 7E31CC7A 7135C886 EFB4318A ED6A1E01 2D9E6832 + A907600A 918130C4 6DC778F9 71AD0038 092999A3 33CB8B7A + 1A1DB93D 7140003C 2A4ECEA9 F98D0ACC 0A8291CD CEC97DCF + 8EC9B55A 7F88A46B 4DB5A851 F44182E1 C68A007E 5E655F6A + FFFFFFFF FFFFFFFF"; + + static readonly Lazy s_P4096 = new(() => RfcTextToBytes(FFDHE4096_HEX)); + + const int k_FFDHE4096_MinExponent = 325; + const int k_FFDHE4096_MaxExponent = 511; + + private static readonly Lazy s_rng = new(() => RandomNumberGenerator.Create()); + + // Generator for FFDHE groups is 2 + static readonly BigInteger s_G = new BigInteger(2); /// - /// Custom deserialization + /// Creates a new RSADiffieHellman instance for the specified group. /// - protected Nonce(SerializationInfo info, StreamingContext context) + public static RSADiffieHellman Create(RSADiffieHellmanGroup group) { - string curveName = info.GetString("CurveName"); + int min = 0; + int max = 0; + BigInteger p; - if (curveName != null) + switch (group) { - var ecParams = new ECParameters - { - Curve = ECCurve.CreateFromFriendlyName(curveName), - Q = new ECPoint - { - X = (byte[])info.GetValue("QX", typeof(byte[])), - Y = (byte[])info.GetValue("QY", typeof(byte[])) - } - }; - m_ecdh = ECDiffieHellman.Create(ecParams); + case RSADiffieHellmanGroup.FFDHE2048: + p = s_P2048.Value; + min = k_FFDHE2048_MinExponent; + max = k_FFDHE2048_MaxExponent; + break; + case RSADiffieHellmanGroup.FFDHE3072: + p = s_P3072.Value; + min = k_FFDHE3072_MinExponent; + max = k_FFDHE3072_MaxExponent; + break; + case RSADiffieHellmanGroup.FFDHE4096: + p = s_P4096.Value; + min = k_FFDHE4096_MinExponent; + max = k_FFDHE4096_MaxExponent; + break; + default: + throw new NotSupportedException("Unsupported RSA DH finite group type."); } - Data = (byte[])info.GetValue("Data", typeof(byte[])); - } - private ECDiffieHellman m_ecdh; -#if CURVE25519 - private AsymmetricCipherKeyPair m_bcKeyPair; -#endif + var dh = new RSADiffieHellman(); - private static readonly RandomNumberGenerator s_rng = RandomNumberGenerator.Create(); - private static uint s_minNonceLength = 32; + byte[] seed = new byte[1]; + s_rng.Value.GetBytes(seed); + int keyLength = seed[0] % (max - min + 1) + min; + + byte[] key = new byte[1 + (keyLength + 7)/ 8]; + s_rng.Value.GetBytes(key); + key[key.Length - 1] = 0; + + dh.m_privateKey = new BigInteger(key); + dh.m_publicKey = BigInteger.ModPow(s_G, dh.m_privateKey, p); + dh.m_nonceLength = max + 1; + + return dh; + } /// - /// Frees any unmanaged resources. + /// Creates a new RSADiffieHellman instance from the nonce. /// - public void Dispose() + public static RSADiffieHellman Create(byte[] nonce) { - Dispose(true); - GC.SuppressFinalize(this); + var dh = new RSADiffieHellman(); + + var bytes = new byte[nonce.Length+1]; + + for (int ii = 0; ii < nonce.Length; ii++) + { + bytes[ii] = nonce[nonce.Length - ii - 1]; + } + + dh.m_publicKey = new BigInteger(bytes); + dh.m_nonceLength = nonce.Length; + + return dh; } /// - /// An overrideable version of the Dispose. + /// Returns the nonce representing the public key. /// - protected virtual void Dispose(bool disposing) + public byte[] GetNonce() { - if (disposing && m_ecdh != null) + var nonce = new byte[m_nonceLength]; + var publicKey = m_publicKey.ToByteArray(); + + for (int ii = 0; ii < publicKey.Length && ii < nonce.Length; ii++) { - m_ecdh.Dispose(); - m_ecdh = null; + nonce[nonce.Length - 1 - ii] = publicKey[ii]; } + + return nonce; } /// - /// Custom serialization + /// Derives the raw secret agreement from the remote key. /// - public void GetObjectData(SerializationInfo info, StreamingContext context) + public byte[] DeriveRawSecretAgreement(RSADiffieHellman remoteKey) { - if (m_ecdh != null) + if (m_privateKey.IsZero) + { + throw new InvalidOperationException("Private key not available."); + } + + BigInteger p; + + switch (m_nonceLength) + { + case 256: + p = s_P2048.Value; + break; + case 384: + p = s_P3072.Value; + break; + case 512: + p = s_P4096.Value; + break; + default: + throw new NotSupportedException("Unsupported RSA DH finite group type."); + } + + var shared = BigInteger.ModPow(remoteKey.m_publicKey, m_privateKey, p); + + var bytes = shared.ToByteArray(); + + if (bytes.Length < m_nonceLength) { - ECParameters ecParams = m_ecdh.ExportParameters(false); - info.AddValue("CurveName", ecParams.Curve.Oid.FriendlyName); - info.AddValue("QX", ecParams.Q.X); - info.AddValue("QY", ecParams.Q.Y); + var padded = new byte[m_nonceLength]; + Array.Copy(bytes, 0, padded, 0, bytes.Length); + bytes = padded; } - else + else if (bytes.Length > m_nonceLength) { - info.AddValue("CurveName", null); - info.AddValue("QX", null); - info.AddValue("QY", null); + var trucated = new byte[m_nonceLength]; + Array.Copy(bytes, 0, trucated, 0, m_nonceLength); + bytes = trucated; } - info.AddValue("Data", Data); + + // make sure bytes are in big-endian order. + Array.Reverse(bytes); + + return bytes; + } + + private static BigInteger RfcTextToBytes(string rfcText) + { + var bytes = new List(); + var digit = new char[2]; + int pos = 0; + + bytes.Add(0); + + for (int ii = 0; ii < rfcText.Length; ii++) + { + if (char.IsWhiteSpace(rfcText[ii])) + { + continue; + } + + digit[pos++] = rfcText[ii]; + + if (pos == 2) + { + bytes.Add(Convert.ToByte(new string(digit), 16)); + pos = 0; + } + } + + bytes.Reverse(); + var integer = new BigInteger(bytes.ToArray()); + return integer; } } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index 6936f7d12..3b53a8a17 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -655,7 +655,7 @@ public static bool IsECDsaSignature(X509Certificate2 cert) /// The certificate. public static string GetECDsaQualifier(X509Certificate2 certificate) { - return EccUtils.GetECDsaQualifier(certificate); + return CryptoUtils.GetECDsaQualifier(certificate); } /// diff --git a/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs new file mode 100644 index 000000000..880b3d0d6 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Opc.Ua +{ + /// + /// The names of additional parameters used in security-related operations. + /// + public static class AdditionalParameterNames + { + /// + /// The algorith to use for the ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHPolicyUri = "ECDHPolicyUri"; + + /// + /// An ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHKey = "ECDHKey"; + + /// + /// Padding bytes added to randomize the length of messages. + /// + public const string Padding = "Padding"; + } +} diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index ee3bea6ca..ae0972f55 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -83,36 +83,106 @@ public static class SecurityPolicies /// public const string Aes256_Sha256_RsaPss = BaseUri + "Aes256_Sha256_RsaPss"; + /// + /// The URI for the RSA_DH_AES_GCM security policy. + /// + public const string RSA_DH_AesGcm = BaseUri + "RSA_DH_AesGcm"; + + /// + /// The URI for the RSA_DH_ChaChaPoly security policy. + /// + public const string RSA_DH_ChaChaPoly = BaseUri + "RSA_DH_ChaChaPoly"; + /// /// The URI for the ECC_nistP256 security policy. /// public const string ECC_nistP256 = BaseUri + "ECC_nistP256"; + /// + /// The URI for the ECC_nistP256 security policy with AES-GCM. + /// + public const string ECC_nistP256_AesGcm = ECC_nistP256 + "_AesGcm"; + + /// + /// The URI for the ECC_nistP256 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP256_ChaChaPoly = ECC_nistP256 + "_ChaChaPoly"; + /// /// The URI for the ECC_nistP384 security policy. /// public const string ECC_nistP384 = BaseUri + "ECC_nistP384"; + /// + /// The URI for the ECC_nistP384 security policy with AES-GCM. + /// + public const string ECC_nistP384_AesGcm = ECC_nistP384 + "_AesGcm"; + + /// + /// The URI for the ECC_nistP384 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP384_ChaChaPoly = ECC_nistP384 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP256r1 security policy. /// public const string ECC_brainpoolP256r1 = BaseUri + "ECC_brainpoolP256r1"; + /// + /// The URI for the ECC_brainpoolP256r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP256r1_AesGcm = ECC_brainpoolP256r1 + "_AesGcm"; + + /// + /// The URI for the ECC_brainpoolP256r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP256r1_ChaChaPoly = ECC_brainpoolP256r1 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP384r1 security policy. /// public const string ECC_brainpoolP384r1 = BaseUri + "ECC_brainpoolP384r1"; /// - /// The URI for the ECC_curve25519 security policy. + /// The URI for the ECC_brainpoolP384r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP384r1_AesGcm = ECC_brainpoolP384r1 + "_AesGcm"; + + /// + /// The URI for the ECC_brainpoolP384r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP384r1_ChaChaPoly = ECC_brainpoolP384r1 + "_ChaChaPoly"; + + /// + /// The URI for the ECC_curve25519 security policy.brainpoolP384r1_AesGcm /// public const string ECC_curve25519 = BaseUri + "ECC_curve25519"; /// - /// The URI for the ECC_curve448 security policy. + /// The URI for the ECC_curve25519 security policy with AES-GCM. + /// + public const string ECC_curve25519_AesGcm = ECC_curve25519 + "_AesGcm"; + + /// + /// The URI for the ECC_curve25519 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve25519_ChaChaPoly = ECC_curve25519 + "_ChaChaPoly"; + + /// + /// The URI for the ECC_curve448 deprecated security policy. /// public const string ECC_curve448 = BaseUri + "ECC_curve448"; + /// + /// The URI for the ECC_curve448 security policy with AES-GCM. + /// + public const string ECC_curve448_AesGcm = ECC_curve448 + "_AesGcm"; + + /// + /// The URI for the ECC_curve448 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve448_ChaChaPoly = ECC_curve448 + "_ChaChaPoly"; + /// /// The URI for the Https security policy. /// @@ -120,12 +190,20 @@ public static class SecurityPolicies private static bool IsPlatformSupportedName(string name) { + // If name contains BaseUri trim the BaseUri part + if (name.StartsWith(BaseUri, StringComparison.Ordinal)) + { + name = name.Substring(BaseUri.Length); + } + // all RSA if (name.Equals(nameof(None), StringComparison.Ordinal) || name.Equals(nameof(Basic256), StringComparison.Ordinal) || name.Equals(nameof(Basic128Rsa15), StringComparison.Ordinal) || name.Equals(nameof(Basic256Sha256), StringComparison.Ordinal) || - name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal)) + name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal) || + name.Equals(nameof(RSA_DH_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(RSA_DH_ChaChaPoly), StringComparison.Ordinal)) { return true; } @@ -135,29 +213,41 @@ private static bool IsPlatformSupportedName(string name) { return true; } - if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP256_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP256_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP256ApplicationCertificateType); } - if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP384_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP384_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP384ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP256r1_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP256r1_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP384r1_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP384r1_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); } if (name.Equals(nameof(ECC_curve25519), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve448), StringComparison.Ordinal)) + name.Equals(nameof(ECC_curve25519_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve25519_ChaChaPoly), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_ChaChaPoly), StringComparison.Ordinal)) { #if CURVE25519 return true; @@ -166,6 +256,34 @@ private static bool IsPlatformSupportedName(string name) return false; } + /// + /// Returns the info object associated with the SecurityPolicyUri. + /// Supports both full URI and short name (without BaseUri prefix). + /// + public static SecurityPolicyInfo GetInfo(string securityPolicyUri) + { + if (String.IsNullOrEmpty(securityPolicyUri)) + { + return SecurityPolicyInfo.None; + } + + // Try full URI lookup first (e.g., "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") + if (s_securityPolicyUriToInfo.Value.TryGetValue(securityPolicyUri, out SecurityPolicyInfo info) && + IsPlatformSupportedName(info.Name)) + { + return info; + } + + // Try short name lookup (e.g., "Basic256Sha256") + if (s_securityPolicyNameToInfo.Value.TryGetValue(securityPolicyUri, out info) && + IsPlatformSupportedName(info.Name)) + { + return info; + } + + return null; + } + /// /// Returns the uri associated with the display name. This includes http and all /// other supported platform security policies. @@ -303,65 +421,65 @@ public static EncryptedData Encrypt( ReadOnlySpan plainText, ILogger logger) { - var encryptedData = new EncryptedData - { - Algorithm = null, - Data = plainText.IsEmpty ? null : plainText.ToArray() - }; + var encryptedData = new EncryptedData { Algorithm = null }; // check if nothing to do. - if (plainText.IsEmpty) + if (plainText.Length == 0 || String.IsNullOrEmpty(securityPolicyUri)) { + encryptedData.Data = plainText.ToArray(); return encryptedData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return encryptedData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } - // encrypt data. - switch (securityPolicyUri) + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - break; - case Basic128Rsa15: - encryptedData.Algorithm = SecurityAlgorithms.Rsa15; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.Pkcs1, - logger); - break; - case Aes256_Sha256_RsaPss: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA256, - logger); - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - return encryptedData; - case None: - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: + { + encryptedData.Algorithm = SecurityAlgorithms.Rsa15; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.Pkcs1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + break; + } + } } return encryptedData; @@ -389,56 +507,68 @@ public static byte[] Decrypt( return dataToDecrypt.Data; } - // decrypt data. - switch (securityPolicyUri) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) - { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - } - break; - case Basic128Rsa15: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) + { + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.Pkcs1, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + } + break; } - break; - case Aes256_Sha256_RsaPss: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA256, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.Pkcs1, + logger); + } + break; } - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - case None: - if (string.IsNullOrEmpty(dataToDecrypt.Algorithm)) + + default: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: { - return dataToDecrypt.Data; + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + } + break; } - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + } + } + + if (String.IsNullOrEmpty(dataToDecrypt.Algorithm)) + { + return dataToDecrypt.Data; } throw ServiceResultException.Create( @@ -448,185 +578,274 @@ public static byte[] Decrypt( } /// - /// Signs the data using the SecurityPolicyUri and returns the signature. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static SignatureData Sign( - X509Certificate2 certificate, + public static SignatureData CreateSignatureData( string securityPolicyUri, - byte[] dataToSign) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] remoteCertificate, + byte[] remoteChannelCertificate, + byte[] localChannelCertificate, + byte[] remoteNonce, + byte[] localNonce) { var signatureData = new SignatureData(); - // check if nothing to do. - if (dataToSign == null) + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return signatureData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return signatureData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } + // create the data to sign. + byte[] dataToSign = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + remoteCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteNonce ?? Array.Empty(), + localNonce ?? Array.Empty()) + : + Utils.Append( + remoteCertificate ?? Array.Empty(), + remoteNonce); + + return CreateSignatureData(info, signingCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + string securityPolicyUri, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var info = GetInfo(securityPolicyUri); + return CreateSignatureData(info, localCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + SecurityPolicyInfo securityPolicy, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var signatureData = new SignatureData(); + // sign data. - switch (securityPolicyUri) + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: signatureData.Algorithm = SecurityAlgorithms.RsaSha1; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); break; - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: signatureData.Algorithm = SecurityAlgorithms.RsaSha256; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); break; - case Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPssSha256: signatureData.Algorithm = SecurityAlgorithms.RsaPssSha256; - signatureData.Signature = RsaUtils.Rsa_Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); break; - case ECC_nistP256: - case ECC_brainpoolP256r1: + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256); break; - case ECC_nistP384: - case ECC_brainpoolP384r1: - signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( - new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA384); - break; - case None: + case AsymmetricSignatureAlgorithm.None: signatureData.Algorithm = null; signatureData.Signature = null; - break; + return signatureData; + ; default: throw ServiceResultException.Create( StatusCodes.BadSecurityPolicyRejected, "Unsupported security policy: {0}", - securityPolicyUri); + securityPolicy.Uri); } + signatureData.Signature = CryptoUtils.Sign( + new ArraySegment(dataToSign), + localCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + return signatureData; } /// - /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static bool Verify( - X509Certificate2 certificate, + public static bool VerifySignatureData( + SignatureData signature, string securityPolicyUri, - byte[] dataToVerify, - SignatureData signature) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] localCertificate, + byte[] localChannelCertificate, + byte[] remoteChannelCertificate, + byte[] localNonce, + byte[] remoteNonce) { - // check if nothing to do. - if (signature == null) + var signatureData = new SignatureData(); + + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return true; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + // create the data to sign. + byte[] dataToVerify = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + localCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localNonce ?? Array.Empty(), + remoteNonce ?? Array.Empty()) + : + Utils.Append( + localCertificate ?? Array.Empty(), + localNonce); + + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + string securityPolicyUri, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + var info = GetInfo(securityPolicyUri); + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + SecurityPolicyInfo securityPolicy, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + // check if nothing to do. + if (signature == null) { return true; } - // decrypt data. - switch (securityPolicyUri) + // sign data. + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + // always accept signatures if security is not used. + case AsymmetricSignatureAlgorithm.None: + return true; + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + { if (signature.Algorithm == SecurityAlgorithms.RsaSha1) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256/Basic128Rsa15: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha1); - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: + break; + } + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + { if (signature.Algorithm == SecurityAlgorithms.RsaSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256Sha256/Aes128_Sha256_RsaOaep: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha256); - case Aes256_Sha256_RsaPss: + break; + } + + case AsymmetricSignatureAlgorithm.RsaPssSha256: + { if (signature.Algorithm == SecurityAlgorithms.RsaPssSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Aes256_Sha256_RsaPss: {0}\n" + - "Expected signature algorithm : {1}", - signature.Algorithm, - SecurityAlgorithms.RsaPssSha256); - case ECC_nistP256: - case ECC_brainpoolP256r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA256); - case ECC_nistP384: - case ECC_brainpoolP384r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA384); - // always accept signatures if security is not used. - case None: - return true; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha256: + { + if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha384: + { + if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } } + + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "Unexpected SignatureData algorithm: {0}", + signature.Algorithm); } /// @@ -669,6 +888,63 @@ public static bool Verify( return keyValuePairs.ToFrozenDictionary(); #else return new ReadOnlyDictionary(keyValuePairs); +#endif + }); + + /// + /// Creates a dictionary of uris to SecurityPolicyInfo excluding base uri + /// + private static readonly Lazy> s_securityPolicyUriToInfo = + new(() => + { +#if NET8_0_OR_GREATER + return s_securityPolicyNameToInfo.Value.ToFrozenDictionary(k => k.Value.Uri, k => k.Value); +#else + return new ReadOnlyDictionary( + s_securityPolicyNameToInfo.Value.ToDictionary(k => k.Value.Uri, k => k.Value)); +#endif + }); + + /// + /// Creates a dictionary for names to SecurityPolicyInfo excluding base uri + /// + private static readonly Lazy> s_securityPolicyNameToInfo = + new(() => + { + FieldInfo[] policyFields = typeof(SecurityPolicies).GetFields( + BindingFlags.Public | BindingFlags.Static); + + FieldInfo[] infoFields = typeof(SecurityPolicyInfo).GetFields( + BindingFlags.Public | BindingFlags.Static); + + var keyValuePairs = new Dictionary(); + foreach (FieldInfo field in policyFields) + { + string policyUri = (string)field.GetValue(typeof(SecurityPolicies)); + if (field.Name == nameof(BaseUri) || + field.Name == nameof(Https) || + !policyUri.StartsWith(BaseUri, StringComparison.Ordinal)) + { + continue; + } + + // Find the corresponding SecurityPolicyInfo field by name + FieldInfo infoField = Array.Find(infoFields, f => f.Name == field.Name); + if (infoField != null && infoField.FieldType == typeof(SecurityPolicyInfo)) + { + SecurityPolicyInfo info = (SecurityPolicyInfo)infoField.GetValue(null); + keyValuePairs.Add(field.Name, info); + } + else + { + // Fallback to creating a minimal instance for unknown policies + keyValuePairs.Add(field.Name, new SecurityPolicyInfo(policyUri, field.Name)); + } + } +#if NET8_0_OR_GREATER + return keyValuePairs.ToFrozenDictionary(); +#else + return new ReadOnlyDictionary(keyValuePairs); #endif }); } diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs new file mode 100644 index 000000000..1d7af8a5c --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -0,0 +1,1275 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Opc.Ua +{ + /// + /// Defines constants for key security policies. + /// + public class SecurityPolicyInfo + { + /// + /// Creates a new instance of the class. + /// + /// The unique identifier. + /// The display name. + /// + public SecurityPolicyInfo(string uri, string name = null) + { + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URI is not a valid security policy.", nameof(uri)); + } + + Uri = uri; + Name = name ?? SecurityPolicies.GetDisplayName(uri) ?? uri; + } + + /// + /// Short name for the policy. + /// + public string Name { get; } + + /// + /// The unique identifier for the policy. + /// + public string Uri { get; } + + /// + /// Returns true if the policy is considered deprecated and should not be used for new deployments. + /// + public bool IsDeprecated { get; private set; } + + /// + /// The symmetric signature algorithm to use. + /// + public SymmetricSignatureAlgorithm SymmetricSignatureAlgorithm { get; private set; } + + /// + /// The symmetric encryption algorithm to use. + /// + public SymmetricEncryptionAlgorithm SymmetricEncryptionAlgorithm { get; private set; } + + /// + /// The asymmetric signature algorithm to use. + /// + public AsymmetricSignatureAlgorithm AsymmetricSignatureAlgorithm { get; private set; } + + /// + /// The symmetric encryption algorithm to use. + /// + public AsymmetricEncryptionAlgorithm AsymmetricEncryptionAlgorithm { get; private set; } + + /// + /// The minimum length, in bits, for an asymmetric key. + /// + public int MinAsymmetricKeyLength { get; private set; } + + /// + /// The maximum length, in bits, for an asymmetric key. + /// + public int MaxAsymmetricKeyLength { get; private set; } + + /// + /// The key derivation algorithm to use. + /// + public KeyDerivationAlgorithm KeyDerivationAlgorithm { get; private set; } + + /// + /// The length in bytes of the derived key used for message authentication. + /// + public int DerivedSignatureKeyLength { get; private set; } + + /// + /// The asymmetric signature algorithm used to sign certificates. + /// + public AsymmetricSignatureAlgorithm CertificateSignatureAlgorithm { get; private set; } + + /// + /// Returns algorithm family used to create asymmetric key pairs used with Certificates. + /// + public CertificateKeyFamily CertificateKeyFamily { get; private set; } + + /// + /// The algorithm used to create asymmetric key pairs used with Certificates. + /// + public CertificateKeyAlgorithm CertificateKeyAlgorithm { get; private set; } + + /// + /// The algorithm used to create asymmetric key pairs used for EphemeralKeys. + /// + public CertificateKeyAlgorithm EphemeralKeyAlgorithm { get; private set; } + + /// + /// The length, in bytes, of the Nonces used when opening a SecureChannel. + /// + public int SecureChannelNonceLength { get; private set; } + + /// + /// The length, in bytes, of the data used to initialize the symmetric algorithm. + /// + public int InitializationVectorLength { get; private set; } + + /// + /// The length, in bytes, of the symmetric signature. + /// + public int SymmetricSignatureLength { get; private set; } + + /// + /// The length, in bytes, of the symmetric encryption key. + /// + public int SymmetricEncryptionKeyLength { get; private set; } + + /// + /// If TRUE, the 1024 based SequenceNumber rules apply to the SecurityPolicy. + /// If FALSE, the 0 based SequenceNumber rules apply. + /// + public bool LegacySequenceNumbers { get; private set; } + + /// + /// If TRUE, the enhancements to the SecureChannel are required for the SecurityPolicy. + /// • Channel-bound Signature calculations in CreateSession/ActivateSession; + /// • Session transfer tokens in ActivateSession; + /// • Chained symmetric key derivation when renewing SecureChannels. + /// • Allow padding when using Authenticated Encryption; + /// + public bool SecureChannelEnhancements { get; private set; } + + /// + /// Whether the padding is required with symmetric encryption. + /// + public bool NoSymmetricEncryptionPadding => + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes256Gcm || + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes128Gcm || + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305; + + /// + /// Returns the derived server key data length. + /// + public int ServerKeyDataLength => + (DerivedSignatureKeyLength + SymmetricEncryptionKeyLength + InitializationVectorLength); + + /// + /// Returns the derived client key data length. + /// + public int ClientKeyDataLength => + (DerivedSignatureKeyLength + SymmetricEncryptionKeyLength + InitializationVectorLength); + + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetUserTokenSignatureData( + byte[] channelThumbprint, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "UserTokenSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientCertificate={CryptoTrace.KeyToString(clientCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + data = Utils.Append( + channelThumbprint, + serverNonce, + serverCertificate, + serverChannelCertificate, + clientCertificate, + clientChannelCertificate, + clientNonce); + } + else + { + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + data = Utils.Append( + serverCertificate, + serverNonce); + } + + CryptoTrace.Finish("UserTokenSignatureData"); + return data; + } + + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetServerSignatureData( + byte[] channelThumbprint, + byte[] clientNonce, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] serverNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "ServerSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + data = Utils.Append( + channelThumbprint, + clientNonce, + serverChannelCertificate, + clientChannelCertificate, + serverNonce); + } + else + { + CryptoTrace.WriteLine($"ClientCertificate={CryptoTrace.KeyToString(clientCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + data = Utils.Append( + clientCertificate, + clientNonce); + } + + CryptoTrace.Finish("ServerSignatureData"); + return data; + } + + /// + /// Returns the data to be signed by the client when creating a session. + /// + public byte[] GetClientSignatureData( + byte[] channelThumbprint, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + CryptoTrace.Start(ConsoleColor.Yellow, "ClientSignatureData"); + + if (SecureChannelEnhancements) + { + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(channelThumbprint)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerChannelCertificate={CryptoTrace.KeyToString(serverChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + + data = Utils.Append( + channelThumbprint, + serverNonce, + serverCertificate, + serverChannelCertificate, + clientChannelCertificate, + clientNonce); + } + else + { + CryptoTrace.WriteLine($"ServerCertificate={CryptoTrace.KeyToString(serverCertificate)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + + data = Utils.Append( + serverCertificate, + serverNonce); + } + + CryptoTrace.Finish("ClientSignatureData"); + return data; + } + + /// + /// Returns a HMAC based on the symmetric signature algorithm. + /// + public HMAC CreateSignatureHmac(byte[] signingKey) + { +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms + return SymmetricSignatureAlgorithm switch + { + SymmetricSignatureAlgorithm.HmacSha1 => new HMACSHA1(signingKey), + SymmetricSignatureAlgorithm.HmacSha256 => new HMACSHA256(signingKey), + SymmetricSignatureAlgorithm.HmacSha384 => new HMACSHA384(signingKey), + _ => null + }; +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms + } + + /// + /// Returns a HashAlgorithmName based on the KeyDerivationAlgorithm. + /// + public HashAlgorithmName GetKeyDerivationHashAlgorithmName() + { + return KeyDerivationAlgorithm switch + { + KeyDerivationAlgorithm.PSha1 => HashAlgorithmName.SHA1, + KeyDerivationAlgorithm.PSha256 => HashAlgorithmName.SHA256, + KeyDerivationAlgorithm.HKDFSha256 => HashAlgorithmName.SHA256, + KeyDerivationAlgorithm.HKDFSha384 => HashAlgorithmName.SHA384, + _ => HashAlgorithmName.SHA256 + }; + } + + /// + /// The security policy that does not provide any security. + /// + public static readonly SecurityPolicyInfo None = new(SecurityPolicies.None) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 0, + InitializationVectorLength = 0, + SymmetricSignatureLength = 0, + MinAsymmetricKeyLength = 0, + MaxAsymmetricKeyLength = 0, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, + CertificateKeyFamily = CertificateKeyFamily.None, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.None, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.None, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.None, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None, + SecureChannelEnhancements = false + }; + + /// + /// The security policy that uses SHA1 and 128 bit encryption. This policy is considered insecure and should not be used for new deployments. + /// + public static readonly SecurityPolicyInfo Basic128Rsa15 = new(SecurityPolicies.Basic128Rsa15) + { + DerivedSignatureKeyLength = 128 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 1024, + MaxAsymmetricKeyLength = 2048, + SecureChannelNonceLength = 16, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, + IsDeprecated = true + }; + + /// + /// The security policy that uses SHA1 and 256 bit encryption. This policy is considered insecure and should not be used for new deployments. + /// + public static readonly SecurityPolicyInfo Basic256 = new(SecurityPolicies.Basic256) + { + DerivedSignatureKeyLength = 192 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 1024, + MaxAsymmetricKeyLength = 2048, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, + IsDeprecated = true + }; + + /// + /// Aes128_Sha256_RsaOaep is a required minimum security policy. It uses SHA256 and 128 bit encryption. + /// + public static readonly SecurityPolicyInfo Aes128_Sha256_RsaOaep = new(SecurityPolicies.Aes128_Sha256_RsaOaep) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + IsDeprecated = false + }; + + /// + /// Basic256Sha256 is a required minimum security policy. It uses SHA256 and 256 bit encryption. + /// + public static readonly SecurityPolicyInfo Basic256Sha256 = new(SecurityPolicies.Basic256Sha256) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + SymmetricSignatureLength = 256 / 8, + InitializationVectorLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + IsDeprecated = false + }; + + /// + /// Aes256_Sha256_RsaPss is a optional high security policy. It uses SHA256 and 256 bit encryption. + /// + public static readonly SecurityPolicyInfo Aes256_Sha256_RsaPss = new(SecurityPolicies.Aes256_Sha256_RsaPss) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = true, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha256, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + IsDeprecated = false + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519 = new(SecurityPolicies.ECC_curve25519) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519_AesGcm = new(SecurityPolicies.ECC_curve25519_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519_ChaChaPoly = new(SecurityPolicies.ECC_curve25519_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448 = new(SecurityPolicies.ECC_curve448) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448_AesGcm = new(SecurityPolicies.ECC_curve448_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// ECC Curve448 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448_ChaChaPoly = new(SecurityPolicies.ECC_curve448_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC nistP256 is a required minimum security policy. + /// + public readonly static SecurityPolicyInfo ECC_nistP256 = new(SecurityPolicies.ECC_nistP256) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC_nistP256_AesGcm is an ECC nistP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_AesGcm = new(SecurityPolicies.ECC_nistP256_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC_nistP256_AesGcm is an ECC nistP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_ChaChaPoly = new(SecurityPolicies.ECC_nistP256_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC nistP384 is an optional high security policy. + /// + public readonly static SecurityPolicyInfo ECC_nistP384 = new(SecurityPolicies.ECC_nistP384) + { + DerivedSignatureKeyLength = 384 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 384 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_AesGcm = new(SecurityPolicies.ECC_nistP384_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_ChaChaPoly = new(SecurityPolicies.ECC_nistP384_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP256r1 is a required minimum security policy. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1 = new(SecurityPolicies.ECC_brainpoolP256r1) + { + DerivedSignatureKeyLength = 256 / 8, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 256 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC_brainpoolP256r1_AesGcm is an ECC brainpoolP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_AesGcm = new (SecurityPolicies.ECC_brainpoolP256r1_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC_brainpoolP256_AES is an ECC brainpoolP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1 = new(SecurityPolicies.ECC_brainpoolP384r1) + { + DerivedSignatureKeyLength = 384 / 8, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_AesGcm = new(SecurityPolicies.ECC_brainpoolP384r1_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes256Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The RSA_DH_AES_GCM is an high security policy that uses AES GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_AesGcm = new(SecurityPolicies.RSA_DH_AesGcm) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 384, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The RSA_DH_ChaChaPoly is an high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_ChaChaPoly = new(SecurityPolicies.RSA_DH_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 384, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + } + + /// + /// The algorithm family used to generate key pairs. + /// + public enum CertificateKeyFamily + { + /// + /// Does not apply. + /// + None, + + /// + /// The RSA algorithm. + /// + RSA, + + /// + /// Ellipic curve algorithms. + /// + ECC + } + + /// + /// The algorithm used to generate key pairs. + /// + public enum CertificateKeyAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// The RSA algorithm. + /// + RSA, + + /// + /// The Diffie-Hellman algorith with RSA public keys. + /// + RSADH, + + /// + /// The NIST P-256 ellipic curve algorithm. + /// + NistP256, + + /// + /// The NIST P-384 ellipic curve algorithm. + /// + NistP384, + + /// + /// The non-twisted Brainpool P-256 ellipic curve algorithm. + /// + BrainpoolP256r1, + + /// + /// The non-twisted Brainpool P-384 ellipic curve algorithm. + /// + BrainpoolP384r1, + + /// + /// The Edward Curve25519 ellipic curve algorithm. + /// + Curve25519, + + /// + /// The Edward Curve25519 ellipic curve algorithm. + /// + Curve448 + } + + /// + /// The symmetric key derivation algorithm used to create shared keys. + /// + public enum KeyDerivationAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// The P_SHA pseudo-random function with SHA1. This algorithm is considered insecure. + /// + PSha1, + + /// + /// The P_SHA pseudo-random function with SHA256. + /// + PSha256, + + /// + /// The HKDF pseudo-random function with SHA256. + /// + HKDFSha256, + + /// + /// The HKDF pseudo-random function with SHA384. + /// + HKDFSha384 + } + + /// + /// The asymmetric encryption algorithm used to encrypt messages. + /// + public enum AsymmetricEncryptionAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// RSA PKCS #1 v1.5. This algorithm is considered insecure. + /// + RsaPkcs15Sha1, + + /// + /// RSA with OAEP padding with SHA1. This algorithm is considered insecure. + /// + RsaOaepSha1, + + /// + /// RSA with OAEP padding with SHA256 . + /// + RsaOaepSha256 + } + + /// + /// The asymmetric signature algorithm used to sign messages. + /// + public enum AsymmetricSignatureAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// RSA PKCS #1 v1.5 with SHA1. This algorithm is considered insecure. + /// + RsaPkcs15Sha1, + + /// + /// RSA PKCS #1 v1.5 with SHA256. + /// + RsaPkcs15Sha256, + + /// + /// RSA PSS with SHA256. + /// + RsaPssSha256, + + /// + /// ECDSA with SHA256. + /// + EcdsaSha256, + + /// + /// ECDSA with SHA384. + /// + EcdsaSha384, + + /// + /// ECDSA with Curve 25519. + /// + EcdsaPure25519, + + /// + /// ECDSA with Curve 448. + /// + EcdsaPure448 + } + + /// + /// The symmetric signature algorithm used to sign messages. + /// + public enum SymmetricSignatureAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// HMAC with SHA1 + /// + HmacSha1, + + /// + /// HMAC with SHA256 + /// + HmacSha256, + + /// + /// HMAC with SHA384 + /// + HmacSha384, + + /// + /// ChaCha20Poly1305 + /// + ChaCha20Poly1305, + + /// + /// AES GCM with 128 bit key + /// + Aes128Gcm, + + /// + /// AES GCM with 256 bit key + /// + Aes256Gcm + } + + /// + /// The symmetric ecryption algorithm used to encrypt messages. + /// + public enum SymmetricEncryptionAlgorithm + { + /// + /// Does not apply. + /// + None, + + /// + /// AES 128 bit in CBC mode + /// + Aes128Cbc, + + /// + /// AES 256 bit in CBC mode + /// + Aes256Cbc, + + /// + /// AES 128 bit in counter mode + /// + Aes128Ctr, + + /// + /// AES 256 bit in counter mode + /// + Aes256Ctr, + + /// + /// ChaCha20Poly1305 + /// + ChaCha20Poly1305, + + /// + /// AES 128 in GCM mode + /// + Aes128Gcm, + + /// + /// AES 256 in GCM mode + /// + Aes256Gcm + } +} diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs index f8dd12b12..012bcada5 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs @@ -378,6 +378,15 @@ private sealed class ClientChannel : ITransportChannel /// public IServiceMessageContext MessageContext => m_channel.MessageContext; + /// + public byte[] ChannelThumbprint => m_channel?.ChannelThumbprint ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; + /// public int OperationTimeout { diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index af9ecdd8c..993988484 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -106,12 +106,12 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurity else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } @@ -176,12 +176,12 @@ public UserTokenPolicy FindUserTokenPolicy( else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ServerProperties.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ServerProperties.cs index e019ca018..18b1aca96 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ServerProperties.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ServerProperties.cs @@ -48,7 +48,6 @@ public ServerProperties() BuildNumber = string.Empty; BuildDate = DateTime.MinValue; DatatypeAssemblies = []; - SoftwareCertificates = []; } /// @@ -85,10 +84,5 @@ public ServerProperties() /// The assemblies that contain encodeable types that could be uses a variable values. /// public StringCollection DatatypeAssemblies { get; } - - /// - /// The software certificates granted to the server. - /// - public SignedSoftwareCertificateCollection SoftwareCertificates { get; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 9cc03ea33..52a4b36b9 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -135,17 +135,22 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => null; + public ChannelToken CurrentToken => new(); + + /// + public byte[] ChannelThumbprint => []; + + /// + public byte[] ClientChannelCertificate { get; private set; } = []; + + /// + public byte[] ServerChannelCertificate { get; private set; } = []; /// public event ChannelTokenActivatedEventHandler OnTokenActivated { - add - { - } - remove - { - } + add {} + remove {} } /// @@ -412,6 +417,7 @@ private void CreateHttpClient() } #endif handler.ClientCertificates.Add(clientCertificate); + ClientChannelCertificate = clientCertificate.RawData; } Func< @@ -458,7 +464,7 @@ private void CreateHttpClient() } m_quotas.CertificateValidator?.ValidateAsync(validationChain, default).GetAwaiter().GetResult(); - + ServerChannelCertificate = cert.RawData; return true; } catch (Exception ex) @@ -486,6 +492,7 @@ private void CreateHttpClient() #pragma warning disable CA5400 // HttpClient is created without enabling CheckCertificateRevocationList m_client = new HttpClient(handler); #pragma warning restore CA5400 // HttpClient is created without enabling CheckCertificateRevocationList + } catch (Exception ex) { diff --git a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs index c5f34f344..6e407db4c 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs @@ -43,14 +43,23 @@ public class SecureChannelContext /// The secure channel identifier. /// The endpoint description. /// The message encoding. + /// The unique hash for the secure channel calculated during channel creation. + /// The client certificate used to establsih the secure channel. + /// The server certificate used to establsih the secure channel. public SecureChannelContext( string secureChannelId, EndpointDescription endpointDescription, - RequestEncoding messageEncoding) + RequestEncoding messageEncoding, + byte[] clientChannelCertificate, + byte[] serverChannelCertificate, + byte[] channelThumbprint = null) { SecureChannelId = secureChannelId; EndpointDescription = endpointDescription; MessageEncoding = messageEncoding; + ClientChannelCertificate = clientChannelCertificate; + ServerChannelCertificate = serverChannelCertificate; + ChannelThumbprint = channelThumbprint; } /// @@ -71,6 +80,21 @@ public SecureChannelContext( /// The message encoding. public RequestEncoding MessageEncoding { get; } + /// + /// The unique hash for the secure channel calculated during channel creation. + /// + public byte[] ChannelThumbprint { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + public byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + public byte[] ServerChannelCertificate { get; } + /// /// The active secure channel for the thread. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs index 010a87c61..c1af365da 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs @@ -37,6 +37,8 @@ namespace Opc.Ua.Bindings /// public sealed class ChannelToken : IDisposable { + private bool m_disposed; + /// /// Creates an object with default values. /// @@ -51,17 +53,18 @@ private void Dispose(bool disposing) { if (!m_disposed) { - if (disposing) + if (ServerHmac != null) + { + ServerHmac.Dispose(); + ServerHmac = null; + } + + if (ClientHmac != null) { - Utils.SilentDispose(ClientHmac); - Utils.SilentDispose(ServerHmac); - Utils.SilentDispose(ClientEncryptor); - Utils.SilentDispose(ServerEncryptor); + ClientHmac.Dispose(); + ClientHmac = null; } - ClientHmac = null; - ServerHmac = null; - ClientEncryptor = null; - ServerEncryptor = null; + m_disposed = true; } } @@ -123,6 +126,21 @@ public void Dispose() (HiResClock.TickCount - CreatedAtTickCount) > (int)Math.Round(Lifetime * TcpMessageLimits.TokenActivationPeriod); + /// + /// The SecurityPolicy used to encrypt and sign the messages. + /// + public SecurityPolicyInfo SecurityPolicy { get; set; } + + /// + /// The secret used to compute the keys. + /// + internal byte[] Secret { get; set; } + + /// + /// The previous server nonce used to compute the keys. + /// + internal byte[] PreviousSecret { get; set; } + /// /// The nonce provided by the client. /// @@ -136,53 +154,41 @@ public void Dispose() /// /// The key used to sign messages sent by the client. /// - public byte[] ClientSigningKey { get; set; } + internal byte[] ClientSigningKey { get; set; } /// /// The key used to encrypt messages sent by the client. /// - public byte[] ClientEncryptingKey { get; set; } + internal byte[] ClientEncryptingKey { get; set; } /// /// The initialization vector by the client when encrypting a message. /// - public byte[] ClientInitializationVector { get; set; } + internal byte[] ClientInitializationVector { get; set; } /// /// The key used to sign messages sent by the server. /// - public byte[] ServerSigningKey { get; set; } + internal byte[] ServerSigningKey { get; set; } /// /// The key used to encrypt messages sent by the server. /// - public byte[] ServerEncryptingKey { get; set; } + internal byte[] ServerEncryptingKey { get; set; } /// /// The initialization vector by the server when encrypting a message. /// - public byte[] ServerInitializationVector { get; set; } - - /// - /// The SymmetricAlgorithm object used by the client to encrypt messages. - /// - public SymmetricAlgorithm ClientEncryptor { get; set; } + internal byte[] ServerInitializationVector { get; set; } /// - /// The SymmetricAlgorithm object used by the server to encrypt messages. + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. /// - public SymmetricAlgorithm ServerEncryptor { get; set; } + internal HMAC ServerHmac { get; set; } /// - /// The HMAC object used by the client to sign messages. + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. /// - public HMAC ClientHmac { get; set; } - - /// - /// The HMAC object used by the server to sign messages. - /// - public HMAC ServerHmac { get; set; } - - private bool m_disposed; + internal HMAC ClientHmac { get; set; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index e26a52c99..c9a6ef700 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -509,71 +509,6 @@ protected virtual void CompleteReverseHello(Exception e) // intentionally left empty } - /// - /// Sends a fault response secured with the asymmetric keys. - /// - protected void SendServiceFault(uint requestId, ServiceResult fault) - { - m_logger.LogDebug( - "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}", - ChannelId, - requestId, - fault.StatusCode); - - BufferCollection chunksToSend = null; - - try - { - // construct fault. - var response = new ServiceFault(); - - response.ResponseHeader.ServiceResult = fault.Code; - - var stringTable = new StringTable(); - - response.ResponseHeader.ServiceDiagnostics = new DiagnosticInfo( - fault, - DiagnosticsMasks.NoInnerStatus, - true, - stringTable, - m_logger); - - response.ResponseHeader.StringTable = stringTable.ToArray(); - - // serialize fault. - byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); - - // secure message. - chunksToSend = WriteAsymmetricMessage( - TcpMessageType.Open, - requestId, - ServerCertificate, - ClientCertificate, - new ArraySegment(buffer, 0, buffer.Length)); - - // write the message to the server. - BeginWriteMessage(chunksToSend, null); - chunksToSend = null; - } - catch (Exception e) - { - chunksToSend?.Release(BufferManager, "SendServiceFault"); - - m_logger.LogError( - e, - "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}: Unexpected error.", - ChannelId, - requestId, - fault.StatusCode); - - ForceChannelFault( - ServiceResult.Create( - e, - StatusCodes.BadTcpInternalError, - "Unexpected error sending a service fault.")); - } - } - /// /// Handles a reconnect request. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index 69b0705e8..5a91a6989 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -27,6 +27,11 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; + namespace Opc.Ua.Bindings { /// @@ -158,6 +163,23 @@ public static bool IsValid(uint messageType) } } + + internal static string GetTypeAndSize(ArraySegment chunk) + { + StringBuilder sb = new StringBuilder(); + + for (int ii = 0; ii < 1; ii++) + { + uint size = BitConverter.ToUInt32(chunk.Array ?? [], 4); + sb.Append(Encoding.ASCII.GetString(chunk.Array ?? [], 0, 4)); + sb.Append("=>"); + sb.Append(BitConverter.ToUInt32(chunk.Array ?? [], 4)); + sb.Append((size != chunk.Count) ? " X " : " O "); + sb.Append(chunk.Count); + } + + return sb.ToString(); + } } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index fd720900d..f306901e0 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -273,7 +274,7 @@ public override void Reconnect( State = TcpChannelState.Open; // send response. - SendOpenSecureChannelResponse(requestId, token, request); + SendOpenSecureChannelResponse(requestId, token, request, true); // send any queued responses. ResetQueuedResponses(OnChannelReconnected); @@ -566,13 +567,28 @@ private bool ProcessOpenSecureChannelRequest( try { + m_oscRequestSignature = null; + byte[] signature; + messageBody = ReadAsymmetricMessage( messageChunk, ServerCertificate, out channelId, out clientCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; + + CryptoTrace.Start(ConsoleColor.Magenta, $"ProcessOpenSecureChannelRequest ({(State != TcpChannelState.Opening ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ClientCertificate={clientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("ProcessOpenSecureChannelRequest"); // check for replay attacks. if (!VerifySequenceNumber(sequenceNumber, "ProcessOpenSecureChannelRequest")) @@ -681,6 +697,8 @@ or StatusCodes.BadCertificateIssuerRevocationUnknown token = CreateToken(); token.TokenId = GetNewTokenId(); token.ServerNonce = CreateNonce(ServerCertificate); + token.PreviousSecret = CurrentToken?.Secret; + // check the client nonce. token.ClientNonce = request.ClientNonce; if (!ValidateNonce(ClientCertificate, token.ClientNonce)) @@ -792,11 +810,11 @@ or StatusCodes.BadCertificateIssuerRevocationUnknown // send the response. if (requestType == SecurityTokenRequestType.Renew) { - SendOpenSecureChannelResponse(requestId, RenewedToken, request); + SendOpenSecureChannelResponse(requestId, RenewedToken, request, true); } else { - SendOpenSecureChannelResponse(requestId, CurrentToken, request); + SendOpenSecureChannelResponse(requestId, CurrentToken, request, false); } // notify reverse @@ -822,11 +840,14 @@ or StatusCodes.BadCertificateIssuerRevocationUnknown // report the audit event for open secure channel ReportAuditOpenSecureChannelEvent?.Invoke(this, request, ClientCertificate, e); - SendServiceFault( - requestId, ServiceResult.Create( + SendServiceFault( + requestId, + State == TcpChannelState.Open, + ServiceResult.Create( e, StatusCodes.BadTcpInternalError, "Unexpected error processing OpenSecureChannel request.")); + CompleteReverseHello(e); return false; } @@ -862,13 +883,85 @@ protected override void CompleteReverseHello(Exception e) } } + /// + /// Sends a fault response secured with the asymmetric keys. + /// + protected void SendServiceFault(uint requestId, bool renew, ServiceResult fault) + { + m_logger.LogDebug( + "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}", + ChannelId, + requestId, + fault.StatusCode); + + BufferCollection chunksToSend = null; + + try + { + // construct fault. + var response = new ServiceFault(); + + response.ResponseHeader.ServiceResult = fault.Code; + + var stringTable = new StringTable(); + + response.ResponseHeader.ServiceDiagnostics = new DiagnosticInfo( + fault, + DiagnosticsMasks.NoInnerStatus, + true, + stringTable, + m_logger); + + response.ResponseHeader.StringTable = stringTable.ToArray(); + + // serialize fault. + byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); + byte[] signature = null; + + CryptoTrace.WriteLine($"messageBody={CryptoTrace.KeyToString(buffer)}"); + + // secure message. + chunksToSend = WriteAsymmetricMessage( + TcpMessageType.Open, + requestId, + ServerCertificate, + null, + ClientCertificate, + new ArraySegment(buffer, 0, buffer.Length), + !renew ? m_oscRequestSignature : null, + out signature); + + // write the message to the server. + BeginWriteMessage(chunksToSend, null); + chunksToSend = null; + } + catch (Exception e) + { + chunksToSend?.Release(BufferManager, "SendServiceFault"); + + m_logger.LogError( + e, + "ChannelId {Id}: Request {RequestId}: SendServiceFault={ServiceFault}: Unexpected error.", + ChannelId, + requestId, + fault.StatusCode); + + ForceChannelFault( + ServiceResult.Create( + e, + StatusCodes.BadTcpInternalError, + "Unexpected error sending a service fault.")); + } + } + /// /// Sends an OpenSecureChannel response. /// private void SendOpenSecureChannelResponse( uint requestId, ChannelToken token, - OpenSecureChannelRequest request) + OpenSecureChannelRequest request, + bool renew) { m_logger.LogDebug("ChannelId {Id}: SendOpenSecureChannelResponse()", ChannelId); @@ -884,6 +977,7 @@ private void SendOpenSecureChannelResponse( response.ServerNonce = token.ServerNonce; byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); + byte[] signature; BufferCollection chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -891,7 +985,22 @@ private void SendOpenSecureChannelResponse( ServerCertificate, ServerCertificateChain, ClientCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + !renew ? m_oscRequestSignature : null, + out signature); + + if (!renew) + { + ChannelThumbprint = signature; + } + + CryptoTrace.Start(ConsoleColor.Magenta, $"SendOpenSecureChannelResponse ({(renew ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(m_oscRequestSignature)}"); + CryptoTrace.WriteLine($"ResponseSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("SendOpenSecureChannelResponse"); // write the message to the server. try @@ -1343,6 +1452,7 @@ private bool ValidateDiscoveryServiceCall( private readonly ILogger m_logger; private SortedDictionary m_queuedResponses; private ReverseConnectAsyncResult m_pendingReverseHello; + private byte[] m_oscRequestSignature; private static readonly string s_implementationString = ".NET Standard ServerChannel UA-TCP " + Utils.GetAssemblyBuildNumber(); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index 712e8cfa5..b09e8612e 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -1029,7 +1029,10 @@ private async void OnRequestReceivedAsync( var context = new SecureChannelContext( channel.GlobalChannelId, channel.EndpointDescription, - RequestEncoding.Binary); + RequestEncoding.Binary, + channel.ClientCertificate?.RawData, + channel.ServerCertificate?.RawData, + channel.ChannelThumbprint); IServiceResponse response = await m_callback.ProcessRequestAsync( context, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index dabb2e5e7..db7a7f19b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -68,7 +68,7 @@ protected set /// /// The certificate for the server. /// - protected X509Certificate2 ServerCertificate { get; private set; } + internal X509Certificate2 ServerCertificate { get; private set; } /// /// The server certificate chain. @@ -83,7 +83,20 @@ protected set /// /// The security policy used with the channel. /// - protected string SecurityPolicyUri { get; private set; } + protected string SecurityPolicyUri + { + get => SecurityPolicy.Uri; + + private set + { + SecurityPolicy = SecurityPolicies.GetInfo(value); + } + } + + /// + /// The security policy used with the channel. + /// + protected SecurityPolicyInfo SecurityPolicy { get; private set; } /// /// Whether the channel is restricted to discovery operations. @@ -93,7 +106,7 @@ protected set /// /// The certificate for the client. /// - protected X509Certificate2 ClientCertificate { get; set; } + internal X509Certificate2 ClientCertificate { get; set; } /// /// The client certificate chain. @@ -187,33 +200,21 @@ protected static void CompareCertificates( /// protected byte[] CreateNonce(X509Certificate2 certificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - uint length = Nonce.GetNonceLength(SecurityPolicyUri); - - if (length > 0) + case CertificateKeyFamily.RSA: + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) { - return Nonce.CreateRandomNonceData(length); + m_localNonce = Nonce.CreateNonce(SecurityPolicy); + return m_localNonce.Data; } - break; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_localNonce = Nonce.CreateNonce(SecurityPolicyUri); + return Nonce.CreateRandomNonceData(SecurityPolicy.SecureChannelNonceLength); + case CertificateKeyFamily.ECC: + m_localNonce = Nonce.CreateNonce(SecurityPolicy); return m_localNonce.Data; default: return null; } - - return null; } /// @@ -228,18 +229,20 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) } // check the length. - if (nonce == null || nonce.Length != Nonce.GetNonceLength(SecurityPolicyUri)) + if (nonce == null || nonce.Length != SecurityPolicy.SecureChannelNonceLength) { return false; } - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case CertificateKeyFamily.RSA: + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.RSADH) + { + m_remoteNonce = Nonce.CreateNonce(SecurityPolicy, nonce); + return true; + } + // try to catch programming errors by rejecting nonces with all zeros. for (int ii = 0; ii < nonce.Length; ii++) { @@ -248,19 +251,13 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) return true; } } - - return false; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_remoteNonce = Nonce.CreateNonce(SecurityPolicyUri, nonce); + break; + case CertificateKeyFamily.ECC: + m_remoteNonce = Nonce.CreateNonce(SecurityPolicy, nonce); return true; - default: - return false; } + + return false; } /// @@ -268,19 +265,23 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) /// protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: + return 1; + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA256); - case SecurityPolicies.Basic128Rsa15: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.Pkcs1); @@ -294,13 +295,17 @@ protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) /// protected int GetCipherTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.Basic128Rsa15: + return 1; + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetCipherTextBlockSize(receiverCertificate); default: return 1; @@ -399,21 +404,17 @@ protected int GetAsymmetricHeaderSize( /// protected int GetAsymmetricSignatureSize(X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.AsymmetricSignatureAlgorithm) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + case AsymmetricSignatureAlgorithm.RsaPssSha256: return RsaUtils.GetSignatureLength(senderCertificate); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.GetSignatureLength(senderCertificate); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + case AsymmetricSignatureAlgorithm.EcdsaPure25519: + case AsymmetricSignatureAlgorithm.EcdsaPure448: + return CryptoUtils.GetSignatureLength(senderCertificate); default: return 0; } @@ -551,13 +552,16 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 receiverCertificate, ArraySegment messageBody) { + byte[] unused = null; return WriteAsymmetricMessage( messageType, requestId, senderCertificate, null, receiverCertificate, - messageBody); + messageBody, + null, + out unused); } /// @@ -571,8 +575,12 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 senderCertificate, X509Certificate2Collection senderCertificateChain, X509Certificate2 receiverCertificate, - ArraySegment messageBody) + ArraySegment messageBody, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + bool success = false; var chunksToSend = new BufferCollection(); @@ -663,7 +671,8 @@ protected BufferCollection WriteAsymmetricMessage( if (SecurityMode != MessageSecurityMode.None) { - if (receiverCertificate.GetRSAPublicKey() != null) + if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None && + receiverCertificate.GetRSAPublicKey() != null) { if (X509Utils.GetRSAPublicKeySize(receiverCertificate) <= TcpMessageLimits.KeySizeExtraPadding) @@ -721,10 +730,30 @@ protected BufferCollection WriteAsymmetricMessage( // put the message size after encryption into the header. UpdateMessageSize(buffer, 0, cipherTextSize + headerSize); + ArraySegment dataToSign; + + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToSign = new ArraySegment( + buffer, + 0, + encoder.Position + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + 0, + buffer, + encoder.Position, + oscRequestSignature.Length); + } + else + { + dataToSign = new ArraySegment(buffer, 0, encoder.Position); + } + // write the signature. - byte[] signature = Sign( - new ArraySegment(buffer, 0, encoder.Position), - senderCertificate); + signature = Sign(dataToSign, senderCertificate); if (signature != null) { @@ -765,6 +794,7 @@ protected BufferCollection WriteAsymmetricMessage( // ensure the buffers don't get clean up on exit. success = true; + return chunksToSend; } catch (Exception ex) @@ -988,8 +1018,12 @@ protected ArraySegment ReadAsymmetricMessage( out uint channelId, out X509Certificate2 senderCertificate, out uint requestId, - out uint sequenceNumber) + out uint sequenceNumber, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + int headerSize; using (var decoder = new BinaryDecoder(buffer, Quotas.MessageContext)) { @@ -1096,23 +1130,43 @@ protected ArraySegment ReadAsymmetricMessage( // extract signature. int signatureSize = GetAsymmetricSignatureSize(senderCertificate); - byte[] signature = new byte[signatureSize]; + signature = new byte[signatureSize]; for (int ii = 0; ii < signatureSize; ii++) { - signature[ii] = plainText.Array[ - plainText.Offset + plainText.Count - signatureSize + ii]; + signature[ii] = plainText.Array[plainText.Offset + plainText.Count - signatureSize + ii]; } - // verify the signature. - var dataToVerify = new ArraySegment( - plainText.Array, - plainText.Offset, - plainText.Count - signatureSize); + ArraySegment dataToVerify; + + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + dataToVerify.Offset, + dataToVerify.Array, + dataToVerify.Count - oscRequestSignature.Length, + oscRequestSignature.Length); + } + else + { + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize); + } + // verify the signature. if (!Verify(dataToVerify, signature, senderCertificate)) { m_logger.LogWarning("Could not verify signature on message."); + throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "Could not verify the signature on the message."); @@ -1122,6 +1176,7 @@ protected ArraySegment ReadAsymmetricMessage( int paddingCount = 0; if (SecurityMode != MessageSecurityMode.None && + SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None && receiverCertificate.GetRSAPublicKey() != null) { int paddingEnd; @@ -1194,39 +1249,7 @@ protected ArraySegment ReadAsymmetricMessage( /// protected byte[] Sign(ArraySegment dataToSign, X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic128Rsa15: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: - return Rsa_Sign( - dataToSign, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA384); - default: - return null; - } + return CryptoUtils.Sign(dataToSign, senderCertificate, SecurityPolicyUri); } /// @@ -1242,53 +1265,11 @@ protected bool Verify( byte[] signature, X509Certificate2 senderCertificate) { - // verify signature. - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - return true; - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA1, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: - return Rsa_Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA384); - default: - return false; - } + return CryptoUtils.Verify( + dataToVerify, + signature, + senderCertificate, + SecurityPolicyUri); } /// @@ -1304,48 +1285,51 @@ protected ArraySegment Encrypt( ArraySegment headerToCopy, X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: + byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); + + Array.Copy( + headerToCopy.Array, + headerToCopy.Offset, + encryptedBuffer, + 0, + headerToCopy.Count); + Array.Copy( + dataToEncrypt.Array, + dataToEncrypt.Offset, + encryptedBuffer, + headerToCopy.Count, + dataToEncrypt.Count); + + return new ArraySegment( + encryptedBuffer, + 0, + dataToEncrypt.Count + headerToCopy.Count); + } + + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA256); - case SecurityPolicies.Basic128Rsa15: + default: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return Rsa_Encrypt( dataToEncrypt, headerToCopy, receiverCertificate, RsaUtils.Padding.Pkcs1); - default: - byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); - - Array.Copy( - headerToCopy.Array, - headerToCopy.Offset, - encryptedBuffer, - 0, - headerToCopy.Count); - Array.Copy( - dataToEncrypt.Array, - dataToEncrypt.Offset, - encryptedBuffer, - headerToCopy.Count, - dataToEncrypt.Count); - - return new ArraySegment( - encryptedBuffer, - 0, - dataToEncrypt.Count + headerToCopy.Count); } } @@ -1361,6 +1345,30 @@ protected ArraySegment Decrypt( ArraySegment headerToCopy, X509Certificate2 receiverCertificate) { + if (SecurityPolicy.AsymmetricSignatureAlgorithm == AsymmetricSignatureAlgorithm.None || + SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + byte[] decryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Decrypt"); + + Array.Copy( + headerToCopy.Array, + headerToCopy.Offset, + decryptedBuffer, + 0, + headerToCopy.Count); + Array.Copy( + dataToDecrypt.Array, + dataToDecrypt.Offset, + decryptedBuffer, + headerToCopy.Count, + dataToDecrypt.Count); + + return new ArraySegment( + decryptedBuffer, + 0, + dataToDecrypt.Count + headerToCopy.Count); + } + switch (SecurityPolicyUri) { case SecurityPolicies.Basic256: @@ -1371,7 +1379,10 @@ protected ArraySegment Decrypt( headerToCopy, receiverCertificate, RsaUtils.Padding.OaepSHA1); + default: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AesGcm: + case SecurityPolicies.RSA_DH_ChaChaPoly: return Rsa_Decrypt( dataToDecrypt, headerToCopy, @@ -1383,26 +1394,6 @@ protected ArraySegment Decrypt( headerToCopy, receiverCertificate, RsaUtils.Padding.Pkcs1); - default: - byte[] decryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Decrypt"); - - Array.Copy( - headerToCopy.Array, - headerToCopy.Offset, - decryptedBuffer, - 0, - headerToCopy.Count); - Array.Copy( - dataToDecrypt.Array, - dataToDecrypt.Offset, - decryptedBuffer, - headerToCopy.Count, - dataToDecrypt.Count); - - return new ArraySegment( - decryptedBuffer, - 0, - dataToDecrypt.Count + headerToCopy.Count); } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs index bd180e54d..04d509782 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs @@ -59,12 +59,14 @@ private static byte[] Rsa_Sign( "No private key for certificate."); // create the signature. - return rsa.SignData( + var signature = rsa.SignData( dataToSign.Array, dataToSign.Offset, dataToSign.Count, algorithm, padding); + + return signature; } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 7293c335d..3445be72a 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -45,12 +45,12 @@ public partial class UaSCUaBinaryChannel /// /// Returns the current security token. /// - protected ChannelToken PreviousToken { get; private set; } + protected internal ChannelToken PreviousToken { get; private set; } /// /// Returns the renewed but not yet activated token. /// - protected ChannelToken RenewedToken { get; private set; } + protected internal ChannelToken RenewedToken { get; private set; } /// /// Called when the token changes @@ -136,11 +136,6 @@ protected void DiscardTokens() OnTokenActivated?.Invoke(null, null); } - /// - /// Indicates that an explicit signature is not present. - /// - private bool AuthenticatedEncryption { get; set; } - /// /// The byte length of the MAC (a.k.a signature) attached to each message. /// @@ -154,71 +149,19 @@ protected void DiscardTokens() /// /// Calculates the symmetric key sizes based on the current security policy. /// + /// protected void CalculateSymmetricKeySizes() { - AuthenticatedEncryption = false; - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - SymmetricSignatureSize = 20; - m_signatureKeySize = 16; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Basic256: - SymmetricSignatureSize = 20; - m_signatureKeySize = 24; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Basic256Sha256: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Aes128_Sha256_RsaOaep: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.Aes256_Sha256_RsaPss: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - SymmetricSignatureSize = 32; - m_signatureKeySize = 32; - m_encryptionKeySize = 16; - EncryptionBlockSize = 16; - break; - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - AuthenticatedEncryption = true; - SymmetricSignatureSize = 16; - m_signatureKeySize = 32; - m_encryptionKeySize = 32; - EncryptionBlockSize = 12; - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - SymmetricSignatureSize = 48; - m_signatureKeySize = 48; - m_encryptionKeySize = 32; - EncryptionBlockSize = 16; - break; - default: - SymmetricSignatureSize = 0; - m_signatureKeySize = 0; - m_encryptionKeySize = 0; - EncryptionBlockSize = 1; - break; - } + SecurityPolicyInfo info = SecurityPolicies.GetInfo(SecurityPolicyUri) + ?? throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + SecurityPolicyUri); + + SymmetricSignatureSize = info.SymmetricSignatureLength; + m_signatureKeySize = info.DerivedSignatureKeyLength; + m_encryptionKeySize = info.SymmetricEncryptionKeyLength; + EncryptionBlockSize = info.InitializationVectorLength != 0 ? info.InitializationVectorLength : 1; } private void DeriveKeysWithPSHA( @@ -228,9 +171,15 @@ private void DeriveKeysWithPSHA( ChannelToken token, bool isServer) { + using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize; - using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + if (!isServer && SecurityPolicy.SecureChannelEnhancements) + { + length += hmac.HashSize/8; + } + byte[] output = Utils.PSHA(hmac, null, seed, 0, length); byte[] signingKey = new byte[m_signatureKeySize]; @@ -246,32 +195,38 @@ private void DeriveKeysWithPSHA( token.ServerSigningKey = signingKey; token.ServerEncryptingKey = encryptingKey; token.ServerInitializationVector = iv; + token.ServerHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } else { token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.ClientHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } } private void DeriveKeysWithHKDF( - HashAlgorithmName algorithmName, - byte[] salt, ChannelToken token, - bool isServer) + byte[] salt, + bool isServer, + int length) { - int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize; + CryptoTrace.WriteLine($"DeriveKeys for {((isServer) ? "SERVER" : "CLIENT")}"); - byte[] output = m_localNonce.DeriveKey(m_remoteNonce, salt, algorithmName, length); + byte[] keyData = m_localNonce.DeriveKeyData( + token.Secret, + salt, + token.SecurityPolicy.KeyDerivationAlgorithm, + length); byte[] signingKey = new byte[m_signatureKeySize]; byte[] encryptingKey = new byte[m_encryptionKeySize]; byte[] iv = new byte[EncryptionBlockSize]; - Buffer.BlockCopy(output, 0, signingKey, 0, signingKey.Length); - Buffer.BlockCopy(output, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length); - Buffer.BlockCopy(output, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length); + Buffer.BlockCopy(keyData, 0, signingKey, 0, signingKey.Length); + Buffer.BlockCopy(keyData, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length); + Buffer.BlockCopy(keyData, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length); if (isServer) { @@ -292,6 +247,8 @@ private void DeriveKeysWithHKDF( /// protected void ComputeKeys(ChannelToken token) { + token.SecurityPolicy = SecurityPolicies.GetInfo(SecurityPolicyUri); + if (SecurityMode == MessageSecurityMode.None) { return; @@ -300,170 +257,50 @@ protected void ComputeKeys(ChannelToken token) byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; - HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; - switch (SecurityPolicyUri) + switch (token.SecurityPolicy.KeyDerivationAlgorithm) { - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: + case KeyDerivationAlgorithm.HKDFSha256: + case KeyDerivationAlgorithm.HKDFSha384: { - algorithmName = HashAlgorithmName.SHA256; - byte[] length = - SecurityMode == MessageSecurityMode.Sign - ? s_hkdfAes128SignOnlyKeyLength - : s_hkdfAes128SignAndEncryptKeyLength; - byte[] serverSalt = Utils.Append( - length, - s_hkdfServerLabel, - serverSecret, - clientSecret); - byte[] clientSalt = Utils.Append( - length, - s_hkdfClientLabel, - clientSecret, - serverSecret); - -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif + token.Secret = m_localNonce.GenerateSecret(m_remoteNonce, token.PreviousSecret); - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); - break; - } - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - { - algorithmName = HashAlgorithmName.SHA384; - byte[] length = - SecurityMode == MessageSecurityMode.Sign - ? s_hkdfAes256SignOnlyKeyLength - : s_hkdfAes256SignAndEncryptKeyLength; - byte[] serverSalt = Utils.Append( - length, - s_hkdfServerLabel, - serverSecret, - clientSecret); byte[] clientSalt = Utils.Append( - length, + BitConverter.GetBytes((ushort)token.SecurityPolicy.ClientKeyDataLength), s_hkdfClientLabel, clientSecret, serverSecret); -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif + DeriveKeysWithHKDF(token, clientSalt, false, token.SecurityPolicy.ClientKeyDataLength); - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); - break; - } - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - algorithmName = HashAlgorithmName.SHA256; - byte[] length = s_hkdfChaCha20Poly1305KeyLength; byte[] serverSalt = Utils.Append( - length, + BitConverter.GetBytes((ushort)token.SecurityPolicy.ServerKeyDataLength), s_hkdfServerLabel, serverSecret, clientSecret); - byte[] clientSalt = Utils.Append( - length, - s_hkdfClientLabel, - clientSecret, - serverSecret); - -#if DEBUG - m_logger.LogDebug("Length={Length}", Utils.ToHexString(length)); - m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret)); - m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt)); - m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt)); -#endif - DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); - DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); + DeriveKeysWithHKDF(token, serverSalt, true, token.SecurityPolicy.ServerKeyDataLength); + + CryptoTrace.Start(ConsoleColor.Green, $"ComputeKeys (TokenId={token.TokenId})"); + CryptoTrace.WriteLine($"IKM={CryptoTrace.KeyToString(token.Secret)}"); + CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverSecret)}"); + CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientSecret)}"); + CryptoTrace.WriteLine($"ServerSalt={CryptoTrace.KeyToString(serverSalt)}"); + CryptoTrace.WriteLine($"ServerEncryptingKey={CryptoTrace.KeyToString(token.ServerEncryptingKey)}"); + CryptoTrace.WriteLine($"ServerInitializationVector={CryptoTrace.KeyToString(token.ServerInitializationVector)}"); + CryptoTrace.WriteLine($"ClientEncryptingKey={CryptoTrace.KeyToString(token.ClientEncryptingKey)}"); + CryptoTrace.WriteLine($"ClientInitializationVector={CryptoTrace.KeyToString(token.ClientInitializationVector)}"); + CryptoTrace.Finish("ComputeKeys"); break; } - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - algorithmName = HashAlgorithmName.SHA1; - goto default; + default: + case KeyDerivationAlgorithm.PSha1: + case KeyDerivationAlgorithm.PSha256: + HashAlgorithmName algorithmName = token.SecurityPolicy.GetKeyDerivationHashAlgorithmName(); DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); break; } - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - // create encryptors. - var aesCbcEncryptorProvider = Aes.Create(); - aesCbcEncryptorProvider.Mode = CipherMode.CBC; - aesCbcEncryptorProvider.Padding = PaddingMode.None; - aesCbcEncryptorProvider.Key = token.ClientEncryptingKey; - aesCbcEncryptorProvider.IV = token.ClientInitializationVector; - token.ClientEncryptor = aesCbcEncryptorProvider; - - var aesCbcDecryptorProvider = Aes.Create(); - aesCbcDecryptorProvider.Mode = CipherMode.CBC; - aesCbcDecryptorProvider.Padding = PaddingMode.None; - aesCbcDecryptorProvider.Key = token.ServerEncryptingKey; - aesCbcDecryptorProvider.IV = token.ServerInitializationVector; - token.ServerEncryptor = aesCbcDecryptorProvider; - break; - default: - // TODO: is this even legal or should we throw? What are the implications - token.ClientEncryptor = null; - token.ServerEncryptor = null; - break; - } - - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: -#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms - token.ServerHmac = new HMACSHA1(token.ServerSigningKey); - token.ClientHmac = new HMACSHA1(token.ClientSigningKey); -#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms - break; - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - token.ServerHmac = new HMACSHA256(token.ServerSigningKey); - token.ClientHmac = new HMACSHA256(token.ClientSigningKey); - break; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - token.ServerHmac = new HMACSHA384(token.ServerSigningKey); - token.ClientHmac = new HMACSHA384(token.ClientSigningKey); - break; - default: - // TODO: is this even legal or should we throw? What are the implications - token.ServerHmac = null; - token.ClientHmac = null; - break; - } } /// @@ -487,20 +324,21 @@ protected BufferCollection WriteSymmetricMessage( int maxCipherTextSize = SendBufferSize - TcpMessageLimits.SymmetricHeaderSize; int maxCipherBlocks = maxCipherTextSize / EncryptionBlockSize; int maxPlainTextSize = maxCipherBlocks * EncryptionBlockSize; + + int paddingCountSize = + (SecurityMode != MessageSecurityMode.SignAndEncrypt || token.SecurityPolicy.NoSymmetricEncryptionPadding) + ? 0 + : 1; + int maxPayloadSize = maxPlainTextSize - SymmetricSignatureSize - - 1 - - TcpMessageLimits.SequenceHeaderSize; + TcpMessageLimits.SequenceHeaderSize - + paddingCountSize; + const int headerSize = TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; - // no padding byte. - if (AuthenticatedEncryption) - { - maxPayloadSize++; - } - // write the body to stream. var ostrm = new ArraySegmentStream( BufferManager, @@ -540,14 +378,12 @@ protected BufferCollection WriteSymmetricMessage( byte[] buffer = BufferManager.TakeBuffer( SendBufferSize, "WriteSymmetricMessage"); + chunksToProcess.Add(new ArraySegment(buffer, 0, 0)); } var 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++) @@ -563,6 +399,7 @@ protected BufferCollection WriteSymmetricMessage( var strm = new MemoryStream(chunkToProcess.Array, 0, SendBufferSize); using var encoder = new BinaryEncoder(strm, Quotas.MessageContext, false); + // check if the message needs to be aborted. if (MessageLimitsExceeded( isRequest, @@ -593,6 +430,7 @@ protected BufferCollection WriteSymmetricMessage( limitsExceeded = true; } + // check if the message is complete. else if (ii == chunksToProcess.Count - 1) { @@ -608,28 +446,22 @@ protected BufferCollection WriteSymmetricMessage( count += TcpMessageLimits.SequenceHeaderSize; count += chunkToProcess.Count; - count += SymmetricSignatureSize; + count += paddingCountSize; - // calculate the padding. int padding = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) + if (paddingCountSize > 0) { - // reserve one byte for the padding size. - count++; + padding = EncryptionBlockSize - (count % EncryptionBlockSize); - // use padding as helper to calc the real padding - padding = count % EncryptionBlockSize; - if (padding != 0) + if (padding < EncryptionBlockSize) { - padding = EncryptionBlockSize - padding; + count += padding; } - - count += padding; } count += TcpMessageLimits.SymmetricHeaderSize; + count += SymmetricSignatureSize; encoder.WriteUInt32(null, (uint)count); encoder.WriteUInt32(null, ChannelId); @@ -637,7 +469,6 @@ protected BufferCollection WriteSymmetricMessage( uint sequenceNumber = GetNewSequenceNumber(); encoder.WriteUInt32(null, sequenceNumber); - encoder.WriteUInt32(null, requestId); // skip body. @@ -646,63 +477,27 @@ protected BufferCollection WriteSymmetricMessage( // update message size count. messageSize += chunkToProcess.Count; - // write padding. - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) - { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - if (padding > 1) - { - Span buffer = paddingBuffer[..(padding + 1)]; - buffer.Fill((byte)padding); - encoder.WriteRawBytes(buffer); - } - else -#endif - { - for (int jj = 0; jj <= padding; jj++) - { - encoder.WriteByte(null, (byte)padding); - } - } - } + ArraySegment dataToSend; - // calculate and write signature. if (SecurityMode != MessageSecurityMode.None) { - if (AuthenticatedEncryption) - { - strm.Seek(SymmetricSignatureSize, SeekOrigin.Current); - } - else - { - byte[] signature = Sign( - token, - new ArraySegment(chunkToProcess.Array, 0, encoder.Position), - isRequest); - - if (signature != null) - { - encoder.WriteRawBytes(signature, 0, signature.Length); - } - } - } - - if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && - !AuthenticatedEncryption) || - (SecurityMode != MessageSecurityMode.None && AuthenticatedEncryption)) - { - // encrypt the data. - var dataToEncrypt = new ArraySegment( + dataToSend = new ArraySegment( chunkToProcess.Array, TcpMessageLimits.SymmetricHeaderSize, encoder.Position - TcpMessageLimits.SymmetricHeaderSize); - Encrypt(token, dataToEncrypt, isRequest); + + dataToSend = EncryptAndSign(token, dataToSend, isRequest); + } + else + { + dataToSend = new ArraySegment( + chunkToProcess.Array, + 0, + encoder.Position); } // add the header into chunk. - chunksToSend.Add( - new ArraySegment(chunkToProcess.Array, 0, encoder.Position)); + chunksToSend.Add(dataToSend); } // ensure the buffers don't get cleaned up on exit. @@ -717,6 +512,22 @@ protected BufferCollection WriteSymmetricMessage( } } } + private ArraySegment EncryptAndSign( + ChannelToken token, + ArraySegment dataToEncrypt, + bool useClientKeys) + { + return CryptoUtils.SymmetricEncryptAndSign( + dataToEncrypt, + token.SecurityPolicy, + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + useClientKeys ? token.ClientHmac : token.ServerHmac, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)(m_localSequenceNumber - 1)); // already incremented to create this message. need the last one sent. + } /// /// Decrypts and verifies a message chunk. @@ -750,6 +561,7 @@ protected ArraySegment ReadSymmetricMessage( { ActivateToken(RenewedToken); } + // check if activation of the new token should be forced. else if (RenewedToken != null && CurrentToken.ActivationRequired) { @@ -803,716 +615,55 @@ protected ArraySegment ReadSymmetricMessage( int headerSize = decoder.Position; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // decrypt the message. - Decrypt( - token, - new ArraySegment( - buffer.Array, - buffer.Offset + headerSize, - buffer.Count - headerSize), - isRequest); - } + var dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset, + buffer.Count); - int paddingCount = 0; if (SecurityMode != MessageSecurityMode.None) { - int signatureStart = buffer.Offset + buffer.Count - SymmetricSignatureSize; - - // extract signature. - byte[] signature = new byte[SymmetricSignatureSize]; - 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)) - { - m_logger.LogError("ChannelId {Id}: Could not verify signature on message.", Id); - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the signature on the message."); - } - - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // verify padding. - int paddingStart = signatureStart - 1; - paddingCount = buffer.Array[paddingStart]; + dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset + headerSize, + buffer.Count - headerSize); - for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) - { - if (buffer.Array[ii] != paddingCount) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the padding in the message."); - } - } - - // add byte for size. - paddingCount++; - } + dataToProcess = DecryptAndVerify( + token, + dataToProcess, + isRequest); } // extract request id and sequence number. sequenceNumber = decoder.ReadUInt32(null); requestId = decoder.ReadUInt32(null); - // return an the data contained in the message. - int startOfBody = - buffer.Offset + - TcpMessageLimits.SymmetricHeaderSize + - TcpMessageLimits.SequenceHeaderSize; - int sizeOfBody = - buffer.Count - - TcpMessageLimits.SymmetricHeaderSize - - TcpMessageLimits.SequenceHeaderSize - - paddingCount - - SymmetricSignatureSize; - - return new ArraySegment(buffer.Array, startOfBody, sizeOfBody); - } - - /// - /// Returns the symmetric signature for the data. - /// - protected byte[] Sign(ChannelToken token, ArraySegment dataToSign, bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_nistP256: - return SymmetricSign(token, dataToSign, useClientKeys); - default: - return null; - } - } - - /// - /// Returns the symmetric signature for the data. - /// - protected bool Verify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - // verify signature. - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - return true; - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - return SymmetricVerify(token, signature, dataToVerify, useClientKeys); - default: - return false; - } - } - - /// - /// Decrypts the data in a buffer using symmetric encryption. - /// - /// - protected void Encrypt( - ChannelToken token, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - break; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - SymmetricEncrypt(token, dataToEncrypt, useClientKeys); - break; - -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // narowing conversion can safely be done on m_localSequenceNumber - SymmetricEncryptWithChaCha20Poly1305( - token, - (uint)m_localSequenceNumber, - dataToEncrypt, - useClientKeys); - break; - } - // narowing conversion can safely be done on m_localSequenceNumber - SymmetricSignWithPoly1305(token, (uint)m_localSequenceNumber, dataToEncrypt, useClientKeys); - break; - } -#endif - default: - throw new NotSupportedException(SecurityPolicyUri); - } - } - - /// - /// Decrypts the data in a buffer using symmetric encryption. - /// - /// - protected void Decrypt( - ChannelToken token, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - break; - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - SymmetricDecrypt(token, dataToDecrypt, useClientKeys); - break; - -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - { - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - SymmetricDecryptWithChaCha20Poly1305( - token, - m_remoteSequenceNumber, - dataToDecrypt, - useClientKeys); - break; - } - - SymmetricVerifyWithPoly1305(token, m_remoteSequenceNumber, dataToDecrypt, useClientKeys); - break; - } -#endif - - default: - throw new NotSupportedException(SecurityPolicyUri); - } - } - -#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 HMAC. - /// - private static byte[] SymmetricSign( - ChannelToken token, - ArraySegment dataToSign, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - // compute hash. - var istrm = new MemoryStream( - dataToSign.Array, - dataToSign.Offset, - dataToSign.Count, - false); - byte[] signature = hmac.ComputeHash(istrm); - istrm.Dispose(); - - // return signature. - return signature; - } -#endif + headerSize += TcpMessageLimits.SequenceHeaderSize; -#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); - System.Diagnostics.Debug.Assert(bytesWritten == hashSizeInBytes); - // compare signatures. - if (!result || !computedSignature.SequenceEqual(signature)) - { - string expectedSignature = Utils.ToHexString(computedSignature.ToArray()); - string messageType = Encoding.UTF8.GetString(dataToVerify[..4]); - int messageLength = BitConverter.ToInt32(dataToVerify[4..]); - string actualSignature = Utils.ToHexString(signature); -#else - /// - /// Verifies a HMAC for a message. - /// - private bool SymmetricVerify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - // get HMAC object. - HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac; - - var istrm = new MemoryStream( - dataToVerify.Array, - dataToVerify.Offset, - dataToVerify.Count, - false); - byte[] computedSignature = hmac.ComputeHash(istrm); - istrm.Dispose(); - // compare signatures. - if (!Utils.IsEqual(computedSignature, signature)) - { - 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 - m_logger.LogError( - "Channel{Id}: Could not validate signature. ChannelId={ChannelId}, TokenId={TokenId}, MessageType={MessageType}, Length={Length} ExpectedSignature={ExpectedSignature} ActualSignature={ActualSignature}", - Id, - token.ChannelId, - token.TokenId, - messageType, - messageLength, - expectedSignature, - actualSignature); - - return false; - } - - return true; - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - /// - private static void SymmetricEncrypt( - ChannelToken token, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - SymmetricAlgorithm encryptingKey = - (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - - using ICryptoTransform encryptor = encryptingKey.CreateEncryptor(); - byte[] blockToEncrypt = dataToEncrypt.Array; - - int start = dataToEncrypt.Offset; - int count = dataToEncrypt.Count; - - if (count % encryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - encryptor.TransformBlock(blockToEncrypt, start, count, blockToEncrypt, start); + // return only the data contained in the message. + return new ArraySegment( + dataToProcess.Array, + dataToProcess.Offset + headerSize, + dataToProcess.Count - headerSize); } - /// - /// Decrypts a message using a symmetric algorithm. - /// - /// - private static void SymmetricDecrypt( + private ArraySegment DecryptAndVerify( ChannelToken token, ArraySegment dataToDecrypt, bool useClientKeys) { - // get the decrypting key. - SymmetricAlgorithm decryptingKey = - (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor) - ?? throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - - using ICryptoTransform decryptor = decryptingKey.CreateDecryptor(); - byte[] blockToDecrypt = dataToDecrypt.Array; - - int start = dataToDecrypt.Offset; - int count = dataToDecrypt.Count; - - if (count % decryptor.InputBlockSize != 0) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Input data is not an even number of encryption blocks."); - } - - decryptor.TransformBlock(blockToDecrypt, start, count, blockToDecrypt, start); - } - -#if CURVE25519 - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricEncryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"EncryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"EncryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(); - encryptor.Init(true, parameters); - encryptor.ProcessAadBytes(plaintext, 0, headerSize); - - byte[] ciphertext = new byte[encryptor.GetOutputSize(plainTextLength - headerSize) + headerSize]; - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, headerSize); - int length = encryptor.ProcessBytes( - plaintext, - headerSize, - plainTextLength - headerSize, - ciphertext, - headerSize); - length += encryptor.DoFinal(ciphertext, length + headerSize); - - if (ciphertext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Cipher text not the expected size. [{ciphertext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(ciphertext, 0, plaintext, 0, plainTextLength + signatureLength); - - // byte[] mac = new byte[16]; - // Buffer.BlockCopy(plaintext, plainTextLength, mac, 0, signatureLength); - // Utils.Trace($"EncryptMAC1={Utils.ToHexString(encryptor.GetMac())}"); - // Utils.Trace($"EncryptMAC2={Utils.ToHexString(mac)}"); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricSignWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - int length = poly.DoFinal(plaintext, plainTextLength); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - } - - /// - /// Decrypts a message using a symmetric algorithm. - /// - private static void SymmetricDecryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"DecryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"DecryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"DecryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var ciphertext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int cipherTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{cipherTextLength}|{signatureLength}|[{cipherTextLength + signatureLength}]"); - - byte[] mac = new byte[16]; - Buffer.BlockCopy(ciphertext, cipherTextLength, mac, 0, signatureLength); - // Utils.Trace($"DecryptMAC={Utils.ToHexString(mac)}"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 decryptor = new ChaCha20Poly1305(); - decryptor.Init(false, parameters); - decryptor.ProcessAadBytes(ciphertext, 0, headerSize); - - var plaintext = new byte[ - decryptor.GetOutputSize(cipherTextLength + signatureLength - headerSize) + headerSize - ]; - Buffer.BlockCopy(ciphertext, headerSize, plaintext, 0, headerSize); - - int length = decryptor.ProcessBytes( - ciphertext, - headerSize, - cipherTextLength + signatureLength - headerSize, - plaintext, - headerSize); - length += decryptor.DoFinal(plaintext, length + headerSize); - - if (plaintext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Plain text not the expected size. [{plaintext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, cipherTextLength); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricVerifyWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - int signatureLength = 16; - - var plaintext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int plainTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - - byte[] mac = new byte[poly.GetMacSize()]; - int length = poly.DoFinal(mac, 0); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - - for (int ii = 0; ii < mac.Length; ii++) - { - if (mac[ii] != plaintext[plainTextLength + ii]) - { - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, $"Invaid MAC on data."); - } - } - } - - private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) - { - iv[0] ^= (byte)((token.TokenId & 0x000000FF)); - iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); - iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); - iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); - iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); - iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); - iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); - iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + return CryptoUtils.SymmetricDecryptAndVerify( + dataToDecrypt, + token.SecurityPolicy, + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)m_remoteSequenceNumber); } -#endif private static readonly byte[] s_hkdfClientLabel = Encoding.UTF8.GetBytes("opcua-client"); private static readonly byte[] s_hkdfServerLabel = Encoding.UTF8.GetBytes("opcua-server"); - private static readonly byte[] s_hkdfAes128SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)32); - private static readonly byte[] s_hkdfAes256SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)48); - private static readonly byte[] s_hkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)64); - private static readonly byte[] s_hkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)96); - private static readonly byte[] s_hkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes( - (ushort)76); private int m_signatureKeySize; private int m_encryptionKeySize; } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 42b65e6ed..0187509ff 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -255,6 +255,15 @@ protected virtual void Dispose(bool disposing) /// public string GlobalChannelId { get; private set; } + /// + internal byte[] ChannelThumbprint { get; set; } + + /// + public byte[] ClientChannelCertificate { get; protected set; } + + /// + public byte[] ServerChannelCertificate { get; protected set; } + /// /// Raised when the state of the channel changes. /// @@ -288,7 +297,7 @@ protected void ChannelStateChanged(TcpChannelState state, ServiceResult reason) /// protected uint GetNewSequenceNumber() { - bool isLegacy = !EccUtils.IsEccPolicy(SecurityPolicyUri); + bool isLegacy = SecurityPolicy.LegacySequenceNumbers; long newSeqNumber = Interlocked.Increment(ref m_sequenceNumber); bool maxValueOverflow = isLegacy @@ -337,8 +346,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // Accept the first sequence number depending on security policy if (m_firstReceivedSequenceNumber && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_remoteSequenceNumber = sequenceNumber; m_firstReceivedSequenceNumber = false; @@ -359,8 +368,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // only one rollover per token is allowed and with valid values depending on security policy if (!m_sequenceRollover && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_sequenceRollover = true; m_remoteSequenceNumber = sequenceNumber; @@ -634,6 +643,8 @@ protected void BeginWriteMessage(BufferCollection buffers, object state) try { + // m_logger.LogWarning("OUT:{Id}", TcpMessageType.GetTypeAndSize(buffers[0])); + Interlocked.Increment(ref m_activeWriteRequests); args.BufferList = buffers; args.Completed += OnWriteComplete; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 7005543ba..fcbbcea5e 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -35,6 +35,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -552,6 +553,11 @@ private void SendOpenSecureChannelRequest(bool renew) ChannelToken token = CreateToken(); token.ClientNonce = CreateNonce(ClientCertificate); + if (renew) + { + token.PreviousSecret = CurrentToken?.Secret; + } + // construct the request. var request = new OpenSecureChannelRequest(); request.RequestHeader.Timestamp = DateTime.UtcNow; @@ -566,6 +572,12 @@ private void SendOpenSecureChannelRequest(bool renew) // encode the request. byte[] buffer = BinaryEncoder.EncodeMessage(request, Quotas.MessageContext); + ClientChannelCertificate = ClientCertificate?.RawData; + ServerChannelCertificate = ServerCertificate?.RawData; + + m_oscRequestSignature = null; + byte[] signature; + // write the asymmetric message. BufferCollection? chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -573,7 +585,18 @@ private void SendOpenSecureChannelRequest(bool renew) ClientCertificate, ClientCertificateChain, ServerCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; + + CryptoTrace.Start(ConsoleColor.Magenta, $"SendOpenSecureChannelRequest ({(renew ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.Finish("SendOpenSecureChannelRequest"); // save token. m_requestedToken = token; @@ -628,13 +651,32 @@ private bool ProcessOpenSecureChannelResponse( uint sequenceNumber; try { + byte[] signature; + messageBody = ReadAsymmetricMessage( messageChunk, ClientCertificate, out channelId, out serverCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + (State == TcpChannelState.Opening) ? m_oscRequestSignature : null, + out signature); + + if (State == TcpChannelState.Opening) + { + ChannelThumbprint = signature; + } + + CryptoTrace.Start(ConsoleColor.Magenta, $"ProcessOpenSecureChannelResponse ({(State != TcpChannelState.Opening ? "RENEW" : "OPEN")})"); + CryptoTrace.WriteLine($"messageBody={CryptoTrace.KeyToString(messageBody)}"); + CryptoTrace.WriteLine($"messageBody.Offset={messageBody.Offset}"); + CryptoTrace.WriteLine($"ClientCertificate={ClientCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"ServerCertificate={ServerCertificate?.Thumbprint}"); + CryptoTrace.WriteLine($"RequestSignature={CryptoTrace.KeyToString(m_oscRequestSignature)}"); + CryptoTrace.WriteLine($"ResponseSignature={CryptoTrace.KeyToString(signature)}"); + CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); + CryptoTrace.Finish("ProcessOpenSecureChannelResponse"); } catch (Exception e) { @@ -1765,5 +1807,6 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message private List? m_queuedOperations; private readonly ILogger m_logger; private readonly ITelemetryContext m_telemetry; + private byte[]? m_oscRequestSignature; } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index c3658c723..639cd07c7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -121,7 +121,16 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => m_channel?.CurrentToken; + public ChannelToken CurrentToken => m_channel?.CurrentToken ?? new(); + + /// + public byte[] ChannelThumbprint => m_channel?.ChannelThumbprint ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; /// public int OperationTimeout { get; set; } @@ -445,6 +454,8 @@ private UaSCUaBinaryClientChannel CreateChannel( } } + var id = Guid.NewGuid().ToString(); + // create the channel. var channel = new UaSCUaBinaryClientChannel( Guid.NewGuid().ToString(), diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index 23ce1b64d..7de67cf0f 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -116,6 +116,21 @@ public interface ITransportChannel : IDisposable /// EndpointConfiguration EndpointConfiguration { get; } + /// + /// The unique identifier for the secure channel. + /// + byte[] ChannelThumbprint { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + byte[] ServerChannelCertificate { get; } + /// /// Gets the context used when serializing messages exchanged /// via the channel. Throws if the channel is not yet opened. diff --git a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs index f0c728beb..b6787e56b 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs @@ -41,6 +41,10 @@ namespace Opc.Ua /// internal sealed class NullChannel : ITransportChannel, ISecureChannel { + /// + public ChannelToken CurrentToken + => throw Unexpected(nameof(CurrentToken)); + /// public TransportChannelFeatures SupportedFeatures => throw Unexpected(nameof(SupportedFeatures)); @@ -58,8 +62,16 @@ public IServiceMessageContext MessageContext => throw Unexpected(nameof(MessageContext)); /// - public ChannelToken CurrentToken - => throw Unexpected(nameof(CurrentToken)); + public byte[] ChannelThumbprint + => throw Unexpected(nameof(ChannelThumbprint)); + + /// + public byte[] ClientChannelCertificate + => throw Unexpected(nameof(ClientChannelCertificate)); + + /// + public byte[] ServerChannelCertificate + => throw Unexpected(nameof(ServerChannelCertificate)); /// public int OperationTimeout { get; set; } diff --git a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs index 3e452d285..0847ce8fa 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs @@ -75,7 +75,9 @@ public override void Encrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) { byte[] dataToEncrypt = Utils.Append(DecryptedPassword, receiverNonce); @@ -90,7 +92,8 @@ public override void Encrypt( m_encryptionAlgorithm = encryptedData.Algorithm; Array.Clear(dataToEncrypt, 0, dataToEncrypt.Length); } - // handle ECC encryption. + + // handle ECC and RSADH encryption. else { // check if the complete chain is included in the sender issuers. @@ -115,7 +118,7 @@ public override void Encrypt( receiverCertificate, receiverEphemeralKey, senderCertificate, - Nonce.CreateNonce(securityPolicyUri), + Nonce.CreateNonce(securityPolicy), null, doNotEncodeSenderCertificate); @@ -155,7 +158,9 @@ public override void Decrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + if (securityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None) { var encryptedData = new EncryptedData { @@ -199,7 +204,8 @@ public override void Decrypt( Array.Copy(decryptedPassword, DecryptedPassword, startOfNonce); Array.Clear(decryptedPassword, 0, decryptedPassword.Length); } - // handle ECC encryption. + + // handle ECC and RSADH encryption. else { var secret = new EncryptedSecret( diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs index 8510b4fc8..c2d375cbd 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs @@ -67,9 +67,11 @@ public override SignatureData Sign( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_certificateData); - SignatureData signatureData = SecurityPolicies.Sign( + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + SignatureData signatureData = SecurityPolicies.CreateSignatureData( + info, certificate, - securityPolicyUri, dataToSign); m_certificateData = certificate.RawData; @@ -92,11 +94,13 @@ public override bool Verify( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_certificateData); - bool valid = SecurityPolicies.Verify( + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + bool valid = SecurityPolicies.VerifySignatureData( + signatureData, + info, certificate, - securityPolicyUri, - dataToVerify, - signatureData); + dataToVerify); m_certificateData = certificate.RawData; diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index e9c72e64d..16191033b 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -672,7 +672,14 @@ public static IPAddress[] GetHostAddresses(string hostNameOrAddress) /// If the platform returns a FQDN, only the host name is returned. public static string GetHostName() { - return Dns.GetHostName().Split('.')[0].ToLowerInvariant(); + var hostName = Dns.GetHostName(); + // If platform returns an IPv4 or IPv6 address return it as is + if (IPAddress.TryParse(hostName, out _)) + { + return hostName; + } + + return hostName.Split('.')[0].ToLowerInvariant(); } /// diff --git a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs index 771881e54..b98d5e969 100644 --- a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs +++ b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs @@ -141,7 +141,7 @@ public void Dispose() [Conditional("DEBUG")] private static void DebugCheck() { - Debug.Fail("Using a NullLogger"); + //Debug.Fail("Using a NullLogger"); } } diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 9e2e4a25f..0dfa97776 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -245,7 +245,8 @@ public IEncodeable DecodeMessage(Type expectedType) // lookup message type. Type actualType = Context.Factory.GetSystemType(absoluteId) - ?? throw ServiceResultException.Create( + ?? + throw ServiceResultException.Create( StatusCodes.BadDecodingError, "Cannot decode message with type id: {0}.", absoluteId); diff --git a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs index 9517ceb48..5a0ad1414 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs @@ -58,7 +58,8 @@ public void ValidateCreateNoncePolicyLength(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonce = Ua.Nonce.CreateNonce(securityPolicyUri); @@ -84,10 +85,11 @@ public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonceByLen = Ua.Nonce.CreateNonce(securityPolicyUri); - var nonceByData = Ua.Nonce.CreateNonce(securityPolicyUri, nonceByLen.Data); + var nonceByData = Ua.Nonce.CreateNonce(info, nonceByLen.Data); Assert.IsNotNull(nonceByData); Assert.IsNotNull(nonceByData.Data); @@ -113,11 +115,12 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; byte[] randomValue = Ua.Nonce.CreateRandomNonceData(nonceLength); - if (securityPolicyUri.Contains("ECC_", StringComparison.Ordinal)) + if (info.CertificateKeyFamily == CertificateKeyFamily.ECC) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && ( @@ -128,11 +131,11 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( .Ignore("No exception is thrown on OSX with NIST curves"); } NUnit.Framework.Assert.Throws(() => - Ua.Nonce.CreateNonce(securityPolicyUri, randomValue)); + Ua.Nonce.CreateNonce(info, randomValue)); } else { - var rsaNonce = Ua.Nonce.CreateNonce(securityPolicyUri, randomValue); + var rsaNonce = Ua.Nonce.CreateNonce(info, randomValue); Assert.AreEqual(rsaNonce.Data, randomValue); } } diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index a4e3ee280..deeef0b05 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -764,7 +764,7 @@ public async Task UpdateCertificateSelfSignedAsync(string keyFormat) X509Certificate2 newCert; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { @@ -1286,7 +1286,7 @@ private async Task CreateCATestCertsAsync(string tempStorePath, ITelemetryContex var certificateStoreIdentifier = new CertificateStoreIdentifier(tempStorePath, false); Assert.IsTrue(EraseStore(certificateStoreIdentifier, telemetry)); const string subjectName = "CN=CA Test Cert, O=OPC Foundation"; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index d04e798ce..1f668787e 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -111,7 +111,7 @@ public CRLTests(string certificateTypeString, NodeId certificateType) [OneTimeSetUp] protected void OneTimeSetUp() { - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs index 8109e9663..19626f70b 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs @@ -293,6 +293,8 @@ public void GetCertificationRequestInfoReturnsValidData() Assert.Greater(requestInfo.Length, 0); } + static readonly string[] kHosts = new[] { "localhost" }; + /// /// Test parsing multiple CSRs in sequence. /// @@ -310,7 +312,7 @@ public void ParseMultipleCsrsInSequence() using X509Certificate2 certificate = CertificateBuilder.Create(subject) .SetNotBefore(DateTime.UtcNow.AddDays(-1)) .SetLifeTime(TimeSpan.FromDays(30)) - .AddExtension(new X509SubjectAltNameExtension(applicationUri, new[] { "localhost" })) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, kHosts)) .CreateForRSA(); byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs index d453ac124..ab28ead41 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs @@ -93,7 +93,12 @@ public static class ServerFixtureUtils // set security context var secureChannelContext - = new SecureChannelContext(sessionName, endpoint, RequestEncoding.Binary); + = new SecureChannelContext( + sessionName, + endpoint, RequestEncoding.Binary, + null, + null, + null); var requestHeader = new RequestHeader(); // Create session diff --git a/UA Reference.sln b/UA Reference.sln index 4ef54f8e4..e38b766c7 100644 --- a/UA Reference.sln +++ b/UA Reference.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Server", "Libraries\Opc.Ua.Server\Opc.Ua.Server.csproj", "{6C449A1E-244E-4515-9949-A7E22012537C}" EndProject