diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 12f7449ad..23f90ab7e 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -81,7 +81,7 @@ jobs: arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' - task: DotNetCoreCLI@2 displayName: Test ${{ parameters.configuration }} - timeoutInMinutes: 30 + timeoutInMinutes: 45 inputs: command: test projects: $(file) diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index 1e377d488..1bf7353de 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -170,7 +170,7 @@ public static async Task Main(string[] args) } // check the application certificate. - bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, minimumKeySize: 0).ConfigureAwait(false); + bool haveAppCertificate = await application.CheckApplicationInstanceCertificates(false).ConfigureAwait(false); if (!haveAppCertificate) { throw new ErrorExitException("Application instance certificate invalid!", ExitCode.ErrorCertificate); diff --git a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml index b82feaf0f..cc3acbaa9 100644 --- a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml +++ b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml @@ -12,13 +12,44 @@ - - Directory - %LocalApplicationData%/OPC Foundation/pki/own - CN=Console Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost - - - + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost + RsaSha256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference client, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP384 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP256r1 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP384r1 + + + + Directory %LocalApplicationData%/OPC Foundation/pki/issuer diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 59d17b590..c6e139f03 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -4,19 +4,52 @@ xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd" xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" > + Quickstart Reference Server urn:localhost:UA:Quickstarts:ReferenceServer uri:opcfoundation.org:Quickstarts:ReferenceServer Server_0 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + RsaSha256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP384 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP256r1 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP384r1 + + - - - Directory - %LocalApplicationData%/OPC Foundation/pki/own - CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost - + @@ -97,14 +130,11 @@ --> + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 - - None_1 - http://opcfoundation.org/UA/SecurityPolicy#None - Sign_2 @@ -113,6 +143,42 @@ SignAndEncrypt_3 + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + + None_1 + http://opcfoundation.org/UA/SecurityPolicy#None + @@ -234,7 +308,7 @@ 0 false - + + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + + .... + +``` + +## Previous Server application configuration supports only RSA certificates + +For Server applications the configuration of the RSA certificate involves also specifying the SecurityPolicies to be supported by the server: + +```xml + + + + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + + None_1 + http://opcfoundation.org/UA/SecurityPolicy#None + + + Sign_2 + + + + SignAndEncrypt_3 + + + .... + +``` + +## New Client and Server application configuration support both RSA and ECC certificates + +With the newly introduced support for ECC certificates, the configuration of the server application has been extended to support both RSA and ECC certificates. The XML tag under which both the RSA and ECC certificates are configured is ``. + +The `` tag is still used for the RSA certificate configuration in backward compatibility mode, meaning that old server configurations are still supported, but they cannot simultaneously coexist. + +The new configuration of the server application is described below: + +```xml + + + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + RsaSha256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP256 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + NistP384 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP256r1 + + + + Directory + %LocalApplicationData%/OPC Foundation/pki/own + CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost + BrainpoolP384r1 + + +.... + +``` + +This layout of the configuration file allows the server to support both RSA and ECC certificates and allows the server to generate certificates of different types. The `` tag is used to specify the type of the certificate. + +The supported types are: + - `RsaSha256` for RSA certificates + - `NistP256` for ECC certificates with NIST P256 curve + - `NistP384` for ECC certificates with NIST P384 curve + - `BrainpoolP256r1` for ECC certificates with Brainpool P256r1 curve + - `BrainpoolP384r1` for ECC certificates with Brainpool P384r1 curve + +Additionally, this layout enables the user to select a specific `` for each certificate. This is useful when the user wants to generate certificates with different ``s. + + +## New Server application configuration related to ECC certificates + +The Server applications can configure the supported SecurityPolicies in the same way as before, but now the SecurityPolicies can be configured for each ECC specific SecurityPolicyUri which is to be supported by the server: + +```xml + + + + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + + Sign_2 + + + + SignAndEncrypt_3 + + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + + .... + +``` + +With the introduction of ECC certificates, the `` section of the configuration file has been extended to support ECC specific SecurityPolicies. The ECC specific SecurityPolicies are the following: + + - `http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256` + - `http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384` + - `http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1` + - `http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1` + +If ECC specific SecurityPolicies are specified in the `` section of the configuration file, then the server will need to support the corresponding ECC certificates configured in the `` section of the configuration file with the corresponding ``s, not having them configured will result in the server not being able to start. + +Since the UserIdentityToken is also encrypted using the RSA or ECC certificate depending on the active security policy or the specified `` of the ``, servers which intend to explicitly state which ECC encryption (if any) of the UserIdentity tokens is supported, should specify the supported `` in the `` section of the configuration file (as it has always been). The supported ``s are the same as the ones specified in the `` section of the configuration file. + +The following example shows how the server can specify the supported ``s for the UserIdentityTokens: + +```xml + + + + + Anonymous_0 + + + + + UserName_1 + + + + + + + UserName_1 + + + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + +``` + +The `` tag can be ommited in the `` tag, in which case the active security policy is used for the encryption of the UserIdentityToken. That implies that there is a dependency between the `` and the `` sections of the configuration file, meaning that the ``s specified in the `` section should be a subset of the ``s specified in the `` section of the configuration file. + +A situation in which a `` is specified in the `` section of the configuration file, but it is not specified in the `` section of the configuration file is logically incorrect and produces an invalid configuration. + + +## "Old" Client and Server application configuration format VS "New" Client and Server application configuration format + +Client and server applications which use the "old" configuration, will continue to work as before, meaning that the server and client certificates will be RSA certificates and the `` tag will be used to configure the RSA certificate. Server applications will have no ECC security policies specified in the `` section of the configuration file. +You should be aware that such applications will not be able to support ECC certificates therefore they will not be able to communicate with clients and servers which use ECC certificates. + +Client and server applications which use the "new" configuration, are be able to support both RSA and ECC certificates. +Server and Client applications are be able to support both RSA and ECC security policies by using the `` tag to configure the RSA and ECC certificates. The `` section of the configuration file are still used to configure the supported security policies which include the new ECC policies. +Additionally the `` section of the configuration file can be used to configure the supported security policies for the UserIdentityTokens which also include the new ECC policies. + +Combining the "old" and the "new" configuration formats is not supported. That means that the `` tag cannot be used in the same configuration file with the `` tag. + + +## Known Limitations + +Not all curves are supported by all OS platforms and not all .NET implementations offer cryptographic API support for all curve types. +Due to these limitations, the support for ECC profiles is available starting with the following target platforms: .NET 4.8, .NET standard 2.1 and .NET 5 and above. +The supported ECC curve types are the following: + + - `NistP256` for ECC certificates with NIST P256 curve + - `NistP384` for ECC certificates with NIST P384 curve + - `BrainpoolP256r1` for ECC certificates with Brainpool P256r1 curve + - `BrainpoolP384r1` for ECC certificates with Brainpool P384r1 curve + + + \ No newline at end of file diff --git a/Libraries/Opc.Ua.Client.ComplexTypes/Opc.Ua.Client.ComplexTypes.csproj b/Libraries/Opc.Ua.Client.ComplexTypes/Opc.Ua.Client.ComplexTypes.csproj index 6be179a1d..1d84393e0 100644 --- a/Libraries/Opc.Ua.Client.ComplexTypes/Opc.Ua.Client.ComplexTypes.csproj +++ b/Libraries/Opc.Ua.Client.ComplexTypes/Opc.Ua.Client.ComplexTypes.csproj @@ -24,7 +24,6 @@ $(DefineConstants);SIGNASSEMBLY - diff --git a/Libraries/Opc.Ua.Client/CoreClientUtils.cs b/Libraries/Opc.Ua.Client/CoreClientUtils.cs index b73f01aba..7fd9078af 100644 --- a/Libraries/Opc.Ua.Client/CoreClientUtils.cs +++ b/Libraries/Opc.Ua.Client/CoreClientUtils.cs @@ -114,6 +114,7 @@ int discoverTimeout /// The discovery URL. /// if set to true select an endpoint that uses security. /// The best available endpoint. + [Obsolete("Use the SelectEndpoint with ApplicationConfiguration instead.")] public static EndpointDescription SelectEndpoint(string discoveryUrl, bool useSecurity) { return SelectEndpoint(discoveryUrl, useSecurity, DefaultDiscoverTimeout); @@ -126,6 +127,7 @@ public static EndpointDescription SelectEndpoint(string discoveryUrl, bool useSe /// if set to true select an endpoint that uses security. /// Operation timeout in milliseconds. /// The best available endpoint. + [Obsolete("Use the SelectEndpoint with ApplicationConfiguration instead.")] public static EndpointDescription SelectEndpoint( string discoveryUrl, bool useSecurity, @@ -173,7 +175,7 @@ int discoverTimeout { var url = new Uri(client.Endpoint.EndpointUrl); var endpoints = client.GetEndpoints(null); - return SelectEndpoint(url, endpoints, useSecurity); + return SelectEndpoint(application, url, endpoints, useSecurity); } } @@ -216,7 +218,7 @@ int discoverTimeout // Connect to the server's discovery endpoint and find the available configuration. Uri url = new Uri(client.Endpoint.EndpointUrl); var endpoints = client.GetEndpoints(null); - var selectedEndpoint = SelectEndpoint(url, endpoints, useSecurity); + var selectedEndpoint = SelectEndpoint(application, url, endpoints, useSecurity); Uri endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl); if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme) @@ -235,10 +237,28 @@ int discoverTimeout /// Select the best supported endpoint from an /// EndpointDescriptionCollection, with or without security. /// + /// The discovery Url of the server. + /// + /// + [Obsolete("Use the SelectEndpoint with ApplicationConfiguration instead.")] + public static EndpointDescription SelectEndpoint( + Uri url, + EndpointDescriptionCollection endpoints, + bool useSecurity) + { + return SelectEndpoint(null, url, endpoints, useSecurity); + } + + /// + /// Select the best supported endpoint from an + /// EndpointDescriptionCollection, with or without security. + /// + /// /// /// /// public static EndpointDescription SelectEndpoint( + ApplicationConfiguration configuration, Uri url, EndpointDescriptionCollection endpoints, bool useSecurity) @@ -261,10 +281,22 @@ public static EndpointDescription SelectEndpoint( continue; } - // skip unsupported security policies - if (SecurityPolicies.GetDisplayName(endpoint.SecurityPolicyUri) == null) + if (configuration != null) { - continue; + // skip unsupported security policies + if (!configuration.SecurityConfiguration.SupportedSecurityPolicies.Contains(endpoint.SecurityPolicyUri)) + { + continue; + } + } + else + { + // skip unsupported security policies, for backward compatibility only + // may contain policies for which no certificate is available + if (SecurityPolicies.GetDisplayName(endpoint.SecurityPolicyUri) == null) + { + continue; + } } } else diff --git a/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj b/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj index a808f4a19..6776fab61 100644 --- a/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj +++ b/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj @@ -21,9 +21,28 @@ $(PackageId).Debug - - - + + + true + + + + + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + + + diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index f9b8149b2..02176b414 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -39,6 +39,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -104,7 +105,8 @@ public Session( : base(channel) { - Initialize(channel, configuration, endpoint, clientCertificate); + Initialize(channel, configuration, endpoint); + LoadInstanceCertificateAsync(clientCertificate).GetAwaiter().GetResult(); m_discoveryServerEndpoints = availableEndpoints; m_discoveryProfileUris = discoveryProfileUris; } @@ -119,8 +121,8 @@ public Session(ITransportChannel channel, Session template, bool copyEventHandle : base(channel) { - Initialize(channel, template.m_configuration, template.ConfiguredEndpoint, template.m_instanceCertificate); - + Initialize(channel, template.m_configuration, template.ConfiguredEndpoint); + LoadInstanceCertificateAsync(template.m_instanceCertificate).GetAwaiter().GetResult(); m_sessionFactory = template.m_sessionFactory; m_defaultSubscription = template.m_defaultSubscription; m_deleteSubscriptionsOnClose = template.m_deleteSubscriptionsOnClose; @@ -166,8 +168,7 @@ public Session(ITransportChannel channel, Session template, bool copyEventHandle private void Initialize( ITransportChannel channel, ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate) + ConfiguredEndpoint endpoint) { Initialize(); @@ -180,55 +181,6 @@ private void Initialize( // update the default subscription. m_defaultSubscription.MinLifetimeInterval = (uint)configuration.ClientConfiguration.MinSubscriptionLifetime; - if (m_endpoint.Description.SecurityPolicyUri != SecurityPolicies.None) - { - // update client certificate. - m_instanceCertificate = clientCertificate; - - if (clientCertificate == null) - { - // load the application instance certificate. - if (m_configuration.SecurityConfiguration.ApplicationCertificate == null) - { - throw new ServiceResultException( - StatusCodes.BadConfigurationError, - "The client configuration does not specify an application instance certificate."); - } - - m_instanceCertificate = m_configuration.SecurityConfiguration.ApplicationCertificate.Find(true).Result; - } - - // check for valid certificate. - if (m_instanceCertificate == null) - { - var cert = m_configuration.SecurityConfiguration.ApplicationCertificate; - throw ServiceResultException.Create( - StatusCodes.BadConfigurationError, - "Cannot find the application instance certificate. Store={0}, SubjectName={1}, Thumbprint={2}.", - cert.StorePath, cert.SubjectName, cert.Thumbprint); - } - - // check for private key. - if (!m_instanceCertificate.HasPrivateKey) - { - throw ServiceResultException.Create( - StatusCodes.BadConfigurationError, - "No private key for the application instance certificate. Subject={0}, Thumbprint={1}.", - m_instanceCertificate.Subject, - m_instanceCertificate.Thumbprint); - } - - // load certificate chain. - m_instanceCertificateChain = new X509Certificate2Collection(m_instanceCertificate); - List issuers = new List(); - configuration.CertificateValidator.GetIssuers(m_instanceCertificate, issuers).Wait(); - - for (int i = 0; i < issuers.Count; i++) - { - m_instanceCertificateChain.Add(issuers[i].Certificate); - } - } - // initialize the message context. IServiceMessageContext messageContext = channel.MessageContext; @@ -358,7 +310,7 @@ private void ValidateServerNonce( if (identity != null && identity.TokenType != UserTokenType.Anonymous) { // the server nonce should be validated if the token includes a secret. - if (!Utils.Nonce.ValidateNonce(serverNonce, MessageSecurityMode.SignAndEncrypt, (uint)m_configuration.SecurityConfiguration.NonceLength)) + if (!Nonce.ValidateNonce(serverNonce, MessageSecurityMode.SignAndEncrypt, (uint)m_configuration.SecurityConfiguration.NonceLength)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) @@ -372,7 +324,7 @@ private void ValidateServerNonce( } // check that new nonce is different from the previously returned server nonce. - if (previousServerNonce != null && Utils.CompareNonce(serverNonce, previousServerNonce)) + if (previousServerNonce != null && Nonce.CompareNonce(serverNonce, previousServerNonce)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) @@ -1074,8 +1026,8 @@ public static async Task CreateChannelAsync( X509Certificate2Collection clientCertificateChain = null; if (endpointDescription.SecurityPolicyUri != SecurityPolicies.None) { - clientCertificate = await LoadCertificate(configuration).ConfigureAwait(false); - clientCertificateChain = await LoadCertificateChain(configuration, clientCertificate).ConfigureAwait(false); + clientCertificate = await LoadCertificateAsync(configuration, endpointDescription.SecurityPolicyUri).ConfigureAwait(false); + clientCertificateChain = await LoadCertificateChainAsync(configuration, clientCertificate).ConfigureAwait(false); } // initialize the channel which will be created with the server. @@ -1432,7 +1384,9 @@ public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) m_serverCertificate = serverCertificate != null ? new X509Certificate2(serverCertificate) : null; m_identity = sessionConfiguration.Identity; m_checkDomain = sessionConfiguration.CheckDomain; - m_serverNonce = sessionConfiguration.ServerNonce; + m_serverNonce = sessionConfiguration.ServerNonce.Data; + m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; + m_eccServerEphemeralKey = sessionConfiguration.ServerEccEphemeralKey; SessionCreated(sessionConfiguration.SessionId, sessionConfiguration.AuthenticationToken); return true; @@ -1441,7 +1395,11 @@ public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) /// public SessionConfiguration SaveSessionConfiguration(Stream stream = null) { - var sessionConfiguration = new SessionConfiguration(this, m_serverNonce, AuthenticationToken); + + Nonce serverNonce = Nonce.CreateNonce(m_endpoint.Description?.SecurityPolicyUri, m_serverNonce); + + var sessionConfiguration = new SessionConfiguration(this, serverNonce, m_userTokenSecurityPolicyUri, m_eccServerEphemeralKey, AuthenticationToken); + if (stream != null) { XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); @@ -1509,12 +1467,14 @@ private void Reconnect(ITransportWaitingConnection connection, ITransportChannel StatusCodeCollection certificateResults = null; DiagnosticInfoCollection certificateDiagnosticInfos = null; - EndActivateSession( + var responseHeader = EndActivateSession( result, out serverNonce, out certificateResults, out certificateDiagnosticInfos); + ProcessResponseAdditionalHeader(responseHeader, m_serverCertificate); + Utils.LogInfo("Session RECONNECT {0} completed successfully.", SessionId); lock (SyncRoot) @@ -2355,7 +2315,7 @@ public void Open( // create a nonce. uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; - byte[] clientNonce = Utils.Nonce.CreateNonce(length); + byte[] clientNonce = Nonce.CreateRandomNonceData(length); NodeId sessionId = null; NodeId sessionCookie = null; byte[] serverNonce = Array.Empty(); @@ -2379,14 +2339,19 @@ public void Open( sessionTimeout = (uint)m_configuration.ClientConfiguration.DefaultSessionTimeout; } + // select the security policy for the user token. + RequestHeader requestHeader = CreateRequestHeaderPerUserTokenPolicy(identityPolicy.SecurityPolicyUri, m_endpoint.Description.SecurityPolicyUri); + bool successCreateSession = false; + ResponseHeader responseHeader = null; + //if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { //first try to connect with client certificate NULL try { - base.CreateSession( + responseHeader = base.CreateSession( null, clientDescription, m_endpoint.Description.Server.ApplicationUri, @@ -2417,8 +2382,8 @@ public void Open( if (!successCreateSession) { - base.CreateSession( - null, + responseHeader = base.CreateSession( + requestHeader, clientDescription, m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), @@ -2461,6 +2426,9 @@ public void Open( HandleSignedSoftwareCertificates(serverSoftwareCertificates); + // process additional header + ProcessResponseAdditionalHeader(responseHeader, serverCertificate); + // create the client signature. byte[] dataToSign = Utils.Append(serverCertificate != null ? serverCertificate.RawData : null, serverNonce); SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, securityPolicyUri, dataToSign); @@ -2488,7 +2456,14 @@ public void Open( SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); // encrypt token. - identityToken.Encrypt(serverCertificate, serverNonce, securityPolicyUri); + identityToken.Encrypt( + serverCertificate, + serverNonce, + m_userTokenSecurityPolicyUri, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); @@ -2503,7 +2478,7 @@ public void Open( DiagnosticInfoCollection certificateDiagnosticInfos = null; // activate session. - ActivateSession( + responseHeader = ActivateSession( null, clientSignature, clientSoftwareCertificates, @@ -2514,6 +2489,8 @@ public void Open( out certificateResults, out certificateDiagnosticInfos); + ProcessResponseAdditionalHeader(responseHeader, serverCertificate); + if (certificateResults != null) { for (int i = 0; i < certificateResults.Count; i++) @@ -2622,7 +2599,7 @@ public void UpdateSession(IUserIdentity identity, StringCollection preferredLoca } // check that the user identity is supported by the endpoint. - UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType); + UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType, securityPolicyUri); if (identityPolicy == null) { @@ -2661,7 +2638,14 @@ public void UpdateSession(IUserIdentity identity, StringCollection preferredLoca userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); // encrypt token. - identityToken.Encrypt(m_serverCertificate, serverNonce, securityPolicyUri); + identityToken.Encrypt( + m_serverCertificate, + serverNonce, + m_userTokenSecurityPolicyUri, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); @@ -2670,7 +2654,7 @@ public void UpdateSession(IUserIdentity identity, StringCollection preferredLoca DiagnosticInfoCollection certificateDiagnosticInfos = null; // activate session. - ActivateSession( + ResponseHeader responseHeader = ActivateSession( null, clientSignature, clientSoftwareCertificates, @@ -2681,6 +2665,8 @@ public void UpdateSession(IUserIdentity identity, StringCollection preferredLoca out certificateResults, out certificateDiagnosticInfos); + ProcessResponseAdditionalHeader(responseHeader, m_serverCertificate); + // save nonce and new values. lock (SyncRoot) { @@ -5333,12 +5319,12 @@ private void OpenValidateIdentity( identityToken = identity.GetIdentityToken(); // check that the user identity is supported by the endpoint. - identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identityToken.PolicyId); + identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identityToken.PolicyId, securityPolicyUri); if (identityPolicy == null) { // try looking up by TokenType if the policy id was not found. - identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType); + identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType, securityPolicyUri); if (identityPolicy == null) { @@ -5639,7 +5625,14 @@ ITransportChannel transportChannel EndpointDescription endpoint = m_endpoint.Description; SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, endpoint.SecurityPolicyUri, dataToSign); +#if TODO // new code inside, please check if the update is valid UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy(m_identity.PolicyId); +#else + // check that the user identity is supported by the endpoint. + UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy(m_identity.TokenType, + m_identity.IssuedTokenType, + endpoint.SecurityPolicyUri); +#endif if (identityPolicy == null) { @@ -5657,6 +5650,7 @@ ITransportChannel transportChannel { securityPolicyUri = endpoint.SecurityPolicyUri; } + m_userTokenSecurityPolicyUri = securityPolicyUri; // need to refresh the identity (reprompt for password, refresh token). if (m_RenewUserIdentity != null) @@ -5678,7 +5672,14 @@ ITransportChannel transportChannel SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); // encrypt token. - identityToken.Encrypt(m_serverCertificate, m_serverNonce, securityPolicyUri); + identityToken.Encrypt( + m_serverCertificate, + m_serverNonce, + m_userTokenSecurityPolicyUri, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); @@ -6134,29 +6135,67 @@ private void DeleteSubscription(uint subscriptionId) } /// - /// Load certificate for connection. + /// Asynchronously load instance certificate /// - private static async Task LoadCertificate(ApplicationConfiguration configuration) + /// + /// + private async Task LoadInstanceCertificateAsync(X509Certificate2 clientCertificate) { - X509Certificate2 clientCertificate; - if (configuration.SecurityConfiguration.ApplicationCertificate == null) + if (m_endpoint.Description.SecurityPolicyUri != SecurityPolicies.None) { - throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "ApplicationCertificate must be specified."); + if (clientCertificate == null) + { + m_instanceCertificate = await LoadCertificateAsync(m_configuration, m_endpoint.Description.SecurityPolicyUri).ConfigureAwait(false); + if (m_instanceCertificate == null) + { + throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "The client configuration does not specify an application instance certificate."); + } + } + else + { + // update client certificate. + m_instanceCertificate = clientCertificate; + } + + // check for private key. + if (!m_instanceCertificate.HasPrivateKey) + { + throw ServiceResultException.Create( + StatusCodes.BadConfigurationError, + "No private key for the application instance certificate. Subject={0}, Thumbprint={1}.", + m_instanceCertificate.Subject, + m_instanceCertificate.Thumbprint); + } + + // load certificate chain. + m_instanceCertificateChain = await LoadCertificateChainAsync(m_configuration, m_instanceCertificate).ConfigureAwait(false); } + } - clientCertificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true).ConfigureAwait(false); + /// + /// Load certificate for connection. + /// + private static async Task LoadCertificateAsync(ApplicationConfiguration configuration, string securityProfile) + { + X509Certificate2 clientCertificate = + await configuration.SecurityConfiguration.FindApplicationCertificateAsync(securityProfile, true).ConfigureAwait(false); if (clientCertificate == null) { - throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "ApplicationCertificate cannot be found."); + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, + "ApplicationCertificate for the security profile {0} cannot be found.", + securityProfile); } + return clientCertificate; } /// /// Load certificate chain for connection. /// - private static async Task LoadCertificateChain(ApplicationConfiguration configuration, X509Certificate2 clientCertificate) + private static async Task LoadCertificateChainAsync(ApplicationConfiguration configuration, X509Certificate2 clientCertificate) { X509Certificate2Collection clientCertificateChain = null; // load certificate chain. @@ -6362,6 +6401,81 @@ private static void UpdateLatestSequenceNumberToSend(ref uint latestSequenceNumb latestSequenceNumberToSend = sequenceNumber; } } + + /// + /// Creates a request header with additional parameters + /// for the ecc user token security policy, if needed. + /// + private RequestHeader CreateRequestHeaderPerUserTokenPolicy(string identityTokenSecurityPolicyUri, string endpointSecurityPolicyUri) + { + var requestHeader = new RequestHeader(); + var userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri; + if (String.IsNullOrEmpty(userTokenSecurityPolicyUri)) + { + userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + } + m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; + + if (EccUtils.IsEccPolicy(userTokenSecurityPolicyUri)) + { + AdditionalParametersType parameters = new AdditionalParametersType(); + parameters.Parameters.Add(new KeyValuePair() { + Key = "ECDHPolicyUri", + Value = userTokenSecurityPolicyUri + }); + requestHeader.AdditionalHeader = new ExtensionObject(parameters); + } + + return requestHeader; + } + + #endregion + + #region Protected Methods + /// + /// Process the AdditionalHeader field of a ResponseHeader + /// + /// + /// + /// + protected virtual void ProcessResponseAdditionalHeader(ResponseHeader responseHeader, X509Certificate2 serverCertificate) + { + AdditionalParametersType parameters = ExtensionObject.ToEncodeable(responseHeader?.AdditionalHeader) as AdditionalParametersType; + + if (parameters != null) + { + foreach (var ii in parameters.Parameters) + { + if (ii.Key == "ECDHKey") + { + if (ii.Value.TypeInfo == TypeInfo.Scalars.StatusCode) + { + throw new ServiceResultException( + (uint)(StatusCode)ii.Value.Value, + "Server could not provide an ECDHKey. User authentication not possible."); + } + + var key = ExtensionObject.ToEncodeable(ii.Value.Value as ExtensionObject) as EphemeralKeyType; + + if (key == null) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Server did not provide a valid ECDHKey. User authentication not possible."); + } + + if (!EccUtils.Verify(new ArraySegment(key.PublicKey), key.Signature, serverCertificate, m_userTokenSecurityPolicyUri)) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Could not verify signature on ECDHKey. User authentication not possible."); + } + + m_eccServerEphemeralKey = Nonce.CreateNonce(m_userTokenSecurityPolicyUri, key.PublicKey); + } + } + } + } #endregion #region Protected Fields @@ -6452,6 +6566,8 @@ private static void UpdateLatestSequenceNumberToSend(ref uint latestSequenceNumb private int m_minPublishRequestCount; private int m_maxPublishRequestCount; private LinkedList m_outstandingRequests; + private string m_userTokenSecurityPolicyUri; + private Nonce m_eccServerEphemeralKey; private readonly EndpointDescriptionCollection m_discoveryServerEndpoints; private readonly StringCollection m_discoveryProfileUris; private uint m_serverMaxContinuationPointsPerBrowse = 0; diff --git a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs index 0b5e74e06..70743ba1a 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs @@ -110,7 +110,7 @@ public async Task OpenAsync( // create a nonce. uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; - byte[] clientNonce = Utils.Nonce.CreateNonce(length); + byte[] clientNonce = Nonce.CreateRandomNonceData(length); // send the application instance certificate for the client. BuildCertificateData(out byte[] clientCertificateData, out byte[] clientCertificateChainData); @@ -127,6 +127,9 @@ public async Task OpenAsync( sessionTimeout = (uint)m_configuration.ClientConfiguration.DefaultSessionTimeout; } + // select the security policy for the user token. + RequestHeader requestHeader = CreateRequestHeaderPerUserTokenPolicy(identityPolicy.SecurityPolicyUri, m_endpoint.Description.SecurityPolicyUri); + bool successCreateSession = false; CreateSessionResponse response = null; @@ -160,7 +163,7 @@ public async Task OpenAsync( if (!successCreateSession) { response = await base.CreateSessionAsync( - null, + requestHeader, clientDescription, m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), @@ -206,6 +209,9 @@ public async Task OpenAsync( HandleSignedSoftwareCertificates(serverSoftwareCertificates); + // process additional header + ProcessResponseAdditionalHeader(response.ResponseHeader, serverCertificate); + // create the client signature. byte[] dataToSign = Utils.Append(serverCertificate != null ? serverCertificate.RawData : null, serverNonce); SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, securityPolicyUri, dataToSign); @@ -233,7 +239,14 @@ public async Task OpenAsync( SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); // encrypt token. - identityToken.Encrypt(serverCertificate, serverNonce, securityPolicyUri); + identityToken.Encrypt( + serverCertificate, + serverNonce, + m_userTokenSecurityPolicyUri, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); // send the software certificates assigned to the client. SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); @@ -254,6 +267,9 @@ public async Task OpenAsync( userTokenSignature, ct).ConfigureAwait(false); + // process additional header + ProcessResponseAdditionalHeader(activateResponse.ResponseHeader, serverCertificate); + serverNonce = activateResponse.ServerNonce; StatusCodeCollection certificateResults = activateResponse.Results; DiagnosticInfoCollection certificateDiagnosticInfos = activateResponse.DiagnosticInfos; diff --git a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs index 0fc2d50f7..7b045a0fa 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs @@ -30,6 +30,7 @@ using System; using System.IO; using System.Runtime.Serialization; +using System.Security.Cryptography; using System.Xml; namespace Opc.Ua.Client @@ -49,7 +50,12 @@ public class SessionConfiguration /// /// Creates a session configuration /// - public SessionConfiguration(ISession session, byte[] serverNonce, NodeId authenthicationToken) + public SessionConfiguration( + ISession session, + Nonce serverNonce, + string userIdentityTokenPolicy, + Nonce eccServerEphemeralKey, + NodeId authenthicationToken) { Timestamp = DateTime.UtcNow; SessionName = session.SessionName; @@ -59,6 +65,17 @@ public SessionConfiguration(ISession session, byte[] serverNonce, NodeId authent ConfiguredEndpoint = session.ConfiguredEndpoint; CheckDomain = session.CheckDomain; ServerNonce = serverNonce; + ServerEccEphemeralKey = eccServerEphemeralKey; + UserIdentityTokenPolicy = userIdentityTokenPolicy; + } + + /// + /// Creates a session configuration + /// + [Obsolete("Use SessionConfiguration(ISession session, Nonce serverNonce, string userIdentityTokenPolicy, Nonce eccServerEphemeralKey, NodeId authenthicationToken)")] + public SessionConfiguration(ISession session, byte[] serverNonce, NodeId authenthicationToken) + :this(session, Nonce.CreateNonce("RSA-only", serverNonce), null, null, authenthicationToken) + { } /// @@ -122,6 +139,19 @@ public static SessionConfiguration Create(Stream stream) /// The last server nonce received. /// [DataMember(IsRequired = true, Order = 80)] - public byte[] ServerNonce { get; set; } + public Nonce ServerNonce { get; set; } + + /// + /// The user identity token policy which was used to create the session. + /// + [DataMember(IsRequired = true, Order = 90)] + public string UserIdentityTokenPolicy { get; set; } + + /// + /// The last server ecc ephemeral key received. + /// + [DataMember(IsRequired = false, Order = 100)] + public Nonce ServerEccEphemeralKey { get; set; } + } } diff --git a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs index a5240c95b..7af5b11c4 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs @@ -28,13 +28,13 @@ * ======================================================================*/ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Xml; + namespace Opc.Ua.Configuration { /// @@ -104,7 +104,9 @@ public IApplicationConfigurationBuilderSecurityOptions AddSecurityConfiguration( var rejectedRootType = CertificateStoreIdentifier.DetermineStoreType(rejectedRoot); ApplicationConfiguration.SecurityConfiguration = new SecurityConfiguration { // app cert store +#pragma warning disable CS0618 // Type or member is obsolete ApplicationCertificate = new CertificateIdentifier() { +#pragma warning restore CS0618 // Type or member is obsolete StoreType = appStoreType, StorePath = DefaultCertificateStorePath(TrustlistType.Application, appRoot), SubjectName = Utils.ReplaceDCLocalhost(subjectName) @@ -147,6 +149,58 @@ public IApplicationConfigurationBuilderSecurityOptions AddSecurityConfiguration( return this; } + /// + public IApplicationConfigurationBuilderSecurityOptions AddSecurityConfiguration( + CertificateIdentifierCollection applicationCertificates, + string pkiRoot = null, + string rejectedRoot = null + ) + { + pkiRoot = DefaultPKIRoot(pkiRoot); + rejectedRoot = rejectedRoot == null ? pkiRoot : DefaultPKIRoot(rejectedRoot); + var pkiRootType = CertificateStoreIdentifier.DetermineStoreType(pkiRoot); + var rejectedRootType = CertificateStoreIdentifier.DetermineStoreType(rejectedRoot); + ApplicationConfiguration.SecurityConfiguration = new SecurityConfiguration { + // app cert store + ApplicationCertificates = applicationCertificates, + // App trusted & issuer + TrustedPeerCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.Trusted, pkiRoot) + }, + TrustedIssuerCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.Issuer, pkiRoot) + }, + // Https trusted & issuer + TrustedHttpsCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.TrustedHttps, pkiRoot) + }, + HttpsIssuerCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.IssuerHttps, pkiRoot) + }, + // User trusted & issuer + TrustedUserCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.TrustedUser, pkiRoot) + }, + UserIssuerCertificates = new CertificateTrustList() { + StoreType = pkiRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.IssuerUser, pkiRoot) + }, + // rejected store + RejectedCertificateStore = new CertificateTrustList() { + StoreType = rejectedRootType, + StorePath = DefaultCertificateStorePath(TrustlistType.Rejected, rejectedRoot) + }, + }; + SetSecureDefaults(ApplicationConfiguration.SecurityConfiguration); + + return this; + } + /// public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigurationStores( string subjectName, @@ -154,7 +208,7 @@ public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigura string trustedRoot, string issuerRoot, string rejectedRoot = null - ) + ) { string appStoreType = CertificateStoreIdentifier.DetermineStoreType(appRoot); string issuerRootType = CertificateStoreIdentifier.DetermineStoreType(issuerRoot); @@ -163,7 +217,9 @@ public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigura string rejectedRootType = CertificateStoreIdentifier.DetermineStoreType(rejectedRoot); ApplicationConfiguration.SecurityConfiguration = new SecurityConfiguration { // app cert store + #pragma warning disable CS0618 // Type or member is obsolete ApplicationCertificate = new CertificateIdentifier() { + #pragma warning restore CS0618 // Type or member is obsolete StoreType = appStoreType, StorePath = DefaultCertificateStorePath(TrustlistType.Application, appRoot), SubjectName = Utils.ReplaceDCLocalhost(subjectName) @@ -192,7 +248,7 @@ public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigura public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigurationUserStore( string trustedRoot, string issuerRoot - ) + ) { string trustedRootType = CertificateStoreIdentifier.DetermineStoreType(trustedRoot); string issuerRootType = CertificateStoreIdentifier.DetermineStoreType(issuerRoot); @@ -208,7 +264,6 @@ string issuerRoot }; return this; } - /// public IApplicationConfigurationBuilderSecurityOptionStores AddSecurityConfigurationHttpsStore( string trustedRoot, @@ -349,10 +404,24 @@ public IApplicationConfigurationBuilderServerSelected AddSignAndEncryptPolicies( return this; } + /// + public IApplicationConfigurationBuilderServerSelected AddEccSignPolicies() + { + AddEccSecurityPolicies(true); + return this; + } + + /// + public IApplicationConfigurationBuilderServerSelected AddEccSignAndEncryptPolicies() + { + AddEccSecurityPolicies(false); + return this; + } + /// public IApplicationConfigurationBuilderServerSelected AddPolicy(MessageSecurityMode securityMode, string securityPolicy) { - if (SecurityPolicies.GetDisplayName(securityPolicy) == null) throw new ArgumentException("Unknown security policy", nameof(securityPolicy)); + if (!SecurityPolicies.IsValidSecurityPolicyUri(securityPolicy)) throw new ArgumentException("Unknown security policy", nameof(securityPolicy)); if (securityMode == MessageSecurityMode.None || securityPolicy.Equals(SecurityPolicies.None)) throw new ArgumentException("Use AddUnsecurePolicyNone to add no security policy."); InternalAddPolicy(ApplicationConfiguration.ServerConfiguration.SecurityPolicies, securityMode, securityPolicy); return this; @@ -373,6 +442,13 @@ public IApplicationConfigurationBuilderServerSelected AddUserTokenPolicy(UserTok return this; } + /// + public IApplicationConfigurationBuilderSecurityOptions SetApplicationCertificates(CertificateIdentifierCollection certIdList) + { + ApplicationConfiguration.SecurityConfiguration.ApplicationCertificates = certIdList; + return this; + } + /// public IApplicationConfigurationBuilderSecurityOptions SetMaxRejectedCertificates(int maxRejectedCertificates) { @@ -857,6 +933,69 @@ public IApplicationConfigurationBuilderExtension AddExtension(XmlQualifiedNam } #endregion + #region Public Static Methods + + /// + /// Create ApplicationCertificates from a PKI root. + /// + /// The cert store type: ex: "Directory" + /// The PKI root. + /// The subject name. + /// The application certificates. + + public static CertificateIdentifierCollection CreateDefaultApplicationCertificates( + string subjectName, + string storeType = null, + string storePath = null) + { + CertificateIdentifierCollection certificateIdentifiers = new CertificateIdentifierCollection{ + new CertificateIdentifier { + StoreType = storeType, + StorePath = storePath, + SubjectName = subjectName, + CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType + }, + new CertificateIdentifier { + StoreType = storeType, + StorePath = storePath, + SubjectName = subjectName, + CertificateType = ObjectTypeIds.EccNistP256ApplicationCertificateType + }, + new CertificateIdentifier { + StoreType = storeType, + StorePath = storePath, + SubjectName = subjectName, + CertificateType = ObjectTypeIds.EccNistP384ApplicationCertificateType + } + }; + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + certificateIdentifiers.AddRange( + new CertificateIdentifierCollection + { + new CertificateIdentifier + { + StoreType = storeType, + StorePath = storePath, + SubjectName = subjectName, + CertificateType = ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType + }, + new CertificateIdentifier + { + StoreType = storeType, + StorePath = storePath, + SubjectName = subjectName, + CertificateType = ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType + } + }); + } + + return certificateIdentifiers; + + } + #endregion + #region Private Methods /// /// Internal enumeration of supported trust lists. @@ -977,20 +1116,10 @@ private string DefaultCertificateStorePath(TrustlistType trustListType, string p private void AddSecurityPolicies(bool includeSign = false, bool deprecated = false, bool policyNone = false) { // create list of supported policies - string[] defaultPolicyUris = SecurityPolicies.GetDefaultUris(); + var defaultPolicyUris = SecurityPolicies.GetDefaultUris().ToList(); if (deprecated) { - string[] names = SecurityPolicies.GetDisplayNames(); - var deprecatedPolicyList = new List(); - foreach (string name in names) - { - string uri = SecurityPolicies.GetUri(name); - if (uri != null) - { - deprecatedPolicyList.Add(uri); - } - } - defaultPolicyUris = deprecatedPolicyList.ToArray(); + defaultPolicyUris.AddRange(SecurityPolicies.GetDefaultDeprecatedUris()); } foreach (MessageSecurityMode securityMode in typeof(MessageSecurityMode).GetEnumValues()) @@ -1011,6 +1140,23 @@ private void AddSecurityPolicies(bool includeSign = false, bool deprecated = fal } } + /// + /// Add default Ecc policies. + /// + private void AddEccSecurityPolicies(bool sign = false) + { + // create list of supported policies + var defaultPolicyUris = SecurityPolicies.GetDefaultEccUris(); + MessageSecurityMode securityMode = sign ? MessageSecurityMode.Sign : MessageSecurityMode.SignAndEncrypt; + { + var policies = ApplicationConfiguration.ServerConfiguration.SecurityPolicies; + foreach (var policyUri in defaultPolicyUris) + { + InternalAddPolicy(policies, securityMode, policyUri); + } + } + } + /// /// Set secure defaults for flags. /// diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 1734790b1..e9da13597 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -29,10 +29,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -411,6 +411,7 @@ string productUri /// /// if set to true no dialogs will be displayed. /// Minimum size of the key. + [Obsolete("This method is obsolete since an application now supports different minKey sizes depending on certificate type")] public Task CheckApplicationInstanceCertificate( bool silent, ushort minimumKeySize) @@ -419,12 +420,26 @@ public Task CheckApplicationInstanceCertificate( } /// - /// Delete the application certificate. + /// Checks for a valid application instance certificate. + /// + /// if set to true no dialogs will be displayed. + public async Task CheckApplicationInstanceCertificates( + bool silent) + { + return await CheckApplicationInstanceCertificates(silent, CertificateFactory.DefaultLifeTime).ConfigureAwait(false); + } + + /// + /// Deletes all application certificates. /// - public async Task DeleteApplicationInstanceCertificate(CancellationToken ct = default) + public async Task DeleteApplicationInstanceCertificate(string[] profileIds = null, CancellationToken ct = default) { + // TODO: delete only selected profiles if (m_applicationConfiguration == null) throw new ArgumentException("Missing configuration."); - await DeleteApplicationInstanceCertificateAsync(m_applicationConfiguration, ct).ConfigureAwait(false); + foreach (var id in m_applicationConfiguration.SecurityConfiguration.ApplicationCertificates) + { + await DeleteApplicationInstanceCertificateAsync(m_applicationConfiguration, id, ct).ConfigureAwait(false); + } } /// @@ -433,12 +448,27 @@ public async Task DeleteApplicationInstanceCertificate(CancellationToken ct = de /// if set to true no dialogs will be displayed. /// Minimum size of the key. /// The lifetime in months. - /// The cancellation token. + /// + [Obsolete("This method is obsolete since an application now supports different minKey sizes depending on certificate type")] public async Task CheckApplicationInstanceCertificate( bool silent, ushort minimumKeySize, ushort lifeTimeInMonths, CancellationToken ct = default) + { + return await CheckApplicationInstanceCertificates(silent, lifeTimeInMonths, ct).ConfigureAwait(false); + } + + /// + /// Checks for a valid application instance certificate. + /// + /// if set to true no dialogs will be displayed. + /// The lifetime in months. + /// + public async Task CheckApplicationInstanceCertificates( + bool silent, + ushort lifeTimeInMonths, + CancellationToken ct = default) { Utils.LogInfo("Checking application instance certificate."); @@ -447,10 +477,45 @@ public async Task CheckApplicationInstanceCertificate( await LoadApplicationConfiguration(silent).ConfigureAwait(false); } - ApplicationConfiguration configuration = m_applicationConfiguration; + // find the existing certificates. + SecurityConfiguration securityConfiguration = m_applicationConfiguration.SecurityConfiguration; + + if (securityConfiguration.ApplicationCertificates.Count == 0) + { + throw new ServiceResultException(StatusCodes.BadConfigurationError, "Need at least one Application Certificate."); + } + + bool result = true; + foreach (var certId in securityConfiguration.ApplicationCertificates) + { + ushort minimumKeySize = certId.GetMinKeySize(securityConfiguration); + bool nextResult = await CheckCertificateTypeAsync(certId, silent, minimumKeySize, lifeTimeInMonths, ct).ConfigureAwait(false); + result = result && nextResult; + } - // find the existing certificate. - CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; + return result; + } + #endregion + + #region Private Methods + /// + /// + /// + /// + /// + /// + /// + /// + /// + private async Task CheckCertificateTypeAsync( + CertificateIdentifier id, + bool silent, + ushort minimumKeySize, + ushort lifeTimeInMonths, + CancellationToken ct = default + ) + { + ApplicationConfiguration configuration = m_applicationConfiguration; if (id == null) { @@ -460,7 +525,7 @@ public async Task CheckApplicationInstanceCertificate( // reload the certificate from disk in the cache. var passwordProvider = configuration.SecurityConfiguration.CertificatePasswordProvider; - await configuration.SecurityConfiguration.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).ConfigureAwait(false); + await id.LoadPrivateKeyEx(passwordProvider).ConfigureAwait(false); // load the certificate X509Certificate2 certificate = await id.Find(true).ConfigureAwait(false); @@ -469,7 +534,7 @@ public async Task CheckApplicationInstanceCertificate( if (certificate != null) { Utils.LogCertificate("Check certificate:", certificate); - bool certificateValid = await CheckApplicationInstanceCertificateAsync(configuration, certificate, silent, minimumKeySize, ct).ConfigureAwait(false); + bool certificateValid = await CheckApplicationInstanceCertificateAsync(configuration, id, certificate, silent, minimumKeySize, ct).ConfigureAwait(false); if (!certificateValid) { @@ -534,8 +599,8 @@ public async Task CheckApplicationInstanceCertificate( { if (!DisableCertificateAutoCreation) { - certificate = await CreateApplicationInstanceCertificateAsync(configuration, - minimumKeySize, lifeTimeInMonths, ct).ConfigureAwait(false); + certificate = await CreateApplicationInstanceCertificateAsync(configuration, id, + lifeTimeInMonths, ct).ConfigureAwait(false); } else { @@ -604,6 +669,7 @@ public void OnCertificateValidation(object sender, CertificateValidationEventArg /// private async Task CheckApplicationInstanceCertificateAsync( ApplicationConfiguration configuration, + CertificateIdentifier id, X509Certificate2 certificate, bool silent, ushort minimumKeySize, @@ -647,12 +713,12 @@ private async Task CheckApplicationInstanceCertificateAsync( configuration.CertificateValidator.CertificateValidation -= certValidator.OnCertificateValidation; } - // check key size. - int keySize = X509Utils.GetRSAPublicKeySize(certificate); + // check key size + int keySize = X509Utils.GetPublicKeySize(certificate); if (minimumKeySize > keySize) { string message = Utils.Format( - "The key size ({0}) in the certificate is less than the minimum allowed ({1}). Use certificate anyway?", + "The key size ({0}) in the certificate is less than the minimum provided ({1}). Use certificate anyway?", keySize, minimumKeySize); @@ -691,7 +757,7 @@ private async Task CheckApplicationInstanceCertificateAsync( Utils.LogInfo("Using the ApplicationUri: {0}", applicationUri); // update configuration. - configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; + id.Certificate = certificate; return true; } @@ -790,23 +856,21 @@ private static async Task CheckDomainsInCertificateAsync( /// Creates the application instance certificate. /// /// The configuration. - /// Size of the key. + /// The certificate identifier. /// The lifetime in months. /// /// The new certificate private static async Task CreateApplicationInstanceCertificateAsync( ApplicationConfiguration configuration, - ushort keySize, + CertificateIdentifier id, ushort lifeTimeInMonths, CancellationToken ct) { // delete any existing certificate. - await DeleteApplicationInstanceCertificateAsync(configuration, ct).ConfigureAwait(false); + await DeleteApplicationInstanceCertificateAsync(configuration, id, ct).ConfigureAwait(false); Utils.LogInfo("Creating application instance certificate."); - CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; - // get the domains from the configuration file. IList serverDomainNames = configuration.GetServerDomainNames(); @@ -821,19 +885,72 @@ private static async Task CreateApplicationInstanceCertificate Utils.GetAbsoluteDirectoryPath(id.StorePath, true, true, true); } + var builder = CertificateFactory.CreateCertificate( + configuration.ApplicationUri, + configuration.ApplicationName, + id.SubjectName, + serverDomainNames) + .SetLifeTime(lifeTimeInMonths); + + if (id.CertificateType == null || + id.CertificateType == ObjectTypeIds.ApplicationCertificateType || + id.CertificateType == ObjectTypeIds.RsaMinApplicationCertificateType || + id.CertificateType == ObjectTypeIds.RsaSha256ApplicationCertificateType) + { + id.Certificate = builder + .SetRSAKeySize(CertificateFactory.DefaultKeySize) + .CreateForRSA(); + + Utils.LogCertificate("Certificate created for RSA.", id.Certificate); + } + else + { +#if !ECC_SUPPORT + throw new ServiceResultException(StatusCodes.BadConfigurationError, "The Ecc certificate type is not supported."); +#else + ECCurve curve = default(ECCurve); + if (id.CertificateType == ObjectTypeIds.EccApplicationCertificateType || + id.CertificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP256; + } + else if (id.CertificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.nistP384; + } + else if (id.CertificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP256r1; + } + else if (id.CertificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) + { + curve = ECCurve.NamedCurves.brainpoolP384r1; + } +#if CURVE25519 + else if (id.CertificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) + { + curve = default(ECCurve); + } + else if (id.CertificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) + { + curve = default(ECCurve); + } +#endif + else + { + throw new ServiceResultException(StatusCodes.BadConfigurationError, "The ECC certificate type is not supported."); + } + + id.Certificate = builder + .SetECCurve(curve) + .CreateForECDsa(); + + Utils.LogCertificate("Certificate created for {0}.", id.Certificate, curve.Oid.FriendlyName); +#endif + } + var passwordProvider = configuration.SecurityConfiguration.CertificatePasswordProvider; - X509Certificate2 certificate = CertificateFactory.CreateCertificate( - configuration.ApplicationUri, - configuration.ApplicationName, - id.SubjectName, - serverDomainNames) - .SetLifeTime(lifeTimeInMonths) - .SetRSAKeySize(keySize) - .CreateForRSA(); - - // need id for password provider - id.Certificate = certificate; - await certificate.AddToStoreAsync( + await id.Certificate.AddToStoreAsync( id.StoreType, id.StorePath, passwordProvider?.GetPassword(id), @@ -842,15 +959,15 @@ await certificate.AddToStoreAsync( // ensure the certificate is trusted. if (configuration.SecurityConfiguration.AddAppCertToTrustedStore) { - await AddToTrustedStoreAsync(configuration, certificate, ct).ConfigureAwait(false); + await AddToTrustedStoreAsync(configuration, id.Certificate, ct).ConfigureAwait(false); } // reload the certificate from disk. - id.Certificate = await configuration.SecurityConfiguration.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).ConfigureAwait(false); + id.Certificate = await id.LoadPrivateKeyEx(passwordProvider).ConfigureAwait(false); await configuration.CertificateValidator.Update(configuration.SecurityConfiguration).ConfigureAwait(false); - Utils.LogCertificate("Certificate created for {0}.", certificate, configuration.ApplicationUri); + Utils.LogCertificate("Certificate created for {0}.", id.Certificate, configuration.ApplicationUri); // do not dispose temp cert, or X509Store certs become unusable @@ -861,12 +978,10 @@ await certificate.AddToStoreAsync( /// Deletes an existing application instance certificate. /// /// The configuration instance that stores the configurable information for a UA application. + /// The certificate identifier. /// - private static async Task DeleteApplicationInstanceCertificateAsync(ApplicationConfiguration configuration, CancellationToken ct) + private static async Task DeleteApplicationInstanceCertificateAsync(ApplicationConfiguration configuration, CertificateIdentifier id, CancellationToken ct) { - // create a default certificate id none specified. - CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; - if (id == null) { return; @@ -987,10 +1102,25 @@ private static async Task AddToTrustedStoreAsync(ApplicationConfiguration config return; } - Utils.LogCertificate("Delete Certificate from trusted store.", certificate); + bool deleteCert = false; + if (X509Utils.IsECDsaSignature(certificates[ii]) && X509Utils.IsECDsaSignature(certificate)) + { + if (X509Utils.GetECDsaQualifier(certificates[ii]).Equals(X509Utils.GetECDsaQualifier(certificate))) + { + deleteCert = true; + } + } + else if (!X509Utils.IsECDsaSignature(certificates[ii]) && !X509Utils.IsECDsaSignature(certificate)) + { + deleteCert = true; + } - await store.Delete(certificates[ii].Thumbprint).ConfigureAwait(false); - break; + if (deleteCert) + { + Utils.LogCertificate("Delete Certificate from trusted store.", certificate); + await store.Delete(certificates[ii].Thumbprint).ConfigureAwait(false); + break; + } } } diff --git a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs index 753c2666a..3ecad157e 100644 --- a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs @@ -29,6 +29,7 @@ using System.Threading.Tasks; using System.Xml; +using System; namespace Opc.Ua.Configuration { @@ -353,6 +354,22 @@ public interface IApplicationConfigurationBuilderServerPolicies /// Add policies if true. IApplicationConfigurationBuilderServerSelected AddSignAndEncryptPolicies(bool addPolicies = true); + /// + /// Add the ECCsign security policies to the server configuration. + /// + /// + /// The policies are only added if the platform supports the ECC policies. + /// + IApplicationConfigurationBuilderServerSelected AddEccSignPolicies(); + + /// + /// Add the ECC sign and encrypt security policies to the server configuration. + /// + /// + /// The policies are only added if the platform supports the ECC policies. + /// + IApplicationConfigurationBuilderServerSelected AddEccSignAndEncryptPolicies(); + /// /// Add the specified security policy with the specified security mode. /// @@ -391,12 +408,31 @@ public interface IApplicationConfigurationBuilderSecurity /// The path to the pki root. By default all cert stores use the pki root. /// The path to the app cert store, if different than the pki root. /// The path to the rejected certificate store. + [Obsolete("Use AddSecurityConfiguration(CertificateIdentifierCollection certIdList, string pkiRoot = null, string rejectedRoot = null) instead.")] IApplicationConfigurationBuilderSecurityOptions AddSecurityConfiguration( string subjectName, string pkiRoot = null, string appRoot = null, string rejectedRoot = null ); + + /// + /// Add the security configuration. + /// + /// + /// The pki root path default to the certificate store + /// location as defined in + /// A defaults to the corresponding default store location. + /// + /// A list of Certificate identifiers + /// The path to the pki root. By default all cert stores use the pki root. + /// The path to the rejected certificate store. + + IApplicationConfigurationBuilderSecurityOptions AddSecurityConfiguration( + CertificateIdentifierCollection certIdList, + string pkiRoot = null, + string rejectedRoot = null + ); /// /// Add the security configuration for mandatory application, issuer and trusted stores. @@ -451,6 +487,16 @@ public interface IApplicationConfigurationBuilderSecurityOptions : IApplicationConfigurationBuilderExtension, IApplicationConfigurationBuilderCreate { + /// + /// The certificate types that should be supported. + /// + /// + /// At this time the following types are supported, if the platform OS and .NET version can handle it. + /// Rsa,nistP256,nistP384,brainpoolP256r1,brainpoolP384r1 + /// + /// A list of Certificate identifiers + IApplicationConfigurationBuilderSecurityOptions SetApplicationCertificates(CertificateIdentifierCollection certIdList); + /// /// The number of rejected certificates to keep in the store. /// diff --git a/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj b/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj index d1886c427..3ce7672b2 100644 --- a/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj +++ b/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj @@ -12,6 +12,17 @@ true + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + $(PackageId).Debug diff --git a/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs b/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs index 4d929cb0f..dade105e4 100644 --- a/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs +++ b/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs @@ -193,7 +193,8 @@ public int KeySize if (Certificate != null) { #if ECC_SUPPORT - // TODO + // TODO use X509Utils.GetPublicKeySize(Certificate); everywhere + #endif return X509Utils.GetRSAPublicKeySize(Certificate); } diff --git a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/CertificateBuilder.cs b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/CertificateBuilder.cs index 9d4ea2727..0e34b98ab 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/CertificateBuilder.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/CertificateBuilder.cs @@ -149,7 +149,7 @@ public static byte[] CreatePfxWithRSAPrivateKey( { return X509Utils.CreatePfxWithPrivateKey( x509, friendlyName, - X509Utils.GetPrivateKeyParameter(privateKey), + X509Utils.GetRsaPrivateKeyParameter(privateKey), passcode, new SecureRandom(cfrg)); } @@ -170,8 +170,8 @@ public static byte[] CreateSigningRequest( SecureRandom random = new SecureRandom(cfrg); // try to get signing/private key from certificate passed in - AsymmetricKeyParameter signingKey = X509Utils.GetPrivateKeyParameter(certificate); - RsaKeyParameters publicKey = X509Utils.GetPublicKeyParameter(certificate); + AsymmetricKeyParameter signingKey = X509Utils.GetRsaPrivateKeyParameter(certificate); + RsaKeyParameters publicKey = X509Utils.GetRsaPublicKeyParameter(certificate); ISignatureFactory signatureFactory = new Asn1SignatureFactory(X509Utils.GetRSAHashAlgorithm(X509Defaults.HashAlgorithmName), signingKey, random); @@ -326,7 +326,7 @@ private void CreateExtensions(X509V3CertificateGenerator cg, AsymmetricKeyParame BigInteger issuerSerialNumber; if (IssuerCAKeyCert != null) { - issuerPublicKey = X509Utils.GetPublicKeyParameter(IssuerCAKeyCert); + issuerPublicKey = X509Utils.GetRsaPublicKeyParameter(IssuerCAKeyCert); issuerSerialNumber = X509Utils.GetSerialNumber(IssuerCAKeyCert); } else @@ -405,7 +405,7 @@ private X509Certificate2 CreateForRSAWithPublicKey(ISignatureFactory signatureFa CreateMandatoryFields(cg); // set public key - AsymmetricKeyParameter subjectPublicKey = X509Utils.GetPublicKeyParameter(m_rsaPublicKey); + AsymmetricKeyParameter subjectPublicKey = X509Utils.GetRsaPublicKeyParameter(m_rsaPublicKey); cg.SetPublicKey(subjectPublicKey); CreateExtensions(cg, subjectPublicKey); @@ -413,7 +413,7 @@ private X509Certificate2 CreateForRSAWithPublicKey(ISignatureFactory signatureFa // sign certificate by issuer if (signatureFactory == null) { - AsymmetricKeyParameter signingKey = X509Utils.GetPrivateKeyParameter(IssuerCAKeyCert); + AsymmetricKeyParameter signingKey = X509Utils.GetRsaPrivateKeyParameter(IssuerCAKeyCert); signatureFactory = new Asn1SignatureFactory(X509Utils.GetRSAHashAlgorithm(HashAlgorithmName), signingKey); } Org.BouncyCastle.X509.X509Certificate x509 = cg.Generate(signatureFactory); @@ -459,8 +459,8 @@ private byte[] CreatePfxForRSA(string passcode, ISignatureFactory signatureFacto AsymmetricKeyParameter subjectPrivateKey = null; using (var rsa = new RSACryptoServiceProvider(m_keySize == 0 ? X509Defaults.RSAKeySize : m_keySize)) { - subjectPublicKey = X509Utils.GetPublicKeyParameter(rsa); - subjectPrivateKey = X509Utils.GetPrivateKeyParameter(rsa); + subjectPublicKey = X509Utils.GetRsaPublicKeyParameter(rsa); + subjectPrivateKey = X509Utils.GetRsaPrivateKeyParameter(rsa); } cg.SetPublicKey(subjectPublicKey); @@ -473,7 +473,7 @@ private byte[] CreatePfxForRSA(string passcode, ISignatureFactory signatureFacto if (IssuerCAKeyCert != null) { // signed by issuer - signingKey = X509Utils.GetPrivateKeyParameter(IssuerCAKeyCert); + signingKey = X509Utils.GetRsaPrivateKeyParameter(IssuerCAKeyCert); } else { diff --git a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMWriter.cs b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMWriter.cs index ebfad7f8e..5a1e7189d 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMWriter.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/PEMWriter.cs @@ -52,14 +52,32 @@ public static byte[] ExportPrivateKeyAsPEM( string password = null ) { - if (!String.IsNullOrEmpty(password)) throw new ArgumentException("Export with password not supported on this platform.", nameof(password)); - RsaPrivateCrtKeyParameters privateKeyParameter = X509Utils.GetPrivateKeyParameter(certificate); - // write private key as PKCS#8 - PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParameter); - byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetDerEncoded(); - return EncodeAsPEM(serializedPrivateBytes, "PRIVATE KEY"); + bool isECDsaSignature = X509PfxUtils.IsECDsaSignature(certificate); + // check if certificate is valid for use as app/sw or user cert + if (!isECDsaSignature) + { + if (!String.IsNullOrEmpty(password)) throw new ArgumentException("Export with password not supported on this platform.", nameof(password)); + RsaPrivateCrtKeyParameters privateKeyParameter = X509Utils.GetRsaPrivateKeyParameter(certificate); + // write private key as PKCS#8 + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParameter); + byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetDerEncoded(); + return EncodeAsPEM(serializedPrivateBytes, "PRIVATE KEY"); + } +#if ECC_SUPPORT + else + { + if (!String.IsNullOrEmpty(password)) throw new ArgumentException("Export with password not supported on this platform.", nameof(password)); + ECPrivateKeyParameters privateKeyParameter = X509Utils.GetECPrivateKeyParameter(certificate.GetECDsaPrivateKey()); + // write private key as PKCS#8 + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParameter); + byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetDerEncoded(); + return EncodeAsPEM(serializedPrivateBytes, "PRIVATE KEY"); + } +#else + throw new ArgumentException("ExportPrivateKeyAsPEM not supported on this platform."); // Only on NETSTANDARD2_0 +#endif } - #endregion +#endregion } } #endif diff --git a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/X509Utils.cs b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/X509Utils.cs index 6e6495c29..0c518e0f2 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/X509Utils.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Org.BouncyCastle/X509Utils.cs @@ -33,7 +33,10 @@ using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; @@ -104,18 +107,18 @@ internal static string GetRSAHashAlgorithm(HashAlgorithmName hashAlgorithmName) /// /// Get public key parameters from a X509Certificate2 /// - internal static RsaKeyParameters GetPublicKeyParameter(X509Certificate2 certificate) + internal static RsaKeyParameters GetRsaPublicKeyParameter(X509Certificate2 certificate) { using (RSA rsa = certificate.GetRSAPublicKey()) { - return GetPublicKeyParameter(rsa); + return GetRsaPublicKeyParameter(rsa); } } /// /// Get public key parameters from a RSA. /// - internal static RsaKeyParameters GetPublicKeyParameter(RSA rsa) + internal static RsaKeyParameters GetRsaPublicKeyParameter(RSA rsa) { RSAParameters rsaParams = rsa.ExportParameters(false); return new RsaKeyParameters( @@ -128,20 +131,24 @@ internal static RsaKeyParameters GetPublicKeyParameter(RSA rsa) /// Get private key parameters from a X509Certificate2. /// The private key must be exportable. /// - internal static RsaPrivateCrtKeyParameters GetPrivateKeyParameter(X509Certificate2 certificate) + internal static RsaPrivateCrtKeyParameters GetRsaPrivateKeyParameter(X509Certificate2 certificate) { // try to get signing/private key from certificate passed in using (RSA rsa = certificate.GetRSAPrivateKey()) { - return GetPrivateKeyParameter(rsa); + if (rsa != null) + { + return GetRsaPrivateKeyParameter(rsa); + } } + return null; } /// /// Get private key parameters from a RSA private key. /// The private key must be exportable. /// - internal static RsaPrivateCrtKeyParameters GetPrivateKeyParameter(RSA rsa) + internal static RsaPrivateCrtKeyParameters GetRsaPrivateKeyParameter(RSA rsa) { RSAParameters rsaParams = rsa.ExportParameters(true); return new RsaPrivateCrtKeyParameters( @@ -155,6 +162,120 @@ internal static RsaPrivateCrtKeyParameters GetPrivateKeyParameter(RSA rsa) new BigInteger(1, rsaParams.InverseQ)); } +#if NET472_OR_GREATER + /// + /// Get BouncyCastle format private key parameters from a System.Security.Cryptography.ECDsa. + /// The private key must be exportable. + /// + internal static ECPrivateKeyParameters GetECPrivateKeyParameter(ECDsa ec) + { + ECParameters ecParams = ec.ExportParameters(true); + BigInteger d = new BigInteger(1, ecParams.D); + + X9ECParameters curve = GetX9ECParameters(ecParams); + + if (curve == null) throw new ArgumentException("Curve OID is not recognized ", ecParams.Curve.Oid.ToString()); + ECDomainParameters domainParameters = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H); + return new ECPrivateKeyParameters(d, domainParameters); + + } + + /// + /// Get BouncyCastle format public key parameters from a System.Security.Cryptography.ECDsa + /// + internal static ECPublicKeyParameters GetECPublicKeyParameters(ECDsa ec) + { + ECParameters ecParams = ec.ExportParameters(false); + + X9ECParameters curve = GetX9ECParameters(ecParams); + + if (curve == null) throw new ArgumentException("Curve OID is not recognized ", ecParams.Curve.Oid.ToString()); + + var q = curve.Curve.CreatePoint( + new BigInteger(1, ecParams.Q.X), + new BigInteger(1, ecParams.Q.Y)); + + ECDomainParameters domainParameters = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed()); + + return new ECPublicKeyParameters(q, domainParameters); + + } + + /// + /// Return Bouncy Castle X9ECParameters value equivalent of System.Security.Cryptography.ECparameters + /// + /// + /// X9ECParameters value equivalent of System.Security.Cryptography.ECparameters if found else null + internal static X9ECParameters GetX9ECParameters(ECParameters ecParams) + { + if (!string.IsNullOrEmpty(ecParams.Curve.Oid.Value)) + { + var oid = new DerObjectIdentifier(ecParams.Curve.Oid.Value); + return ECNamedCurveTable.GetByOid(oid); + } + else if (!string.IsNullOrEmpty(ecParams.Curve.Oid.FriendlyName)) + { + // nist curve names do not contain "nist" in the bouncy castle ECNamedCurveTable + // for ex: the form is "P-256" while the microsoft is "nistP256" + // brainpool bouncy castle curve names are identic to the microsoft ones + string msFriendlyName = ecParams.Curve.Oid.FriendlyName; + string bcFriendlyName = msFriendlyName; + string nistCurveName = "nist"; + if (msFriendlyName.StartsWith(nistCurveName)) + { + string patternMatch = @"(.*?)(\d+)$"; // divide string in two capture groups (string & numeric) + bcFriendlyName = Regex.Replace(msFriendlyName, patternMatch, m => { + string lastChar = m.Groups[1].Value.Length > 0 ? m.Groups[1].Value.Last().ToString() : ""; + string number = m.Groups[2].Value; + return lastChar + "-" + number; + }); + } + return ECNamedCurveTable.GetByName(bcFriendlyName); + } + + return null; + } + + /// + /// Identifies a named curve by the provided coeficients A and B from their first 4 bytes + /// + /// + /// + /// The successfully identified named System.Security.Cryptography.ECCurve curve + /// or throws if no curve is identified + internal static ECCurve IdentifyEccCurveByCoefficients(byte[] a, byte[] b) + { + byte[] brainpoolP256AStart = new byte[] { 0x7D, 0x5A, 0x09, 0x75 }; + byte[] brainpoolP256BStart = new byte[] { 0x26, 0xDC, 0x5C, 0x6C }; + byte[] brainpoolP384AStart = new byte[] { 0x7B, 0xC3, 0x82, 0xC6 }; + byte[] brainpoolP384BStart = new byte[] { 0x04, 0xA8, 0xC7, 0xDD }; + byte[] nistP256AStart = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + byte[] nistP256BStart = new byte[] { 0x5A, 0xC6, 0x35, 0xD8 }; + byte[] nistP384AStart = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + byte[] nistP384BStart = new byte[] { 0xB3, 0x31, 0x2F, 0xA7 }; + + + if (a.Take(4).SequenceEqual(brainpoolP256AStart) && b.Take(4).SequenceEqual(brainpoolP256BStart)) + { + return ECCurve.NamedCurves.brainpoolP256r1; + } + else if (a.Take(4).SequenceEqual(brainpoolP384AStart) && b.Take(4).SequenceEqual(brainpoolP384BStart)) + { + return ECCurve.NamedCurves.brainpoolP384r1; + } + else if (a.Take(4).SequenceEqual(nistP256AStart) && b.Take(4).SequenceEqual(nistP256BStart)) + { + return ECCurve.NamedCurves.nistP256; + } + else if (a.Take(4).SequenceEqual(nistP384AStart) && b.Take(4).SequenceEqual(nistP384BStart)) + { + return ECCurve.NamedCurves.nistP384; + } + + throw new ArgumentException("EccCurveByCoefficients cannot be identified"); + + } +#endif /// /// Get the serial number from a certificate as BigInteger. diff --git a/Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs b/Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs index 1f0e6e43b..0d4bd6de2 100644 --- a/Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs +++ b/Libraries/Opc.Ua.Security.Certificates/PEM/PEMReader.cs @@ -41,7 +41,7 @@ namespace Opc.Ua.Security.Certificates /// public static class PEMReader { - #region Public Methods +#region Public Methods /// /// Import a PKCS#8 private key or RSA private key from PEM. /// The PKCS#8 private key may be encrypted using a password. @@ -109,10 +109,10 @@ public static RSA ImportPrivateKeyFromPEM( } throw new ArgumentException("No private PEM key found."); } - #endregion +#endregion - #region Private Methods - #endregion +#region Private Methods +#endregion } } #endif diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/CertificateBuilder.cs b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/CertificateBuilder.cs index fe8acad42..1bce108a0 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/CertificateBuilder.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/CertificateBuilder.cs @@ -30,10 +30,20 @@ #if NETSTANDARD2_1 || NET472_OR_GREATER || NET5_0_OR_GREATER using System; +using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + +#if NET472_OR_GREATER +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Math.EC; +#endif + namespace Opc.Ua.Security.Certificates { /// @@ -188,7 +198,7 @@ public override X509Certificate2 CreateForECDsa() ECDsa publicKey = m_ecdsaPublicKey; if (publicKey == null) { - key = ECDsa.Create((ECCurve)m_curve); + key = ECDsa.Create((System.Security.Cryptography.ECCurve)m_curve); publicKey = key; } @@ -242,7 +252,7 @@ public override X509Certificate2 CreateForECDsa(X509SignatureGenerator generator ECDsa publicKey = m_ecdsaPublicKey; if (publicKey == null) { - key = ECDsa.Create((ECCurve)m_curve); + key = ECDsa.Create((System.Security.Cryptography.ECCurve)m_curve); publicKey = key; } @@ -266,14 +276,60 @@ public override X509Certificate2 CreateForECDsa(X509SignatureGenerator generator public override ICertificateBuilderCreateForECDsaAny SetECDsaPublicKey(byte[] publicKey) { if (publicKey == null) throw new ArgumentNullException(nameof(publicKey)); -#if NET472_OR_GREATER - throw new NotSupportedException("Import a ECDsaPublicKey is not supported on this platform."); -#else + int bytes = 0; try { m_ecdsaPublicKey = ECDsa.Create(); +#if NET472_OR_GREATER + + var asymmetricPubKeyParameters = Org.BouncyCastle.Security.PublicKeyFactory.CreateKey(publicKey) as Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters; + if (asymmetricPubKeyParameters == null) + { + throw new ArgumentException("Invalid public key format or key type."); + } + + var asn1Obj = Asn1Object.FromByteArray(publicKey); + var publicKeyInfo = SubjectPublicKeyInfo.GetInstance(asn1Obj); + var algParams = publicKeyInfo.Algorithm.Parameters; + var x962Params = X962Parameters.GetInstance(algParams); + var ecPoint = publicKeyInfo.PublicKey.GetBytes(); + + ECParameters ecParameters = new ECParameters(); + X9ECParameters ecParametersAsn1 = null; + + if (x962Params.IsNamedCurve) + { + // Named + var namedCurveOid = (DerObjectIdentifier)x962Params.Parameters; + string curveName = namedCurveOid.Id; + + var ecCurve = System.Security.Cryptography.ECCurve.CreateFromOid(new Oid(curveName)); + ecParameters.Curve = ecCurve; + } + else + { + // Explicit but still need to create the curve as named since the platform does not support it's creation + // otherwise + ecParametersAsn1 = X9ECParameters.GetInstance(x962Params.Parameters); + ecParameters.Curve = BouncyCastle.X509Utils.IdentifyEccCurveByCoefficients(ecParametersAsn1.Curve.A.GetEncoded(), + ecParametersAsn1.Curve.B.GetEncoded());// instead of ecParametersAsn1.Curve; + + } + + //Extract the public key coordinates + var publicKeyPoint = new X9ECPoint(ecParametersAsn1.Curve, ecPoint).Point; + ecParameters.Q = new System.Security.Cryptography.ECPoint { + X = publicKeyPoint.AffineXCoord.ToBigInteger().ToByteArrayUnsigned(), + Y = publicKeyPoint.AffineYCoord?.ToBigInteger().ToByteArrayUnsigned() + }; + + m_ecdsaPublicKey.ImportParameters(ecParameters); + bytes = publicKey.Length; + +#else m_ecdsaPublicKey.ImportSubjectPublicKeyInfo(publicKey, out bytes); +#endif } catch (Exception e) { @@ -287,7 +343,6 @@ public override ICertificateBuilderCreateForECDsaAny SetECDsaPublicKey(byte[] pu return this; #endif } -#endif /// public override ICertificateBuilderCreateForRSAAny SetRSAPublicKey(byte[] publicKey) @@ -359,7 +414,7 @@ private void CreateX509Extensions(CertificateRequest request, bool forECDsa) // Authority Key Identifier if (X509Extensions.FindExtension(m_extensions) == null) { - X509Extension authorityKeyIdentifier = IssuerCAKeyCert != null + System.Security.Cryptography.X509Certificates.X509Extension authorityKeyIdentifier = IssuerCAKeyCert != null ? X509Extensions.BuildAuthorityKeyIdentifier(IssuerCAKeyCert) : new X509AuthorityKeyIdentifierExtension( ski.SubjectKeyIdentifier.FromHexString(), @@ -403,7 +458,7 @@ private void CreateX509Extensions(CertificateRequest request, bool forECDsa) true)); } - if (!m_isCA) + if (!m_isCA && !forECDsa) { if (X509Extensions.FindExtension(m_extensions) == null) { @@ -417,7 +472,7 @@ private void CreateX509Extensions(CertificateRequest request, bool forECDsa) } } - foreach (X509Extension extension in m_extensions) + foreach (System.Security.Cryptography.X509Certificates.X509Extension extension in m_extensions) { request.CertificateExtensions.Add(extension); } diff --git a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs index 03ad62b1f..4edf156f4 100644 --- a/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs +++ b/Libraries/Opc.Ua.Security.Certificates/X509Certificate/X509PfxUtils.cs @@ -63,6 +63,28 @@ private static X509KeyUsageFlags GetKeyUsage(X509Certificate2 cert) return allFlags; } + /// + /// Verify key pair of two certificates. + /// + public static bool VerifyKeyPair( + X509Certificate2 certWithPublicKey, + X509Certificate2 certWithPrivateKey, + bool throwOnError = false) + { + if (IsECDsaSignature(certWithPublicKey)) + { +#if ECC_SUPPORT + return VerifyECDsaKeyPair(certWithPublicKey, certWithPrivateKey, throwOnError); +#else + throw new NotSupportedException("This platform does not support ECC."); +#endif + } + else + { + return VerifyRSAKeyPair(certWithPublicKey, certWithPrivateKey, throwOnError); + } + } + /// /// Verify RSA key pair of two certificates. /// @@ -157,8 +179,7 @@ public static X509Certificate2 CreateCertificateFromPKCS12( rawData, password ?? string.Empty, flag); - // can we really access the private key? - if (VerifyRSAKeyPair(certificate, certificate, true)) + if (VerifyKeyPair(certificate, certificate, true)) { return certificate; } @@ -210,6 +231,22 @@ internal static bool VerifyRSAKeyPairSign( return rsaPublicKey.VerifyData(testBlock, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } + /// + /// If the certificate has a ECDsa signature. + /// + public static bool IsECDsaSignature(X509Certificate2 cert) + { + switch (cert.SignatureAlgorithm.Value) + { + case Oids.ECDsaWithSha1: + case Oids.ECDsaWithSha256: + case Oids.ECDsaWithSha384: + case Oids.ECDsaWithSha512: + return true; + } + return false; + } + #if ECC_SUPPORT /// /// Verify ECDsa key pair of two certificates. diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 55d0f35d7..46ff7b3ae 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -33,6 +33,8 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.Xml; +using Opc.Ua.Security; + namespace Opc.Ua.Server { @@ -80,11 +82,20 @@ ApplicationConfiguration configuration ServerCertificateGroup defaultApplicationGroup = new ServerCertificateGroup { NodeId = Opc.Ua.ObjectIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup, BrowseName = Opc.Ua.BrowseNames.DefaultApplicationGroup, - CertificateTypes = new NodeId[] { ObjectTypeIds.RsaSha256ApplicationCertificateType }, - ApplicationCertificate = configuration.SecurityConfiguration.ApplicationCertificate, + CertificateTypes = new NodeId[]{}, + ApplicationCertificates = new CertificateIdentifierCollection(), IssuerStore = new CertificateStoreIdentifier(configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath), TrustedStore = new CertificateStoreIdentifier(configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath) }; + + // For each certificate in ApplicationCertificates, add the certificate type to ServerConfiguration_CertificateGroups_DefaultApplicationGroup + // under the CertificateTypes field. + foreach (var cert in configuration.SecurityConfiguration.ApplicationCertificates) + { + defaultApplicationGroup.CertificateTypes = defaultApplicationGroup.CertificateTypes.Concat(new NodeId[] { cert.CertificateType }).ToArray(); + defaultApplicationGroup.ApplicationCertificates.Add(cert); + } + m_certificateGroups.Add(defaultApplicationGroup); } #endregion @@ -377,6 +388,28 @@ private ServiceResult UpdateCertificate( ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(certificateGroupId, certificateTypeId); certificateGroup.UpdateCertificate = null; + try + { + newCert = new X509Certificate2(certificate); + } + catch + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Certificate data is invalid."); + } + + // identify the existing certificate to be updated + // it should be of the same type and same subject name as the new certificate + CertificateIdentifier existingCertIdentifier = certificateGroup.ApplicationCertificates.FirstOrDefault(cert => + X509Utils.CompareDistinguishedName(cert.Certificate.Subject, newCert.Subject) && + cert.CertificateType == certificateTypeId); + + // if there is no such existing certificate then this is an error + if (existingCertIdentifier == null) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, "No existing certificate found for the specified certificate type and subject name."); + } + + X509Certificate2Collection newIssuerCollection = new X509Certificate2Collection(); try @@ -391,7 +424,6 @@ private ServiceResult UpdateCertificate( } } - newCert = new X509Certificate2(certificate); } catch { @@ -403,7 +435,7 @@ private ServiceResult UpdateCertificate( // TODO: An issuer may modify the subject of an issued certificate, // but then the configuration must be updated too! // NOTE: not a strict requirement here for ASN.1 byte compare - if (!X509Utils.CompareDistinguishedName(certificateGroup.ApplicationCertificate.Certificate.Subject, newCert.Subject)) + if (!X509Utils.CompareDistinguishedName(existingCertIdentifier.Certificate.Subject, newCert.Subject)) { throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Subject Name of new certificate doesn't match the application."); } @@ -421,6 +453,8 @@ private ServiceResult UpdateCertificate( { // verify cert with issuer chain CertificateValidator certValidator = new CertificateValidator(); +// TODO: why? + certValidator.MinimumCertificateKeySize = 1024; CertificateTrustList issuerStore = new CertificateTrustList(); CertificateIdentifierCollection issuerCollection = new CertificateIdentifierCollection(); foreach (var issuerCert in newIssuerCollection) @@ -446,20 +480,20 @@ private ServiceResult UpdateCertificate( case null: case "": { - X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; + X509Certificate2 certWithPrivateKey = existingCertIdentifier.LoadPrivateKeyEx(passwordProvider).Result; var exportableKey = X509Utils.CreateCopyWithPrivateKey(certWithPrivateKey, false); updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPrivateKey(newCert, exportableKey); break; } case "PFX": { - X509Certificate2 certWithPrivateKey = X509Utils.CreateCertificateFromPKCS12(privateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate), true); + X509Certificate2 certWithPrivateKey = X509Utils.CreateCertificateFromPKCS12(privateKey, passwordProvider?.GetPassword(existingCertIdentifier), true); updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPrivateKey(newCert, certWithPrivateKey); break; } case "PEM": { - updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(newCert, privateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)); + updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(newCert, privateKey, passwordProvider?.GetPassword(existingCertIdentifier)); break; } } @@ -478,13 +512,13 @@ private ServiceResult UpdateCertificate( { try { - using (ICertificateStore appStore = certificateGroup.ApplicationCertificate.OpenStore()) + using (ICertificateStore appStore = existingCertIdentifier.OpenStore()) { - Utils.LogCertificate(Utils.TraceMasks.Security, "Delete application certificate: ", certificateGroup.ApplicationCertificate.Certificate); - appStore.Delete(certificateGroup.ApplicationCertificate.Thumbprint).Wait(); + Utils.LogCertificate(Utils.TraceMasks.Security, "Delete application certificate: ", existingCertIdentifier.Certificate); + appStore.Delete(existingCertIdentifier.Thumbprint).Wait(); Utils.LogCertificate(Utils.TraceMasks.Security, "Add new application certificate: ", updateCertificate.CertificateWithPrivateKey); var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; - appStore.Add(updateCertificate.CertificateWithPrivateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)).Wait(); + appStore.Add(updateCertificate.CertificateWithPrivateKey, passwordProvider?.GetPassword(existingCertIdentifier)).Wait(); // keep only track of cert without private key var certOnly = new X509Certificate2(updateCertificate.CertificateWithPrivateKey.RawData); updateCertificate.CertificateWithPrivateKey.Dispose(); @@ -548,6 +582,11 @@ private ServiceResult CreateSigningRequest( ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(certificateGroupId, certificateTypeId); + // identify the existing certificate for which to CreateSigningRequest + // it should be of the same type + CertificateIdentifier existingCertIdentifier = certificateGroup.ApplicationCertificates.FirstOrDefault(cert => + cert.CertificateType == certificateTypeId); + if (!String.IsNullOrEmpty(subjectName)) { throw new ArgumentNullException(nameof(subjectName)); @@ -557,7 +596,7 @@ private ServiceResult CreateSigningRequest( // TODO: use nonce for generating the private key var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; - X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; + X509Certificate2 certWithPrivateKey = existingCertIdentifier.LoadPrivateKeyEx(passwordProvider).Result; Utils.LogCertificate(Utils.TraceMasks.Security, "Create signing request: ", certWithPrivateKey); certificateRequest = CertificateFactory.CreateSigningRequest(certWithPrivateKey, X509Utils.GetDomainsFromCertificate(certWithPrivateKey)); return ServiceResult.Good; @@ -662,7 +701,7 @@ private ServiceResult GetCertificates( { certificateTypeIds = new NodeId[1] { certificateTypeId }; certificates = new byte[1][]; - certificates[0] = certificateGroup.ApplicationCertificate.Certificate.GetRawCertData(); + certificates[0] = certificateGroup.ApplicationCertificates[0].Certificate.GetRawCertData(); } else { @@ -815,7 +854,7 @@ private class ServerCertificateGroup public NodeId NodeId; public CertificateGroupState Node; public NodeId[] CertificateTypes; - public CertificateIdentifier ApplicationCertificate; + public CertificateIdentifierCollection ApplicationCertificates; public CertificateStoreIdentifier IssuerStore; public CertificateStoreIdentifier TrustedStore; public UpdateCertificateData UpdateCertificate; diff --git a/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj b/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj index a2cc79d83..0c4e12c59 100644 --- a/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj +++ b/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj @@ -21,6 +21,17 @@ $(DefineConstants);SIGNASSEMBLY + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index ba9f82df2..94cf3a92a 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -65,13 +65,13 @@ public class ServerInternalData : IServerInternal, IDisposable /// The configuration. /// The message context. /// The certificate validator. - /// The instance certificate. + /// The certificate type provider. public ServerInternalData( ServerProperties serverDescription, ApplicationConfiguration configuration, IServiceMessageContext messageContext, CertificateValidator certificateValidator, - X509Certificate2 instanceCertificate) + CertificateTypesProvider instanceCertificateProvider) { m_serverDescription = serverDescription; m_configuration = configuration; @@ -448,7 +448,8 @@ public bool IsRunning if (m_serverStatus.Value.State == ServerState.Running) return true; - if (m_serverStatus.Value.State == ServerState.Shutdown && m_serverStatus.Value.SecondsTillShutdown > 0) + if (m_serverStatus.Value.State == ServerState.Shutdown && + m_serverStatus.Value.SecondsTillShutdown > 0) return true; return false; diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 706f6647b..f353f7de5 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -368,6 +368,8 @@ public override ResponseHeader CreateSession( requireEncryption = true; } + X509Certificate2Collection clientIssuerCertifficates = null; + // validate client application instance certificate. X509Certificate2 parsedClientCertificate = null; @@ -378,6 +380,15 @@ public override ResponseHeader CreateSession( X509Certificate2Collection clientCertificateChain = Utils.ParseCertificateChainBlob(clientCertificate); parsedClientCertificate = clientCertificateChain[0]; + if (clientCertificateChain.Count > 1) + { + clientIssuerCertifficates = new X509Certificate2Collection(); + for (int i = 1; i < clientCertificateChain.Count; i++) + { + clientIssuerCertifficates.Add(clientCertificateChain[i]); + } + } + if (context.SecurityPolicyUri != SecurityPolicies.None) { string certificateApplicationUri = X509Utils.GetApplicationUriFromCertificate(parsedClientCertificate); @@ -423,15 +434,19 @@ public override ResponseHeader CreateSession( } } + // load the certificate for the security profile + X509Certificate2 instanceCertificate = InstanceCertificateTypesProvider.GetInstanceCertificate(context.SecurityPolicyUri); + // create the session. session = ServerInternal.SessionManager.CreateSession( context, - requireEncryption ? InstanceCertificate : null, + instanceCertificate, sessionName, clientNonce, clientDescription, endpointUrl, parsedClientCertificate, + clientIssuerCertifficates, requestedSessionTimeout, maxResponseMessageSize, out sessionId, @@ -448,7 +463,7 @@ public override ResponseHeader CreateSession( EndpointUrl = new Uri(endpointUrl) }; - CertificateValidator.ValidateDomains(InstanceCertificate, configuredEndpoint, true); + CertificateValidator.ValidateDomains(instanceCertificate, configuredEndpoint, true); } catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadCertificateHostNameInvalid) { @@ -457,28 +472,27 @@ public override ResponseHeader CreateSession( } } +#if ECC_SUPPORT + var parameters = ExtensionObject.ToEncodeable(requestHeader.AdditionalHeader) as AdditionalParametersType; + + if (parameters != null) + { + parameters = CreateSessionProcessAdditionalParameters(session, parameters); + } +#endif lock (m_lock) { // return the application instance certificate for the server. if (requireEncryption) { // check if complete chain should be sent. - if (Configuration.SecurityConfiguration.SendCertificateChain && - InstanceCertificateChain != null && - InstanceCertificateChain.Count > 1) + if (Configuration.SecurityConfiguration.SendCertificateChain) { - List serverCertificateChain = new List(); - - for (int i = 0; i < InstanceCertificateChain.Count; i++) - { - serverCertificateChain.AddRange(InstanceCertificateChain[i].RawData); - } - - serverCertificate = serverCertificateChain.ToArray(); + serverCertificate = InstanceCertificateTypesProvider.LoadCertificateChainRawAsync(instanceCertificate).GetAwaiter().GetResult(); } else { - serverCertificate = InstanceCertificate.RawData; + serverCertificate = instanceCertificate.RawData; } } @@ -495,7 +509,7 @@ public override ResponseHeader CreateSession( if (parsedClientCertificate != null && clientNonce != null) { byte[] dataToSign = Utils.Append(parsedClientCertificate.RawData, clientNonce); - serverSignature = SecurityPolicies.Sign(InstanceCertificate, context.SecurityPolicyUri, dataToSign); + serverSignature = SecurityPolicies.Sign(instanceCertificate, context.SecurityPolicyUri, dataToSign); } } @@ -510,7 +524,16 @@ public override ResponseHeader CreateSession( // report audit for successful create session ServerInternal.ReportAuditCreateSessionEvent(context?.AuditEntryId, session, revisedSessionTimeout); - return CreateResponse(requestHeader, StatusCodes.Good); + ResponseHeader responseHeader = CreateResponse(requestHeader, StatusCodes.Good); + +#if ECC_SUPPORT + if (parameters != null) + { + responseHeader.AdditionalHeader = new ExtensionObject(parameters); + } +#endif + + return responseHeader; } catch (ServiceResultException e) { @@ -544,6 +567,68 @@ public override ResponseHeader CreateSession( } } +#if ECC_SUPPORT + /// + /// Process additional parameters during the ECC session creation and set the session's UserToken security policy + /// + /// The session + /// The additional parameters for the session + /// An AdditionalParametersType object containing the processed parameters + protected virtual AdditionalParametersType CreateSessionProcessAdditionalParameters(Session session, AdditionalParametersType parameters) + { + AdditionalParametersType response = null; + + if (parameters != null && parameters.Parameters != null) + { + response = new AdditionalParametersType(); + + foreach (var ii in parameters.Parameters) + { + if (ii.Key == "ECDHPolicyUri") + { + var policyUri = ii.Value.ToString(); + + if (EccUtils.IsEccPolicy(policyUri)) + { + session.SetEccUserTokenSecurityPolicy(policyUri); + var key = session.GetNewEccKey(); + response.Parameters.Add(new KeyValuePair() { Key = "ECDHKey", Value = new ExtensionObject(key) }); + } + else + { + response.Parameters.Add(new KeyValuePair() { Key = "ECDHKey", Value = StatusCodes.BadSecurityPolicyRejected }); + } + } + } + } + + return response; + } + + /// + /// Process additional parameters during ECC session activation + /// + /// The session + /// The additional parameters for the session + /// An AdditionalParametersType object containing the processed parameters + protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParameters(Session session, AdditionalParametersType parameters) + { + AdditionalParametersType response = null; + + var key = session.GetNewEccKey(); + + if (key != null) + { + response = new AdditionalParametersType(); + response.Parameters.Add(new KeyValuePair() { Key = "ECDHKey", Value = new ExtensionObject(key) }); + } + + return response; + } + +#endif + + /// /// Invokes the ActivateSession service. /// @@ -651,13 +736,26 @@ public override ResponseHeader ActivateSession( // TBD - call Node Manager and Subscription Manager. } + Session session = ServerInternal.SessionManager.GetSession(requestHeader.AuthenticationToken); +#if ECC_SUPPORT + var parameters = ExtensionObject.ToEncodeable(requestHeader.AdditionalHeader) as AdditionalParametersType; + parameters = ActivateSessionProcessAdditionalParameters(session, parameters); +#endif + Utils.LogInfo("Server - SESSION ACTIVATED."); // report the audit event for session activate - Session session = ServerInternal.SessionManager.GetSession(requestHeader.AuthenticationToken); ServerInternal.ReportAuditActivateSessionEvent(context?.AuditEntryId, session, softwareCertificates); - return CreateResponse(requestHeader, StatusCodes.Good); + ResponseHeader responseHeader = CreateResponse(requestHeader, StatusCodes.Good); + +#if ECC_SUPPORT + if (parameters != null) + { + responseHeader.AdditionalHeader = new ExtensionObject(parameters); + } +#endif + return responseHeader; } catch (ServiceResultException e) { @@ -2205,9 +2303,9 @@ public override ResponseHeader Call( OnRequestComplete(context); } } - #endregion +#endregion - #region Public Methods used by the Host Process +#region Public Methods used by the Host Process /// /// The state object associated with the server. /// It provides the shared components for the Server. @@ -2296,11 +2394,12 @@ public bool RegisterWithDiscoveryServer() requestHeader.Timestamp = DateTime.UtcNow; // create the client. + var instanceCertificate = InstanceCertificateTypesProvider.GetInstanceCertificate(null); client = RegistrationClient.Create( configuration, endpoint.Description, endpoint.Configuration, - base.InstanceCertificate); + instanceCertificate); client.OperationTimeout = 10000; @@ -2457,9 +2556,9 @@ private void OnRegisterServer(object state) Utils.LogError(e, "Unexpected exception handling registration timer."); } } - #endregion +#endregion - #region Protected Members used for Request Processing +#region Protected Members used for Request Processing /// /// The synchronization object. /// @@ -2715,9 +2814,9 @@ protected virtual void OnRequestComplete(OperationContext context) m_serverInternal.RequestManager.RequestCompleted(context); } } - #endregion +#endregion - #region Protected Members used for Initialization +#region Protected Members used for Initialization /// /// Raised when the configuration changes. /// @@ -2856,8 +2955,7 @@ protected override IList InitializeServiceHosts( configuration.ServerConfiguration.BaseAddresses, serverDescription, configuration.ServerConfiguration.SecurityPolicies, - InstanceCertificate, - InstanceCertificateChain + InstanceCertificateTypesProvider ); endpoints.AddRange(endpointsForHost); } @@ -2904,13 +3002,16 @@ protected override void StartApplication(ApplicationConfiguration configuration) { Utils.LogInfo(TraceMasks.StartStop, "Server - Start application {0}.", configuration.ApplicationName); + // Setup the minimum nonce length + Nonce.SetMinNonceValue((uint)configuration.SecurityConfiguration.NonceLength); + // create the datastore for the instance. m_serverInternal = new ServerInternalData( ServerProperties, configuration, MessageContext, new CertificateValidator(), - InstanceCertificate); + InstanceCertificateTypesProvider); // create the manager responsible for providing localized string resources. Utils.LogInfo(TraceMasks.StartStop, "Server - CreateResourceManager."); @@ -3345,7 +3446,7 @@ public virtual void RemoveNodeManager(INodeManagerFactory nodeManagerFactory) { m_nodeManagerFactories.Remove(nodeManagerFactory); } - #endregion +#endregion #region Private Methods /// @@ -3367,9 +3468,9 @@ private void SessionChannelKeepAliveEvent(Session session, SessionEventReason re #region Private Properties private OperationLimitsState OperationLimits => ServerInternal.ServerObject.ServerCapabilities.OperationLimits; - #endregion +#endregion - #region Private Fields +#region Private Fields private readonly object m_lock = new object(); private readonly object m_registrationLock = new object(); private ServerInternalData m_serverInternal; diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index 8212349b5..ed268b842 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -53,6 +53,7 @@ public class Session : IDisposable /// Application description for the client application. /// The endpoint URL. /// The client certificate. + /// The client certifiate chain /// The session timeout. /// The maximum size of a response message /// The max request age. @@ -65,11 +66,12 @@ public Session( X509Certificate2 serverCertificate, NodeId authenticationToken, byte[] clientNonce, - byte[] serverNonce, + Nonce serverNonce, string sessionName, ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, + X509Certificate2Collection clientCertificateChain, double sessionTimeout, uint maxResponseMessageSize, double maxRequestAge, @@ -92,6 +94,9 @@ public Session( m_sessionName = sessionName; m_serverCertificate = serverCertificate; m_clientCertificate = clientCertificate; + + m_clientIssuerCertificates = clientCertificateChain; + m_secureChannelId = context.ChannelContext.SecureChannelId; m_maxResponseMessageSize = maxResponseMessageSize; m_maxRequestAge = maxRequestAge; @@ -150,9 +155,9 @@ public Session( TraceState("CREATED"); } - #endregion +#endregion - #region IDisposable Members +#region IDisposable Members /// /// Frees any unmanaged resources. /// @@ -202,9 +207,9 @@ protected virtual void Dispose(bool disposing) } } } - #endregion +#endregion - #region Public Interface +#region Public Interface /// /// Gets the identifier assigned to the session when it was created. /// @@ -334,6 +339,56 @@ public bool Activated } } + /// + /// Set the ECC security policy URI + /// + /// + public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri) + { + lock (m_lock) + { + m_eccUserTokenSecurityPolicyUri = securityPolicyUri; + m_eccUserTokenNonce = null; + } + } + +#if ECC_SUPPORT + /// + /// Create new ECC ephemeral key + /// + /// A new ephemeral key + public virtual EphemeralKeyType GetNewEccKey() + { + lock (m_lock) + { + if (m_eccUserTokenSecurityPolicyUri == null) + { + return null; + } + + m_eccUserTokenNonce = Nonce.CreateNonce(m_eccUserTokenSecurityPolicyUri); + + EphemeralKeyType key = new EphemeralKeyType() { + PublicKey = m_eccUserTokenNonce.Data + }; + + key.Signature = EccUtils.Sign(new ArraySegment(key.PublicKey), m_serverCertificate, m_eccUserTokenSecurityPolicyUri); + + return key; + } + } + + /// + /// The Server generated ephemeral key + /// + public EphemeralKeyType EphemeralKey + { + set + { + m_ephemeralKey = value; + } + } +#endif /// /// Returns the session's endpoint /// @@ -356,6 +411,8 @@ public string SecureChannelId } } + + /// /// allow derived classes access /// @@ -460,8 +517,6 @@ public void ValidateBeforeActivate( List clientSoftwareCertificates, ExtensionObject userIdentityToken, SignatureData userTokenSignature, - StringCollection localeIds, - byte[] serverNonce, out UserIdentityToken identityToken, out UserTokenPolicy userTokenPolicy) { @@ -489,7 +544,7 @@ public void ValidateBeforeActivate( throw new ServiceResultException(StatusCodes.BadApplicationSignatureInvalid); } - byte[] dataToSign = Utils.Append(m_serverCertificate.RawData, m_serverNonce); + byte[] dataToSign = Utils.Append(m_serverCertificate.RawData, m_serverNonce.Data); if (!SecurityPolicies.Verify(m_clientCertificate, m_endpoint.SecurityPolicyUri, dataToSign, clientSignature)) { @@ -507,7 +562,8 @@ public void ValidateBeforeActivate( } byte[] serverCertificateChainData = serverCertificateChainList.ToArray(); - dataToSign = Utils.Append(serverCertificateChainData, m_serverNonce); + + dataToSign = Utils.Append(serverCertificateChainData, m_serverNonce.Data); if (!SecurityPolicies.Verify(m_clientCertificate, m_endpoint.SecurityPolicyUri, dataToSign, clientSignature)) { @@ -555,7 +611,7 @@ public bool Activate( IUserIdentity identity, IUserIdentity effectiveIdentity, StringCollection localeIds, - byte[] serverNonce) + Nonce serverNonce) { lock (m_lock) { @@ -783,9 +839,9 @@ private class HistoryContinuationPoint public object Value; public DateTime Timestamp; } - #endregion +#endregion - #region Private Methods +#region Private Methods /// /// Dumps the current state of the session queue. /// @@ -881,7 +937,7 @@ private UserIdentityToken ValidateUserIdentityToken( throw ServiceResultException.Create(StatusCodes.BadUserAccessDenied, "Invalid user identity token provided."); } - policy = m_endpoint.FindUserTokenPolicy(newToken.PolicyId); + policy = m_endpoint.FindUserTokenPolicy(newToken.PolicyId, m_endpoint.SecurityPolicyUri); if (policy == null) { throw ServiceResultException.Create(StatusCodes.BadUserAccessDenied, "User token policy not supported.", "Opc.Ua.Server.Session.ValidateUserIdentityToken"); @@ -916,7 +972,7 @@ private UserIdentityToken ValidateUserIdentityToken( } // find the user token policy. - policy = m_endpoint.FindUserTokenPolicy(token.PolicyId); + policy = m_endpoint.FindUserTokenPolicy(token.PolicyId, m_endpoint.SecurityPolicyUri); if (policy == null) { @@ -955,7 +1011,12 @@ private UserIdentityToken ValidateUserIdentityToken( try { - token.Decrypt(m_serverCertificate, m_serverNonce, securityPolicyUri); + token.Decrypt(m_serverCertificate, + m_serverNonce, + securityPolicyUri, + m_eccUserTokenNonce, + m_clientCertificate, + m_clientIssuerCertificates); } catch (Exception e) { @@ -970,7 +1031,8 @@ private UserIdentityToken ValidateUserIdentityToken( // verify the signature. if (securityPolicyUri != SecurityPolicies.None) { - byte[] dataToSign = Utils.Append(m_serverCertificate.RawData, m_serverNonce); + + byte[] dataToSign = Utils.Append(m_serverCertificate.RawData, m_serverNonce.Data); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri)) { @@ -988,7 +1050,8 @@ private UserIdentityToken ValidateUserIdentityToken( } byte[] serverCertificateChainData = serverCertificateChainList.ToArray(); - dataToSign = Utils.Append(serverCertificateChainData, m_serverNonce); + + dataToSign = Utils.Append(serverCertificateChainData, m_serverNonce.Data); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri)) { @@ -1115,9 +1178,9 @@ private void UpdateDiagnosticCounters(RequestType requestType, bool error, bool } } } - #endregion +#endregion - #region Private Fields +#region Private Fields private readonly object m_lock = new object(); private NodeId m_sessionId; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] @@ -1130,16 +1193,21 @@ private void UpdateDiagnosticCounters(RequestType requestType, bool error, bool private bool m_activated; private X509Certificate2 m_clientCertificate; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] private List m_softwareCertificates; private byte[] m_clientNonce; - private byte[] m_serverNonce; private string m_sessionName; private string m_secureChannelId; private EndpointDescription m_endpoint; private X509Certificate2 m_serverCertificate; private byte[] m_serverCertificateChain; + private Nonce m_serverNonce; + private string m_eccUserTokenSecurityPolicyUri; + private Nonce m_eccUserTokenNonce; + private X509Certificate2Collection m_clientIssuerCertificates; + private string[] m_localeIds; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] private uint m_maxResponseMessageSize; @@ -1151,6 +1219,11 @@ private void UpdateDiagnosticCounters(RequestType requestType, bool error, bool private SessionSecurityDiagnosticsDataType m_securityDiagnostics; private List m_browseContinuationPoints; private List m_historyContinuationPoints; - #endregion + +#if ECC_SUPPORT + private EphemeralKeyType m_ephemeralKey; +#endif + +#endregion } } diff --git a/Libraries/Opc.Ua.Server/Session/SessionManager.cs b/Libraries/Opc.Ua.Server/Session/SessionManager.cs index 9c95129ab..adc411ea1 100644 --- a/Libraries/Opc.Ua.Server/Session/SessionManager.cs +++ b/Libraries/Opc.Ua.Server/Session/SessionManager.cs @@ -61,10 +61,9 @@ public SessionManager( m_maxRequestAge = configuration.ServerConfiguration.MaxRequestAge; m_maxBrowseContinuationPoints = configuration.ServerConfiguration.MaxBrowseContinuationPoints; m_maxHistoryContinuationPoints = configuration.ServerConfiguration.MaxHistoryContinuationPoints; - m_minNonceLength = configuration.SecurityConfiguration.NonceLength; m_sessions = new ConcurrentDictionary(Environment.ProcessorCount, m_maxSessionCount); - m_lastSessionId = BitConverter.ToInt64(Utils.Nonce.CreateNonce(sizeof(long)), 0); + m_lastSessionId = BitConverter.ToInt64(Nonce.CreateRandomNonceData(sizeof(long)), 0); // create a event to signal shutdown. m_shutdownEvent = new ManualResetEvent(true); @@ -148,6 +147,7 @@ public virtual Session CreateSession( ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, + X509Certificate2Collection clientCertificateChain, double requestedSessionTimeout, uint maxResponseMessageSize, out NodeId sessionId, @@ -156,6 +156,7 @@ public virtual Session CreateSession( out double revisedSessionTimeout) { sessionId = 0; + serverNonce = null; revisedSessionTimeout = requestedSessionTimeout; Session session = null; @@ -175,7 +176,7 @@ public virtual Session CreateSession( foreach (var sessionKeyValueIterator in m_sessions) { byte[] sessionClientNonce = sessionKeyValueIterator.Value?.ClientNonce; - if (Utils.CompareNonce(sessionClientNonce, clientNonce)) + if (Nonce.CompareNonce(sessionClientNonce, clientNonce)) { throw new ServiceResultException(StatusCodes.BadNonceInvalid); } @@ -195,7 +196,7 @@ public virtual Session CreateSession( // must assign a hard-to-guess id if not secured. if (authenticationToken == null) { - byte[] token = Utils.Nonce.CreateNonce(32); + byte[] token = Nonce.CreateRandomNonceData(32); authenticationToken = new NodeId(token); } @@ -211,7 +212,8 @@ public virtual Session CreateSession( } // create server nonce. - serverNonce = Utils.Nonce.CreateNonce((uint)m_minNonceLength); + var serverNonceObject = Nonce.CreateNonce(context.ChannelContext.EndpointDescription.SecurityPolicyUri); + // assign client name. if (String.IsNullOrEmpty(sessionName)) @@ -226,18 +228,18 @@ public virtual Session CreateSession( serverCertificate, authenticationToken, clientNonce, - serverNonce, + serverNonceObject, sessionName, clientDescription, endpointUrl, clientCertificate, + clientCertificateChain, revisedSessionTimeout, - maxResponseMessageSize, - m_maxRequestAge, - m_maxBrowseContinuationPoints); + maxResponseMessageSize); // get the session id. sessionId = session.Id; + serverNonce = serverNonceObject.Data; // save session. if (!m_sessions.TryAdd(authenticationToken, session)) @@ -253,6 +255,42 @@ public virtual Session CreateSession( return session; } + /// + /// Creates a new session. + /// + [Obsolete("Use CreateSession that passes X509Certificate2Collection)")] + public virtual Session CreateSession( + OperationContext context, + X509Certificate2 serverCertificate, + string sessionName, + byte[] clientNonce, + ApplicationDescription clientDescription, + string endpointUrl, + X509Certificate2 clientCertificate, + double requestedSessionTimeout, + uint maxResponseMessageSize, + out NodeId sessionId, + out NodeId authenticationToken, + out byte[] serverNonce, + out double revisedSessionTimeout) + { + return CreateSession( + context, + serverCertificate, + sessionName, + clientNonce, + clientDescription, + endpointUrl, + clientCertificate, + null, + requestedSessionTimeout, + maxResponseMessageSize, + out sessionId, + out authenticationToken, + out serverNonce, + out revisedSessionTimeout); + } + /// /// Activates an existing session /// @@ -268,6 +306,8 @@ public virtual bool ActivateSession( { serverNonce = null; + Nonce serverNonceObject = null; + Session session = null; UserIdentityToken newIdentity = null; UserTokenPolicy userTokenPolicy = null; @@ -298,7 +338,7 @@ public virtual bool ActivateSession( } // create new server nonce. - serverNonce = Utils.Nonce.CreateNonce((uint)m_minNonceLength); + serverNonceObject = Nonce.CreateNonce(context.ChannelContext.EndpointDescription.SecurityPolicyUri); // validate before activation. session.ValidateBeforeActivate( @@ -307,10 +347,10 @@ public virtual bool ActivateSession( clientSoftwareCertificates, userIdentityToken, userTokenSignature, - localeIds, - serverNonce, out newIdentity, out userTokenPolicy); + + serverNonce = serverNonceObject.Data; } IUserIdentity identity = null; IUserIdentity effectiveIdentity = null; @@ -371,6 +411,7 @@ public virtual bool ActivateSession( } // activate session. + bool contextChanged = session.Activate( context, clientSoftwareCertificates, @@ -378,7 +419,7 @@ public virtual bool ActivateSession( identity, effectiveIdentity, localeIds, - serverNonce); + serverNonceObject); // raise session related event. if (contextChanged) @@ -510,15 +551,14 @@ protected virtual Session CreateSession( X509Certificate2 serverCertificate, NodeId sessionCookie, byte[] clientNonce, - byte[] serverNonce, + Nonce serverNonce, string sessionName, ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, + X509Certificate2Collection clientCertificateChain, double sessionTimeout, - uint maxResponseMessageSize, - int maxRequestAge, // TBD - Remove unused parameter. - int maxContinuationPoints) // TBD - Remove unused parameter. + uint maxResponseMessageSize) { Session session = new Session( context, @@ -531,6 +571,7 @@ protected virtual Session CreateSession( clientDescription, endpointUrl, clientCertificate, + clientCertificateChain, sessionTimeout, maxResponseMessageSize, m_maxRequestAge, @@ -570,9 +611,9 @@ protected virtual void RaiseSessionEvent(Session session, SessionEventReason rea } } } - #endregion +#endregion - #region Private Methods +#region Private Methods /// /// Periodically checks if the sessions have timed out. /// @@ -626,7 +667,7 @@ private void MonitorSessions(object data) } #endregion - #region Private Fields +#region Private Fields private readonly object m_lock = new object(); private IServerInternal m_server; private ConcurrentDictionary m_sessions; @@ -640,7 +681,6 @@ private void MonitorSessions(object data) private int m_maxBrowseContinuationPoints; private int m_maxHistoryContinuationPoints; - private int m_minNonceLength; private readonly object m_eventLock = new object(); private event SessionEventHandler m_sessionCreated; @@ -649,9 +689,9 @@ private void MonitorSessions(object data) private event SessionEventHandler m_sessionChannelKeepAlive; private event ImpersonateEventHandler m_impersonateUser; private event EventHandler m_validateSessionLessRequest; - #endregion +#endregion - #region ISessionManager Members +#region ISessionManager Members /// public event SessionEventHandler SessionCreated { @@ -792,7 +832,7 @@ public Session GetSession(NodeId authenticationToken) } return null; } - #endregion +#endregion } /// @@ -883,13 +923,13 @@ public enum SessionEventReason /// public delegate void SessionEventHandler(Session session, SessionEventReason reason); - #region ImpersonateEventArgs Class +#region ImpersonateEventArgs Class /// /// A class which provides the event arguments for session related event. /// public class ImpersonateEventArgs : EventArgs { - #region Constructors +#region Constructors /// /// Creates a new instance. /// @@ -899,9 +939,9 @@ public ImpersonateEventArgs(UserIdentityToken newIdentity, UserTokenPolicy userT m_userTokenPolicy = userTokenPolicy; m_endpointDescription = endpointDescription; } - #endregion +#endregion - #region Public Properties +#region Public Properties /// /// The new user identity for the session. /// @@ -952,31 +992,31 @@ public EndpointDescription EndpointDescription { get { return m_endpointDescription; } } - #endregion +#endregion - #region Private Fields +#region Private Fields private UserIdentityToken m_newIdentity; private UserTokenPolicy m_userTokenPolicy; private ServiceResult m_identityValidationError; private IUserIdentity m_identity; private IUserIdentity m_effectiveIdentity; private EndpointDescription m_endpointDescription; - #endregion +#endregion } /// /// The delegate for functions used to receive impersonation events. /// public delegate void ImpersonateEventHandler(Session session, ImpersonateEventArgs args); - #endregion +#endregion - #region ImpersonateEventArgs Class +#region ImpersonateEventArgs Class /// /// A class which provides the event arguments for session related event. /// public class ValidateSessionLessRequestEventArgs : EventArgs { - #region Constructors +#region Constructors /// /// Creates a new instance. /// @@ -985,9 +1025,9 @@ public ValidateSessionLessRequestEventArgs(NodeId authenticationToken, RequestTy AuthenticationToken = authenticationToken; RequestType = requestType; } - #endregion +#endregion - #region Public Properties +#region Public Properties /// /// The request type for the request. /// @@ -1007,7 +1047,7 @@ public ValidateSessionLessRequestEventArgs(NodeId authenticationToken, RequestTy /// Set to indicate that an error occurred validating the session-less request and that it should be rejected. /// public ServiceResult Error { get; set; } - #endregion +#endregion } - #endregion +#endregion } diff --git a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs index 5e0275441..87d91efd8 100644 --- a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs +++ b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs @@ -66,7 +66,7 @@ public SubscriptionManager( m_subscriptions = new Dictionary(); m_publishQueues = new Dictionary(); m_statusMessages = new Dictionary>(); - m_lastSubscriptionId = BitConverter.ToInt64(Utils.Nonce.CreateNonce(sizeof(long)), 0); + m_lastSubscriptionId = BitConverter.ToInt64(Nonce.CreateRandomNonceData(sizeof(long)), 0); // create a event to signal shutdown. m_shutdownEvent = new ManualResetEvent(true); diff --git a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj index d63a36fd5..38d1ef7c9 100644 --- a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj +++ b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj @@ -19,6 +19,17 @@ $(DefineConstants);SIGNASSEMBLY + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs index f76c18d2e..ede6ea46a 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs @@ -46,8 +46,7 @@ public List CreateServiceHost( IList baseAddresses, ApplicationDescription serverDescription, List securityPolicies, - X509Certificate2 instanceCertificate, - X509Certificate2Collection instanceCertificateChain + CertificateTypesProvider certificateTypesProvider ) { // generate a unique host name. @@ -112,22 +111,15 @@ X509Certificate2Collection instanceCertificateChain description.EndpointUrl = uri.ToString(); description.Server = serverDescription; - if (instanceCertificate != null) + if (certificateTypesProvider != null) { + var instanceCertificate = certificateTypesProvider.GetInstanceCertificate(bestPolicy.SecurityPolicyUri); description.ServerCertificate = instanceCertificate.RawData; + // check if complete chain should be sent. - if (configuration.SecurityConfiguration.SendCertificateChain && - instanceCertificateChain != null && - instanceCertificateChain.Count > 1) + if (configuration.SecurityConfiguration.SendCertificateChain) { - List serverCertificateChain = new List(); - - for (int i = 0; i < instanceCertificateChain.Count; i++) - { - serverCertificateChain.AddRange(instanceCertificateChain[i].RawData); - } - - description.ServerCertificate = serverCertificateChain.ToArray(); + description.ServerCertificate = certificateTypesProvider.LoadCertificateChainRawAsync(instanceCertificate).GetAwaiter().GetResult(); } } @@ -151,11 +143,11 @@ X509Certificate2Collection instanceCertificateChain } // create the host. - ServiceHost serviceHost = serverBase.CreateServiceHost(serverBase, uris.ToArray()); + hosts[hostName] = serverBase.CreateServiceHost(serverBase, uris.ToArray()); - hosts[hostName] = serviceHost; return endpoints; - } - } + + } + } } diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index c11742160..2ac25477c 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -202,8 +202,7 @@ public void Open( // save the callback to the server. m_callback = callback; - m_serverCertificate = settings.ServerCertificate; - m_serverCertificateChain = settings.ServerCertificateChain; + m_serverCertProvider = settings.ServerCertificateTypesProvider; // start the listener Start(); @@ -265,7 +264,7 @@ public void Start() m_hostBuilder = new WebHostBuilder(); // prepare the server TLS certificate - var serverCertificate = m_serverCertificate; + var serverCertificate = m_serverCertProvider.GetInstanceCertificate(SecurityPolicies.Https); #if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1 || NET472_OR_GREATER || NET5_0_OR_GREATER try { @@ -273,7 +272,7 @@ public void Start() // which default to the ephemeral KeySet. Also a new certificate must be reloaded. // If the key fails to copy, its probably a non exportable key from the X509Store. // Then we can use the original certificate, the private key is already in the key store. - serverCertificate = X509Utils.CreateCopyWithPrivateKey(m_serverCertificate, false); + serverCertificate = X509Utils.CreateCopyWithPrivateKey(serverCertificate, false); } catch (CryptographicException ce) { @@ -285,7 +284,7 @@ public void Start() CheckCertificateRevocation = false, ClientCertificateMode = ClientCertificateMode.NoCertificate, // note: this is the TLS certificate! - ServerCertificate = serverCertificate, + ServerCertificate = serverCertificate }; #if NET462 @@ -468,32 +467,26 @@ public async Task SendAsync(HttpContext context) /// public void CertificateUpdate( ICertificateValidator validator, - X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain) + CertificateTypesProvider certificateTypeProvider) { Stop(); m_quotas.CertificateValidator = validator; - m_serverCertificate = serverCertificate; - m_serverCertificateChain = serverCertificateChain; + m_serverCertProvider = certificateTypeProvider; foreach (var description in m_descriptions) { - // check if complete chain should be sent. - if (m_serverCertificateChain != null && - m_serverCertificateChain.Count > 1) + if (description.ServerCertificate != null) { - var byteServerCertificateChain = new List(); - - for (int i = 0; i < m_serverCertificateChain.Count; i++) + X509Certificate2 serverCertificate = m_serverCertProvider.GetInstanceCertificate(description.SecurityPolicyUri); + if (m_serverCertProvider.SendCertificateChain) { - byteServerCertificateChain.AddRange(m_serverCertificateChain[i].RawData); + byte[] serverCertificateChainRaw = m_serverCertProvider.LoadCertificateChainRawAsync(serverCertificate).Result; + description.ServerCertificate = serverCertificateChainRaw; + } + else + { + description.ServerCertificate = serverCertificate.RawData; } - - description.ServerCertificate = byteServerCertificateChain.ToArray(); - } - else if (description.ServerCertificate != null) - { - description.ServerCertificate = serverCertificate.RawData; } } @@ -544,8 +537,7 @@ private static async Task ReadBodyAsync(HttpRequest req) private ITransportListenerCallback m_callback; private IWebHostBuilder m_hostBuilder; private IWebHost m_host; - private X509Certificate2 m_serverCertificate; - private X509Certificate2Collection m_serverCertificateChain; + private CertificateTypesProvider m_serverCertProvider; #endregion } } diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 280460251..84a713692 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -1,4 +1,4 @@ - + $(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC @@ -11,6 +11,17 @@ true + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + $(PackageId).Debug diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 43ac29014..527ee7741 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -12,6 +12,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; +using System.IO; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using Opc.Ua.Bindings; @@ -773,6 +774,7 @@ public SecurityConfiguration() /// private void Initialize() { + m_applicationCertificates = new CertificateIdentifierCollection(); m_trustedIssuerCertificates = new CertificateTrustList(); m_trustedPeerCertificates = new CertificateTrustList(); m_nonceLength = 32; @@ -784,6 +786,7 @@ private void Initialize() m_addAppCertToTrustedStore = true; m_sendCertificateChain = true; m_suppressNonceValidationErrors = false; + m_isDeprecatedConfiguration = false; } /// @@ -796,17 +799,84 @@ private void Initialize() #region Persistent Properties /// /// The application instance certificate. + /// Kept for backward compatibility with configuration files which only support RSA certificates. /// /// The application certificate. /// /// This certificate must contain the application uri. /// For servers, URLs for each supported protocol must also be present. /// - [DataMember(IsRequired = true, EmitDefaultValue = false, Order = 0)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 0)] public CertificateIdentifier ApplicationCertificate { - get { return m_applicationCertificate; } - set { m_applicationCertificate = value; } + get + { + if (m_applicationCertificates.Count > 0) + { + return m_applicationCertificates[0]; + } + return null; + } + set + { + if (m_applicationCertificates.Count > 0) + { + if (value == null) + { + m_applicationCertificates.RemoveAt(0); + } + else + { + m_applicationCertificates[0] = value; + } + } + else + { + m_applicationCertificates.Add(value); + } + SupportedSecurityPolicies = BuildSupportedSecurityPolicies(); + + m_applicationCertificates[0].CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType; + m_isDeprecatedConfiguration = true; + } + } + + /// + /// The application instance certificates in use for the application. + /// + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 1)] + public CertificateIdentifierCollection ApplicationCertificates + { + get => m_applicationCertificates; + set + { + m_applicationCertificates = value ?? new CertificateIdentifierCollection(); + + m_isDeprecatedConfiguration = false; + + // Remove any unsupported certificate types. + for (int i = m_applicationCertificates.Count - 1; i >= 0; i--) + { + if (!Utils.IsSupportedCertificateType(m_applicationCertificates[i].CertificateType)) + { + m_applicationCertificates.RemoveAt(i); + } + } + + // Remove any duplicates + for (int i = 0; i < m_applicationCertificates.Count; i++) + { + for (int j = m_applicationCertificates.Count - 1; j > i; j--) + { + if (m_applicationCertificates[i].CertificateType == m_applicationCertificates[j].CertificateType) + { + m_applicationCertificates.RemoveAt(j); + } + } + } + + SupportedSecurityPolicies = BuildSupportedSecurityPolicies(); + } } /// @@ -938,6 +1008,7 @@ public bool RejectUnknownRevocationStatus /// /// Gets or sets a value indicating which minimum certificate key strength is accepted. + /// The value is ignored for certificates with a ECDSA signature. /// /// /// This value can be set to 1024, 2048 or 4096 by servers @@ -1091,16 +1162,31 @@ public bool SuppressNonceValidationErrors get { return m_suppressNonceValidationErrors; } set { m_suppressNonceValidationErrors = value; } } + + + #endregion + + #region Non-Persistent Properties + /// + /// The type of Configuration (deprecated or not) + /// + public bool IsDeprecatedConfiguration + { + get { return m_isDeprecatedConfiguration; } + set { m_isDeprecatedConfiguration = value; } + } + #endregion - #region Private Fields - private CertificateIdentifier m_applicationCertificate; + #region Private Fields + private CertificateIdentifierCollection m_applicationCertificates; private CertificateTrustList m_trustedIssuerCertificates; private CertificateTrustList m_trustedPeerCertificates; private CertificateTrustList m_httpsIssuerCertificates; private CertificateTrustList m_trustedHttpsCertificates; private CertificateTrustList m_userIssuerCertificates; private CertificateTrustList m_trustedUserCertificates; + // TODO: is this really necessary? private int m_nonceLength; private CertificateStoreIdentifier m_rejectedCertificateStore; private int m_maxRejectedCertificates; @@ -1113,6 +1199,7 @@ public bool SuppressNonceValidationErrors private bool m_addAppCertToTrustedStore; private bool m_sendCertificateChain; private bool m_suppressNonceValidationErrors; + private bool m_isDeprecatedConfiguration; #endregion } #endregion @@ -2926,7 +3013,7 @@ public CertificateIdentifierCollection TrustedCertificates #endregion #region CertificateIdentifierCollection Class - [CollectionDataContract(Name = "ListOfCertificateIdentifier", Namespace = Namespaces.OpcUaConfig, ItemName = "CertificateIdentifier")] + [CollectionDataContract(Name = "ApplicationCertificates", Namespace = Namespaces.OpcUaConfig, ItemName = "CertificateIdentifier")] public partial class CertificateIdentifierCollection : List { /// @@ -3008,7 +3095,7 @@ private void Initialize() /// The type of certificate store. /// /// The type of the store - defined in the . - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 0)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 10)] public string StoreType { get @@ -3031,7 +3118,7 @@ public string StoreType /// The path that identifies the certificate store. /// /// The store path in the form StoreName\\Store Location . - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 1)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 15)] public string StorePath { get @@ -3068,7 +3155,7 @@ public string StorePath /// /// The name of the store. /// - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 2)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 20)] [Obsolete("Use StoreType/StorePath instead")] public string StoreName { @@ -3081,7 +3168,7 @@ public string StoreName /// /// The store location. /// - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 3)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 30)] [Obsolete("Use StoreType/StorePath instead")] public string StoreLocation { @@ -3136,7 +3223,7 @@ public string StoreLocation /// /// /// - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 4)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 40)] public string SubjectName { get @@ -3168,7 +3255,7 @@ public string SubjectName /// /// The thumbprint of a certificate.. /// - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 5)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 50)] public string Thumbprint { get @@ -3199,7 +3286,7 @@ public string Thumbprint /// Gets the DER encoded certificate data or create embedded in this instance certificate using the DER encoded certificate data. /// /// A byte array containing the X.509 certificate data. - [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 6)] + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 60)] public byte[] RawData { get @@ -3230,12 +3317,34 @@ public byte[] RawData /// Gets or sets the XML encoded validation options - use to serialize the validation options. /// /// The XML encoded validation options. - [DataMember(Name = "ValidationOptions", IsRequired = false, EmitDefaultValue = false, Order = 7)] + [DataMember(Name = "ValidationOptions", IsRequired = false, EmitDefaultValue = false, Order = 70)] private int XmlEncodedValidationOptions { get { return (int)m_validationOptions; } set { m_validationOptions = (CertificateValidationOptions)value; } } + + /// + /// Gets or sets the certificate type. + /// + /// The NodeId of the certificate type, e.g. EccNistP256ApplicationCertificateType. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 80)] + public NodeId CertificateType + { + get => m_certificateType; + set => m_certificateType = value; + } + + /// + /// The string representation of the certificate + /// + /// Rsa, RsaMin, RsaSha256, NistP256, NistP384, BrainpoolP256r1, BrainpoolP384r1, Curve25519, Curve448 + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 90)] + public string CertificateTypeString + { + get => EncodeCertificateType(m_certificateType); + set => m_certificateType = DecodeCertificateType(value); + } #endregion #region Private Fields @@ -3246,6 +3355,7 @@ private int XmlEncodedValidationOptions private string m_subjectName; private string m_thumbprint; private X509Certificate2 m_certificate; + private NodeId m_certificateType; private CertificateValidationOptions m_validationOptions; #endregion } diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index b395942d4..c9025e062 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -394,11 +394,21 @@ public static byte CalculateSecurityLevel(MessageSecurityMode mode, string polic result = 2; break; } - case SecurityPolicies.Basic256: result = 4; break; + case SecurityPolicies.Basic256: + { + Utils.LogWarning("Deprecated Security Policy Basic256 requested - Not rcommended."); + result = 4; + break; + } case SecurityPolicies.Basic256Sha256: result = 6; break; case SecurityPolicies.Aes128_Sha256_RsaOaep: result = 8; break; case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; - case SecurityPolicies.None: result = 0; break; + case SecurityPolicies.ECC_brainpoolP256r1: result = 11; break; + case SecurityPolicies.ECC_nistP256: result = 12; break; + case SecurityPolicies.ECC_brainpoolP384r1: result = 13; break; + case SecurityPolicies.ECC_nistP384: result = 14; break; + case SecurityPolicies.None: + return 0; default: Utils.LogWarning("Security level requested for unknown Security Policy {policy}. Returning security level 0", policyUri); return 0; @@ -436,11 +446,19 @@ private static ServerSecurityPolicy CreatePolicy(string profileUri) case SecurityPolicies.Basic256Sha256: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: { policy.SecurityMode = MessageSecurityMode.SignAndEncrypt; break; } + default: + break; } } @@ -457,6 +475,15 @@ private static Opc.Ua.Security.SecurityProfile CreateProfile(string profileUri) policy.Enabled = false; return policy; } + + /// + /// TODO: Holds the application certificates but should be generated and the Opc.Ua.Security namespace automatically + /// TODO: Should replace ApplicationCertificateField in the generated Opc.Ua.Security.SecuredApplication class + /// + public CertificateList ApplicationCertificates + { + get; set; + } } /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index 2f3acaf63..ec7935ba4 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -35,6 +35,7 @@ public static class CertificateFactory /// Supported values are 1024(deprecated), 2048, 3072 or 4096. /// public static readonly ushort DefaultKeySize = 2048; + /// /// The default hash size for RSA certificates in bits. /// @@ -94,7 +95,15 @@ public static X509Certificate2 Load(X509Certificate2 certificate, bool ensurePri // check for existing cached certificate. if (m_certificates.TryGetValue(certificate.Thumbprint, out cachedCertificate)) { - return cachedCertificate; + // cached certificate might be disposed, if so do not return but try to update value in the cache + if (cachedCertificate.Handle != IntPtr.Zero) + { + return cachedCertificate; + } + else + { + m_certificates.Remove(certificate.Thumbprint); + } } // nothing more to do if no private key or don't care about accessibility. @@ -105,7 +114,7 @@ public static X509Certificate2 Load(X509Certificate2 certificate, bool ensurePri if (ensurePrivateKeyAccessible) { - if (!X509Utils.VerifyRSAKeyPair(certificate, certificate)) + if (!X509Utils.VerifyKeyPair(certificate, certificate)) { Utils.LogWarning("Trying to add certificate to cache with invalid private key."); return null; @@ -205,7 +214,7 @@ public static X509Certificate2 CreateCertificate( } /// - /// Revoke the certificate. + /// Revoke the certificate. /// The CRL number is increased by one and the new CRL is returned. /// public static X509CRL RevokeCertificate( @@ -291,6 +300,7 @@ DateTime nextUpdate /// public static byte[] CreateSigningRequest( X509Certificate2 certificate, + // TODO: provide CertificateType to return CSR per certificate type IList domainNames = null ) { @@ -299,10 +309,20 @@ public static byte[] CreateSigningRequest( throw new NotSupportedException("Need a certificate with a private key."); } - RSA rsaPublicKey = certificate.GetRSAPublicKey(); - var request = new CertificateRequest(certificate.SubjectName, rsaPublicKey, - Oids.GetHashAlgorithmName(certificate.SignatureAlgorithm.Value), RSASignaturePadding.Pkcs1); + CertificateRequest request = null; + bool isECDsaSignature = X509PfxUtils.IsECDsaSignature(certificate); + if (!isECDsaSignature) + { + RSA rsaPublicKey = certificate.GetRSAPublicKey(); + request = new CertificateRequest(certificate.SubjectName, rsaPublicKey, + Oids.GetHashAlgorithmName(certificate.SignatureAlgorithm.Value), RSASignaturePadding.Pkcs1); + } + else + { + var eCDsaPublicKey = certificate.GetECDsaPublicKey(); + request = new CertificateRequest(certificate.SubjectName, eCDsaPublicKey, Oids.GetHashAlgorithmName(certificate.SignatureAlgorithm.Value)); + } var alternateName = X509Extensions.FindExtension(certificate); domainNames = domainNames ?? new List(); if (alternateName != null) @@ -328,17 +348,27 @@ public static byte[] CreateSigningRequest( // Subject Alternative Name var subjectAltName = new X509SubjectAltNameExtension(applicationUri, domainNames); request.CertificateExtensions.Add(new X509Extension(subjectAltName, false)); - - using (RSA rsa = certificate.GetRSAPrivateKey()) + if (!isECDsaSignature) { - var x509SignatureGenerator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); - return request.CreateSigningRequest(x509SignatureGenerator); + using (RSA rsa = certificate.GetRSAPrivateKey()) + { + var x509SignatureGenerator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + return request.CreateSigningRequest(x509SignatureGenerator); + } + } + else + { + using (ECDsa key = certificate.GetECDsaPrivateKey()) + { + var x509SignatureGenerator = X509SignatureGenerator.CreateForECDsa(key); + return request.CreateSigningRequest(x509SignatureGenerator); + } } } /// - /// Create a X509Certificate2 with a private key by combining + /// Create a X509Certificate2 with a private key by combining /// the new certificate with a private key from an existing certificate /// public static X509Certificate2 CreateCertificateWithPrivateKey( @@ -350,12 +380,28 @@ public static X509Certificate2 CreateCertificateWithPrivateKey( throw new NotSupportedException("Need a certificate with a private key."); } - if (!X509Utils.VerifyRSAKeyPair(certificate, certificateWithPrivateKey)) + if (X509Utils.IsECDsaSignature(certificate)) { - throw new NotSupportedException("The public and the private key pair doesn't match."); + if (!X509Utils.VerifyECDsaKeyPair(certificate, certificateWithPrivateKey)) + { + throw new NotSupportedException("The public and the private key pair doesn't match."); + } + using (ECDsa privateKey = certificateWithPrivateKey.GetECDsaPrivateKey()) + { + return certificate.CopyWithPrivateKey(privateKey); + } + } + else + { + if (!X509Utils.VerifyRSAKeyPair(certificate, certificateWithPrivateKey)) + { + throw new NotSupportedException("The public and the private key pair doesn't match."); + } + using (RSA privateKey = certificateWithPrivateKey.GetRSAPrivateKey()) + { + return certificate.CopyWithPrivateKey(privateKey); + } } - - return certificate.CopyWithPrivateKey(certificateWithPrivateKey.GetRSAPrivateKey()); } /// @@ -453,18 +499,18 @@ public static X509Certificate2 CreateCertificateWithPEMPrivateKey( /// The certificate with a private key. [Obsolete("Use the new CreateCertificate methods with CertificateBuilder.")] internal static X509Certificate2 CreateCertificate( - string applicationUri, - string applicationName, - string subjectName, - IList domainNames, - ushort keySize, - DateTime startTime, - ushort lifetimeInMonths, - ushort hashSizeInBits, - bool isCA = false, - X509Certificate2 issuerCAKeyCert = null, - byte[] publicKey = null, - int pathLengthConstraint = 0) + string applicationUri, + string applicationName, + string subjectName, + IList domainNames, + ushort keySize, + DateTime startTime, + ushort lifetimeInMonths, + ushort hashSizeInBits, + bool isCA = false, + X509Certificate2 issuerCAKeyCert = null, + byte[] publicKey = null, + int pathLengthConstraint = 0) { ICertificateBuilder builder = null; if (isCA) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 5a7456a81..b3af4e0ab 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -108,6 +109,11 @@ public override bool Equals(object obj) return false; } + if (CertificateType != id.CertificateType) + { + return false; + } + return true; } @@ -117,7 +123,7 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - return HashCode.Combine(Thumbprint, m_storeLocation, m_storeName, SubjectName); + return HashCode.Combine(Thumbprint, m_storeLocation, m_storeName, SubjectName, CertificateType); } #endregion @@ -161,7 +167,7 @@ public Task LoadPrivateKey(string password) => LoadPrivateKeyEx(password != null ? new CertificatePasswordProvider(password) : null); /// - /// Loads the private key for the certificate with an optional password. + /// Loads the private key for the certificate with an optional password provider. /// public async Task LoadPrivateKeyEx(ICertificatePasswordProvider passwordProvider) { @@ -173,7 +179,7 @@ public async Task LoadPrivateKeyEx(ICertificatePasswordProvide if (store?.SupportsLoadPrivateKey == true) { string password = passwordProvider?.GetPassword(this); - m_certificate = await store.LoadPrivateKey(this.Thumbprint, this.SubjectName, password).ConfigureAwait(false); + m_certificate = await store.LoadPrivateKey(this.Thumbprint, this.SubjectName, this.CertificateType, password).ConfigureAwait(false); return m_certificate; } } @@ -185,6 +191,7 @@ public async Task LoadPrivateKeyEx(ICertificatePasswordProvide /// /// Finds a certificate in a store. /// + /// The certificate type is used to match the signature and public key type. /// if set to true the returned certificate must contain the private key. /// An instance of the that is embedded by this instance or find it in /// the selected store pointed out by the using selected . @@ -210,7 +217,7 @@ public async Task Find(bool needPrivateKey) X509Certificate2Collection collection = await store.Enumerate().ConfigureAwait(false); - certificate = Find(collection, m_thumbprint, m_subjectName, needPrivateKey); + certificate = Find(collection, m_thumbprint, m_subjectName, m_certificateType, needPrivateKey); if (certificate != null) { @@ -247,6 +254,7 @@ private void Paste(CertificateIdentifier certificate) this.RawData = certificate.RawData; this.ValidationOptions = certificate.ValidationOptions; this.Certificate = certificate.Certificate; + this.CertificateType = certificate.CertificateType; } /// @@ -316,9 +324,15 @@ private static string GetDisplayName(X509Certificate2 certificate) /// The collection. /// The thumbprint of the certificate. /// Subject name of the certificate. + /// The certificate type. /// if set to true [need private key]. /// - public static X509Certificate2 Find(X509Certificate2Collection collection, string thumbprint, string subjectName, bool needPrivateKey) + public static X509Certificate2 Find( + X509Certificate2Collection collection, + string thumbprint, + string subjectName, + NodeId certificateType, + bool needPrivateKey) { // find by thumbprint. if (!String.IsNullOrEmpty(thumbprint)) @@ -352,9 +366,10 @@ public static X509Certificate2 Find(X509Certificate2Collection collection, strin foreach (X509Certificate2 certificate in collection) { - if (X509Utils.CompareDistinguishedName(certificate, subjectName2)) + if (ValidateCertificateType(certificate, certificateType) && + X509Utils.CompareDistinguishedName(certificate, subjectName2)) { - if ((!needPrivateKey || certificate.HasPrivateKey) && X509Utils.GetRSAPublicKeySize(certificate) >= 0) + if (!needPrivateKey || certificate.HasPrivateKey) { return certificate; } @@ -365,7 +380,8 @@ public static X509Certificate2 Find(X509Certificate2Collection collection, strin foreach (X509Certificate2 certificate in collection) { - if ((!needPrivateKey || certificate.HasPrivateKey) && X509Utils.GetRSAPublicKeySize(certificate) >= 0) + if (ValidateCertificateType(certificate, certificateType) && + (!needPrivateKey || certificate.HasPrivateKey)) { return certificate; } @@ -491,8 +507,182 @@ public ICertificateStore OpenStore() store.Open(this.StorePath, false); return store; } + + /// + /// Retrieves the minimum accepted key size given the security configuration + /// + /// + /// + public ushort GetMinKeySize(SecurityConfiguration securityConfiguration) + { + if (CertificateType == ObjectTypeIds.RsaMinApplicationCertificateType || + CertificateType == ObjectTypeIds.RsaSha256ApplicationCertificateType || + securityConfiguration.IsDeprecatedConfiguration) // Deprecated configurations are implicitly RSA + { + return securityConfiguration.MinimumCertificateKeySize; + } + else + { + // non RSA + return 0; + } + + throw new ArgumentException("Certificate type is unknown"); + + } + + + /// + /// Get the OPC UA CertificateType. + /// + /// The certificate with a signature. + public static NodeId GetCertificateType(X509Certificate2 certificate) + { + switch (certificate.SignatureAlgorithm.Value) + { + case Oids.ECDsaWithSha1: + case Oids.ECDsaWithSha384: + case Oids.ECDsaWithSha256: + case Oids.ECDsaWithSha512: + return EccUtils.GetEccCertificateTypeId(certificate); + + case Oids.RsaPkcs1Sha256: + case Oids.RsaPkcs1Sha384: + case Oids.RsaPkcs1Sha512: + return ObjectTypeIds.RsaSha256ApplicationCertificateType; + case Oids.RsaPkcs1Sha1: + return ObjectTypeIds.RsaMinApplicationCertificateType; + } + return NodeId.Null; + } + + /// + /// Validate if the certificate matches the CertificateType. + /// + /// The certificate with a signature. + /// The NodeId of the certificate type. + public static bool ValidateCertificateType(X509Certificate2 certificate, NodeId certificateType) + { + switch (certificate.SignatureAlgorithm.Value) + { + case Oids.ECDsaWithSha1: + case Oids.ECDsaWithSha384: + case Oids.ECDsaWithSha256: + case Oids.ECDsaWithSha512: + var certType = EccUtils.GetEccCertificateTypeId(certificate); + if (certType.IsNullNodeId) + { + return false; + } + else if (certType == certificateType) + { + return true; + } + + + // special cases + if (certType == ObjectTypeIds.EccNistP384ApplicationCertificateType && + certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) + { + return true; + } + + if (certType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType && + certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) + { + return true; + } + + break; + + default: + // TODO: check SHA1/key size + if (certificateType == null || + certificateType == ObjectTypeIds.RsaSha256ApplicationCertificateType || + certificateType == ObjectTypeIds.RsaMinApplicationCertificateType || + certificateType == ObjectTypeIds.ApplicationCertificateType) + { + return true; + } + break; + } + return false; + } + + /// + /// Map a security policy to a list of supported certificate types. + /// + /// + public static IList MapSecurityPolicyToCertificateTypes(string securityPolicy) + { + var result = new List(); + switch (securityPolicy) + { + case SecurityPolicies.Basic128Rsa15: + case SecurityPolicies.Basic256: + result.Add(ObjectTypeIds.RsaMinApplicationCertificateType); + goto case SecurityPolicies.Basic256Sha256; + case SecurityPolicies.Basic256Sha256: + case SecurityPolicies.Aes128_Sha256_RsaOaep: + case SecurityPolicies.Aes256_Sha256_RsaPss: + result.Add(ObjectTypeIds.RsaSha256ApplicationCertificateType); + break; + case SecurityPolicies.ECC_nistP256: + result.Add(ObjectTypeIds.EccNistP256ApplicationCertificateType); + goto case SecurityPolicies.ECC_nistP384; + case SecurityPolicies.ECC_nistP384: + result.Add(ObjectTypeIds.EccNistP384ApplicationCertificateType); + break; + case SecurityPolicies.ECC_brainpoolP256r1: + result.Add(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + goto case SecurityPolicies.ECC_brainpoolP384r1; + case SecurityPolicies.ECC_brainpoolP384r1: + result.Add(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + break; + case SecurityPolicies.ECC_curve25519: + result.Add(ObjectTypeIds.EccCurve25519ApplicationCertificateType); + break; + case SecurityPolicies.ECC_curve448: + result.Add(ObjectTypeIds.EccCurve448ApplicationCertificateType); + break; + case SecurityPolicies.Https: + result.Add(ObjectTypeIds.HttpsCertificateType); + break; + default: + break; + } + return result; + } + + + /// + /// Disposes and deletes the reference to the certificate. + /// + public void DisposeCertificate() + { + var certificate = m_certificate; + m_certificate = null; + Utils.SilentDispose(certificate); + } #endregion + #region Private Members + /// + /// The tags of the supported certificate types. + /// + private static Dictionary m_supportedCertificateTypes = new Dictionary() { + { ObjectTypes.EccNistP256ApplicationCertificateType, "NistP256"}, + { ObjectTypes.EccNistP384ApplicationCertificateType, "NistP384"}, + { ObjectTypes.EccBrainpoolP256r1ApplicationCertificateType, "BrainpoolP256r1"}, + { ObjectTypes.EccBrainpoolP384r1ApplicationCertificateType, "BrainpoolP384r1"}, + { ObjectTypes.EccCurve25519ApplicationCertificateType, "Curve25519"}, + { ObjectTypes.EccCurve448ApplicationCertificateType, "Curve448"}, + { ObjectTypes.RsaSha256ApplicationCertificateType, "RsaSha256"}, + { ObjectTypes.RsaMinApplicationCertificateType, "RsaMin"}, + { ObjectTypes.ApplicationCertificateType, "Rsa"}, + }; +#endregion + #region Private Methods /// /// Checks if the certificate data represents a valid X509v3 certificate header. @@ -563,6 +753,50 @@ private static bool IsValidCertificateBlob(byte[] rawData) // potentially valid. return true; } + + /// + /// The tags of the supported certificate types used to encode the NodeId coressponding to existing value. + /// + // TODO: remove if not used + private static string EncodeCertificateType(NodeId certificateType) + { + if (certificateType == null) + { + return null; + } + + foreach (KeyValuePair supportedCertificateType in m_supportedCertificateTypes) + { + if (supportedCertificateType.Key == (uint)certificateType.Identifier) + { + return supportedCertificateType.Value; + } + } + + return null; + } + + /// + /// The tags of the supported certificate types used to decode the NodeId coressponding to existing value. + /// + // TODO: remove if not used + private static NodeId DecodeCertificateType(string certificateType) + { + if (certificateType == null) + { + return null; + } + + foreach (var supportedCertificateType in m_supportedCertificateTypes) + { + if (supportedCertificateType.Value == certificateType) + { + return new NodeId(supportedCertificateType.Key); + } + } + + return null; + } #endregion } @@ -738,6 +972,12 @@ public Task LoadPrivateKey(string thumbprint, string subjectNa return Task.FromResult(null); } + /// + public Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password) + { + return Task.FromResult(null); + } + /// public bool SupportsCRLs => false; diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateTypesProvider.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateTypesProvider.cs new file mode 100644 index 000000000..48be51fca --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateTypesProvider.cs @@ -0,0 +1,175 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 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.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace Opc.Ua +{ + /// + /// The provider for the X509 application certificates. + /// + public class CertificateTypesProvider + { + /// + /// Create an instance of the certificate provider. + /// + public CertificateTypesProvider(ApplicationConfiguration config) + { + m_securityConfiguration = config.SecurityConfiguration; + m_certificateValidator = new CertificateValidator(); + m_certificateValidator.Update(m_securityConfiguration).GetAwaiter().GetResult(); + // for application certificates, allow untrusted and revocation status unknown, cache the known certs + m_certificateValidator.RejectUnknownRevocationStatus = false; + m_certificateValidator.AutoAcceptUntrustedCertificates = true; + m_certificateValidator.UseValidatedCertificates = true; + } + + /// + /// Gets or sets a value indicating whether the application should send the complete certificate chain. + /// + /// + /// If set to true the complete certificate chain will be sent for CA signed certificates. + /// + public bool SendCertificateChain => m_securityConfiguration.SendCertificateChain; + + /// + /// Return the instance certificate for a security policy. + /// + /// The security policy Uri + public X509Certificate2 GetInstanceCertificate(string securityPolicyUri) + { + if (securityPolicyUri == SecurityPolicies.None) + { + // return the default certificate for None + return m_securityConfiguration.ApplicationCertificates.FirstOrDefault().Certificate; + } + var certificateTypes = CertificateIdentifier.MapSecurityPolicyToCertificateTypes(securityPolicyUri); + foreach (var certType in certificateTypes) + { + var instanceCertificate = m_securityConfiguration.ApplicationCertificates.FirstOrDefault(id => id.CertificateType == certType); + if (instanceCertificate == null && + certType == ObjectTypeIds.RsaSha256ApplicationCertificateType) + { + instanceCertificate = m_securityConfiguration.ApplicationCertificates.FirstOrDefault(id => id.CertificateType == null); + } + if (instanceCertificate == null && + certType == ObjectTypeIds.ApplicationCertificateType) + { + instanceCertificate = m_securityConfiguration.ApplicationCertificates.FirstOrDefault(); + } + if (instanceCertificate == null && + certType == ObjectTypeIds.HttpsCertificateType) + { + instanceCertificate = m_securityConfiguration.ApplicationCertificates.FirstOrDefault(); + } + if (instanceCertificate != null) + { + return instanceCertificate.Certificate; + } + } + return null; + } + + /// + /// Load the instance certificate with a private key. + /// + /// + /// + public Task GetInstanceCertificateAsync(IList certificateTypes, bool privateKey) + { + foreach (var certType in certificateTypes) + { + var instanceCertificate = m_securityConfiguration.ApplicationCertificates.FirstOrDefault(id => id.CertificateType == certType); + if (instanceCertificate != null) + { + return instanceCertificate.Find(privateKey); + } + } + return Task.FromResult(null); + } + + /// + /// Loads the certificate chain of a certificate for use in a secure channel as raw byte array. + /// + /// The application certificate. + public async Task LoadCertificateChainRawAsync(X509Certificate2 certificate) + { + var instanceCertificateChain = await LoadCertificateChainAsync(certificate).ConfigureAwait(false); + if (instanceCertificateChain != null) + { + List serverCertificateChain = new List(); + for (int i = 0; i < instanceCertificateChain.Count; i++) + { + serverCertificateChain.AddRange(instanceCertificateChain[i].RawData); + } + return serverCertificateChain.ToArray(); + } + return null; + } + + /// + /// Loads the certificate chain for an application certificate. + /// + /// The application certificate. + public async Task LoadCertificateChainAsync(X509Certificate2 certificate) + { + if (certificate == null) + { + return null; + } + + // load certificate chain. + var certificateChain = new X509Certificate2Collection(certificate); + List issuers = new List(); + if (await m_certificateValidator.GetIssuers(certificate, issuers).ConfigureAwait(false)) + { + for (int i = 0; i < issuers.Count; i++) + { + certificateChain.Add(issuers[i].Certificate); + } + } + return certificateChain; + } + + /// + /// Update the security configuration of the cert type provider. + /// + /// The new security configuration. + public void Update(SecurityConfiguration securityConfiguration) + { + m_securityConfiguration = securityConfiguration; + } + + CertificateValidator m_certificateValidator; + SecurityConfiguration m_securityConfiguration; + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 4e30f5d1a..535560a02 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -40,6 +40,7 @@ public class CertificateValidator : ICertificateValidator public CertificateValidator() { m_validatedCertificates = new Dictionary(); + m_applicationCertificates = new List(); m_protectFlags = 0; m_autoAcceptUntrustedCertificates = false; m_rejectSHA1SignedCertificates = CertificateFactory.DefaultHashSize >= 256; @@ -220,16 +221,32 @@ public virtual async Task Update(SecurityConfiguration configuration) { m_maxRejectedCertificates = configuration.MaxRejectedCertificates; } + + if (configuration.ApplicationCertificates != null) + { + foreach (var applicationCertificate in configuration.ApplicationCertificates) + { + X509Certificate2 certificate = await applicationCertificate.Find(true).ConfigureAwait(false); + if (certificate == null) + { + Utils.Trace(Utils.TraceMasks.Security, "Could not find application certificate: {0}", applicationCertificate); + continue; + } + // Add to list of application certificates only if not already in list + // necessary since the application certificates may be updated multiple times + if (!m_applicationCertificates.Exists(cert => Utils.IsEqual(cert.RawData, certificate.RawData))) + { + m_applicationCertificates.Add(certificate); + } + } + } + } finally { m_semaphore.Release(); } - if (configuration.ApplicationCertificate != null) - { - m_applicationCertificate = await configuration.ApplicationCertificate.Find(true).ConfigureAwait(false); - } } /// @@ -241,10 +258,18 @@ public virtual async Task UpdateCertificate(SecurityConfiguration securityConfig try { - securityConfiguration.ApplicationCertificate.Certificate = null; - await securityConfiguration.ApplicationCertificate.LoadPrivateKeyEx( - securityConfiguration.CertificatePasswordProvider).ConfigureAwait(false); + foreach (var applicationCertificate in securityConfiguration.ApplicationCertificates) + { + m_applicationCertificates.RemoveAll(cert => Utils.IsEqual(cert.RawData, applicationCertificate.RawData)); + applicationCertificate.DisposeCertificate(); + } + + foreach (var applicationCertificate in securityConfiguration.ApplicationCertificates) + { + await applicationCertificate.LoadPrivateKeyEx( + securityConfiguration.CertificatePasswordProvider).ConfigureAwait(false); + } } finally { @@ -266,7 +291,7 @@ await securityConfiguration.ApplicationCertificate.LoadPrivateKeyEx( /// /// Reset the list of validated certificates. /// - public void ResetValidatedCertificates() + private void ResetValidatedCertificates() { m_semaphore.Wait(); @@ -372,7 +397,7 @@ public bool RejectUnknownRevocationStatus } /// - /// The minimum size of a certificate key to be trusted. + /// The minimum size of an RSA certificate key to be trusted. /// public ushort MinimumCertificateKeySize { @@ -1328,9 +1353,24 @@ protected virtual async Task InternalValidateAsync(X509Certificate2Collection ce // check if certificate is trusted. if (trustedCertificate == null && !isIssuerTrusted) { - if (m_applicationCertificate == null || !Utils.IsEqual(m_applicationCertificate.RawData, certificate.RawData)) + // If the certificate is not trusted, check if the certificate is amongst the application certificates + bool isApplicationCertificate = false; + if (m_applicationCertificates != null) + { + foreach (var appCert in m_applicationCertificates) + { + if (Utils.IsEqual(appCert.RawData, certificate.RawData)) + { + // certificate is the application certificate + isApplicationCertificate = true; + break; + } + } + } + + if (m_applicationCertificates == null || !isApplicationCertificate) { - var message = "Certificate is not trusted."; + string message = "Certificate is not trusted."; sresult = new ServiceResult(StatusCodes.BadCertificateUntrusted, null, null, message, null, sresult); } @@ -1347,13 +1387,25 @@ protected virtual async Task InternalValidateAsync(X509Certificate2Collection ce ); } + bool isECDsaSignature = X509PfxUtils.IsECDsaSignature(certificate); + // check if certificate is valid for use as app/sw or user cert X509KeyUsageFlags certificateKeyUsage = X509Utils.GetKeyUsage(certificate); - - if ((certificateKeyUsage & X509KeyUsageFlags.DataEncipherment) == 0) + if (isECDsaSignature) { - sresult = new ServiceResult(StatusCodes.BadCertificateUseNotAllowed, - null, null, "Usage of certificate is not allowed.", null, sresult); + if ((certificateKeyUsage & X509KeyUsageFlags.DigitalSignature) == 0) + { + sresult = new ServiceResult(StatusCodes.BadCertificateUseNotAllowed, + null, null, "Usage of ECDSA certificate is not allowed.", null, sresult); + } + } + else + { + if ((certificateKeyUsage & X509KeyUsageFlags.DataEncipherment) == 0) + { + sresult = new ServiceResult(StatusCodes.BadCertificateUseNotAllowed, + null, null, "Usage of RSA certificate is not allowed.", null, sresult); + } } // check if minimum requirements are met @@ -1363,13 +1415,14 @@ protected virtual async Task InternalValidateAsync(X509Certificate2Collection ce null, null, "SHA1 signed certificates are not trusted.", null, sresult); } - int keySize = X509Utils.GetRSAPublicKeySize(certificate); - if (keySize < m_minimumCertificateKeySize) + if (!isECDsaSignature) { - sresult = new ServiceResult(StatusCodes.BadCertificatePolicyCheckFailed, - null, null, - $"Certificate doesn't meet minimum key length requirement. ({keySize}<{m_minimumCertificateKeySize})", - null, sresult); + int keySize = X509Utils.GetRSAPublicKeySize(certificate); + if (keySize < m_minimumCertificateKeySize) + { + sresult = new ServiceResult(StatusCodes.BadCertificatePolicyCheckFailed, + null, null, "Certificate doesn't meet minimum key length requirement.", null, sresult); + } } if (issuedByCA && chainIncomplete) @@ -1669,7 +1722,7 @@ private static bool IsSHA1SignatureAlgorithm(Oid oid) { return oid.Value == "1.3.14.3.2.29" || // sha1RSA oid.Value == "1.2.840.10040.4.3" || // sha1DSA - oid.Value == "1.2.840.10045.4.1" || // sha1ECDSA + oid.Value == Oids.ECDsaWithSha1 || // sha1ECDSA oid.Value == "1.2.840.113549.1.1.5" || // sha1RSA oid.Value == "1.3.14.3.2.13" || // sha1DSA oid.Value == "1.3.14.3.2.27"; // dsaSHA1 @@ -1719,6 +1772,21 @@ private static bool IsSignatureValid(X509Certificate2 cert) StatusCodes.BadCertificateUntrusted }); + /// + /// Dictionary of named curves and their bit sizes. + /// + private static readonly Dictionary NamedCurveBitSizes = new Dictionary + { + // NIST Curves + { ECCurve.NamedCurves.nistP256.Oid.Value ?? "1.2.840.10045.3.1.7", 256 }, // NIST P-256 + { ECCurve.NamedCurves.nistP384.Oid.Value ?? "1.3.132.0.34" , 384 }, // NIST P-384 + { ECCurve.NamedCurves.nistP521.Oid.Value ?? "1.3.132.0.35" , 521 }, // NIST P-521 + + // Brainpool Curves + { ECCurve.NamedCurves.brainpoolP256r1.Oid.Value ?? "1.3.36.3.3.2.8.1.1.7", 256 }, // BrainpoolP256r1 + { ECCurve.NamedCurves.brainpoolP384r1.Oid.Value ?? "1.3.36.3.3.2.8.1.1.11", 384 }, // BrainpoolP384r1 + }; + /// /// Find the domain in a certificate in the /// endpoint that was used to connect a session. @@ -1777,6 +1845,45 @@ private bool FindDomain(X509Certificate2 serverCertificate, Uri endpointUrl) } return domainFound; } + + /// + /// Returns if the certificate is secure enough for the profile. + /// + /// The certificate to check. + /// The required key size in bits. + public static bool IsECSecureForProfile(X509Certificate2 certificate, int requiredKeySizeInBits) + { + using (ECDsa ecdsa = certificate.GetECDsaPublicKey()) + { + if (ecdsa == null) + { + throw new ArgumentException("Certificate does not contain an ECC public key"); + } + + if (ecdsa.KeySize != 0) + { + return ecdsa.KeySize >= requiredKeySizeInBits; + } + else + { + ECCurve curve = ecdsa.ExportParameters(false).Curve; + + if (curve.IsNamed) + { + if (NamedCurveBitSizes.TryGetValue(curve.Oid.Value, out int curveSize)) + { + return curveSize >= requiredKeySizeInBits; + } + throw new NotSupportedException($"Unknown named curve: {curve.Oid.Value}"); + } + else + { + throw new NotSupportedException("Unsupported curve type."); + } + + } + } + } #endregion #region Private Enum @@ -1807,7 +1914,7 @@ private enum ProtectFlags private CertificateStoreIdentifier m_rejectedCertificateStore; private event CertificateValidationEventHandler m_CertificateValidation; private event CertificateUpdateEventHandler m_CertificateUpdate; - private X509Certificate2 m_applicationCertificate; + private List m_applicationCertificates; private ProtectFlags m_protectFlags; private bool m_autoAcceptUntrustedCertificates; private bool m_rejectSHA1SignedCertificates; diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index c0fcc4288..f477cb1b0 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -456,9 +456,18 @@ public string GetPrivateKeyFilePath(string thumbprint) public bool SupportsLoadPrivateKey => true; /// - /// Loads the private key from a PFX/PEM file in the certificate store. + /// Loads the private key certificate with RSA signature from a PFX file in the certificate store. /// - public async Task LoadPrivateKey(string thumbprint, string subjectName, string password) + [Obsolete("Method is deprecated. Use only for RSA certificates, the replacing LoadPrivateKey with certificateType parameter should be used.")] + public Task LoadPrivateKey(string thumbprint, string subjectName, string password) + { + return LoadPrivateKey(thumbprint, subjectName, null, password); + } + + /// + /// Loads the private key from a PFX file in the certificate store. + /// + public async Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password) { if (NoPrivateKeys || m_privateKeySubdir == null || m_certificateSubdir == null || !m_certificateSubdir.Exists) @@ -509,8 +518,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub } } - // skip if not RSA certificate - if (X509Utils.GetRSAPublicKeySize(certificate) < 0) + if (!CertificateIdentifier.ValidateCertificateType(certificate, certificateType)) { continue; } @@ -550,7 +558,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub privateKeyFilePfx.FullName, password, flag); - if (X509Utils.VerifyRSAKeyPair(certificate, certificate, true)) + if (X509Utils.VerifyKeyPair(certificate, certificate, true)) { Utils.LogInfo(Utils.TraceMasks.Security, "Imported the PFX private key for [{0}].", certificate.Thumbprint); return certificate; @@ -571,7 +579,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub { byte[] pemDataBlob = File.ReadAllBytes(privateKeyFilePem.FullName); certificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(certificate, pemDataBlob, password); - if (X509Utils.VerifyRSAKeyPair(certificate, certificate, true)) + if (X509Utils.VerifyKeyPair(certificate, certificate, true)) { Utils.LogInfo(Utils.TraceMasks.Security, "Imported the PEM private key for [{0}].", certificate.Thumbprint); return certificate; @@ -977,6 +985,14 @@ private static string GetFileName(X509Certificate2 certificate) fileName.Append(ch); } + var signatureQualifier = X509Utils.GetECDsaQualifier(certificate); + if (!string.IsNullOrEmpty(signatureQualifier)) + { + fileName.Append(" ["); + fileName.Append(signatureQualifier); + fileName.Append(']'); + } + fileName.Append(" ["); fileName.Append(certificate.Thumbprint); fileName.Append(']'); diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs new file mode 100644 index 000000000..4b5f18890 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs @@ -0,0 +1,1380 @@ +/* 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.Text; +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 + { + #region Public constants + + + /// + /// 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); + + #endregion + + #region Private constants + + + private const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; + private const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; + private const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; + private const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; + + #endregion + + + + /// + /// 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; + } + } + } + + return false; + } + + /// + /// Returns the NodeId for the certificate type for the specified certificate. + /// + /// + /// + public static NodeId GetEccCertificateTypeId(X509Certificate2 certificate) + { + var keyAlgorithm = certificate.GetKeyAlgorithm(); + if (keyAlgorithm != Oids.ECPublicKey) + { + return NodeId.Null; + } + + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData); + switch (keyParameters) + { + // 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 the signature algorithm for the specified certificate. + /// + /// + /// + public static string GetECDsaQualifier(X509Certificate2 certificate) + { + if (X509Utils.IsECDsaSignature(certificate)) + { + string signatureQualifier = "ECDsa"; + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData); + + // New values can be determined by running the dotted-decimal OID value + // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); + + switch (keyParameters) + { + case NistP256KeyParameters: + { + signatureQualifier = NistP256; + break; + } + + case NistP384KeyParameters: + { + signatureQualifier = NistP384; + break; + } + + case BrainpoolP256r1KeyParameters: + { + signatureQualifier = BrainpoolP256r1; + break; + } + + case BrainpoolP384r1KeyParameters: + { + signatureQualifier = BrainpoolP384r1; + break; + } + } + return signatureQualifier; + } + return string.Empty; + } + +#if ECC_SUPPORT + /// + /// Returns the public key for the specified certificate. + /// + /// + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate) + { + string[] securityPolicyUris; + return GetPublicKey(certificate, out securityPolicyUris); + } + + /// + /// Returns the public key for the specified certificate and ouputs the security policy uris. + /// + /// + /// + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate, out string[] securityPolicyUris) + { + securityPolicyUris = null; + + var keyAlgorithm = certificate.GetKeyAlgorithm(); + + if (certificate == null || keyAlgorithm != Oids.ECPublicKey) + { + return null; + } + + const X509KeyUsageFlags SufficientFlags = + X509KeyUsageFlags.KeyAgreement | + X509KeyUsageFlags.DigitalSignature | + X509KeyUsageFlags.NonRepudiation | + X509KeyUsageFlags.CrlSign | + X509KeyUsageFlags.KeyCertSign; + + foreach (X509Extension extension in certificate.Extensions) + { + if (extension.Oid.Value == "2.5.29.15") + { + X509KeyUsageExtension kuExt = (X509KeyUsageExtension)extension; + + if ((kuExt.KeyUsages & SufficientFlags) == 0) + { + return null; + } + } + } + + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData); + byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; + + ECParameters 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 = new string[] { SecurityPolicies.ECC_nistP256 }; + break; + } + + case NistP384KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.nistP384; + securityPolicyUris = new string[] { SecurityPolicies.ECC_nistP384, SecurityPolicies.ECC_nistP256 }; + break; + } + + case BrainpoolP256r1KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; + securityPolicyUris = new string[] { SecurityPolicies.ECC_brainpoolP256r1 }; + break; + } + + case BrainpoolP384r1KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; + securityPolicyUris = new string[] { 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 (var publicKey = GetPublicKey(signingCertificate)) + { + if (publicKey == null) + { + throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "No public key for certificate."); + } + + return publicKey.KeySize / 4; + } + + throw new NotImplementedException(); + } + + /// + /// Returns the hash algorithm for the specified security policy. + /// + /// + /// + public static HashAlgorithmName GetSignatureAlgorithmName(string securityPolicyUri) + { + if (securityPolicyUri == null) + { + throw new ArgumentNullException(nameof(securityPolicyUri)); + } + + switch (securityPolicyUri) + { + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + return HashAlgorithmName.SHA256; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + return HashAlgorithmName.SHA384; + } + + case SecurityPolicies.None: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + default: + { + return HashAlgorithmName.SHA256; + } + } + } + + /// + /// Computes an ECDSA signature. + /// + public static byte[] Sign( + ArraySegment dataToSign, + X509Certificate2 signingCertificate, + string securityPolicyUri) + { + var 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 + var senderPrivateKey = signingCertificate.GetECDsaPrivateKey() as ECDsa; + + if (senderPrivateKey == null) + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Missing private key needed for create a signature."); + } + + using (senderPrivateKey) + { + var 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 = EccUtils.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 + { + /// + /// Gets or sets the X.509 certificate of the sender. + /// + public X509Certificate2 SenderCertificate { get; set; } + + /// + /// Gets or sets the collection of X.509 certificates of the sender's issuer. + /// + public X509Certificate2Collection SenderIssuerCertificates { get; set; } + + /// + /// Gets or sets a value indicating whether the sender's certificate should not be encoded. + /// + public bool DoNotEncodeSenderCertificate { get; set; } + + /// + /// Gets or sets the nonce of the sender. + /// + public Nonce SenderNonce { get; set; } + + /// + /// Gets or sets the nonce of the receiver. + /// + public Nonce ReceiverNonce { get; set; } + + /// + /// Gets or sets the X.509 certificate of the receiver. + /// + public X509Certificate2 ReceiverCertificate { get; set; } + + /// + /// Gets or sets the certificate validator. + /// + public CertificateValidator Validator { get; set; } + + /// + /// Gets or sets the security policy URI. + /// + public string SecurityPolicyUri { get; set; } + + /// + /// 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 static 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(ServiceMessageContext.GlobalContext)) + { + 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 (Aes aes = Aes.Create()) + { + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = encryptingKey; + aes.IV = iv; + + using (ICryptoTransform encryptor = aes.CreateEncryptor()) + { + 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 (Aes 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 = new UTF8Encoding().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 = 32; + int blockSize = 16; + HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; + + switch (securityPolicyUri) + { + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + encryptingKeySize = 16; + break; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + encryptingKeySize = 32; + algorithmName = HashAlgorithmName.SHA384; + break; + } + + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + { + encryptingKeySize = 32; + blockSize = 12; + algorithmName = HashAlgorithmName.SHA256; + break; + } + } + + encryptingKey = new byte[encryptingKeySize]; + iv = new byte[blockSize]; + + var keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); + var salt = Utils.Append(keyLength, s_Label, senderNonce.Data, receiverNonce.Data); + + byte[] keyData = null; + + 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; + + var signatureLength = EccUtils.GetSignatureLength(SenderCertificate); + + using (BinaryEncoder encoder = new BinaryEncoder(ServiceMessageContext.GlobalContext)) + { + // 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 (var issuer in SenderIssuerCertificates) + { + blobSize += issuer.RawData.Length; + } + + var blob = new byte[blobSize]; + Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length); + + int pos = senderCertificate.Length; + + foreach (var issuer in SenderIssuerCertificates) + { + var 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); + + var senderNonce = SenderNonce.Data; + var 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, + var 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(); + } + + var 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 = HashAlgorithmName.SHA256; + + switch (SecurityPolicyUri) + { + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + signatureAlgorithm = HashAlgorithmName.SHA384; + break; + } + } + + ArraySegment dataToSign = new ArraySegment(message, 0, message.Length - signatureLength); + var 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 encrypted data. + private ArraySegment VerifyHeaderForEcc( + ArraySegment dataToDecrypt, + DateTime earliestTime) + { + using (BinaryDecoder decoder = new BinaryDecoder(dataToDecrypt.Array, dataToDecrypt.Offset, dataToDecrypt.Count, ServiceMessageContext.GlobalContext)) + { + var 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); + } + + var 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 = HashAlgorithmName.SHA256; + + switch (SecurityPolicyUri) + { + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + signatureAlgorithm = HashAlgorithmName.SHA384; + break; + } + } + + // extract the send certificate and any chain. + var senderCertificate = decoder.ReadByteString(null); + + if (senderCertificate == null || senderCertificate.Length == 0) + { + if (SenderCertificate == null) + { + throw new ServiceResultException(StatusCodes.BadCertificateInvalid); + } + } + else + { + var senderCertificateChain = Utils.ParseCertificateChainBlob(senderCertificate); + + SenderCertificate = senderCertificateChain[0]; + SenderIssuerCertificates = new X509Certificate2Collection(); + + for (int ii = 1; ii < senderCertificateChain.Count; ii++) + { + SenderIssuerCertificates.Add(senderCertificateChain[ii]); + } + + // validate the sender. + if (Validator != null) + { + Validator.Validate(senderCertificateChain); + } + } + + // extract the send certificate and any chain. + var signingTime = decoder.ReadDateTime(null); + + if (signingTime < earliestTime) + { + throw new ServiceResultException(StatusCodes.BadInvalidTimestamp); + } + + // extract the policy header. + var headerLength = decoder.ReadUInt16(null); + + if (headerLength == 0 || headerLength > length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError); + } + + // read the policy header. + var senderPublicKey = decoder.ReadByteString(null); + var receiverPublicKey = decoder.ReadByteString(null); + + if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8) + { + throw new ServiceResultException(StatusCodes.BadDecodingError, "Unexpected policy header length"); + } + + var 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); + + ArraySegment 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 decrypted data. + public byte[] Decrypt(DateTime earliestTime, byte[] expectedNonce, byte[] data, int offset, int count) + { + byte[] encryptingKey = null; + byte[] iv = null; + byte[] secret = null; + + var dataToDecrypt = VerifyHeaderForEcc(new ArraySegment(data, offset, count), earliestTime); + + CreateKeysForEcc(SecurityPolicyUri, SenderNonce, ReceiverNonce, true, out encryptingKey, out iv); + + var plainText = DecryptSecret(dataToDecrypt.Array, dataToDecrypt.Offset, dataToDecrypt.Count, encryptingKey, iv); + + using (BinaryDecoder decoder = new BinaryDecoder(plainText.Array, plainText.Offset, plainText.Count, ServiceMessageContext.GlobalContext)) + { + var 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); + } + } + + secret = decoder.ReadByteString(null); + } + + return secret; + } +#else + /// + /// 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 = EccUtils.GetPublicKey(signingCertificate)) + { + return ecdsa.VerifyData(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count, signature, algorithm); + } + } + + /// + /// Returns the public key for the specified certificate and ouputs the security policy uris. + /// + /// + /// + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate, out string[] securityPolicyUris) + { + securityPolicyUris = null; + + var keyAlgorithm = certificate.GetKeyAlgorithm(); + + if (certificate == null || keyAlgorithm != Oids.ECPublicKey) + { + return null; + } + + const X509KeyUsageFlags SufficientFlags = + X509KeyUsageFlags.KeyAgreement | + X509KeyUsageFlags.DigitalSignature | + X509KeyUsageFlags.NonRepudiation | + X509KeyUsageFlags.CrlSign | + X509KeyUsageFlags.KeyCertSign; + + foreach (X509Extension extension in certificate.Extensions) + { + if (extension.Oid.Value == "2.5.29.15") + { + X509KeyUsageExtension kuExt = (X509KeyUsageExtension)extension; + + if ((kuExt.KeyUsages & SufficientFlags) == 0) + { + return null; + } + } + } + + PublicKey encodedPublicKey = certificate.PublicKey; + string keyParameters = BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData); + byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; + + ECParameters 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 = new string[] { SecurityPolicies.ECC_nistP256 }; + break; + } + + case NistP384KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.nistP384; + securityPolicyUris = new string[] { SecurityPolicies.ECC_nistP384, SecurityPolicies.ECC_nistP256 }; + break; + } + + case BrainpoolP256r1KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; + securityPolicyUris = new string[] { SecurityPolicies.ECC_brainpoolP256r1 }; + break; + } + + case BrainpoolP384r1KeyParameters: + { + ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; + securityPolicyUris = new string[] { SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies.ECC_brainpoolP256r1 }; + break; + } + + default: + { + throw new NotImplementedException(keyParameters); + } + } + + return ECDsa.Create(ecParameters); + } + + /// + /// Returns the public key for the specified certificate. + /// + /// + /// + public static ECDsa GetPublicKey(X509Certificate2 certificate) + { + string[] securityPolicyUris; + return GetPublicKey(certificate, out securityPolicyUris); + } + + /// + /// Returns the hash algorithm for the specified security policy. + /// + /// + /// + public static HashAlgorithmName GetSignatureAlgorithmName(string securityPolicyUri) + { + if (securityPolicyUri == null) + { + throw new ArgumentNullException(nameof(securityPolicyUri)); + } + + switch (securityPolicyUri) + { + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + return HashAlgorithmName.SHA256; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + return HashAlgorithmName.SHA384; + } + + case SecurityPolicies.None: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + default: + { + return HashAlgorithmName.SHA256; + } + } + } + +#endif + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/ICertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/ICertificateStore.cs index bc514e300..1c7139f5d 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/ICertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/ICertificateStore.cs @@ -102,8 +102,20 @@ public interface ICertificateStore : IDisposable /// The certificate password. /// Returns always null if SupportsLoadPrivateKey returns false. /// The matching certificate with private key + [Obsolete("Method is deprecated. Use only for RSA certificates, the replacing LoadPrivateKey with certificateType parameter should be used.")] Task LoadPrivateKey(string thumbprint, string subjectName, string password); + /// + /// Finds the certificate with the specified thumprint. + /// + /// The thumbprint. + /// The certificate subject. + /// The certificate type to load. + /// The certificate password. + /// Returns always null if SupportsLoadPrivateKey returns false. + /// The matching certificate with private key + Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password); + /// /// Checks if issuer has revoked the certificate. /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs new file mode 100644 index 000000000..605c71296 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -0,0 +1,676 @@ +/* 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.Security.Cryptography; +using System.Runtime.Serialization; + +#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 +{ + /// + /// Represents a cryptographic nonce used for secure communication. + /// + [Serializable] +#if ECC_SUPPORT + public class Nonce : IDisposable, ISerializable +#else + public class Nonce : ISerializable +#endif + { + #region Constructor + + /// + /// Constructor + /// + private Nonce() + { +#if ECC_SUPPORT + m_ecdh = null; +#endif +#if CURVE25519 + m_bcKeyPair = null; +#endif + } +#endregion + + #region Public Properties + + /// + /// Gets the nonce data. + /// + public byte[] Data + { + get + { + return m_data; + } + private set + { + m_data = value; + } + } + + #endregion + +#region Public Methods + +#region Instance Methods + +#if ECC_SUPPORT + /// + /// Derives a key from the remote nonce, using the specified salt, hash algorithm, and length. + /// + /// The remote nonce 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, byte[] salt, HashAlgorithmName algorithm, int length) + { +#if CURVE25519 + if (m_bcKeyPair != null) + { + var localPublicKey = m_bcKeyPair.Public; + + 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); + + HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); + generator.Init(new HkdfParameters(secret, salt, salt)); + + byte[] output = new byte[length]; + generator.GenerateBytes(output, 0, output.Length); + return output; + } + + if (localPublicKey is X448PublicKeyParameters) + { + X448Agreement agreement = new X448Agreement(); + agreement.Init(m_bcKeyPair.Private); + + var key = new X448PublicKeyParameters(remoteNonce.Data, 0); + byte[] secret = new byte[agreement.AgreementSize]; + agreement.CalculateAgreement(key, secret, 0); + + HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); + generator.Init(new HkdfParameters(secret, salt, salt)); + + byte[] output = new byte[length]; + generator.GenerateBytes(output, 0, output.Length); + return output; + } + + throw new NotSupportedException(); + } +#endif + if (m_ecdh != null) + { + var secret = m_ecdh.DeriveKeyFromHmac(remoteNonce.m_ecdh.PublicKey, algorithm, salt, null, null); + + byte[] output = new byte[length]; + + HMAC hmac = returnHMACInstance(secret, algorithm); + + 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++; + + byte[] hash = hmac.ComputeHash(info, 0, salt.Length + 1); + + int pos = 0; + + 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[info.Length - 1] = counter++; + + hash = hmac.ComputeHash(info, 0, info.Length); + + for (int ii = 0; ii < hash.Length && pos < length; ii++) + { + output[pos++] = hash[ii]; + } + } + + return output; + } + + return Data; + } +#endif + +#endregion + + #region Factory Methods + /// + /// Creates a nonce for the specified security policy URI and nonce length. + /// + /// The security policy URI. + /// A object containing the generated nonce. + public static Nonce CreateNonce(string securityPolicyUri) + { + if (securityPolicyUri == null) + { + throw new ArgumentNullException(nameof(securityPolicyUri)); + } + + Nonce nonce = null; + + switch (securityPolicyUri) + { + case SecurityPolicies.ECC_nistP256: { return CreateNonce(ECCurve.NamedCurves.nistP256); } + case SecurityPolicies.ECC_nistP384: { return CreateNonce(ECCurve.NamedCurves.nistP384); } + case SecurityPolicies.ECC_brainpoolP256r1: { return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1); } + case SecurityPolicies.ECC_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); + nonce = new Nonce() { + Data = CreateRandomNonceData(rsaNonceLength) + }; + + 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. + public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) + { + if (securityPolicyUri == null) + { + throw new ArgumentNullException(nameof(securityPolicyUri)); + } + + if (nonceData == null) + { + throw new ArgumentNullException(nameof(nonceData)); + } + + Nonce nonce = new Nonce() { + Data = nonceData + }; + + switch (securityPolicyUri) + { + case SecurityPolicies.ECC_nistP256: { return CreateNonce(ECCurve.NamedCurves.nistP256, nonceData); } + case SecurityPolicies.ECC_nistP384: { return CreateNonce(ECCurve.NamedCurves.nistP384, nonceData); } + case SecurityPolicies.ECC_brainpoolP256r1: { return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1, nonceData); } + case SecurityPolicies.ECC_brainpoolP384r1: { return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1, nonceData); } + + case SecurityPolicies.ECC_curve25519: + { + return CreateNonceForCurve25519(nonceData); + } + + case SecurityPolicies.ECC_curve448: + { + return CreateNonceForCurve448(nonceData); + } + + default: + { + break; + } + } + + return nonce; + } + #endregion + + #region Utility Methods + + /// + /// Generates a Nonce for cryptographic functions of a given length. + /// + /// + /// The requested Nonce as a + public static byte[] CreateRandomNonceData(uint length) + { + byte[] randomBytes = new byte[length]; + m_rng.GetBytes(randomBytes); + return randomBytes; + } + + /// + /// Validates the nonce for a message security mode and security policy. + /// + public static bool ValidateNonce(byte[] nonce, MessageSecurityMode securityMode, string securityPolicyUri) + { + return ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); + } + + /// + /// Validates the nonce for a message security mode and a minimum length. + /// + public static bool ValidateNonce(byte[] nonce, MessageSecurityMode securityMode, uint minNonceLength) + { + // no nonce needed for no security. + if (securityMode == MessageSecurityMode.None) + { + return true; + } + + // check the length. + if (nonce == null || nonce.Length < minNonceLength) + { + return false; + } + + // try to catch programming errors by rejecting nonces with all zeros. + for (int ii = 0; ii < nonce.Length; ii++) + { + if (nonce[ii] != 0) + { + return true; + } + } + + return false; + } + + /// + /// Returns the length of the symmetric encryption key for a security policy. + /// + public static uint GetNonceLength(string securityPolicyUri) + { + switch (securityPolicyUri) + { + 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: + case SecurityPolicies.None: + { + // Minimum nonce length by default + return m_minNonceLength; + } + } + } + + /// + /// Compare Nonce for equality. + /// + public static bool CompareNonce(byte[] a, byte[] b) + { + if (a == null || b == null) return false; + if (a.Length != b.Length) return false; + + byte result = 0; + for (int i = 0; i < a.Length; i++) + result |= (byte)(a[i] ^ b[i]); + + return result == 0; + } + + /// + /// Sets the minimum nonce value to be used as default + /// + /// + public static void SetMinNonceValue(uint nonceLength) + { + m_minNonceLength = nonceLength; + } + #endregion + +#endregion + + #region Private Methods + + /// + /// Creates a new Nonce object for use with Curve25519. + /// + /// The nonce data to use. + /// A new Nonce object. + private static Nonce CreateNonceForCurve25519(byte[] nonceData) + { + var nonce = new Nonce() { + Data = nonceData, + }; + + return nonce; + } + + /// + /// Creates a new Nonce instance for Curve448. + /// + /// The nonce data. + /// A new Nonce instance. + private static Nonce CreateNonceForCurve448(byte[] nonceData) + { + var nonce = new Nonce() { + Data = nonceData, + }; + + return nonce; + } + + /// + /// Creates a new Nonce instance with the specified ECC curve and nonce data. + /// + /// The elliptic curve to use for the ECDH key exchange. + /// The nonce data to use for the ECDH key exchange. + /// A new Nonce instance with the specified curve and nonce data. + private static Nonce CreateNonce(ECCurve curve, byte[] nonceData) + { +#if ECC_SUPPORT + Nonce nonce = new Nonce() { + Data = nonceData + }; + + int keyLength = nonceData.Length; + + byte[] qx = new byte[keyLength / 2]; + byte[] qy = new byte[keyLength / 2]; + Buffer.BlockCopy(nonceData, 0, qx, 0, keyLength / 2); + Buffer.BlockCopy(nonceData, keyLength / 2, qy, 0, keyLength / 2); + + var ecdhParameters = new ECParameters { + Curve = curve, + Q = { X = qx, Y = qy } + }; + //validate curve parameters as ECDiffieHellman.Create expects already validated curve parameters + try + { + ecdhParameters.Validate(); + nonce.m_ecdh = ECDiffieHellman.Create(ecdhParameters); + } + catch (CryptographicException e) + { + throw new ArgumentException("Invalid nonce data provided", nameof(nonceData), e); + } + //On Windows a PlatformNotSupportedException is thrown when invalid parameters are provided + catch (PlatformNotSupportedException e) + { + throw new ArgumentException("Invalid nonce data provided", nameof(nonceData), e); + } + + return nonce; +#else + throw new NotSupportedException("Platform does not support ECC curves"); +#endif + } + + /// + /// Creates a new Nonce instance using the specified elliptic curve. + /// + /// The elliptic curve to use for the ECDH key exchange. + /// A new Nonce instance. + private static Nonce CreateNonce(ECCurve curve) + { +#if ECC_SUPPORT + var ecdh = (ECDiffieHellman)ECDiffieHellman.Create(curve); + var ecdhParameters = ecdh.ExportParameters(false); + int xLen = ecdhParameters.Q.X.Length; + int yLen = ecdhParameters.Q.Y.Length; + + byte[] senderNonce = new byte[xLen + yLen]; + Array.Copy(ecdhParameters.Q.X, senderNonce, xLen); + Array.Copy(ecdhParameters.Q.Y, 0, senderNonce, xLen, yLen); + + var nonce = new Nonce() { + Data = senderNonce, + m_ecdh = ecdh + }; + + return nonce; + +#else + throw new NotSupportedException("Platform does not support ECC curves"); +#endif + } + + + + /// + /// Return the HMAC instance depending on secret and algortihm + /// + /// + /// + /// + private HMAC returnHMACInstance(byte[] secret, HashAlgorithmName algorithm) + { + switch (algorithm.Name) + { + case "SHA256": + return new HMACSHA256(secret); + case "SHA384": + return new HMACSHA384(secret); + default: + return new HMACSHA256(secret); + } + } + +#if CURVE25519 + /// + /// Creates a new Nonce object to be used in Curve25519 cryptography. + /// + /// A new Nonce object. + private static Nonce CreateNonceForCurve25519() + { + 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; + } + + /// + /// Creates a Nonce object using the X448 elliptic curve algorithm. + /// + /// A Nonce object containing the generated nonce data and key pair. + private static Nonce CreateNonceForCurve448() + { + SecureRandom random = new SecureRandom(); + IAsymmetricCipherKeyPairGenerator generator = new X448KeyPairGenerator(); + generator.Init(new X448KeyGenerationParameters(random)); + + var keyPair = generator.GenerateKeyPair(); + + byte[] senderNonce = new byte[X448PublicKeyParameters.KeySize]; + ((X448PublicKeyParameters)(keyPair.Public)).Encode(senderNonce, 0); + + var nonce = new Nonce() { + Data = senderNonce, + m_bcKeyPair = keyPair + }; + + return nonce; + } +#endif + + +#endregion + + #region Protected Methods + /// + /// Custom deserialization + /// + /// + /// + protected Nonce(SerializationInfo info, StreamingContext context) + { + var curveName = info.GetString("CurveName"); + +#if ECC_SUPPORT + if (curveName != null) + { + 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); + } +#endif + Data = (byte[])info.GetValue("Data", typeof(byte[])); + } + #endregion + +#region Private Members + +#if ECC_SUPPORT + private ECDiffieHellman m_ecdh; +#endif + + private byte[] m_data; + +#if CURVE25519 + private AsymmetricCipherKeyPair m_bcKeyPair; +#endif + +#endregion + + #region Private Static Members + private static readonly RandomNumberGenerator m_rng = RandomNumberGenerator.Create(); + + private static uint m_minNonceLength = 0; + #endregion + +#region IDisposable +#if ECC_SUPPORT + /// + /// Frees any unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// An overrideable version of the Dispose. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (m_ecdh != null) + { + m_ecdh.Dispose(); + m_ecdh = null; + } + } + } +#endif +#endregion + + #region ISerializable + + /// + /// Custom serialization + /// + /// + /// + public void GetObjectData(SerializationInfo info, StreamingContext context) + { +#if ECC_SUPPORT + if (m_ecdh != null) + { + var ecParams = m_ecdh.ExportParameters(false); + info.AddValue("CurveName", ecParams.Curve.Oid.FriendlyName); + info.AddValue("QX", ecParams.Q.X); + info.AddValue("QY", ecParams.Q.Y); + } + else + { + info.AddValue("CurveName", null); + info.AddValue("QX", null); + info.AddValue("QY", null); + } +#endif + info.AddValue("Data", Data); + } + +#endregion + } +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs index bf7b79ffe..db2c381e4 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs @@ -10,7 +10,13 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; namespace Opc.Ua { @@ -20,13 +26,27 @@ namespace Opc.Ua /// public partial class SecurityConfiguration { + #region Public Properties + /// + /// The security profiles which are supported for this configuration. + /// + public StringCollection SupportedSecurityPolicies { get; private set; } + + /// + /// Get the provider which is invoked when a password + /// for a private key is requested. + /// + public ICertificatePasswordProvider CertificatePasswordProvider { get; set; } + #endregion + + #region Public Methods /// /// Adds a certificate as a trusted peer. /// public void AddTrustedPeer(byte[] certificate) { - this.TrustedPeerCertificates.TrustedCertificates.Add(new CertificateIdentifier(certificate)); + TrustedPeerCertificates.TrustedCertificates.Add(new CertificateIdentifier(certificate)); } /// @@ -34,7 +54,8 @@ public void AddTrustedPeer(byte[] certificate) /// public void Validate() { - if (m_applicationCertificate == null) + if (m_applicationCertificates == null || + m_applicationCertificates.Count == 0) { throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "ApplicationCertificate must be specified."); } @@ -44,8 +65,117 @@ public void Validate() // replace subjectName DC=localhost with DC=hostname - ApplicationCertificate.SubjectName = Utils.ReplaceDCLocalhost(ApplicationCertificate.SubjectName); + foreach (var applicationCertificate in m_applicationCertificates) + { + applicationCertificate.SubjectName = Utils.ReplaceDCLocalhost(applicationCertificate.SubjectName); + } + } + + /// + /// Find application certificate for a security policy. + /// + /// + /// + public async Task FindApplicationCertificateAsync(string securityPolicy, bool privateKey) + { + var certificateTypes = CertificateIdentifier.MapSecurityPolicyToCertificateTypes(securityPolicy); + foreach (var certType in certificateTypes) + { + CertificateIdentifier id = ApplicationCertificates.FirstOrDefault(certId => certId.CertificateType == certType); + if (id == null) + { + if (certType == ObjectTypeIds.RsaSha256ApplicationCertificateType) + { + // undefined certificate type as RsaSha256 + id = ApplicationCertificates.FirstOrDefault(certId => certId.CertificateType == null); + } + else if (certType == ObjectTypeIds.ApplicationCertificateType) + { + // first certificate + id = ApplicationCertificates.FirstOrDefault(); + } + else if (certType == ObjectTypeIds.EccApplicationCertificateType) + { + // first Ecc certificate + id = ApplicationCertificates.FirstOrDefault(certId => X509Utils.IsECDsaSignature(certId.Certificate)); + } + } + + if (id != null) + { + return await id.Find(privateKey).ConfigureAwait(false); + } + } + return null; + } + #endregion + + #region Private Methods + /// + /// Use the list of application certificates to build a list + /// of supported security policies. + /// + private StringCollection BuildSupportedSecurityPolicies() + { + var securityPolicies = new StringCollection(); + securityPolicies.Add(SecurityPolicies.None); + foreach (var applicationCertificate in m_applicationCertificates) + { + if (applicationCertificate.CertificateType == null) + { + securityPolicies.Add(SecurityPolicies.Basic256Sha256); + securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep); + securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss); + continue; + } + if (applicationCertificate.CertificateType.Identifier is uint identifier) + { + switch (identifier) + { + case ObjectTypes.EccNistP256ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_nistP256); + break; + case ObjectTypes.EccNistP384ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_nistP256); + securityPolicies.Add(SecurityPolicies.ECC_nistP384); + break; + case ObjectTypes.EccBrainpoolP256r1ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1); + break; + case ObjectTypes.EccBrainpoolP384r1ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1); + break; + case ObjectTypes.EccCurve25519ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_curve25519); + break; + case ObjectTypes.EccCurve448ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.ECC_curve448); + break; + case ObjectTypes.RsaMinApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.Basic128Rsa15); + securityPolicies.Add(SecurityPolicies.Basic256); + break; + case ObjectTypes.ApplicationCertificateType: + case ObjectTypes.RsaSha256ApplicationCertificateType: + securityPolicies.Add(SecurityPolicies.Basic256Sha256); + securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep); + securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss); + goto case ObjectTypes.RsaMinApplicationCertificateType; + } + } + } + // filter based on platform support + var result = new StringCollection(); + foreach (var securityPolicyUri in securityPolicies.Distinct()) + { + if (SecurityPolicies.GetDisplayName(securityPolicyUri) != null) + { + result.Add(securityPolicyUri); + } + } + return result; } /// @@ -64,11 +194,9 @@ private CertificateTrustList CreateDefaultTrustList(CertificateTrustList trustLi return new CertificateTrustList(); } - /// - /// Get the provider which is invoked when a password - /// for a private key is requested. - /// - public ICertificatePasswordProvider CertificatePasswordProvider { get; set; } + + + #endregion } #endregion diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs index 8cb54b9aa..686e93ad6 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs @@ -216,15 +216,25 @@ public Task FindByThumbprint(string thumbprint) /// /// The LoadPrivateKey special handling is not necessary in this store. + [Obsolete("Method is deprecated. Use only for RSA certificates, the replacing LoadPrivateKey with certificateType parameter should be used.")] public Task LoadPrivateKey(string thumbprint, string subjectName, string password) { return Task.FromResult(null); } + /// + /// The LoadPrivateKey special handling is not necessary in this store. + public Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password) + { + return Task.FromResult(null); + } + /// /// CRLs are only supported on Windows Platform. public bool SupportsCRLs => PlatformHelper.IsWindowsWithCrlSupport(); + + /// /// CRLs are only supported on Windows Platform. public async Task IsRevoked(X509Certificate2 issuer, X509Certificate2 certificate) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index e0698029f..03508118e 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -121,6 +121,31 @@ public static int GetRSAPublicKeySize(X509Certificate2 certificate) } } + /// + /// Returns the size of the public key of a given certificate + /// + /// The certificate + public static int GetPublicKeySize(X509Certificate2 certificate) + { + using (RSA rsaPublicKey = certificate.GetRSAPublicKey()) + { + if (rsaPublicKey != null) + { + return rsaPublicKey.KeySize; + } + } + + using (ECDsa ecdsaPublicKey = certificate.GetECDsaPublicKey()) + { + if (ecdsaPublicKey != null) + { + return ecdsaPublicKey.KeySize; + } + } + + return -1; + } + /// /// Extracts the application URI specified in the certificate. /// @@ -466,6 +491,50 @@ public static List ParseDistinguishedName(string name) return fields; } + /// + /// Return if a certificate has a ECDsa signature. + /// + /// The certificate to test. + public static bool IsECDsaSignature(X509Certificate2 cert) + { + return X509PfxUtils.IsECDsaSignature(cert); + } + + /// + /// Return a qualifier string if a ECDsa signature algorithm used. + /// + /// The certificate. + public static string GetECDsaQualifier(X509Certificate2 certificate) + { + return EccUtils.GetECDsaQualifier(certificate); + } + + /// + /// Verify RSA key pair of two certificates. + /// + public static bool VerifyKeyPair( + X509Certificate2 certWithPublicKey, + X509Certificate2 certWithPrivateKey, + bool throwOnError = false) + { + return X509PfxUtils.VerifyKeyPair(certWithPublicKey, certWithPrivateKey, throwOnError); + } + + /// + /// Verify ECDsa key pair of two certificates. + /// + public static bool VerifyECDsaKeyPair( + X509Certificate2 certWithPublicKey, + X509Certificate2 certWithPrivateKey, + bool throwOnError = false) + { +#if ECC_SUPPORT + return X509PfxUtils.VerifyECDsaKeyPair(certWithPublicKey, certWithPrivateKey, throwOnError); +#else + throw new NotSupportedException(); +#endif + } + /// /// Verify RSA key pair of two certificates. /// @@ -733,7 +802,7 @@ public static HashAlgorithmName GetRSAHashAlgorithmName(uint hashSizeInBits) internal static string GeneratePasscode() { const int kLength = 18; - byte[] tokenBuffer = Utils.Nonce.CreateNonce(kLength); + byte[] tokenBuffer = Nonce.CreateRandomNonceData(kLength); return Convert.ToBase64String(tokenBuffer); } diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityConstants.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityConstants.cs index 74fc727e8..028df4dd3 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityConstants.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityConstants.cs @@ -39,7 +39,7 @@ public static class SecurityAlgorithms /// /// The RSA-PSS-SHA256 algorithm used to create asymmetric key signatures. - /// + /// public const string RsaPssSha256 = "http://opcfoundation.org/UA/security/rsa-pss-sha2-256"; /// @@ -54,32 +54,32 @@ public static class SecurityAlgorithms /// /// The RSA-OAEP algorithm used to encrypt data. - /// + /// public const string RsaOaep = "http://www.w3.org/2001/04/xmlenc#rsa-oaep"; /// /// The RSA-OAEP-SHA256 algorithm used to encrypt data. - /// + /// public const string RsaOaepSha256 = "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256"; /// /// The RSA-PKCSv1.5 algorithm used to encrypt data. - /// + /// public const string Rsa15 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5"; /// /// The RSA-PKCSv1.5 SHA256 algorithm used to encrypt data. - /// + /// public const string Rsa15Sha256 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5-sha2-256"; /// /// The RSA-OAEP algorithm used to encrypt keys. - /// + /// public const string KwRsaOaep = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"; /// /// The RSA-PKCSv1.5 algorithm used to encrypt keys. - /// + /// public const string KwRsa15 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5"; /// diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index df4adb345..f3a55fa0d 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -59,6 +60,36 @@ public static class SecurityPolicies /// public const string Aes256_Sha256_RsaPss = BaseUri + "Aes256_Sha256_RsaPss"; + /// + /// The URI for the ECC_nistP256 security policy. + /// + public const string ECC_nistP256 = BaseUri + "ECC_nistP256"; + + /// + /// The URI for the ECC_nistP384 security policy. + /// + public const string ECC_nistP384 = BaseUri + "ECC_nistP384"; + + /// + /// The URI for the ECC_brainpoolP256r1 security policy. + /// + public const string ECC_brainpoolP256r1 = BaseUri + "ECC_brainpoolP256r1"; + + /// + /// The URI for the ECC_brainpoolP384r1 security policy. + /// + public const string ECC_brainpoolP384r1 = BaseUri + "ECC_brainpoolP384r1"; + + /// + /// The URI for the ECC_curve25519 security policy. + /// + public const string ECC_curve25519 = BaseUri + "ECC_curve25519"; + + /// + /// The URI for the ECC_curve448 security policy. + /// + public const string ECC_curve448 = BaseUri + "ECC_curve448"; + /// /// The URI for the Https security policy. /// @@ -66,14 +97,53 @@ public static class SecurityPolicies #endregion #region Static Methods - private static bool IsPlatformSupportedUri(string name) + private static bool IsPlatformSupportedName(string name) { + // 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)) + { + return true; + } + if (name.Equals(nameof(Aes256_Sha256_RsaPss), StringComparison.Ordinal) && - !RsaUtils.IsSupportingRSAPssSign.Value) + RsaUtils.IsSupportingRSAPssSign.Value) + { + return true; + } + +#if ECC_SUPPORT + // ECC policy + if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal)) { - return false; + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); } - return true; + + // ECC policy + if (name.Equals(nameof(ECC_curve25519), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448), StringComparison.Ordinal)) + { +#if CURVE25519 + return true; +#endif + } +#endif + return false; } /// @@ -84,7 +154,7 @@ public static string GetUri(string displayName) FieldInfo[] fields = typeof(SecurityPolicies).GetFields(BindingFlags.Public | BindingFlags.Static); foreach (FieldInfo field in fields) { - if (field.Name == displayName && IsPlatformSupportedUri(field.Name)) + if (field.Name == displayName && IsPlatformSupportedName(field.Name)) { return (string)field.GetValue(typeof(SecurityPolicies)); } @@ -103,7 +173,7 @@ public static string GetDisplayName(string policyUri) foreach (FieldInfo field in fields) { if (policyUri == (string)field.GetValue(typeof(SecurityPolicies)) && - IsPlatformSupportedUri(field.Name)) + IsPlatformSupportedName(field.Name)) { return field.Name; } @@ -112,6 +182,30 @@ public static string GetDisplayName(string policyUri) return null; } + /// + /// If a security policy is known and spelled according to the spec. + /// + /// + /// This functions returns only information if a security policy Uri is + /// valid and existing according to the spec. + /// It does not provide the information if the policy is supported + /// by the application or by the platform. + /// + public static bool IsValidSecurityPolicyUri(string policyUri) + { + FieldInfo[] fields = typeof(SecurityPolicies).GetFields(BindingFlags.Public | BindingFlags.Static); + + foreach (FieldInfo field in fields) + { + if (policyUri == (string)field.GetValue(typeof(SecurityPolicies))) + { + return true; + } + } + + return false; + } + /// /// Returns the display names for all security policy uris. /// @@ -120,10 +214,10 @@ public static string[] GetDisplayNames() FieldInfo[] fields = typeof(SecurityPolicies).GetFields(BindingFlags.Public | BindingFlags.Static); var names = new List(); - // skip base Uri + // skip base Uri, ignore Https for (int ii = 1; ii < fields.Length - 1; ii++) { - if (IsPlatformSupportedUri(fields[ii].Name)) + if (IsPlatformSupportedName(fields[ii].Name)) { names.Add(fields[ii].Name); } @@ -133,7 +227,28 @@ public static string[] GetDisplayNames() } /// - /// Returns the default security policy uri. + /// Returns the deprecated RSA security policy uri. + /// + public static string[] GetDefaultDeprecatedUris() + { + string[] defaultNames = { + nameof(Basic128Rsa15), + nameof(Basic256) + }; + var defaultUris = new List(); + foreach (var name in defaultNames) + { + var uri = GetUri(name); + if (uri != null) + { + defaultUris.Add(uri); + } + } + return defaultUris.ToArray(); + } + + /// + /// Returns the default RSA security policy uri. /// public static string[] GetDefaultUris() { @@ -153,6 +268,29 @@ public static string[] GetDefaultUris() return defaultUris.ToArray(); } + /// + /// Returns the default ECC security policy uri. + /// + public static string[] GetDefaultEccUris() + { + string[] defaultNames = { + nameof(ECC_nistP256), + nameof(ECC_nistP384), + nameof(ECC_brainpoolP256r1), + nameof(ECC_brainpoolP384r1) + }; + var defaultUris = new List(); + foreach (var name in defaultNames) + { + var uri = GetUri(name); + if (uri != null) + { + defaultUris.Add(uri); + } + } + return defaultUris.ToArray(); + } + /// /// Encrypts the text using the SecurityPolicyUri and returns the result. /// @@ -201,11 +339,21 @@ public static EncryptedData Encrypt(X509Certificate2 certificate, string securit break; } + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + { + return encryptedData; + } + case SecurityPolicies.None: { break; } + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: default: { throw ServiceResultException.Create( @@ -267,6 +415,10 @@ public static byte[] Decrypt(X509Certificate2 certificate, string securityPolicy break; } + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: case SecurityPolicies.None: { if (String.IsNullOrEmpty(dataToDecrypt.Algorithm)) @@ -276,6 +428,8 @@ public static byte[] Decrypt(X509Certificate2 certificate, string securityPolicy break; } + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: default: { throw ServiceResultException.Create( @@ -336,6 +490,24 @@ public static SignatureData Sign(X509Certificate2 certificate, string securityPo break; } +#if ECC_SUPPORT + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + signatureData.Algorithm = null; + signatureData.Signature = EccUtils.Sign(new ArraySegment(dataToSign), certificate, HashAlgorithmName.SHA256); + break; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + signatureData.Algorithm = null; + signatureData.Signature = EccUtils.Sign(new ArraySegment(dataToSign), certificate, HashAlgorithmName.SHA384); + break; + } +#endif + case SecurityPolicies.None: { signatureData.Algorithm = null; @@ -343,6 +515,8 @@ public static SignatureData Sign(X509Certificate2 certificate, string securityPo break; } + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: default: { throw ServiceResultException.Create( @@ -418,13 +592,27 @@ public static bool Verify(X509Certificate2 certificate, string securityPolicyUri signature.Algorithm, SecurityAlgorithms.RsaPssSha256); } +#if ECC_SUPPORT + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + return EccUtils.Verify(new ArraySegment(dataToVerify), signature.Signature, certificate, HashAlgorithmName.SHA256); + } + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + return EccUtils.Verify(new ArraySegment(dataToVerify), signature.Signature, certificate, HashAlgorithmName.SHA384); + } +#endif // always accept signatures if security is not used. case SecurityPolicies.None: { return true; } + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: default: { throw ServiceResultException.Create( diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ITransportBindings.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ITransportBindings.cs index 9390e5b01..5adf836b6 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/ITransportBindings.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/ITransportBindings.cs @@ -93,8 +93,7 @@ public interface ITransportListenerFactory : /// The base addreses for the service host. /// The server description. /// The list of supported security policies. - /// The server certificate. - /// The cert cahin of the server certificate. + /// The provider for application certificates. List CreateServiceHost( ServerBase serverBase, IDictionary hosts, @@ -102,8 +101,7 @@ List CreateServiceHost( IList baseAddresses, ApplicationDescription serverDescription, List securityPolicies, - X509Certificate2 instanceCertificate, - X509Certificate2Collection instanceCertificateChain + CertificateTypesProvider instanceCertificateTypesProvider ); } diff --git a/Stack/Opc.Ua.Core/Stack/Client/DiscoveryClient.cs b/Stack/Opc.Ua.Core/Stack/Client/DiscoveryClient.cs index 1cc658329..96980ef26 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/DiscoveryClient.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/DiscoveryClient.cs @@ -109,7 +109,7 @@ public static DiscoveryClient Create( } /// - /// Creates a binding for to use for discovering servers. + /// Creates a binding to use for discovering servers. /// /// The discovery URL. /// The endpoint configuration. @@ -130,6 +130,7 @@ public static DiscoveryClient Create( try { + // Will always use the first certificate clientCertificate = applicationConfiguration?.SecurityConfiguration?.ApplicationCertificate?.Find(true).Result; } catch diff --git a/Stack/Opc.Ua.Core/Stack/Client/ReverseConnectHost.cs b/Stack/Opc.Ua.Core/Stack/Client/ReverseConnectHost.cs index 30b03ec26..090e4db5e 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ReverseConnectHost.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ReverseConnectHost.cs @@ -67,8 +67,6 @@ public void Open() CertificateValidator = null, NamespaceUris = null, Factory = null, - ServerCertificate = null, - ServerCertificateChain = null, ReverseConnectListener = true, MaxChannelCount = 0, }; diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs index 9ee2696ad..d387dbd1b 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs @@ -439,8 +439,11 @@ public virtual async Task Validate(ApplicationType applicationType) SecurityConfiguration.Validate(); - // load private key - await SecurityConfiguration.ApplicationCertificate.LoadPrivateKeyEx(SecurityConfiguration.CertificatePasswordProvider).ConfigureAwait(false); + // load private keys + foreach (var applicationCertificate in SecurityConfiguration.ApplicationCertificates) + { + await applicationCertificate.LoadPrivateKeyEx(SecurityConfiguration.CertificatePasswordProvider).ConfigureAwait(false); + } Func generateDefaultUri = () => { var sb = new StringBuilder(); diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 8de37f269..57324aacf 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -91,6 +91,7 @@ public Uri ProxyUrl /// /// Finds the user token policy with the specified id. /// + [Obsolete] public UserTokenPolicy FindUserTokenPolicy(string policyId) { foreach (UserTokenPolicy policy in m_userIdentityTokens) @@ -104,9 +105,52 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId) return null; } + /// + /// Finds the user token policy with the specified id and securtyPolicyUri + /// + public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurityPolicyUri) + { + UserTokenPolicy sameEncryptionAlgorithm = null; + UserTokenPolicy unspecifiedSecPolicy = null; + // The specified security policies take precedence + foreach (UserTokenPolicy policy in m_userIdentityTokens) + { + if (policy.PolicyId == policyId) + { + if (policy.SecurityPolicyUri == tokenSecurityPolicyUri) + { + return policy; + } + else if ( + policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && + (EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && EccUtils.IsEccPolicy(tokenSecurityPolicyUri)) || + (!EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && !EccUtils.IsEccPolicy(tokenSecurityPolicyUri)) + ) + { + if (sameEncryptionAlgorithm == null) + { + sameEncryptionAlgorithm = policy; + } + } + else if (policy.SecurityPolicyUri == null) + { + unspecifiedSecPolicy = policy; + } + } + } + // The first token with the same encryption algorithm (RSA/ECC) follows + if (sameEncryptionAlgorithm != null) + { + return sameEncryptionAlgorithm; + } + // The first token with unspecified security policy follows / no policy + return unspecifiedSecPolicy; + } + /// /// Finds a token policy that matches the user identity specified. /// + [Obsolete] public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, XmlQualifiedName issuedTokenType) { if (issuedTokenType == null) @@ -120,6 +164,22 @@ public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, XmlQualified /// /// Finds a token policy that matches the user identity specified. /// + public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, + XmlQualifiedName issuedTokenType, + string tokenSecurityPolicyUri) + { + if (issuedTokenType == null) + { + return FindUserTokenPolicy(tokenType, (string)null, tokenSecurityPolicyUri); + } + + return FindUserTokenPolicy(tokenType, issuedTokenType.Namespace, tokenSecurityPolicyUri); + } + + /// + /// Finds a token policy that matches the user identity specified. + /// + [Obsolete] public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, string issuedTokenType) { // construct issuer type. @@ -146,6 +206,57 @@ public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, string issue // no policy found return null; } + + /// + /// Finds a token policy that matches the user identity specified. + /// + public UserTokenPolicy FindUserTokenPolicy(UserTokenType tokenType, + string issuedTokenType, + string tokenSecurityPolicyUri) + { + // construct issuer type. + string issuedTokenTypeText = issuedTokenType; + + UserTokenPolicy sameEncryptionAlgorithm = null; + UserTokenPolicy unspecifiedSecPolicy = null; + // The specified security policies take precedence + foreach (UserTokenPolicy policy in m_userIdentityTokens) + { + if ((policy.TokenType == tokenType) && (issuedTokenTypeText == policy.IssuedTokenType)) + { + if ((policy.SecurityPolicyUri == tokenSecurityPolicyUri) || (tokenType == UserTokenType.Anonymous)) + { + return policy; + } + else if ( + policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && + (EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && EccUtils.IsEccPolicy(tokenSecurityPolicyUri)) || + (!EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && !EccUtils.IsEccPolicy(tokenSecurityPolicyUri)) + ) + { + if (sameEncryptionAlgorithm == null) + { + sameEncryptionAlgorithm = policy; + } + } + else if (policy.SecurityPolicyUri == null) + { + if (sameEncryptionAlgorithm == null) + { + unspecifiedSecPolicy = policy; + } + } + } + } + // The first token with the same encryption algorithm (RSA/ECC) follows + if (sameEncryptionAlgorithm != null) + { + return sameEncryptionAlgorithm; + } + // The first token with unspecified security policy follows / no policy + return unspecifiedSecPolicy; + + } #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/SecurityConfigurationManager.cs b/Stack/Opc.Ua.Core/Stack/Configuration/SecurityConfigurationManager.cs index 230791f77..b90b2602a 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/SecurityConfigurationManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/SecurityConfigurationManager.cs @@ -148,7 +148,17 @@ public SecuredApplication ReadConfiguration(string filePath) // copy the security settings. if (applicationConfiguration.SecurityConfiguration != null) { - application.ApplicationCertificate = SecuredApplication.ToCertificateIdentifier(applicationConfiguration.SecurityConfiguration.ApplicationCertificate); + + if (applicationConfiguration.SecurityConfiguration.IsDeprecatedConfiguration) + { +#pragma warning disable CS0618 // Type or member is obsolete + application.ApplicationCertificate = SecuredApplication.ToCertificateIdentifier(applicationConfiguration.SecurityConfiguration.ApplicationCertificate); +#pragma warning restore CS0618 // Type or member is obsolete + } + else + { + application.ApplicationCertificates = SecuredApplication.ToCertificateList(applicationConfiguration.SecurityConfiguration.ApplicationCertificates); + } if (applicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates != null) { @@ -318,7 +328,15 @@ private static void UpdateDocument(XmlElement element, SecuredApplication applic if (application.ApplicationCertificate != null) { +#pragma warning disable CS0618 // Type or member is obsolete security.ApplicationCertificate = SecuredApplication.FromCertificateIdentifier(application.ApplicationCertificate); +#pragma warning restore CS0618 // Type or member is obsolete + security.IsDeprecatedConfiguration = true; + } + + if (application.ApplicationCertificates != null) + { + security.ApplicationCertificates = SecuredApplication.FromCertificateList(application.ApplicationCertificates); } security.TrustedIssuerCertificates = SecuredApplication.FromCertificateStoreIdentifierToTrustList(application.IssuerCertificateStore); diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index 9d75de2b0..b3fe16db0 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -630,35 +630,19 @@ private set } /// - /// The server's application instance certificate. + /// The server's application instance certificate types provider. /// - /// The instance X.509 certificate. - protected X509Certificate2 InstanceCertificate + /// The provider for the X.509 certificates. + public CertificateTypesProvider InstanceCertificateTypesProvider { get { - return (X509Certificate2)m_instanceCertificate; + return m_instanceCertificateTypesProvider; } private set { - m_instanceCertificate = value; - } - } - - /// - /// Gets the instance certificate chain. - /// - protected X509Certificate2Collection InstanceCertificateChain - { - get - { - return m_instanceCertificateChain; - } - - private set - { - m_instanceCertificateChain = value; + m_instanceCertificateTypesProvider = value; } } @@ -784,30 +768,10 @@ public static bool RequireEncryption(EndpointDescription description) protected virtual void OnCertificateUpdate(object sender, CertificateUpdateEventArgs e) { // disconnect all sessions - InstanceCertificateChain = null; - InstanceCertificate = e.SecurityConfiguration.ApplicationCertificate.Certificate; - if (Configuration.SecurityConfiguration.SendCertificateChain) - { - InstanceCertificateChain = new X509Certificate2Collection(InstanceCertificate); - var issuers = new List(); - var validationErrors = new Dictionary(); - Configuration.CertificateValidator.GetIssuersNoExceptionsOnGetIssuer(InstanceCertificateChain, issuers, validationErrors).GetAwaiter().GetResult(); - foreach (var error in validationErrors) - { - if (error.Value != null) - { - Utils.LogCertificate("OnCertificateUpdate: GetIssuers Validation Error: {0}", error.Key, error.Value.Result); - } - } - for (int i = 0; i < issuers.Count; i++) - { - InstanceCertificateChain.Add(issuers[i].Certificate); - } - } - + InstanceCertificateTypesProvider.Update(e.SecurityConfiguration); foreach (var listener in TransportListeners) { - listener.CertificateUpdate(e.CertificateValidator, InstanceCertificate, InstanceCertificateChain); + listener.CertificateUpdate(e.CertificateValidator, InstanceCertificateTypesProvider); } } @@ -833,7 +797,7 @@ ICertificateValidator certificateValidator TransportListenerSettings settings = new TransportListenerSettings { Descriptions = endpoints, Configuration = endpointConfiguration, - ServerCertificate = InstanceCertificate, + ServerCertificateTypesProvider = InstanceCertificateTypesProvider, CertificateValidator = certificateValidator, NamespaceUris = MessageContext.NamespaceUris, Factory = MessageContext.Factory, @@ -1354,47 +1318,38 @@ protected virtual void OnServerStarting(ApplicationConfiguration configuration) } // load the instance certificate. - if (configuration.SecurityConfiguration.ApplicationCertificate != null) + X509Certificate2 defaultInstanceCertificate = null; + InstanceCertificateTypesProvider = new CertificateTypesProvider(configuration); + foreach (var securityPolicy in configuration.ServerConfiguration.SecurityPolicies) { - InstanceCertificate = configuration.SecurityConfiguration.ApplicationCertificate.Find(true).GetAwaiter().GetResult(); - } + if (securityPolicy.SecurityMode == MessageSecurityMode.None) + { + continue; + } - if (InstanceCertificate == null) - { - throw new ServiceResultException( - StatusCodes.BadConfigurationError, - "Server does not have an instance certificate assigned."); - } + var instanceCertificate = InstanceCertificateTypesProvider.GetInstanceCertificate(securityPolicy.SecurityPolicyUri); - if (!InstanceCertificate.HasPrivateKey) - { - throw new ServiceResultException( - StatusCodes.BadConfigurationError, - "Server does not have access to the private key for the instance certificate."); - } + if (instanceCertificate == null) + { + throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "Server does not have an instance certificate assigned."); + } - // load certificate chain. - InstanceCertificateChain = new X509Certificate2Collection(InstanceCertificate); - var issuers = new List(); - var validationErrors = new Dictionary(); - configuration.CertificateValidator.GetIssuersNoExceptionsOnGetIssuer(InstanceCertificateChain, issuers, validationErrors).Wait(); + if (!instanceCertificate.HasPrivateKey) + { + throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "Server does not have access to the private key for the instance certificate."); + } - if (validationErrors.Count > 0) - { - Utils.LogWarning("Issuer validation errors ignored on startup:"); - // only list warning for errors to avoid that the server can not start - foreach (var error in validationErrors) + if (defaultInstanceCertificate == null) { - if (error.Value != null) - { - Utils.LogCertificate(LogLevel.Warning, "- " + error.Value.Message, error.Key); - } + defaultInstanceCertificate = instanceCertificate; } - } - for (int i = 0; i < issuers.Count; i++) - { - InstanceCertificateChain.Add(issuers[i].Certificate); + // preload chain + InstanceCertificateTypesProvider.LoadCertificateChainAsync(instanceCertificate).GetAwaiter().GetResult(); } // use the message context from the configuration to ensure the channels are using the same one. @@ -1405,7 +1360,10 @@ protected virtual void OnServerStarting(ApplicationConfiguration configuration) // assign a unique identifier if none specified. if (String.IsNullOrEmpty(configuration.ApplicationUri)) { - configuration.ApplicationUri = X509Utils.GetApplicationUriFromCertificate(InstanceCertificate); + var instanceCertificate = InstanceCertificateTypesProvider.GetInstanceCertificate( + configuration.ServerConfiguration.SecurityPolicies[0].SecurityPolicyUri); + + configuration.ApplicationUri = X509Utils.GetApplicationUriFromCertificate(instanceCertificate); if (String.IsNullOrEmpty(configuration.ApplicationUri)) { @@ -1421,9 +1379,9 @@ protected virtual void OnServerStarting(ApplicationConfiguration configuration) MessageContext.NamespaceUris.Append(configuration.ApplicationUri); // assign an instance name. - if (String.IsNullOrEmpty(configuration.ApplicationName) && InstanceCertificate != null) + if (String.IsNullOrEmpty(configuration.ApplicationName) && defaultInstanceCertificate != null) { - configuration.ApplicationName = InstanceCertificate.GetNameInfo(X509NameType.DnsName, false); + configuration.ApplicationName = defaultInstanceCertificate.GetNameInfo(X509NameType.DnsName, false); } // save the certificate validator. @@ -1737,8 +1695,7 @@ private void OnProcessRequestQueue(object state) private object m_messageContext; private object m_serverError; private object m_certificateValidator; - private object m_instanceCertificate; - private X509Certificate2Collection m_instanceCertificateChain; + private CertificateTypesProvider m_instanceCertificateTypesProvider; private object m_serverProperties; private object m_configuration; private object m_serverDescription; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index 763b71f1a..f44d2586b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -31,26 +31,10 @@ public TcpListenerChannel( ITcpChannelListener listener, BufferManager bufferManager, ChannelQuotas quotas, - X509Certificate2 serverCertificate, + CertificateTypesProvider serverCertificateTypeProvider, EndpointDescriptionCollection endpoints) : - this(contextId, listener, bufferManager, quotas, serverCertificate, null, endpoints) - { - } - - /// - /// Attaches the object to an existing socket. - /// - public TcpListenerChannel( - string contextId, - ITcpChannelListener listener, - BufferManager bufferManager, - ChannelQuotas quotas, - X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain, - EndpointDescriptionCollection endpoints) - : - base(contextId, bufferManager, quotas, serverCertificate, serverCertificateChain, endpoints, MessageSecurityMode.None, SecurityPolicies.None) + base(contextId, bufferManager, quotas, serverCertificateTypeProvider, endpoints, MessageSecurityMode.None, SecurityPolicies.None) { m_listener = listener; } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs index abc79ce09..45ebb3ef3 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs @@ -32,7 +32,7 @@ public TcpReverseConnectChannel( ChannelQuotas quotas, EndpointDescriptionCollection endpoints) : - base(contextId, listener, bufferManager, quotas, null, null, endpoints) + base(contextId, listener, bufferManager, quotas, null, endpoints) { } #endregion diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 8ce1377c4..ade29a3df 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -35,27 +35,10 @@ public TcpServerChannel( ITcpChannelListener listener, BufferManager bufferManager, ChannelQuotas quotas, - X509Certificate2 serverCertificate, + CertificateTypesProvider serverCertificateTypesProvider, EndpointDescriptionCollection endpoints) : - this(contextId, listener, bufferManager, quotas, serverCertificate, null, endpoints) - { - m_queuedResponses = new SortedDictionary(); - } - - /// - /// Attaches the object to an existing socket. - /// - public TcpServerChannel( - string contextId, - ITcpChannelListener listener, - BufferManager bufferManager, - ChannelQuotas quotas, - X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain, - EndpointDescriptionCollection endpoints) - : - base(contextId, listener, bufferManager, quotas, serverCertificate, serverCertificateChain, endpoints) + base(contextId, listener, bufferManager, quotas, serverCertificateTypesProvider, endpoints) { m_queuedResponses = new SortedDictionary(); } @@ -592,11 +575,11 @@ private bool ProcessOpenSecureChannelRequest(uint messageType, ArraySegment CreateServiceHost( IList baseAddresses, ApplicationDescription serverDescription, List securityPolicies, - X509Certificate2 instanceCertificate, - X509Certificate2Collection instanceCertificateChain) + CertificateTypesProvider instanceCertificateTypesProvider) { // generate a unique host name. string hostName = "/Tcp"; @@ -100,21 +99,17 @@ public List CreateServiceHost( if (requireEncryption) { - description.ServerCertificate = instanceCertificate.RawData; - - // check if complete chain should be sent. - if (configuration.SecurityConfiguration.SendCertificateChain && - instanceCertificateChain != null && - instanceCertificateChain.Count > 1) + if (instanceCertificateTypesProvider != null) { - List serverCertificateChain = new List(); + var instanceCertificate = instanceCertificateTypesProvider.GetInstanceCertificate(description.SecurityPolicyUri); + description.ServerCertificate = instanceCertificate?.RawData; - for (int i = 0; i < instanceCertificateChain.Count; i++) + // check if complete chain should be sent. + if (instanceCertificate != null && + configuration.SecurityConfiguration.SendCertificateChain) { - serverCertificateChain.AddRange(instanceCertificateChain[i].RawData); + description.ServerCertificate = instanceCertificateTypesProvider.LoadCertificateChainRawAsync(instanceCertificate).GetAwaiter().GetResult(); } - - description.ServerCertificate = serverCertificateChain.ToArray(); } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index bb0d93bf7..917ae8718 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -155,8 +155,7 @@ public void Open( m_quotas.CertificateValidator = settings.CertificateValidator; // save the server certificate. - m_serverCertificate = settings.ServerCertificate; - m_serverCertificateChain = settings.ServerCertificateChain; + m_serverCertificateTypesProvider = settings.ServerCertificateTypesProvider; m_bufferManager = new BufferManager("Server", m_quotas.MaxBufferSize); m_channels = new ConcurrentDictionary(); @@ -269,7 +268,7 @@ public void CreateReverseConnection(Uri url, int timeout) this, m_bufferManager, m_quotas, - m_serverCertificate, + m_serverCertificateTypesProvider, m_descriptions); uint channelId = GetNextChannelId(); @@ -477,30 +476,26 @@ public async Task TransferListenerChannel( /// public void CertificateUpdate( ICertificateValidator validator, - X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain) + CertificateTypesProvider certificateTypesProvider + ) { m_quotas.CertificateValidator = validator; - m_serverCertificate = serverCertificate; - m_serverCertificateChain = serverCertificateChain; + m_serverCertificateTypesProvider = certificateTypesProvider; foreach (var description in m_descriptions) { - // check if complete chain should be sent. - if (m_serverCertificateChain != null && - m_serverCertificateChain.Count > 1) + // TODO: why only if SERVERCERT != null + if (description.ServerCertificate != null) { - var byteServerCertificateChain = new List(); - - for (int i = 0; i < m_serverCertificateChain.Count; i++) + X509Certificate2 serverCertificate = certificateTypesProvider.GetInstanceCertificate(description.SecurityPolicyUri); + if (certificateTypesProvider.SendCertificateChain) { - byteServerCertificateChain.AddRange(m_serverCertificateChain[i].RawData); + byte[] serverCertificateChainRaw = certificateTypesProvider.LoadCertificateChainRawAsync(serverCertificate).Result; + description.ServerCertificate = serverCertificateChainRaw; + } + else + { + description.ServerCertificate = serverCertificate.RawData; } - - description.ServerCertificate = byteServerCertificateChain.ToArray(); - } - else if (description.ServerCertificate != null) - { - description.ServerCertificate = serverCertificate.RawData; } } } @@ -539,33 +534,32 @@ private void OnAccept(object sender, SocketAsyncEventArgs e) Utils.SilentDispose(e.AcceptSocket); } - // check if the accept socket has been created. - if (serveChannel && e.AcceptSocket != null && e.SocketError == SocketError.Success) + // check if the accept socket has been created. + if (serveChannel && e.AcceptSocket != null && e.SocketError == SocketError.Success) + { + try { - try + if (m_reverseConnectListener) { - if (m_reverseConnectListener) - { - // create the channel to manage incoming reverse connections. - channel = new TcpReverseConnectChannel( - m_listenerId, - this, - m_bufferManager, - m_quotas, - m_descriptions); - } - else - { - // create the channel to manage incoming connections. - channel = new TcpServerChannel( - m_listenerId, - this, - m_bufferManager, - m_quotas, - m_serverCertificate, - m_serverCertificateChain, - m_descriptions); - } + // create the channel to manage incoming reverse connections. + channel = new TcpReverseConnectChannel( + m_listenerId, + this, + m_bufferManager, + m_quotas, + m_descriptions); + } + else + { + // create the channel to manage incoming connections. + channel = new TcpServerChannel( + m_listenerId, + this, + m_bufferManager, + m_quotas, + m_serverCertificateTypesProvider, + m_descriptions); + } if (m_callback != null) { @@ -804,8 +798,7 @@ private void SetUri(Uri baseAddress, string relativeAddress) private EndpointDescriptionCollection m_descriptions; private BufferManager m_bufferManager; private ChannelQuotas m_quotas; - private X509Certificate2 m_serverCertificate; - private X509Certificate2Collection m_serverCertificateChain; + private CertificateTypesProvider m_serverCertificateTypesProvider; private int m_lastChannelId; private Socket m_listeningSocket; private Socket m_listeningSocketIPv6; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index 1e23efba9..23acb3f84 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -97,19 +97,6 @@ internal X509Certificate2Collection ClientCertificateChain set { m_clientCertificateChain = value; } } - /// - /// Creates a new nonce. - /// - protected byte[] CreateNonce() - { - uint length = GetNonceLength(); - if (length > 0) - { - return Utils.Nonce.CreateNonce(length); - } - return null; - } - /// /// Returns the thumbprint as a uppercase string. /// @@ -190,22 +177,105 @@ protected static void CompareCertificates(X509Certificate2 expected, X509Certifi #endregion #region Asymmetric Cryptography Functions + /// - /// Returns the length of the symmetric encryption key. + /// Validates the nonce. /// - protected uint GetNonceLength() + protected byte[] CreateNonce(X509Certificate2 certificate) { - return Utils.Nonce.GetNonceLength(SecurityPolicyUri); + switch (SecurityPolicyUri) + { + 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) + { + return Nonce.CreateRandomNonceData(length); + } + + break; + } +#if ECC_SUPPORT + 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 m_localNonce.Data; + } +#endif + default: + case SecurityPolicies.None: + { + return null; + } + } + + return null; } /// /// Validates the nonce. /// - protected bool ValidateNonce(byte[] nonce) + protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) { - return Utils.Nonce.ValidateNonce(nonce, SecurityMode, SecurityPolicyUri); + // no nonce needed for no security. + if (SecurityMode == MessageSecurityMode.None) + { + return true; + } + + // check the length. + if (nonce == null || nonce.Length != Nonce.GetNonceLength(SecurityPolicyUri)) + { + return false; + } + + switch (SecurityPolicyUri) + { + case SecurityPolicies.Basic128Rsa15: + case SecurityPolicies.Basic256: + case SecurityPolicies.Basic256Sha256: + case SecurityPolicies.Aes128_Sha256_RsaOaep: + case SecurityPolicies.Aes256_Sha256_RsaPss: + { + // try to catch programming errors by rejecting nonces with all zeros. + for (int ii = 0; ii < nonce.Length; ii++) + { + if (nonce[ii] != 0) + { + return true; + } + } + + break; + } +#if ECC_SUPPORT + 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); + return true; + } +#endif + } + + return false; } + /// /// Returns the plain text block size for key in the specified certificate. /// @@ -230,6 +300,16 @@ protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) return RsaUtils.GetPlainTextBlockSize(receiverCertificate, RsaUtils.Padding.Pkcs1); } + 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 1; + } + default: case SecurityPolicies.None: { @@ -262,6 +342,16 @@ protected int GetCipherTextBlockSize(X509Certificate2 receiverCertificate) return RsaUtils.GetCipherTextBlockSize(receiverCertificate, RsaUtils.Padding.Pkcs1); } + 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 1; + } + default: case SecurityPolicies.None: { @@ -362,7 +452,17 @@ protected int GetAsymmetricSignatureSize(X509Certificate2 senderCertificate) { return RsaUtils.GetSignatureLength(senderCertificate); } - +#if ECC_SUPPORT + 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); + } +#endif default: case SecurityPolicies.None: { @@ -596,43 +696,46 @@ protected BufferCollection WriteAsymmetricMessage( if (SecurityMode != MessageSecurityMode.None) { - if (X509Utils.GetRSAPublicKeySize(receiverCertificate) <= TcpMessageLimits.KeySizeExtraPadding) + if (receiverCertificate.GetRSAPublicKey() != null) { - // need to reserve one byte for the padding. - plainTextSize++; - - if (plainTextSize % plainTextBlockSize != 0) + if (X509Utils.GetRSAPublicKeySize(receiverCertificate) <= TcpMessageLimits.KeySizeExtraPadding) { - padding = plainTextBlockSize - (plainTextSize % plainTextBlockSize); - } + // need to reserve one byte for the padding. + plainTextSize++; - encoder.WriteByte(null, (byte)padding); - for (int ii = 0; ii < padding; ii++) - { - encoder.WriteByte(null, (byte)padding); - } - } - else - { - // need to reserve one byte for the padding. - plainTextSize++; - // need to reserve one byte for the extrapadding. - plainTextSize++; + if (plainTextSize % plainTextBlockSize != 0) + { + padding = plainTextBlockSize - (plainTextSize % plainTextBlockSize); + } - if (plainTextSize % plainTextBlockSize != 0) - { - padding = plainTextBlockSize - (plainTextSize % plainTextBlockSize); + encoder.WriteByte(null, (byte)padding); + for (int ii = 0; ii < padding; ii++) + { + encoder.WriteByte(null, (byte)padding); + } } - - byte paddingSize = (byte)(padding & 0xff); - byte extraPaddingByte = (byte)((padding >> 8) & 0xff); - - encoder.WriteByte(null, paddingSize); - for (int ii = 0; ii < padding; ii++) + else { - encoder.WriteByte(null, (byte)paddingSize); + // need to reserve one byte for the padding. + plainTextSize++; + // need to reserve one byte for the extrapadding. + plainTextSize++; + + if (plainTextSize % plainTextBlockSize != 0) + { + padding = plainTextBlockSize - (plainTextSize % plainTextBlockSize); + } + + byte paddingSize = (byte)(padding & 0xff); + byte extraPaddingByte = (byte)((padding >> 8) & 0xff); + + encoder.WriteByte(null, paddingSize); + for (int ii = 0; ii < padding; ii++) + { + encoder.WriteByte(null, (byte)paddingSize); + } + encoder.WriteByte(null, extraPaddingByte); } - encoder.WriteByte(null, extraPaddingByte); } // update the plaintext size with the padding size. @@ -714,7 +817,7 @@ protected BufferCollection WriteAsymmetricMessage( [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "messageType"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "messageSize")] protected void ReadAsymmetricMessageHeader( BinaryDecoder decoder, - X509Certificate2 receiverCertificate, + ref X509Certificate2 receiverCertificate, out uint secureChannelId, out X509Certificate2Collection senderCertificateChain, out string securityPolicyUri) @@ -773,10 +876,29 @@ protected void ReadAsymmetricMessageHeader( // verify receiver thumbprint. if (thumbprintData != null && thumbprintData.Length > 0) { + bool loadChain = false; + // TODO: client should use the proider too! + if (m_serverCertificateTypesProvider != null) + { + receiverCertificate = m_serverCertificateTypesProvider.GetInstanceCertificate(securityPolicyUri); + m_serverCertificate = receiverCertificate; + loadChain = true; + } + + if (receiverCertificate == null) + { + throw ServiceResultException.Create(StatusCodes.BadCertificateInvalid, "The receiver has no matching certificate for the selected profile."); + } + if (receiverCertificate.Thumbprint.ToUpperInvariant() != GetThumbprintString(thumbprintData)) { throw ServiceResultException.Create(StatusCodes.BadCertificateInvalid, "The receiver's certificate thumbprint is not valid."); } + + if (loadChain) + { + m_serverCertificateChain = m_serverCertificateTypesProvider?.LoadCertificateChainAsync(receiverCertificate).GetAwaiter().GetResult(); + } } else { @@ -806,6 +928,8 @@ protected void ReviseSecurityMode(bool firstCall, MessageSecurityMode requestedM { m_securityMode = endpoint.SecurityMode; m_selectedEndpoint = endpoint; + m_serverCertificate = m_serverCertificateTypesProvider.GetInstanceCertificate(m_securityPolicyUri); + m_serverCertificateChain = m_serverCertificateTypesProvider.LoadCertificateChainAsync(m_serverCertificate).GetAwaiter().GetResult(); supported = true; break; } @@ -847,6 +971,8 @@ protected virtual bool SetEndpointUrl(string endpointUrl) m_securityMode = endpoint.SecurityMode; m_securityPolicyUri = endpoint.SecurityPolicyUri; + m_serverCertificate = m_serverCertificateTypesProvider.GetInstanceCertificate(m_securityPolicyUri); + m_serverCertificateChain = m_serverCertificateTypesProvider.LoadCertificateChainAsync(m_serverCertificate).GetAwaiter().GetResult(); m_selectedEndpoint = endpoint; return true; } @@ -860,6 +986,7 @@ protected virtual bool SetEndpointUrl(string endpointUrl) protected ArraySegment ReadAsymmetricMessage( ArraySegment buffer, X509Certificate2 receiverCertificate, + out uint channelId, out X509Certificate2 senderCertificate, out uint requestId, @@ -871,13 +998,13 @@ protected ArraySegment ReadAsymmetricMessage( string securityPolicyUri = null; X509Certificate2Collection senderCertificateChain; - // parse the security header. - ReadAsymmetricMessageHeader( - decoder, - receiverCertificate, - out channelId, - out senderCertificateChain, - out securityPolicyUri); + // parse the security header. + ReadAsymmetricMessageHeader( + decoder, + ref receiverCertificate, + out channelId, + out senderCertificateChain, + out securityPolicyUri); if (senderCertificateChain != null && senderCertificateChain.Count > 0) { @@ -981,7 +1108,7 @@ protected ArraySegment ReadAsymmetricMessage( // verify padding. int paddingCount = 0; - if (SecurityMode != MessageSecurityMode.None) + if (SecurityMode != MessageSecurityMode.None && receiverCertificate.GetRSAPublicKey() != null) { int paddingEnd = -1; if (X509Utils.GetRSAPublicKeySize(receiverCertificate) > TcpMessageLimits.KeySizeExtraPadding) @@ -1073,6 +1200,21 @@ protected byte[] Sign( { return Rsa_Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } +#if ECC_SUPPORT + 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); + } +#endif } } @@ -1113,7 +1255,21 @@ protected bool Verify( { return Rsa_Verify(dataToVerify, signature, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } +#if ECC_SUPPORT + 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); + } +#endif default: { return false; @@ -1137,6 +1293,12 @@ protected ArraySegment Encrypt( switch (SecurityPolicyUri) { default: + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: case SecurityPolicies.None: { byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); @@ -1181,6 +1343,12 @@ protected ArraySegment Decrypt( switch (SecurityPolicyUri) { default: + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: case SecurityPolicies.None: { byte[] decryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Decrypt"); @@ -1217,11 +1385,16 @@ protected ArraySegment Decrypt( private string m_securityPolicyUri; private bool m_discoveryOnly; private EndpointDescription m_selectedEndpoint; + private CertificateTypesProvider m_serverCertificateTypesProvider; private X509Certificate2 m_serverCertificate; private X509Certificate2Collection m_serverCertificateChain; private X509Certificate2 m_clientCertificate; private X509Certificate2Collection m_clientCertificateChain; private bool m_uninitialized; +#if ECC_SUPPORT + private Nonce m_localNonce; + private Nonce m_remoteNonce; +#endif #endregion } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 8d91e253a..ffa08af7a 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -89,6 +89,14 @@ protected void DiscardTokens() #endregion #region Symmetric Cryptography Functions + /// + /// Indicates that an explicit signature is not present. + /// + private bool UseAuthenticatedEncryption + { + get; set; + } + /// /// The byte length of the MAC (a.k.a signature) attached to each message. /// @@ -104,6 +112,8 @@ protected void DiscardTokens() /// protected void CalculateSymmetricKeySizes() { + UseAuthenticatedEncryption = false; + switch (SecurityPolicyUri) { case SecurityPolicies.Basic128Rsa15: @@ -151,6 +161,37 @@ protected void CalculateSymmetricKeySizes() break; } + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + m_hmacHashSize = 32; + m_signatureKeySize = 32; + m_encryptionKeySize = 16; + m_encryptionBlockSize = 16; + break; + } + + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + { + UseAuthenticatedEncryption = true; + m_hmacHashSize = 16; + m_signatureKeySize = 32; + m_encryptionKeySize = 32; + m_encryptionBlockSize = 12; + break; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + m_hmacHashSize = 48; + m_signatureKeySize = 48; + m_encryptionKeySize = 32; + m_encryptionBlockSize = 16; + break; + } + default: case SecurityPolicies.None: { @@ -162,7 +203,74 @@ protected void CalculateSymmetricKeySizes() } } } + private void DeriveKeysWithPSHA(HashAlgorithmName algorithmName, byte[] secret, byte[] seed, ChannelToken token, bool isServer) + { + int length = m_signatureKeySize + m_encryptionKeySize + m_encryptionBlockSize; + + using (var hmac = Utils.CreateHMAC(algorithmName, secret)) + { + var output = Utils.PSHA(hmac, null, seed, 0, length); + + var signingKey = new byte[m_signatureKeySize]; + var encryptingKey = new byte[m_encryptionKeySize]; + var iv = new byte[m_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); + + if (isServer) + { + token.ServerSigningKey = signingKey; + token.ServerEncryptingKey = encryptingKey; + token.ServerInitializationVector = iv; + } + else + { + token.ClientSigningKey = signingKey; + token.ClientEncryptingKey = encryptingKey; + token.ClientInitializationVector = iv; + } + } + } +#if ECC_SUPPORT + private void DeriveKeysWithHKDF(HashAlgorithmName algorithmName, byte[] salt, ChannelToken token, bool isServer) + { + int length = m_signatureKeySize + m_encryptionKeySize + m_encryptionBlockSize; + + var output = m_localNonce.DeriveKey(m_remoteNonce, salt, algorithmName, length); + + var signingKey = new byte[m_signatureKeySize]; + var encryptingKey = new byte[m_encryptionKeySize]; + var iv = new byte[m_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); + + if (isServer) + { + token.ServerSigningKey = signingKey; + token.ServerEncryptingKey = encryptingKey; + token.ServerInitializationVector = iv; + } + else + { + token.ClientSigningKey = signingKey; + token.ClientEncryptingKey = encryptingKey; + token.ClientInitializationVector = iv; + } + } + + static readonly byte[] s_HkdfClientLabel = new UTF8Encoding().GetBytes("opcua-client"); + static readonly byte[] s_HkdfServerLabel = new UTF8Encoding().GetBytes("opcua-server"); + static readonly byte[] s_HkdfAes128SignOnlyKeyLength = BitConverter.GetBytes((ushort)32); + static readonly byte[] s_HkdfAes256SignOnlyKeyLength = BitConverter.GetBytes((ushort)48); + static readonly byte[] s_HkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)64); + static readonly byte[] s_HkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes((ushort)96); + static readonly byte[] s_HkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes((ushort)76); +#endif /// /// Computes the keys for a token. @@ -176,25 +284,84 @@ protected void ComputeKeys(ChannelToken token) return; } - if (SecurityPolicyUri == SecurityPolicies.Basic256Sha256 || - SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep || - SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss) - { - token.ClientSigningKey = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, 0, m_signatureKeySize); - token.ClientEncryptingKey = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); - token.ClientInitializationVector = Utils.PSHA256(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); - token.ServerSigningKey = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, 0, m_signatureKeySize); - token.ServerEncryptingKey = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); - token.ServerInitializationVector = Utils.PSHA256(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); - } - else + byte[] serverSecret = token.ServerNonce; + byte[] clientSecret = token.ClientNonce; + HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; + + switch (SecurityPolicyUri) { - token.ClientSigningKey = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, 0, m_signatureKeySize); - token.ClientEncryptingKey = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize, m_encryptionKeySize); - token.ClientInitializationVector = Utils.PSHA1(token.ServerNonce, null, token.ClientNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); - token.ServerSigningKey = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, 0, m_signatureKeySize); - token.ServerEncryptingKey = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize, m_encryptionKeySize); - token.ServerInitializationVector = Utils.PSHA1(token.ClientNonce, null, token.ServerNonce, m_signatureKeySize + m_encryptionKeySize, m_encryptionBlockSize); + default: + { + DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); + DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); + break; + } +#if ECC_SUPPORT + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1: + { + algorithmName = HashAlgorithmName.SHA256; + var length = (SecurityMode == MessageSecurityMode.Sign) ? s_HkdfAes128SignOnlyKeyLength : s_HkdfAes128SignAndEncryptKeyLength; + var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); + var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); + + Utils.LogTrace("Length={0}", Utils.ToHexString(length)); + Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); + Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); + + DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); + DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); + break; + } + + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP384r1: + { + algorithmName = HashAlgorithmName.SHA384; + var length = (SecurityMode == MessageSecurityMode.Sign) ? s_HkdfAes256SignOnlyKeyLength : s_HkdfAes256SignAndEncryptKeyLength; + var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); + var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); + + Utils.LogTrace("Length={0}", Utils.ToHexString(length)); + Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); + Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); + + DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); + DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); + break; + } + + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + { + algorithmName = HashAlgorithmName.SHA256; + var length = s_HkdfChaCha20Poly1305KeyLength; + var serverSalt = Utils.Append(length, s_HkdfServerLabel, serverSecret, clientSecret); + var clientSalt = Utils.Append(length, s_HkdfClientLabel, clientSecret, serverSecret); + + Utils.LogTrace("Length={0}", Utils.ToHexString(length)); + Utils.LogTrace("ClientSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSecret={0}", Utils.ToHexString(clientSecret)); + Utils.LogTrace("ServerSalt={0}", Utils.ToHexString(serverSalt)); + Utils.LogTrace("ClientSalt={0}", Utils.ToHexString(clientSalt)); + + DeriveKeysWithHKDF(algorithmName, serverSalt, token, true); + DeriveKeysWithHKDF(algorithmName, clientSalt, token, false); + break; + } +#endif + case SecurityPolicies.Basic128Rsa15: + case SecurityPolicies.Basic256: + { + algorithmName = HashAlgorithmName.SHA1; + DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); + DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); + break; + } } switch (SecurityPolicyUri) @@ -204,8 +371,12 @@ protected void ComputeKeys(ChannelToken token) 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. + // create encryptors. SymmetricAlgorithm AesCbcEncryptorProvider = Aes.Create(); AesCbcEncryptorProvider.Mode = CipherMode.CBC; AesCbcEncryptorProvider.Padding = PaddingMode.None; @@ -219,21 +390,12 @@ protected void ComputeKeys(ChannelToken token) AesCbcDecryptorProvider.Key = token.ServerEncryptingKey; AesCbcDecryptorProvider.IV = token.ServerInitializationVector; token.ServerEncryptor = AesCbcDecryptorProvider; + break; + } - // create HMACs. - if (SecurityPolicyUri == SecurityPolicies.Basic256Sha256 || - SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep || - SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss) - { - // SHA256 - token.ServerHmac = new HMACSHA256(token.ServerSigningKey); - token.ClientHmac = new HMACSHA256(token.ClientSigningKey); - } - else - { // SHA1 - token.ServerHmac = new HMACSHA1(token.ServerSigningKey); - token.ClientHmac = new HMACSHA1(token.ClientSigningKey); - } + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + { break; } @@ -243,6 +405,50 @@ protected void ComputeKeys(ChannelToken token) break; } } + + switch (SecurityPolicyUri) + { + case SecurityPolicies.Basic128Rsa15: + case SecurityPolicies.Basic256: + { + token.ServerHmac = new HMACSHA1(token.ServerSigningKey); + token.ClientHmac = new HMACSHA1(token.ClientSigningKey); + 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; + } + + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve448: + { + break; + } + + default: + case SecurityPolicies.None: + { + break; + } + } + + } /// @@ -269,6 +475,12 @@ protected BufferCollection WriteSymmetricMessage( int maxPayloadSize = maxPlainTextSize - SymmetricSignatureSize - 1 - TcpMessageLimits.SequenceHeaderSize; int headerSize = TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; + // no padding byte. + if (UseAuthenticatedEncryption) + { + maxPayloadSize++; + } + // write the body to stream. ArraySegmentStream ostrm = new ArraySegmentStream( BufferManager, @@ -372,7 +584,7 @@ protected BufferCollection WriteSymmetricMessage( // calculate the padding. int padding = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) { // reserve one byte for the padding size. count++; @@ -403,7 +615,7 @@ protected BufferCollection WriteSymmetricMessage( messageSize += chunkToProcess.Count; // write padding. - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) { for (int jj = 0; jj <= padding; jj++) { @@ -411,18 +623,26 @@ protected BufferCollection WriteSymmetricMessage( } } + // calculate and write signature. if (SecurityMode != MessageSecurityMode.None) { - // calculate and write signature. - byte[] signature = Sign(token, new ArraySegment(chunkToProcess.Array, 0, encoder.Position), isRequest); - - if (signature != null) + if (UseAuthenticatedEncryption) + { + strm.Seek(SymmetricSignatureSize, SeekOrigin.Current); + } + else { - encoder.WriteRawBytes(signature, 0, signature.Length); + 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) + if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && !UseAuthenticatedEncryption) || + (SecurityMode != MessageSecurityMode.None && UseAuthenticatedEncryption)) { // encrypt the data. ArraySegment dataToEncrypt = new ArraySegment(chunkToProcess.Array, TcpMessageLimits.SymmetricHeaderSize, encoder.Position - TcpMessageLimits.SymmetricHeaderSize); @@ -604,6 +824,10 @@ protected byte[] Sign(ChannelToken token, ArraySegment dataToSign, bool us 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); } @@ -630,12 +854,15 @@ protected bool Verify( case SecurityPolicies.Basic128Rsa15: case SecurityPolicies.Basic256: case SecurityPolicies.Basic256Sha256: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.ECC_nistP256: { return SymmetricVerify(token, signature, dataToVerify, useClientKeys); } - default: { return false; @@ -650,7 +877,6 @@ protected void Encrypt(ChannelToken token, ArraySegment dataToEncrypt, boo { switch (SecurityPolicyUri) { - default: case SecurityPolicies.None: { break; @@ -659,12 +885,36 @@ protected void Encrypt(ChannelToken token, ArraySegment dataToEncrypt, boo 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: { 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 + //case SecurityPolicies.Aes128_Gcm256_RsaOaep: + default: + { + throw new NotSupportedException(SecurityPolicyUri); + } } } @@ -675,7 +925,6 @@ protected void Decrypt(ChannelToken token, ArraySegment dataToDecrypt, boo { switch (SecurityPolicyUri) { - default: case SecurityPolicies.None: { break; @@ -684,12 +933,35 @@ protected void Decrypt(ChannelToken token, ArraySegment dataToDecrypt, boo 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 + //case SecurityPolicies.Aes128_Gcm256_RsaOaep: + default: + { + throw new NotSupportedException(SecurityPolicyUri); + } } } @@ -815,6 +1087,264 @@ private static void SymmetricDecrypt( decryptor.TransformBlock(blockToDecrypt, start, count, blockToDecrypt, start); } } + + private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) + { + iv[0] ^= (byte)((token.TokenId & 0x000000FF)); + iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); + iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); + iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); + iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); + iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); + iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); + iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + } + +#if CURVE25519 + /// + /// Encrypts a message using a symmetric algorithm. + /// + 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."); + } + } + } +#endif #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index f69fc55d9..d6aea6fc4 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -24,7 +24,6 @@ namespace Opc.Ua.Bindings public partial class UaSCUaBinaryChannel : IMessageSink, IDisposable { #region Constructors - /// /// Attaches the object to an existing socket. /// @@ -35,10 +34,10 @@ public UaSCUaBinaryChannel( X509Certificate2 serverCertificate, EndpointDescriptionCollection endpoints, MessageSecurityMode securityMode, - string securityPolicyUri) - : - this(contextId, bufferManager, quotas, serverCertificate, null, endpoints, securityMode, securityPolicyUri) - { } + string securityPolicyUri) : + this(contextId, bufferManager, quotas, null, serverCertificate, endpoints, securityMode, securityPolicyUri) + { + } /// /// Attaches the object to an existing socket. @@ -47,12 +46,28 @@ public UaSCUaBinaryChannel( string contextId, BufferManager bufferManager, ChannelQuotas quotas, + CertificateTypesProvider serverCertificateTypesProvider, + EndpointDescriptionCollection endpoints, + MessageSecurityMode securityMode, + string securityPolicyUri) : + this(contextId, bufferManager, quotas, serverCertificateTypesProvider, null, endpoints, securityMode, securityPolicyUri) + { + } + + /// + /// Attaches the object to an existing socket. + /// + private UaSCUaBinaryChannel( + string contextId, + BufferManager bufferManager, + ChannelQuotas quotas, + CertificateTypesProvider serverCertificateTypesProvider, X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain, EndpointDescriptionCollection endpoints, MessageSecurityMode securityMode, string securityPolicyUri) { + if (bufferManager == null) throw new ArgumentNullException(nameof(bufferManager)); if (quotas == null) throw new ArgumentNullException(nameof(quotas)); @@ -70,8 +85,12 @@ public UaSCUaBinaryChannel( securityPolicyUri = SecurityPolicies.None; } - if (securityMode != MessageSecurityMode.None) + X509Certificate2Collection serverCertificateChain = null; + if (serverCertificateTypesProvider != null && + securityMode != MessageSecurityMode.None) { + serverCertificate = serverCertificateTypesProvider.GetInstanceCertificate(securityPolicyUri); + if (serverCertificate == null) throw new ArgumentNullException(nameof(serverCertificate)); if (serverCertificate.RawData.Length > TcpMessageLimits.MaxCertificateSize) @@ -80,6 +99,8 @@ public UaSCUaBinaryChannel( Utils.Format("The DER encoded certificate may not be more than {0} bytes.", TcpMessageLimits.MaxCertificateSize), nameof(serverCertificate)); } + + serverCertificateChain = serverCertificateTypesProvider.LoadCertificateChainAsync(serverCertificate).GetAwaiter().GetResult(); } if (Encoding.UTF8.GetByteCount(securityPolicyUri) > TcpMessageLimits.MaxSecurityPolicyUriSize) @@ -91,6 +112,7 @@ public UaSCUaBinaryChannel( m_bufferManager = bufferManager; m_quotas = quotas; + m_serverCertificateTypesProvider = serverCertificateTypesProvider; m_serverCertificate = serverCertificate; m_serverCertificateChain = serverCertificateChain; m_endpoints = endpoints; @@ -131,6 +153,7 @@ public UaSCUaBinaryChannel( m_maxResponseChunkCount = CalculateChunkCount(m_maxResponseMessageSize, TcpMessageLimits.MinBufferSize); CalculateSymmetricKeySizes(); + } #endregion @@ -150,7 +173,19 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // nothing to do. +#if ECC_SUPPORT + if (m_localNonce != null) + { + m_localNonce.Dispose(); + m_localNonce = null; + } + + if (m_remoteNonce != null) + { + m_remoteNonce.Dispose(); + m_remoteNonce = null; + } +#endif } } #endregion @@ -213,11 +248,42 @@ protected void ChannelStateChanged(TcpChannelState state, ServiceResult reason) /// /// Returns a new sequence number. /// - protected uint GetNewSequenceNumber() + protected uint GetNewSequenceNumber() { - return Utils.IncrementIdentifier(ref m_sequenceNumber); - } + bool isLegacy = !EccUtils.IsEccPolicy(SecurityPolicyUri); + long newSeqNumber = Interlocked.Increment(ref m_sequenceNumber); + bool maxValueOverflow = isLegacy ? newSeqNumber > kMaxValueLegacyTrue : newSeqNumber > kMaxValueLegacyFalse; + + // LegacySequenceNumbers are TRUE for non ECC profiles + // https://reference.opcfoundation.org/Core/Part6/v105/docs/6.7.2.4 + if (isLegacy) + { + if (maxValueOverflow) + { + // First number after wrap around shall be less than 1024 + // 1 for legaccy reasons + Interlocked.Exchange(ref m_sequenceNumber, 1); + return 1; + } + return (uint)newSeqNumber; + } + else + { + uint retVal = (uint)newSeqNumber - 1; + if (maxValueOverflow) + { + // First number after wrap around and as initial value shall be 0 + Interlocked.Exchange(ref m_sequenceNumber, 0); + Interlocked.Exchange(ref m_localSequenceNumber, 0); + return retVal; + } + Interlocked.Exchange(ref m_localSequenceNumber, retVal); + return retVal; + } + } + + /// /// Resets the sequence number after a connect. /// @@ -231,6 +297,17 @@ protected void ResetSequenceNumber(uint sequenceNumber) /// 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) ))) + { + m_remoteSequenceNumber = sequenceNumber; + m_firstReceivedSequenceNumber = false; + return true; + } + // everything ok if new number is greater. if (sequenceNumber > m_remoteSequenceNumber) { @@ -241,8 +318,10 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // check for a valid rollover. if (m_remoteSequenceNumber > TcpMessageLimits.MinSequenceNumber && sequenceNumber < TcpMessageLimits.MaxRolloverSequenceNumber) { - // only one rollover per token is allowed. - if (!m_sequenceRollover) + // 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) ))) { m_sequenceRollover = true; m_remoteSequenceNumber = sequenceNumber; @@ -876,8 +955,10 @@ public void UpdateLastActiveTime() private uint m_channelId; private string m_globalChannelId; private long m_sequenceNumber; + private long m_localSequenceNumber; private uint m_remoteSequenceNumber; private bool m_sequenceRollover; + private bool m_firstReceivedSequenceNumber = true; private uint m_partialRequestId; private BufferCollection m_partialMessageChunks; @@ -885,6 +966,11 @@ public void UpdateLastActiveTime() private int m_lastActiveTickCount; #endregion + + #region Constants + private const uint kMaxValueLegacyTrue = TcpMessageLimits.MinSequenceNumber; + private const uint kMaxValueLegacyFalse = UInt32.MaxValue; + #endregion } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 9c6915cec..03c893b24 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -27,22 +27,6 @@ namespace Opc.Ua.Bindings public class UaSCUaBinaryClientChannel : UaSCUaBinaryChannel { #region Constructors - /// - /// Creates a channel for for a client. - /// - public UaSCUaBinaryClientChannel( - string contextId, - BufferManager bufferManager, - IMessageSocketFactory socketFactory, - ChannelQuotas quotas, - X509Certificate2 clientCertificate, - X509Certificate2 serverCertificate, - EndpointDescription endpoint) - : - this(contextId, bufferManager, socketFactory, quotas, clientCertificate, null, serverCertificate, endpoint) - { - } - /// /// Creates a channel for for a client. /// @@ -538,7 +522,7 @@ private void SendOpenSecureChannelRequest(bool renew) { // create a new token. ChannelToken token = CreateToken(); - token.ClientNonce = CreateNonce(); + token.ClientNonce = CreateNonce(ClientCertificate); // construct the request. OpenSecureChannelRequest request = new OpenSecureChannelRequest(); @@ -656,7 +640,12 @@ private bool ProcessOpenSecureChannelResponse(uint messageType, ArraySegment void CertificateUpdate( ICertificateValidator validator, - X509Certificate2 serverCertificate, - X509Certificate2Collection serverCertificateChain); + CertificateTypesProvider serverCertificateTypes); /// /// Raised when a new connection is waiting for a client. diff --git a/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs index a67ce0f4d..465eae485 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/TransportListenerSettings.cs @@ -39,25 +39,14 @@ public EndpointConfiguration Configuration } /// - /// Gets or sets the server certificate. + /// Gets or sets the server certificate type provider. /// - public X509Certificate2 ServerCertificate + public CertificateTypesProvider ServerCertificateTypesProvider { - get { return m_serverCertificate; } - set { m_serverCertificate = value; } + get { return m_serverCertificateTypes; } + set { m_serverCertificateTypes = value; } } - /// - /// Gets or sets the server certificate chain. - /// - /// - /// The server certificate chain. - /// - public X509Certificate2Collection ServerCertificateChain - { - get { return m_serverCertificateChain; } - set { m_serverCertificateChain = value; } - } /// /// Gets or Sets the certificate validator. @@ -132,8 +121,7 @@ public int MaxChannelCount #region Private Fields private EndpointDescriptionCollection m_descriptions; private EndpointConfiguration m_configuration; - private X509Certificate2 m_serverCertificate; - private X509Certificate2Collection m_serverCertificateChain; + private CertificateTypesProvider m_serverCertificateTypes; private ICertificateValidator m_certificateValidator; private NamespaceTable m_namespaceUris; private IEncodeableFactory m_channelFactory; diff --git a/Stack/Opc.Ua.Core/Stack/Types/UserIdentityToken.cs b/Stack/Opc.Ua.Core/Stack/Types/UserIdentityToken.cs index e2aeb4d70..ce9c2fca7 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/UserIdentityToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/UserIdentityToken.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Text; using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Security.Certificates; namespace Opc.Ua { @@ -25,6 +26,7 @@ public partial class UserIdentityToken /// /// Encrypts the token (implemented by the subclass). /// + [Obsolete("Use Encrypt(X509Certificate2, byte[], string securityPolicyUri, Nonce, X509Certificate2, X509Certificate2Collection, bool) ")] public virtual void Encrypt(X509Certificate2 certificate, byte[] receiverNonce, string securityPolicyUri) { } @@ -32,10 +34,39 @@ public virtual void Encrypt(X509Certificate2 certificate, byte[] receiverNonce, /// /// Decrypts the token (implemented by the subclass). /// + [Obsolete("Use Decrypt(X509Certificate2, Nonce, string, Nonce, X509Certificate2, X509Certificate2Collection, CertificateValidator) ")] public virtual void Decrypt(X509Certificate2 certificate, byte[] receiverNonce, string securityPolicyUri) { } + /// + /// Encrypts the token (implemented by the subclass). + /// + public virtual void Encrypt( + X509Certificate2 receiverCertificate, + byte[] receiverNonce, + string securityPolicyUri, + Nonce receiverEphemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + bool doNotEncodeSenderCertificate = false) + { + } + + /// + /// Decrypts the token (implemented by the subclass). + /// + public virtual void Decrypt( + X509Certificate2 certificate, + Nonce receiverNonce, + string securityPolicyUri, + Nonce ephemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + CertificateValidator validator = null) + { + } + /// /// Creates a signature with the token (implemented by the subclass). /// @@ -74,7 +105,32 @@ public string DecryptedPassword /// /// Encrypts the DecryptedPassword using the EncryptionAlgorithm and places the result in Password /// + [Obsolete("Use Encrypt(X509Certificate2, byte[], string securityPolicyUri, Nonce, X509Certificate2, X509Certificate2Collection, bool)")] public override void Encrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + { + Encrypt(certificate, senderNonce, securityPolicyUri, null); + } + + /// + /// Decrypts the Password using the EncryptionAlgorithm and places the result in DecryptedPassword + /// + [Obsolete("Use Decrypt(X509Certificate2, Nonce, string, Nonce, X509Certificate2, X509Certificate2Collection, CertificateValidator) ")] + public override void Decrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + { + Decrypt(certificate, Nonce.CreateNonce(securityPolicyUri, senderNonce), securityPolicyUri); + } + + /// + /// Encrypts the DecryptedPassword using the EncryptionAlgorithm and places the result in Password + /// + public override void Encrypt( + X509Certificate2 receiverCertificate, + byte[] receiverNonce, + string securityPolicyUri, + Nonce receiverEphemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + bool doNotEncodeSenderCertificate = false) { if (m_decryptedPassword == null) { @@ -85,73 +141,144 @@ public override void Encrypt(X509Certificate2 certificate, byte[] senderNonce, s // handle no encryption. if (String.IsNullOrEmpty(securityPolicyUri) || securityPolicyUri == SecurityPolicies.None) { - m_password = Encoding.UTF8.GetBytes(m_decryptedPassword); + m_password = new UTF8Encoding().GetBytes(m_decryptedPassword); m_encryptionAlgorithm = null; return; } - // encrypt the password. - byte[] dataToEncrypt = Utils.Append(Encoding.UTF8.GetBytes(m_decryptedPassword), senderNonce); + // handle RSA encryption. + if (!EccUtils.IsEccPolicy(securityPolicyUri)) + { + byte[] dataToEncrypt = Utils.Append(new UTF8Encoding().GetBytes(m_decryptedPassword), receiverNonce); - EncryptedData encryptedData = SecurityPolicies.Encrypt( - certificate, - securityPolicyUri, - dataToEncrypt); + EncryptedData encryptedData = SecurityPolicies.Encrypt( + receiverCertificate, + securityPolicyUri, + dataToEncrypt); - m_password = encryptedData.Data; - m_encryptionAlgorithm = encryptedData.Algorithm; + m_password = encryptedData.Data; + m_encryptionAlgorithm = encryptedData.Algorithm; + } + // handle ECC encryption. + else + { +#if ECC_SUPPORT + EncryptedSecret secret = new EncryptedSecret(); + + secret.ReceiverCertificate = receiverCertificate; + secret.SecurityPolicyUri = securityPolicyUri; + secret.ReceiverNonce = receiverEphemeralKey; + secret.SenderCertificate = senderCertificate; + secret.SenderIssuerCertificates = senderIssuerCertificates; + secret.DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate; + + // check if the complete chain is included in the sender issuers. + if (senderIssuerCertificates != null && senderIssuerCertificates.Count > 0) + { + if (senderIssuerCertificates[0].Thumbprint == senderCertificate.Thumbprint) + { + var issuers = new X509Certificate2Collection(); + + for (int ii = 1; ii < senderIssuerCertificates.Count; ii++) + { + issuers.Add(senderIssuerCertificates[ii]); + } + + senderIssuerCertificates = issuers; + } + } + + secret.SenderIssuerCertificates = senderIssuerCertificates; + secret.SenderNonce = Nonce.CreateNonce(securityPolicyUri); + + var utf8 = new UTF8Encoding(false).GetBytes(m_decryptedPassword); + m_password = secret.Encrypt(utf8, receiverNonce); + m_encryptionAlgorithm = null; +#else + throw new NotSupportedException("Platform does not support ECC curves"); +#endif + } } /// /// Decrypts the Password using the EncryptionAlgorithm and places the result in DecryptedPassword /// - public override void Decrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + public override void Decrypt( + X509Certificate2 certificate, + Nonce receiverNonce, + string securityPolicyUri, + Nonce ephemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + CertificateValidator validator = null) { // handle no encryption. if (String.IsNullOrEmpty(securityPolicyUri) || securityPolicyUri == SecurityPolicies.None) { - m_decryptedPassword = Encoding.UTF8.GetString(m_password, 0, m_password.Length); + m_decryptedPassword = new UTF8Encoding().GetString(m_password, 0, m_password.Length); return; } - // decrypt. - EncryptedData encryptedData = new EncryptedData(); - encryptedData.Data = m_password; - encryptedData.Algorithm = m_encryptionAlgorithm; - - byte[] decryptedPassword = SecurityPolicies.Decrypt( - certificate, - securityPolicyUri, - encryptedData); - - if (decryptedPassword == null) + // handle RSA encryption. + if (!EccUtils.IsEccPolicy(securityPolicyUri)) { - m_decryptedPassword = null; - return; - } + EncryptedData encryptedData = new EncryptedData(); + encryptedData.Data = m_password; + encryptedData.Algorithm = m_encryptionAlgorithm; - // verify the sender's nonce. - int startOfNonce = decryptedPassword.Length; - - if (senderNonce != null) - { - startOfNonce -= senderNonce.Length; + byte[] decryptedPassword = SecurityPolicies.Decrypt( + certificate, + securityPolicyUri, + encryptedData); - int result = 0; - for (int ii = 0; ii < senderNonce.Length; ii++) + if (decryptedPassword == null) { - result |= senderNonce[ii] ^ decryptedPassword[ii + startOfNonce]; + m_decryptedPassword = null; + return; } - if (result != 0) + // verify the sender's nonce. + int startOfNonce = decryptedPassword.Length; + if (receiverNonce != null) { - throw new ServiceResultException(StatusCodes.BadIdentityTokenRejected); + startOfNonce -= receiverNonce.Data.Length; + + int result = 0; + for (int ii = 0; ii < receiverNonce.Data.Length; ii++) + { + result |= receiverNonce.Data[ii] ^ decryptedPassword[ii + startOfNonce]; + } + + if (result != 0) + { + throw new ServiceResultException(StatusCodes.BadIdentityTokenRejected); + } } - } - // convert to UTF-8. - m_decryptedPassword = Encoding.UTF8.GetString(decryptedPassword, 0, startOfNonce); + // convert to UTF-8. + m_decryptedPassword = new UTF8Encoding().GetString(decryptedPassword, 0, startOfNonce); + } + // handle ECC encryption. + else + { +#if ECC_SUPPORT + EncryptedSecret secret = new EncryptedSecret(); + + secret.SenderCertificate = senderCertificate; + secret.SenderIssuerCertificates = senderIssuerCertificates; + secret.Validator = validator; + secret.ReceiverCertificate = certificate; + secret.ReceiverNonce = ephemeralKey; + secret.SecurityPolicyUri = securityPolicyUri; + + var plainText = secret.Decrypt(DateTime.UtcNow.AddHours(-1), receiverNonce.Data, m_password, 0, m_password.Length); + m_decryptedPassword = new UTF8Encoding().GetString(plainText); +#else + throw new NotSupportedException("Platform does not support ECC curves"); +#endif + } } + #endregion #region Private Fields @@ -293,7 +420,32 @@ public byte[] DecryptedTokenData /// /// Encrypts the DecryptedTokenData using the EncryptionAlgorithm and places the result in Password /// + [Obsolete("Use Encrypt(X509Certificate2, byte[], string securityPolicyUri, Nonce, X509Certificate2, X509Certificate2Collection, bool)")] public override void Encrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + { + Encrypt(certificate, senderNonce, securityPolicyUri, null); + } + + /// + /// Decrypts the Password using the EncryptionAlgorithm and places the result in DecryptedPassword + /// + [Obsolete("Use Decrypt(X509Certificate2, Nonce, string, Nonce, X509Certificate2, X509Certificate2Collection, CertificateValidator) ")] + public override void Decrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + { + Decrypt(certificate, Nonce.CreateNonce(securityPolicyUri, senderNonce), securityPolicyUri); + } + + /// + /// Encrypts the DecryptedTokenData using the EncryptionAlgorithm and places the result in Password + /// + public override void Encrypt( + X509Certificate2 receiverCertificate, + byte[] receiverNonce, + string securityPolicyUri, + Nonce receiverEphemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + bool doNotEncodeSenderCertificate = false) { // handle no encryption. if (String.IsNullOrEmpty(securityPolicyUri) || securityPolicyUri == SecurityPolicies.None) @@ -303,10 +455,10 @@ public override void Encrypt(X509Certificate2 certificate, byte[] senderNonce, s return; } - byte[] dataToEncrypt = Utils.Append(m_decryptedTokenData, senderNonce); + byte[] dataToEncrypt = Utils.Append(m_decryptedTokenData, receiverNonce); EncryptedData encryptedData = SecurityPolicies.Encrypt( - certificate, + receiverCertificate, securityPolicyUri, dataToEncrypt); @@ -317,7 +469,14 @@ public override void Encrypt(X509Certificate2 certificate, byte[] senderNonce, s /// /// Decrypts the Password using the EncryptionAlgorithm and places the result in DecryptedPassword /// - public override void Decrypt(X509Certificate2 certificate, byte[] senderNonce, string securityPolicyUri) + public override void Decrypt( + X509Certificate2 certificate, + Nonce receiverNonce, + string securityPolicyUri, + Nonce ephemeralKey = null, + X509Certificate2 senderCertificate = null, + X509Certificate2Collection senderIssuerCertificates = null, + CertificateValidator validator = null) { // handle no encryption. if (String.IsNullOrEmpty(securityPolicyUri) || securityPolicyUri == SecurityPolicies.None) @@ -339,13 +498,13 @@ public override void Decrypt(X509Certificate2 certificate, byte[] senderNonce, s // verify the sender's nonce. int startOfNonce = decryptedTokenData.Length; - if (senderNonce != null) + if (receiverNonce != null) { - startOfNonce -= senderNonce.Length; + startOfNonce -= receiverNonce.Data.Length; - for (int ii = 0; ii < senderNonce.Length; ii++) + for (int ii = 0; ii < receiverNonce.Data.Length; ii++) { - if (senderNonce[ii] != decryptedTokenData[ii + startOfNonce]) + if (receiverNonce.Data[ii] != decryptedTokenData[ii + startOfNonce]) { throw new ServiceResultException(StatusCodes.BadIdentityTokenRejected); } diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 4a0c55e73..2772e91b4 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -29,6 +29,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using Opc.Ua.Security.Certificates; using System.Runtime.InteropServices; +using NewNonceImplementation = Opc.Ua.Nonce; + namespace Opc.Ua { /// @@ -2793,98 +2795,52 @@ public static X509Certificate2Collection ParseCertificateChainBlob(ReadOnlyMemor /// /// Compare Nonce for equality. /// + [Obsolete("Use equivalent methods from the Opc.Ua.Nonce class")] public static bool CompareNonce(byte[] a, byte[] b) { - if (a == null || b == null) return false; - if (a.Length != b.Length) return false; - - byte result = 0; - for (int i = 0; i < a.Length; i++) - result |= (byte)(a[i] ^ b[i]); - - return result == 0; + return NewNonceImplementation.CompareNonce(a, b); } /// /// Cryptographic Nonce helper functions. /// + [Obsolete("Use equivalent methods from the Opc.Ua.Nonce class")] public static class Nonce { - static readonly RandomNumberGenerator m_rng = RandomNumberGenerator.Create(); - /// /// Generates a Nonce for cryptographic functions. /// + [Obsolete("Use equivalent CreateRandomNonceData method from the Opc.Ua.Nonce class")] public static byte[] CreateNonce(uint length) { - byte[] randomBytes = new byte[length]; - m_rng.GetBytes(randomBytes); - return randomBytes; + return NewNonceImplementation.CreateRandomNonceData(length); } /// /// Returns the length of the symmetric encryption key for a security policy. /// + [Obsolete("Use equivalent method from the Opc.Ua.Nonce class")] 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: - { - return 32; - } - - default: - case SecurityPolicies.None: - { - return 0; - } - } + return NewNonceImplementation.GetNonceLength(securityPolicyUri); } /// /// Validates the nonce for a message security mode and security policy. /// + [Obsolete("Use equivalent method from the Opc.Ua.Nonce class")] public static bool ValidateNonce(byte[] nonce, MessageSecurityMode securityMode, string securityPolicyUri) { - return ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); + return NewNonceImplementation.ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); } /// /// Validates the nonce for a message security mode and a minimum length. /// + [Obsolete("Use equivalent method from the Opc.Ua.Nonce class")] public static bool ValidateNonce(byte[] nonce, MessageSecurityMode securityMode, uint minNonceLength) { - // no nonce needed for no security. - if (securityMode == MessageSecurityMode.None) - { - return true; - } - - // check the length. - if (nonce == null || nonce.Length < minNonceLength) - { - return false; - } - - // try to catch programming errors by rejecting nonces with all zeros. - for (int ii = 0; ii < nonce.Length; ii++) - { - if (nonce[ii] != 0) - { - return true; - } - } - - return false; + return NewNonceImplementation.ValidateNonce(nonce,securityMode, minNonceLength); } } @@ -2916,7 +2872,7 @@ public static byte[] PSHA256(byte[] secret, string label, byte[] data, int offse /// /// Generates a Pseudo random sequence of bits using the HMAC algorithm. /// - private static byte[] PSHA(HMAC hmac, string label, byte[] data, int offset, int length) + public static byte[] PSHA(HMAC hmac, string label, byte[] data, int offset, int length) { if (hmac == null) throw new ArgumentNullException(nameof(hmac)); if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); @@ -2992,6 +2948,32 @@ private static byte[] PSHA(HMAC hmac, string label, byte[] data, int offset, int return output; } + + /// + /// Creates an HMAC. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", + "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "")] + public static HMAC CreateHMAC(HashAlgorithmName algorithmName, byte[] secret) + { + if (algorithmName == HashAlgorithmName.SHA256) + { + return new HMACSHA256(secret); + } + + if (algorithmName == HashAlgorithmName.SHA384) + { + return new HMACSHA384(secret); + } + + if (algorithmName == HashAlgorithmName.SHA1) + { + return new HMACSHA1(secret); + } + + throw new NotImplementedException(); + } + /// /// Checks if the target is in the list. Comparisons ignore case. /// @@ -3013,6 +2995,74 @@ public static bool FindStringIgnoreCase(IList strings, string target) return false; } + /// + /// Returns if the certificate type is supported on the platform OS. + /// + /// The certificate type to check. + public static bool IsSupportedCertificateType(NodeId certificateType) + { + if (certificateType.Identifier is uint identifier) + { + switch (identifier) + { +#if ECC_SUPPORT + case ObjectTypes.EccApplicationCertificateType: + return true; + case ObjectTypes.EccBrainpoolP256r1ApplicationCertificateType: + return s_eccCurveSupportCache[ECCurve.NamedCurves.brainpoolP256r1.Oid.FriendlyName].Value; + case ObjectTypes.EccBrainpoolP384r1ApplicationCertificateType: + return s_eccCurveSupportCache[ECCurve.NamedCurves.brainpoolP384r1.Oid.FriendlyName].Value; + case ObjectTypes.EccNistP256ApplicationCertificateType: + return s_eccCurveSupportCache[ECCurve.NamedCurves.nistP256.Oid.FriendlyName].Value; + case ObjectTypes.EccNistP384ApplicationCertificateType: + return s_eccCurveSupportCache[ECCurve.NamedCurves.nistP384.Oid.FriendlyName].Value; + //case ObjectTypes.EccCurve25519ApplicationCertificateType: + //case ObjectTypes.EccCurve448ApplicationCertificateType: +#endif + case ObjectTypes.ApplicationCertificateType: + case ObjectTypes.RsaMinApplicationCertificateType: + case ObjectTypes.RsaSha256ApplicationCertificateType: + case ObjectTypes.HttpsCertificateType: + return true; + } + } + return false; + } + + /// + /// Check if known curve is supported by platform + /// + /// + private static bool IsCurveSupported(ECCurve eCCurve) + { + try + { + // Create a ECDsa object and generate a new keypair on the given curve + using (ECDsa eCDsa = ECDsa.Create(eCCurve)) + { + ECParameters parameters = eCDsa.ExportParameters(false); + return parameters.Q.X != null && parameters.Q.Y != null; + } + } + catch (Exception ex) when ( + ex is PlatformNotSupportedException || + ex is ArgumentException || + ex is CryptographicException) + { + return false; + } + } + + /// + /// Lazy helper for checking ECC eliptic curve support for running OS + /// + private static readonly Dictionary> s_eccCurveSupportCache = new Dictionary>{ + { ECCurve.NamedCurves.nistP256.Oid.FriendlyName, new Lazy(() => IsCurveSupported(ECCurve.NamedCurves.nistP256)) }, + { ECCurve.NamedCurves.nistP384.Oid.FriendlyName, new Lazy(() => IsCurveSupported(ECCurve.NamedCurves.nistP384)) }, + { ECCurve.NamedCurves.brainpoolP256r1.Oid.FriendlyName, new Lazy(() => IsCurveSupported(ECCurve.NamedCurves.brainpoolP256r1)) }, + { ECCurve.NamedCurves.brainpoolP384r1.Oid.FriendlyName, new Lazy(() => IsCurveSupported(ECCurve.NamedCurves.brainpoolP384r1)) }, + }; + /// /// Lazy helper to allow runtime check for Mono. /// diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index 7cd79df37..a57154298 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -21,6 +21,18 @@ + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index f0e6337b8..a59848e73 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -30,6 +30,9 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NUnit.Framework; @@ -91,6 +94,12 @@ public async Task LoadClientConfiguration(string pkiRoot = null, string clientNa pkiRoot = pkiRoot ?? Path.Combine("%LocalApplicationData%", "OPC", "pki"); + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + "CN=" + clientName + ", O=OPC Foundation, DC=localhost", + CertificateStoreType.Directory, + pkiRoot + ); + // build the application configuration. Config = await application .Build( @@ -106,17 +115,18 @@ public async Task LoadClientConfiguration(string pkiRoot = null, string clientNa MaxNodesPerWrite = kDefaultOperationLimits }) .AddSecurityConfiguration( - "CN=" + clientName + ", O=OPC Foundation, DC=localhost", + applicationCerts, pkiRoot) + + // .SetApplicationCertificates(applicationCerts) .SetAutoAcceptUntrustedCertificates(true) .SetRejectSHA1SignedCertificates(false) - .SetMinimumCertificateKeySize(1024) .SetOutputFilePath(Path.Combine(pkiRoot, "Logs", "Opc.Ua.Client.Tests.log.txt")) .SetTraceMasks(TraceMasks) .Create().ConfigureAwait(false); // check the application certificate. - bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool haveAppCertificate = await application.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); if (!haveAppCertificate) { throw new Exception("Application instance certificate invalid!"); @@ -304,7 +314,8 @@ public static EndpointDescription SelectEndpoint( if (endpoint.EndpointUrl.StartsWith(url.Scheme)) { // skip unsupported security policies - if (SecurityPolicies.GetDisplayName(endpoint.SecurityPolicyUri) == null) + if (!configuration.SecurityConfiguration.SupportedSecurityPolicies. + Contains(endpoint.SecurityPolicyUri)) { continue; } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 36463b881..494b81129 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -45,6 +45,11 @@ using Assert = NUnit.Framework.Legacy.ClassicAssert; +using Opc.Ua.Security.Certificates.Tests; +using System.Security.Cryptography; +using System.Runtime.InteropServices; +using Opc.Ua.Security.Certificates; + namespace Opc.Ua.Client.Tests { /// @@ -294,7 +299,8 @@ public void ReadOnDiscoveryChannel(int readCount) /// but an oversized message should return an error. /// [Test, Order(105)] - public void GetEndpointsOnDiscoveryChannel() + [TestCase(false)] + public void GetEndpointsOnDiscoveryChannel(bool securityNoneEnabled) { var endpointConfiguration = EndpointConfiguration.Create(); endpointConfiguration.OperationTimeout = 10000; @@ -307,14 +313,24 @@ public void GetEndpointsOnDiscoveryChannel() // dummy uri to create a bigger message profileUris.Add($"https://opcfoundation.org/ProfileUri={i}"); } - var sre = Assert.Throws(() => client.GetEndpoints(profileUris)); - // race condition, if socket closed is detected before the error was returned, - // client may report channel closed instead of security policy rejected - if (StatusCodes.BadSecureChannelClosed == sre.StatusCode) + + if (securityNoneEnabled) { - Assert.Inconclusive($"Unexpected Status: {sre}"); + // test can pass, there is no limit for discovery messages + // because the server supports security None + client.GetEndpoints(profileUris); + } + else + { + var sre = Assert.Throws(() => client.GetEndpoints(profileUris)); + // race condition, if socket closed is detected before the error was returned, + // client may report channel closed instead of security policy rejected + if (StatusCodes.BadSecureChannelClosed == sre.StatusCode) + { + Assert.Inconclusive($"Unexpected Status: {sre}"); + } + Assert.AreEqual((StatusCode)StatusCodes.BadSecurityPolicyRejected, (StatusCode)sre.StatusCode, "Unexpected Status: {0}", sre); } - Assert.AreEqual((StatusCode)StatusCodes.BadSecurityPolicyRejected, (StatusCode)sre.StatusCode, "Unexpected Status: {0}", sre); } } @@ -325,9 +341,14 @@ public async Task InvalidConfiguration() ApplicationName = ClientFixture.Config.ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + ClientFixture.Config.SecurityConfiguration.ApplicationCertificate.SubjectName + ); + ApplicationConfiguration config = await applicationInstance.Build(ClientFixture.Config.ApplicationUri, ClientFixture.Config.ProductUri) .AsClient() - .AddSecurityConfiguration(ClientFixture.Config.SecurityConfiguration.ApplicationCertificate.SubjectName) + .AddSecurityConfiguration(applicationCerts) .Create().ConfigureAwait(false); } @@ -726,6 +747,18 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel, bool asy [TestCase(SecurityPolicies.Basic256Sha256, true, false)] [TestCase(SecurityPolicies.Basic256Sha256, false, false)] [TestCase(SecurityPolicies.Basic256Sha256, false, true)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1, true, false)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1, false, false)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1, false, true)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1, true, false)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1, false, false)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1, false, true)] + [TestCase(SecurityPolicies.ECC_nistP256, true, false)] + [TestCase(SecurityPolicies.ECC_nistP256, false, false)] + [TestCase(SecurityPolicies.ECC_nistP256, false, true)] + [TestCase(SecurityPolicies.ECC_nistP384, true, false)] + [TestCase(SecurityPolicies.ECC_nistP384, false, false)] + [TestCase(SecurityPolicies.ECC_nistP384, false, true)] public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(string securityPolicy, bool anonymous, bool asyncReconnect) { ServiceResultException sre; @@ -736,7 +769,9 @@ public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(stri ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); Assert.NotNull(endpoint); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, userIdentity.IssuedTokenType); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); if (identityPolicy == null) { Assert.Ignore($"No UserTokenPolicy found for {userIdentity.TokenType} / {userIdentity.IssuedTokenType}"); @@ -751,6 +786,7 @@ public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(stri // save the session configuration var stream = new MemoryStream(); + session1.SaveSessionConfiguration(stream); var streamArray = stream.ToArray(); @@ -1550,6 +1586,152 @@ public void ReadBuildInfo() Assert.AreEqual(buildInfo.BuildNumber, values[5].Value); Assert.AreEqual(buildInfo.BuildDate, values[6].Value); } + + /// + /// Open a session on a channel using ECC encrypted UserIdentityToken + /// + [Test, Combinatorial, Order(10100)] + public async Task OpenSessionECCUserNamePwdIdentityToken( + [Values(SecurityPolicies.ECC_nistP256, + SecurityPolicies.ECC_nistP384, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1)] string securityPolicy) + { + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1) || + (securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) + { + IUserIdentity userIdentity = new UserIdentity("user1", "password"); + + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); + Assert.NotNull(endpoint); + + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); + if (identityPolicy == null) + { + Assert.Ignore($"No UserTokenPolicy found for {userIdentity.TokenType} / {userIdentity.IssuedTokenType}"); + } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity).ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = (ServerStatusDataType)session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + Assert.NotNull(value1); + } + + } + + /// + /// Open a session on a channel using ECC encrypted IssuedIdentityToken + /// + [Test, Combinatorial, Order(10200)] + public async Task OpenSessionECCIssuedIdentityToken( + [Values(SecurityPolicies.ECC_nistP256, + SecurityPolicies.ECC_nistP384, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1)] string securityPolicy) + { + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1) || + (securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) + + { + var identityToken = "fakeTokenString"; + + var issuedToken = new IssuedIdentityToken() { + IssuedTokenType = IssuedTokenType.JWT, + PolicyId = Profiles.JwtUserToken, + DecryptedTokenData = Encoding.UTF8.GetBytes(identityToken), + }; + + IUserIdentity userIdentity = new UserIdentity(issuedToken); + + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); + Assert.NotNull(endpoint); + + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, + userIdentity.IssuedTokenType, + securityPolicy); + + if (identityPolicy == null) + { + Assert.Ignore($"No UserTokenPolicy found for {userIdentity.TokenType} / {userIdentity.IssuedTokenType}"); + } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity).ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = (ServerStatusDataType)session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + Assert.NotNull(value1); + } + } + + /// + /// Open a session on a channel using ECC encrypted UserCertificateIdentityToken + /// + [Test, Combinatorial, Order(10300)] + public async Task OpenSessionECCUserCertIdentityToken( + [Values(SecurityPolicies.ECC_nistP256, + SecurityPolicies.ECC_nistP384, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1)] string securityPolicy) + { + + var eccCurveHashPairs = new ECCurveHashPairCollection { + { ECCurve.NamedCurves.nistP256, HashAlgorithmName.SHA256 }, + { ECCurve.NamedCurves.nistP384, HashAlgorithmName.SHA384 } }; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + eccCurveHashPairs.AddRange(new ECCurveHashPairCollection { + { ECCurve.NamedCurves.brainpoolP256r1, HashAlgorithmName.SHA256 }, + { ECCurve.NamedCurves.brainpoolP384r1, HashAlgorithmName.SHA384 }}); + } + + foreach (var eccurveHashPair in eccCurveHashPairs) + { + string extractedFriendlyNamae = null; + string[] friendlyNameContext = securityPolicy.Split('_'); + if (friendlyNameContext.Length > 1) + { + extractedFriendlyNamae = friendlyNameContext[1]; + } + if (eccurveHashPair.Curve.Oid.FriendlyName.Contains(extractedFriendlyNamae)) + { + X509Certificate2 cert = CertificateBuilder.Create("CN=Client Test ECC Subject, O=OPC Foundation") + .SetECCurve(eccurveHashPair.Curve) + .CreateForECDsa(); + + IUserIdentity userIdentity = new UserIdentity(cert); + + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); + Assert.NotNull(endpoint); + + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); + if (identityPolicy == null) + { + Assert.Ignore($"No UserTokenPolicy found for {userIdentity.TokenType} / {userIdentity.IssuedTokenType}"); + } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity).ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = (ServerStatusDataType)session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + Assert.NotNull(value1); + } + } + } #endregion #region Benchmarks diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index a93c6e512..ca7ef7a5b 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -54,6 +54,7 @@ public class ClientTestFramework new object [] { Utils.UriSchemeOpcHttps}, }; + public const int MaxChannelCount = 100; public const int MaxReferences = 100; public const int MaxTimeout = 10000; public const int TransportQuotaMaxMessageSize = 4 * 1024 * 1024; @@ -61,7 +62,6 @@ public class ClientTestFramework public TokenValidatorMock TokenValidator { get; set; } = new TokenValidatorMock(); public bool SingleSession { get; set; } = true; - public int MaxChannelCount { get; set; } = 10; public bool SupportsExternalServerUrl { get; set; } = false; public ServerFixture ServerFixture { get; set; } public ClientFixture ClientFixture { get; set; } @@ -207,6 +207,57 @@ virtual public async Task CreateReferenceServerFixture( ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( new UserTokenPolicy(UserTokenType.IssuedToken) { IssuedTokenType = Opc.Ua.Profiles.JwtUserToken }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.UserName) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.UserName) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.UserName) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.UserName) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" + }); + + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.Certificate) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.Certificate) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.Certificate) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.Certificate) { + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" + }); + + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.IssuedToken) { + IssuedTokenType = Opc.Ua.Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.IssuedToken) + { + IssuedTokenType = Opc.Ua.Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.IssuedToken) + { + IssuedTokenType = Opc.Ua.Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" + }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.IssuedToken) + { + IssuedTokenType = Opc.Ua.Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" + }); + + ServerFixture.Config.ServerConfiguration.MaxChannelCount = MaxChannelCount; ReferenceServer = await ServerFixture.StartAsync(writer ?? TestContext.Out).ConfigureAwait(false); ReferenceServer.TokenValidator = this.TokenValidator; ServerFixturePort = ServerFixture.Port; diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestNoSecurity.cs b/Tests/Opc.Ua.Client.Tests/ClientTestNoSecurity.cs index 7745a4a9b..86bdd8bf7 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestNoSecurity.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestNoSecurity.cs @@ -101,6 +101,16 @@ public Task TearDown() } #endregion + /// + /// GetEndpoints on the discovery channel, + /// the oversized message can pass because security None is enabled. + /// + [Test, Order(105)] + public void GetEndpointsOnDiscoveryChannel() + { + _clientTest.GetEndpointsOnDiscoveryChannel(true); + } + [Test, Order(230)] public Task ReconnectJWTSecurityNone() { diff --git a/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs b/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs index b51e77674..a476ad73e 100644 --- a/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs @@ -122,12 +122,13 @@ public ServerSessionForTest( X509Certificate2 serverCertificate, NodeId authenticationToken, byte[] clientNonce, - byte[] serverNonce, + Nonce serverNonce, string sessionName, ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, + X509Certificate2Collection clientCertificateChain, double sessionTimeout, uint maxResponseMessageSize, double maxRequestAge, @@ -144,6 +145,7 @@ int maxHistoryContinuationPoints clientDescription, endpointUrl, clientCertificate, + clientCertificateChain, sessionTimeout, maxResponseMessageSize, maxRequestAge, @@ -179,15 +181,14 @@ protected override Opc.Ua.Server.Session CreateSession( X509Certificate2 serverCertificate, NodeId sessionCookie, byte[] clientNonce, - byte[] serverNonce, + Nonce serverNonce, string sessionName, ApplicationDescription clientDescription, string endpointUrl, X509Certificate2 clientCertificate, + X509Certificate2Collection clientCertificateChain, double sessionTimeout, - uint maxResponseMessageSize, - int maxRequestAge, // TBD - Remove unused parameter. - int maxContinuationPoints) // TBD - Remove unused parameter. + uint maxResponseMessageSize) { ServerSessionForTest session = new ServerSessionForTest( context, @@ -200,6 +201,7 @@ protected override Opc.Ua.Server.Session CreateSession( clientDescription, endpointUrl, clientCertificate, + clientCertificateChain, sessionTimeout, maxResponseMessageSize, m_4TestMaxRequestAge, diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index 89000169d..bad5919c9 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -8,6 +8,18 @@ false + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + @@ -30,6 +42,7 @@ + diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index e930d1b06..f21876704 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -62,7 +62,6 @@ public class SubscriptionTest : ClientTestFramework SupportsExternalServerUrl = true; // create a new session for every test SingleSession = false; - MaxChannelCount = 1000; return base.OneTimeSetUpAsync(writer: null, securityNone: true); } @@ -478,7 +477,12 @@ public void SequentialPublishingSubscription(bool enabled) /// [Test, Combinatorial, Order(350), Explicit] public Task ReconnectWithSavedSessionSecretsSync( - [Values(SecurityPolicies.None, SecurityPolicies.Basic256Sha256)] string securityPolicy, + [Values(SecurityPolicies.None, + SecurityPolicies.Basic256Sha256, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_nistP384)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) @@ -491,7 +495,12 @@ public Task ReconnectWithSavedSessionSecretsSync( /// [Test, Combinatorial, Order(351)] public Task ReconnectWithSavedSessionSecretsOnlyAsync( - [Values(SecurityPolicies.None, SecurityPolicies.Basic256Sha256)] string securityPolicy, + [Values(SecurityPolicies.None, + SecurityPolicies.Basic256Sha256, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_brainpoolP384r1, + SecurityPolicies.ECC_brainpoolP256r1, + SecurityPolicies.ECC_nistP384)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) @@ -511,7 +520,9 @@ public async Task ReconnectWithSavedSessionSecretsAsync(string securityPolicy, b ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); Assert.NotNull(endpoint); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, userIdentity.IssuedTokenType); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); if (identityPolicy == null) { Assert.Ignore($"No UserTokenPolicy found for {userIdentity.TokenType} / {userIdentity.IssuedTokenType}"); diff --git a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs index 95f0130fe..8a2663853 100644 --- a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs +++ b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs @@ -97,7 +97,7 @@ public async Task TestFileConfig() Assert.NotNull(configPath); ApplicationConfiguration applicationConfiguration = await applicationInstance.LoadApplicationConfiguration(configPath, true).ConfigureAwait(false); Assert.NotNull(applicationConfiguration); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -108,12 +108,18 @@ public async Task TestNoFileConfigAsClient() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create().ConfigureAwait(false); Assert.NotNull(config); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -123,10 +129,16 @@ public async Task TestBadApplicationInstance() // no app name var applicationInstance = new ApplicationInstance(); Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + Assert.ThrowsAsync(async () => await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); @@ -138,14 +150,14 @@ await applicationInstance.Build(ApplicationUri, ProductUri) Assert.ThrowsAsync(async () => await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); Assert.ThrowsAsync(async () => await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); @@ -157,7 +169,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) var config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false); Assert.AreEqual(ApplicationType.Server, applicationInstance.ApplicationType); @@ -170,7 +182,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false); Assert.AreEqual(ApplicationType.Client, applicationInstance.ApplicationType); @@ -184,7 +196,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) .AddPolicy(MessageSecurityMode.None, SecurityPolicies.None) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); @@ -193,7 +205,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.None) - .AddSecurityConfiguration(SubjectName) + .AddSecurityConfiguration(applicationCerts) .Create() .ConfigureAwait(false) ); @@ -202,7 +214,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) .AddPolicy(MessageSecurityMode.Sign, "123") - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); @@ -211,7 +223,7 @@ await applicationInstance.Build(ApplicationUri, ProductUri) await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) .AddUserTokenPolicy(null) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts,m_pkiRoot) .Create() .ConfigureAwait(false) ); @@ -224,13 +236,19 @@ public async Task TestNoFileConfigAsServerMinimal() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .SetOperationTimeout(10000) .AsServer(new string[] { EndpointUrl }) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .Create().ConfigureAwait(false); Assert.NotNull(config); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -241,6 +259,12 @@ public async Task TestNoFileConfigAsServerMaximal() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .SetTransportQuotas(new TransportQuotas() { OperationTimeout = 10000 }) .AsServer(new string[] { EndpointUrl }) @@ -256,7 +280,7 @@ public async Task TestNoFileConfigAsServerMaximal() .AddUserTokenPolicy(new UserTokenPolicy(UserTokenType.Certificate) { SecurityPolicyUri = SecurityPolicies.Basic256Sha256 }) .SetDiagnosticsEnabled(true) .SetPublishingResolution(100) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .SetAddAppCertToTrustedStore(true) .SetAutoAcceptUntrustedCertificates(true) .SetMinimumCertificateKeySize(1024) @@ -266,7 +290,7 @@ public async Task TestNoFileConfigAsServerMaximal() .SetRejectUnknownRevocationStatus(true) .Create().ConfigureAwait(false); Assert.NotNull(config); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -277,6 +301,12 @@ public async Task TestNoFileConfigAsClientAndServer() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .SetMaxBufferSize(32768) .AsServer(new string[] { EndpointUrl }) @@ -286,10 +316,10 @@ public async Task TestNoFileConfigAsClientAndServer() .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic256) .SetDiagnosticsEnabled(true) .AsClient() - .AddSecurityConfiguration(SubjectName, CertificateStoreType.Directory, CertificateStoreType.X509Store) + .AddSecurityConfiguration(applicationCerts, CertificateStoreType.Directory, CertificateStoreType.X509Store) .Create().ConfigureAwait(false); Assert.NotNull(config); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -311,6 +341,12 @@ public async Task TestNoFileConfigAsServerX509Store() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl }) .AddUnsecurePolicyNone() @@ -318,13 +354,13 @@ public async Task TestNoFileConfigAsServerX509Store() .AddUserTokenPolicy(UserTokenType.UserName) .AsClient() .SetDefaultSessionTimeout(10000) - .AddSecurityConfiguration(SubjectName, CertificateStoreType.X509Store) + .AddSecurityConfiguration(applicationCerts, CertificateStoreType.X509Store) .Create().ConfigureAwait(false); Assert.NotNull(config); var applicationCertificate = applicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate; bool deleteAfterUse = applicationCertificate.Certificate != null; - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); using (ICertificateStore store = applicationInstance.ApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.OpenStore()) { @@ -356,13 +392,19 @@ public async Task TestNoFileConfigAsServerCustom() ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + ApplicationConfiguration config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { EndpointUrl, "opc.https://localhost:51001" }, new string[] { "opc.tcp://192.168.1.100:51000" }) - .AddSecurityConfiguration(SubjectName, m_pkiRoot) + .AddSecurityConfiguration(applicationCerts, m_pkiRoot) .SetAddAppCertToTrustedStore(true) .Create().ConfigureAwait(false); Assert.NotNull(config); - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } @@ -396,19 +438,25 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool var applicationInstance = new ApplicationInstance() { ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + pkiRoot); + ApplicationConfiguration config; if (server) { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { "opc.tcp://localhost:12345/Configuration" }) - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } else { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } @@ -434,7 +482,7 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool { if (suppress) { - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0) + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true) .ConfigureAwait(false); Assert.True(certOK); @@ -443,8 +491,8 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool else { var sre = Assert.ThrowsAsync(async () => - await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false)); - Assert.AreEqual((StatusCode)StatusCodes.BadConfigurationError, (StatusCode)sre.StatusCode); + await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false)); + Assert.AreEqual(StatusCodes.BadConfigurationError, sre.StatusCode); } } } @@ -471,19 +519,25 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, ApplicationName = ApplicationName }; Assert.NotNull(applicationInstance); + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + SubjectName, + CertificateStoreType.Directory, + pkiRoot); + ApplicationConfiguration config; if (server) { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { "opc.tcp://localhost:12345/Configuration" }) - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } else { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } Assert.NotNull(config); @@ -521,7 +575,7 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, { if (suppress) { - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0) + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true) .ConfigureAwait(false); Assert.True(certOK); @@ -530,8 +584,8 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, else { var sre = Assert.ThrowsAsync(async () => - await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false)); - Assert.AreEqual((StatusCode)StatusCodes.BadConfigurationError, (StatusCode)sre.StatusCode); + await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false)); + Assert.AreEqual(StatusCodes.BadConfigurationError, sre.StatusCode); } } } @@ -589,18 +643,23 @@ public async Task TestDisableCertificateAutoCreationAsync(bool server, bool disa }; Assert.NotNull(applicationInstance); ApplicationConfiguration config; + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates(SubjectName, + CertificateStoreType.Directory, + m_pkiRoot); + if (server) { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsServer(new string[] { "opc.tcp://localhost:12345/Configuration" }) - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } else { config = await applicationInstance.Build(ApplicationUri, ProductUri) .AsClient() - .AddSecurityConfiguration(SubjectName, pkiRoot) + .AddSecurityConfiguration(applicationCerts, pkiRoot) .Create().ConfigureAwait(false); } Assert.NotNull(config); @@ -611,12 +670,12 @@ public async Task TestDisableCertificateAutoCreationAsync(bool server, bool disa if (disableCertificateAutoCreation) { var sre = Assert.ThrowsAsync(async () => - await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false)); - Assert.AreEqual((StatusCode)StatusCodes.BadConfigurationError, (StatusCode)sre.StatusCode); + await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false)); + Assert.AreEqual(StatusCodes.BadConfigurationError, sre.StatusCode); } else { - bool certOK = await applicationInstance.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await applicationInstance.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); } } diff --git a/Tests/Opc.Ua.Configuration.Tests/CertificateStoreTypeTest.cs b/Tests/Opc.Ua.Configuration.Tests/CertificateStoreTypeTest.cs index 12dab4a74..2cbd3949d 100644 --- a/Tests/Opc.Ua.Configuration.Tests/CertificateStoreTypeTest.cs +++ b/Tests/Opc.Ua.Configuration.Tests/CertificateStoreTypeTest.cs @@ -96,7 +96,7 @@ public async Task CertificateStoreTypeNoConfigTest() // patch custom stores before creating the config ApplicationConfiguration appConfig = await appConfigBuilder.Create().ConfigureAwait(false); - bool certOK = await application.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool certOK = await application.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); Assert.True(certOK); int instancesCreatedWhileLoadingConfig = TestCertStore.InstancesCreated; @@ -244,6 +244,10 @@ public Task IsRevoked(X509Certificate2 issuer, X509Certificate2 cert public bool SupportsLoadPrivateKey => m_innerStore.SupportsLoadPrivateKey; /// + public Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password) + => m_innerStore.LoadPrivateKey(thumbprint, subjectName, certificateType, password); + + [Obsolete("Method is deprecated. Use only for RSA certificates, the replacing LoadPrivateKey with certificateType parameter should be used.")] public Task LoadPrivateKey(string thumbprint, string subjectName, string password) => m_innerStore.LoadPrivateKey(thumbprint, subjectName, password); diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index d6d55125c..ffec1d8ef 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -5,8 +5,21 @@ $(TestsTargetFrameworks) Opc.Ua.Configuration.Tests false + CS0618 + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 52a87fcf0..4b27fbcfb 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -30,7 +30,7 @@ - + diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTypeTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTypeTest.cs index 02ccf05ae..48b237cc3 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTypeTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTypeTest.cs @@ -156,9 +156,14 @@ public Task IsRevoked(X509Certificate2 issuer, X509Certificate2 cert public bool SupportsLoadPrivateKey => m_innerStore.SupportsLoadPrivateKey; /// + [Obsolete("Method is deprecated. Use only for RSA certificates, the replacing LoadPrivateKey with certificateType parameter should be used.")] public Task LoadPrivateKey(string thumbprint, string subjectName, string password) => m_innerStore.LoadPrivateKey(thumbprint, subjectName, password); + /// + public Task LoadPrivateKey(string thumbprint, string subjectName, NodeId certificateType, string password) + => m_innerStore.LoadPrivateKey(thumbprint, subjectName, certificateType, password); + /// public Task AddRejected(X509Certificate2Collection certificates, int maxCertificates) => m_innerStore.AddRejected(certificates, maxCertificates); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs index 362cce179..52a5fe753 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs @@ -128,18 +128,18 @@ public CertificateValidator Update() { var certValidator = new CertificateValidator(); var issuerTrustList = new CertificateTrustList { - StoreType = "Directory", + StoreType = CertificateStoreType.Directory, StorePath = m_issuerStore.Directory.FullName }; var trustedTrustList = new CertificateTrustList { - StoreType = "Directory", + StoreType = CertificateStoreType.Directory, StorePath = m_trustedStore.Directory.FullName }; CertificateStoreIdentifier rejectedList = null; if (m_rejectedStore != null) { rejectedList = new CertificateStoreIdentifier { - StoreType = "Directory", + StoreType = CertificateStoreType.Directory, StorePath = m_rejectedStore.Directory.FullName }; } diff --git a/Tests/Opc.Ua.Core.Tests/Types/BuiltIn/BuiltInTests.cs b/Tests/Opc.Ua.Core.Tests/Types/BuiltIn/BuiltInTests.cs index 7b4940d72..5383a190c 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/BuiltIn/BuiltInTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/BuiltIn/BuiltInTests.cs @@ -617,7 +617,7 @@ public void NodeIdComparison(Opc.Ua.IdType idType) case Opc.Ua.IdType.Numeric: nodeId = new NodeId(DataGenerator.GetRandomUInt16(), DataGenerator.GetRandomByte()); break; case Opc.Ua.IdType.String: nodeId = new NodeId(DataGenerator.GetRandomString(), 0); break; case Opc.Ua.IdType.Guid: nodeId = new NodeId(DataGenerator.GetRandomGuid()); break; - case Opc.Ua.IdType.Opaque: nodeId = new NodeId(Utils.Nonce.CreateNonce(32)); break; + case Opc.Ua.IdType.Opaque: nodeId = new NodeId(Ua.Nonce.CreateRandomNonceData(32)); break; } NodeId nodeIdClone = (NodeId)nodeId.Clone(); Assert.AreEqual(nodeId, nodeIdClone); @@ -645,7 +645,7 @@ public void NodeIdComparison(Opc.Ua.IdType idType) case Opc.Ua.IdType.Numeric: nodeId2 = new NodeId(DataGenerator.GetRandomUInt16(), DataGenerator.GetRandomByte()); break; case Opc.Ua.IdType.String: nodeId2 = new NodeId(DataGenerator.GetRandomString(), 0); break; case Opc.Ua.IdType.Guid: nodeId2 = new NodeId(DataGenerator.GetRandomGuid()); break; - case Opc.Ua.IdType.Opaque: nodeId2 = new NodeId(Utils.Nonce.CreateNonce(32)); break; + case Opc.Ua.IdType.Opaque: nodeId2 = new NodeId(Ua.Nonce.CreateRandomNonceData(32)); break; } dictionary.Add(nodeId2, "TestClone"); Assert.AreEqual(2, dictionary.Distinct().ToList().Count); diff --git a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs new file mode 100644 index 000000000..bc018e142 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs @@ -0,0 +1,170 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using NUnit.Framework; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Core.Tests.Types.Nonce +{ + /// + /// Tests for the Binary Schema Validator class. + /// + [TestFixture, Category("NonceTests")] + [Parallelizable] + public class NonceTests + { + #region Test Methods + /// + /// Test the CreateNonce - securitypolicy and valid nonceLength + /// + [Theory] + [TestCase(SecurityPolicies.ECC_nistP256)] + [TestCase(SecurityPolicies.ECC_nistP384)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] + [TestCase(SecurityPolicies.Basic256)] + [TestCase(SecurityPolicies.Basic256Sha256)] + [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] + [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + public void ValidateCreateNoncePolicyLength(string securityPolicyUri) + { + if (IsSupportedByPlatform(securityPolicyUri)) + { + uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + + Ua.Nonce nonce = Ua.Nonce.CreateNonce(securityPolicyUri); + + Assert.IsNotNull(nonce); + Assert.IsNotNull(nonce.Data); + Assert.AreEqual(nonceLength, nonce.Data.Length); + } + } + + /// + /// Test the CreateEccNonce - securitypolicy and nonceData + /// + [Theory] + [TestCase(SecurityPolicies.ECC_nistP256)] + [TestCase(SecurityPolicies.ECC_nistP384)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] + [TestCase(SecurityPolicies.Basic256)] + [TestCase(SecurityPolicies.Basic256Sha256)] + [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] + [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) + { + if (IsSupportedByPlatform(securityPolicyUri)) + { + uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + Ua.Nonce nonceByLen = Ua.Nonce.CreateNonce(securityPolicyUri); + + Ua.Nonce nonceByData = Ua.Nonce.CreateNonce(securityPolicyUri, nonceByLen.Data); + + Assert.IsNotNull(nonceByData); + Assert.IsNotNull(nonceByData.Data); + Assert.AreEqual(nonceLength, nonceByData.Data.Length); + Assert.AreEqual(nonceByData.Data, nonceByLen.Data); + } + } + + /// + /// Test the CreateEccNonce - securitypolicy and invalid nonceData + /// + [Theory] + [TestCase(SecurityPolicies.ECC_nistP256)] + [TestCase(SecurityPolicies.ECC_nistP384)] + [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] + [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] + [TestCase(SecurityPolicies.Basic256)] + [TestCase(SecurityPolicies.Basic256Sha256)] + [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] + [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength(string securityPolicyUri) + { + if (IsSupportedByPlatform(securityPolicyUri)) + { + uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + + byte[] randomValue = Ua.Nonce.CreateRandomNonceData(nonceLength); + + if (securityPolicyUri.Contains("ECC_")) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && + (securityPolicyUri == SecurityPolicies.ECC_nistP256 || securityPolicyUri == SecurityPolicies.ECC_nistP384)) + { + Assert.Ignore("No exception is thrown on OSX with NIST curves"); + } + Assert.Throws(typeof(ArgumentException), () => Ua.Nonce.CreateNonce(securityPolicyUri, randomValue)); + } + else + { + Ua.Nonce rsaNonce = Ua.Nonce.CreateNonce(securityPolicyUri, randomValue); + Assert.AreEqual(rsaNonce.Data, randomValue); + } + } + } + #endregion + + #region Helper + + /// + /// Determines if security policy is supported by platform + /// + /// + /// + private bool IsSupportedByPlatform(string securityPolicyUri) + { + if (securityPolicyUri.Equals(SecurityPolicies.ECC_nistP256, StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } + else if (securityPolicyUri.Equals(SecurityPolicies.ECC_nistP384, StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } + else if (securityPolicyUri.Equals(SecurityPolicies.ECC_brainpoolP256r1, StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } + else if (securityPolicyUri.Equals(SecurityPolicies.ECC_brainpoolP384r1, StringComparison.Ordinal)) + { + return Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + } + + return true; + } + + #endregion + } + +} diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index bff7f8ddd..c9df6309e 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -1430,6 +1430,7 @@ public void VerifyUnregisterGoodApplications() [Test, Order(920)] public void UnregisterUnregisteredGoodApplications() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in m_goodApplicationTestSet) { diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs index e7ea4a97e..b1ec5aa5f 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs @@ -92,6 +92,11 @@ public async Task LoadClientConfiguration(int port = -1) AdminPassword = "demo" }; + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + "CN=Global Discovery Test Client, O=OPC Foundation, DC=localhost", + CertificateStoreType.Directory, + pkiRoot); + // build the application configuration. Configuration = await m_application .Build( @@ -101,7 +106,7 @@ public async Task LoadClientConfiguration(int port = -1) .SetDefaultSessionTimeout(600000) .SetMinSubscriptionLifetime(10000) .AddSecurityConfiguration( - "CN=Global Discovery Test Client, O=OPC Foundation, DC=localhost", + applicationCerts, pkiRoot) .SetAutoAcceptUntrustedCertificates(true) .SetRejectSHA1SignedCertificates(false) @@ -113,7 +118,7 @@ public async Task LoadClientConfiguration(int port = -1) .Create().ConfigureAwait(false); #endif // check the application certificate. - bool haveAppCertificate = await m_application.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool haveAppCertificate = await m_application.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); if (!haveAppCertificate) { throw new Exception("Application instance certificate invalid!"); diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs index 1c0d0058a..f56ba7342 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestServer.cs @@ -93,7 +93,7 @@ public async Task StartServer(bool clean, int basePort = -1, string storeType = } // check the application certificate. - bool haveAppCertificate = await Application.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool haveAppCertificate = await Application.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); if (!haveAppCertificate) { throw new Exception("Application instance certificate invalid!"); @@ -202,7 +202,7 @@ private static void CertificateValidator_CertificateValidation(CertificateValida private static async Task Load(ApplicationInstance application, int basePort) { -#if !USE_FILE_CONFIG +#if USE_FILE_CONFIG // load the application configuration. ApplicationConfiguration config = await application.LoadApplicationConfiguration(true).ConfigureAwait(false); #else @@ -231,6 +231,12 @@ private static async Task Load(ApplicationInstance app UsersDatabaseStorePath = Path.Combine(gdsRoot, "gdsusersdb.json") }; + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + "CN=Global Discovery Test Client, O=OPC Foundation, DC=localhost", + CertificateStoreType.Directory, + gdsRoot + ); + // build the application configuration. ApplicationConfiguration config = await application .Build( @@ -244,11 +250,11 @@ private static async Task Load(ApplicationInstance app .AddServerProfile("http://opcfoundation.org/UA-Profile/Server/GlobalDiscoveryAndCertificateManagement2017") .SetShutdownDelay(0) .AddSecurityConfiguration( - "CN=Global Discovery Test Server, O=OPC Foundation, DC=localhost", + applicationCerts, gdsRoot) .SetAutoAcceptUntrustedCertificates(true) .SetRejectSHA1SignedCertificates(false) - .SetRejectUnknownRevocationStatus(true) + .SetRejectUnknownRevocationStatus(false) .SetMinimumCertificateKeySize(1024) .AddExtension(null, gdsConfig) .SetDeleteOnLoad(true) diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 4299a5f25..533d21bf6 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -11,6 +11,18 @@ $(DefineConstants);USE_FILE_CONFIG + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 5ba625797..a368ca0f7 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -382,13 +382,6 @@ public void UpdateCertificateSelfSignedNoPrivateKey() [Test, Order(510)] public void UpdateCertificateCASigned() { -#if NETCOREAPP3_1_OR_GREATER - // this test fails on macOS, ignore - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Assert.Ignore("Update CA signed certificate fails on mac OS."); - } -#endif ConnectPushClient(true); ConnectGDSClient(true); TestContext.Out.WriteLine("Create Signing Request"); diff --git a/Tests/Opc.Ua.Gds.Tests/ServerConfigurationPushTestClient.cs b/Tests/Opc.Ua.Gds.Tests/ServerConfigurationPushTestClient.cs index 11e45a9e4..fb9d937d0 100644 --- a/Tests/Opc.Ua.Gds.Tests/ServerConfigurationPushTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/ServerConfigurationPushTestClient.cs @@ -86,6 +86,11 @@ public async Task LoadClientConfiguration(int port = -1) SecurityTokenLifetime = 3600000, }; + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + "CN=Server Configuration Push Test Client, O=OPC Foundation", + CertificateStoreType.Directory, + pkiRoot); + // build the application configuration. Config = await application .Build( @@ -94,8 +99,8 @@ public async Task LoadClientConfiguration(int port = -1) .SetTransportQuotas(transportQuotas) .AsClient() .AddSecurityConfiguration( - "CN=Server Configuration Push Test Client, O=OPC Foundation", - pkiRoot, pkiRoot, pkiRoot) + applicationCerts, + pkiRoot, pkiRoot) .SetAutoAcceptUntrustedCertificates(true) .SetRejectSHA1SignedCertificates(false) .SetRejectUnknownRevocationStatus(true) @@ -106,7 +111,7 @@ public async Task LoadClientConfiguration(int port = -1) .Create().ConfigureAwait(false); #endif // check the application certificate. - bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(true, 0).ConfigureAwait(false); + bool haveAppCertificate = await application.CheckApplicationInstanceCertificates(true).ConfigureAwait(false); if (!haveAppCertificate) { throw new Exception("Application instance certificate invalid!"); diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index dee5340e7..8167ae65c 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -7,6 +7,18 @@ false + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs index 2c2d24ab7..72939db30 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs @@ -29,12 +29,16 @@ #if ECC_SUPPORT using System; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using NUnit.Framework; using Opc.Ua.Tests; using Assert = NUnit.Framework.Legacy.ClassicAssert; +#if NETFRAMEWORK +using Org.BouncyCastle.X509; +#endif namespace Opc.Ua.Security.Certificates.Tests { @@ -348,6 +352,39 @@ ECCurveHashPair ecCurveHashPair } }); } + + [Theory] + public void SetECDsaPublicKeyByteArray( + ECCurveHashPair ecCurveHashPair + ) + { + // default signing cert with custom key + X509Certificate2 signingCert = CertificateBuilder.Create(Subject) + .SetCAConstraint() + .SetHashAlgorithm(HashAlgorithmName.SHA512) + .SetECCurve(ecCurveHashPair.Curve) + .CreateForECDsa(); + + WriteCertificate(signingCert, $"Signing ECDsa {signingCert.GetECDsaPublicKey().KeySize} cert"); + + + using (ECDsa ecdsaPrivateKey = signingCert.GetECDsaPrivateKey()) + using (ECDsa ecdsaPublicKey = signingCert.GetECDsaPublicKey()) + { + byte[] pubKeyBytes = GetPublicKey(ecdsaPublicKey); + + var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); + var cert = CertificateBuilder.Create("CN=App Cert") + .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) + .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetECDsaPublicKey(pubKeyBytes) + .CreateForECDsa(generator); + Assert.NotNull(cert); + WriteCertificate(cert, "Default signed ECDsa cert with Public Key"); + } + + } + #endregion #region Private Methods @@ -382,15 +419,26 @@ private void CheckPEMWriter(X509Certificate2 certificate, string password = null { #if !NETFRAMEWORK PEMWriter.ExportPrivateKeyAsPEM(certificate, password); +#if NETCOREAPP3_1 || NET5_0_OR_GREATER PEMWriter.ExportECDsaPrivateKeyAsPEM(certificate); +#endif #endif } } - #endregion + + private static byte[] GetPublicKey(ECDsa ecdsa) + { +#if NETFRAMEWORK + var pubKeyParams = BouncyCastle.X509Utils.GetECPublicKeyParameters(ecdsa); + return SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pubKeyParams).ToAsn1Object().GetDerEncoded(); +#elif NETCOREAPP3_1 || NET5_0_OR_GREATER + return ecdsa.ExportSubjectPublicKeyInfo(); +#endif + } +#endregion #region Private Fields #endregion } } #endif - diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs index e9563031e..4e7000b68 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs @@ -265,14 +265,16 @@ public void CreateRSADefaultWithSerialTest() { // default cert Assert.Throws( - () => { + () => + { CertificateBuilder.Create(Subject) .SetSerialNumberLength(0) .CreateForRSA(); } ); Assert.Throws( - () => { + () => + { CertificateBuilder.Create(Subject) .SetSerialNumberLength(X509Defaults.SerialNumberLengthMax + 1) .CreateForRSA(); @@ -296,14 +298,16 @@ public void CreateRSAManualSerialTest() { // default cert Assert.Throws( - () => { + () => + { CertificateBuilder.Create(Subject) .SetSerialNumber(Array.Empty()) .CreateForRSA(); } ); Assert.Throws( - () => { + () => + { CertificateBuilder.Create(Subject) .SetSerialNumber(new byte[X509Defaults.SerialNumberLengthMax + 1]) .CreateForRSA(); @@ -451,6 +455,9 @@ KeyHashPair keyHashPair { Assert.NotNull(cert); WriteCertificate(cert, "Default signed RSA cert with Public Key"); + Assert.AreEqual(issuer.SubjectName.Name, cert.IssuerName.Name); + Assert.AreEqual(issuer.SubjectName.RawData, cert.IssuerName.RawData); + CheckPEMWriter(cert); } } @@ -473,7 +480,8 @@ KeyHashPair keyHashPair } // ensure invalid path throws argument exception - Assert.Throws(() => { + Assert.Throws(() => + { using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); @@ -520,5 +528,4 @@ private void CheckPEMWriter(X509Certificate2 certificate, string password = null #region Private Fields #endregion } - } diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 9d6a520f3..b384d12a9 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -4,9 +4,21 @@ Exe $(TestsTargetFrameworks) Opc.Ua.Server.Tests - false + false + + + + + + + + $(DefineConstants);ECC_SUPPORT + + + + @@ -23,7 +35,7 @@ - + diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs index d45597610..bc56a897f 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs @@ -30,6 +30,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Configuration; @@ -110,7 +111,9 @@ public async Task LoadConfiguration(string pkiRoot = null) .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15) .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256) .AddSignPolicies() - .AddSignAndEncryptPolicies(); + .AddSignAndEncryptPolicies() + .AddEccSignPolicies() + .AddEccSignAndEncryptPolicies(); } if (OperationLimits) @@ -144,9 +147,14 @@ public async Task LoadConfiguration(string pkiRoot = null) RejectTimeout = ReverseConnectTimeout / 4 }); } + + CertificateIdentifierCollection applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( + "CN=" + typeof(T).Name + ", C=US, S=Arizona, O=OPC Foundation, DC=localhost", + CertificateStoreType.Directory, + pkiRoot); Config = await serverConfig.AddSecurityConfiguration( - "CN=" + typeof(T).Name + ", C=US, S=Arizona, O=OPC Foundation, DC=localhost", + applicationCerts, pkiRoot) .SetAutoAcceptUntrustedCertificates(AutoAccept) .Create().ConfigureAwait(false); @@ -219,8 +227,8 @@ private async Task InternalStartServerAsync(TextWriter writer, int port) } // check the application certificate. - bool haveAppCertificate = await Application.CheckApplicationInstanceCertificate( - true, CertificateFactory.DefaultKeySize, CertificateFactory.DefaultLifeTime).ConfigureAwait(false); + bool haveAppCertificate = await Application.CheckApplicationInstanceCertificates( + true, CertificateFactory.DefaultLifeTime).ConfigureAwait(false); if (!haveAppCertificate) { throw new Exception("Application instance certificate invalid!"); diff --git a/common.props b/common.props index 755327f6f..e092c792f 100644 --- a/common.props +++ b/common.props @@ -7,7 +7,6 @@ Copyright © 2004-2024 OPC Foundation, Inc OPC Foundation OPC Foundation - NU5125;CA2254;CA1307 en-US true false diff --git a/version.json b/version.json index 2868ff5f7..2ecb27ee8 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.5.374-preview", - "versionHeightOffset": 10, + "version": "1.5.375-ECC-preview", + "versionHeightOffset": 70, "nugetPackageVersion": { "semVer": 2 },