diff --git a/Libraries/Opc.Ua.Client/ReverseConnectManager.cs b/Libraries/Opc.Ua.Client/ReverseConnectManager.cs index d8a998545..bf5afe2c2 100644 --- a/Libraries/Opc.Ua.Client/ReverseConnectManager.cs +++ b/Libraries/Opc.Ua.Client/ReverseConnectManager.cs @@ -152,18 +152,21 @@ public Registration( } /// - /// Register with the server certificate. + /// Register with the server certificate to extract the application Uri. /// - /// - /// - /// + /// + /// The first Uri in the subject alternate name field is considered the application Uri. + /// + /// The server certificate with the application Uri. + /// The endpoint Url of the server. + /// The connection to use. public Registration( X509Certificate2 serverCertificate, Uri endpointUrl, EventHandler onConnectionWaiting) : this(endpointUrl, onConnectionWaiting) { - ServerUri = X509Utils.GetApplicationUriFromCertificate(serverCertificate); + ServerUri = X509Utils.GetApplicationUrisFromCertificate(serverCertificate).FirstOrDefault(); } private Registration( diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 2bef15dfd..8d5798cab 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -2338,7 +2338,6 @@ public void Open( if (requireEncryption) { - ValidateServerCertificateApplicationUri(serverCertificate); if (checkDomain) { m_configuration.CertificateValidator.Validate(serverCertificateChain, m_endpoint); @@ -2417,24 +2416,24 @@ public void Open( if (!successCreateSession) { base.CreateSession( - null, - clientDescription, - m_endpoint.Description.Server.ApplicationUri, - m_endpoint.EndpointUrl.ToString(), - sessionName, - clientNonce, - clientCertificateChainData != null ? clientCertificateChainData : clientCertificateData, - sessionTimeout, - (uint)MessageContext.MaxMessageSize, - out sessionId, - out sessionCookie, - out m_sessionTimeout, - out serverNonce, - out serverCertificateData, - out serverEndpoints, - out serverSoftwareCertificates, - out serverSignature, - out m_maxRequestMessageSize); + null, + clientDescription, + m_endpoint.Description.Server.ApplicationUri, + m_endpoint.EndpointUrl.ToString(), + sessionName, + clientNonce, + clientCertificateChainData != null ? clientCertificateChainData : clientCertificateData, + sessionTimeout, + (uint)MessageContext.MaxMessageSize, + out sessionId, + out sessionCookie, + out m_sessionTimeout, + out serverNonce, + out serverCertificateData, + out serverEndpoints, + out serverSoftwareCertificates, + out serverSignature, + out m_maxRequestMessageSize); } // save session id. @@ -2456,6 +2455,8 @@ public void Open( ValidateServerEndpoints(serverEndpoints); + ValidateServerCertificateApplicationUri(m_endpoint, serverCertificate); + ValidateServerSignature(serverCertificate, serverSignature, clientCertificateData, clientCertificateChainData, clientNonce); HandleSignedSoftwareCertificates(serverSoftwareCertificates); @@ -5352,26 +5353,53 @@ private void OpenValidateIdentity( !String.IsNullOrEmpty(identityPolicy.SecurityPolicyUri); } } + /// - /// Validates the ServerCertificate ApplicationUri to match the ApplicationUri of the Endpoint for an open call (Spec Part 4 5.4.1) + /// Validates the ServerCertificate ApplicationUri to match the ApplicationUri + /// of the Endpoint (Spec Part 4 5.4.1) returned by the CreateSessionResponse. + /// Ensure the endpoint was matched in + /// with the applicationUri of the server description before the validation. /// - private void ValidateServerCertificateApplicationUri( - X509Certificate2 serverCertificate) + private static void ValidateServerCertificateApplicationUri(ConfiguredEndpoint endpoint, X509Certificate2 serverCertificate) { - var applicationUri = m_endpoint?.Description?.Server?.ApplicationUri; - //check is only neccessary if the ApplicatioUri is specified for the Endpoint - if (string.IsNullOrEmpty(applicationUri)) + if (serverCertificate != null) { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "No ApplicationUri is specified for the server in the EndpointDescription."); - } - string certificateApplicationUri = X509Utils.GetApplicationUriFromCertificate(serverCertificate); - if (!string.Equals(certificateApplicationUri, applicationUri, StringComparison.Ordinal)) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Server did not return a Certificate matching the ApplicationUri specified in the EndpointDescription."); + var applicationUri = endpoint?.Description?.Server?.ApplicationUri; + + // check that an ApplicatioUri is specified for the Endpoint + if (string.IsNullOrEmpty(applicationUri)) + { + throw ServiceResultException.Create( + StatusCodes.BadCertificateUriInvalid, + "Server did not return an ApplicationUri in the EndpointDescription."); + } + + var certificateApplicationUris = X509Utils.GetApplicationUrisFromCertificate(serverCertificate); + if (certificateApplicationUris.Count == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadCertificateUriInvalid, + "The Server Certificate ({1}) does not contain an applicationUri.", + serverCertificate.Subject); + } + + bool noMatch = true; + foreach (var certificateApplicationUri in certificateApplicationUris) + { + if (string.Equals(certificateApplicationUri, applicationUri, StringComparison.Ordinal)) + { + noMatch = false; + break; + } + } + + if (noMatch) + { + throw ServiceResultException.Create( + StatusCodes.BadCertificateUriInvalid, + "The Application in the EndpointDescription ({0}) is not in the Server Certificate ({1}).", + applicationUri, serverCertificate.Subject); + } } } @@ -5538,7 +5566,7 @@ private void ValidateServerEndpoints(EndpointDescriptionCollection serverEndpoin } } - // find the matching description (TBD - check domains against certificate). + // find the matching description (TBD - check domains and application URI against certificate). bool found = false; var foundDescription = FindMatchingDescription(serverEndpoints, m_endpoint.Description, true); diff --git a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs index 1b26df310..68bb5b988 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs @@ -93,7 +93,6 @@ public async Task OpenAsync( if (requireEncryption) { - ValidateServerCertificateApplicationUri(serverCertificate); if (checkDomain) { await m_configuration.CertificateValidator.ValidateAsync(serverCertificateChain, m_endpoint, ct).ConfigureAwait(false); @@ -201,6 +200,8 @@ public async Task OpenAsync( ValidateServerEndpoints(serverEndpoints); + ValidateServerCertificateApplicationUri(m_endpoint, serverCertificate); + ValidateServerSignature(serverCertificate, serverSignature, clientCertificateData, clientCertificateChainData, clientNonce); HandleSignedSoftwareCertificates(serverSoftwareCertificates); diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 64dba21e1..3c447a4b5 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -671,23 +671,38 @@ private async Task CheckApplicationInstanceCertificateAsync( } } - // check uri. - string applicationUri = X509Utils.GetApplicationUriFromCertificate(certificate); + // default application uri + string applicationUri = configuration.ApplicationUri; - if (String.IsNullOrEmpty(applicationUri)) + // check certificate Uri and find a match + var applicationUris = X509Utils.GetApplicationUrisFromCertificate(certificate); + bool foundMatch = false; + foreach (var appUri in applicationUris) { - string message = "The Application URI could not be read from the certificate. Use certificate anyway?"; - if (!await ApplicationInstance.ApproveMessageAsync(message, silent).ConfigureAwait(false)) + if (configuration.ApplicationUri.Equals(appUri, StringComparison.Ordinal)) { - return false; + foundMatch = true; + break; } } - else if (!configuration.ApplicationUri.Equals(applicationUri, StringComparison.Ordinal)) + + if (!foundMatch && applicationUris.Count > 0) { - Utils.LogInfo("Updated the ApplicationUri: {0} --> {1}", configuration.ApplicationUri, applicationUri); + foundMatch = true; + applicationUri = applicationUris[0]; + Utils.LogInfo("Updating the ApplicationUri: {0} --> {1}", configuration.ApplicationUri, applicationUris[0]); configuration.ApplicationUri = applicationUri; } + if (!foundMatch) + { + string message = "The Application URI could not be found in the certificate. Use certificate anyway?"; + if (!await ApplicationInstance.ApproveMessageAsync(message, silent).ConfigureAwait(false)) + { + return false; + } + } + Utils.LogInfo("Using the ApplicationUri: {0}", applicationUri); // update configuration. diff --git a/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs b/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs index 4d929cb0f..5cc3514b1 100644 --- a/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs +++ b/Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs @@ -29,6 +29,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using Opc.Ua.Security.Certificates; @@ -213,7 +214,7 @@ public string ApplicationUri { try { - return X509Utils.GetApplicationUriFromCertificate(Certificate); + return X509Utils.GetApplicationUrisFromCertificate(Certificate).FirstOrDefault(); } catch (Exception e) { diff --git a/Libraries/Opc.Ua.Security.Certificates/Extensions/X509SubjectAltNameExtension.cs b/Libraries/Opc.Ua.Security.Certificates/Extensions/X509SubjectAltNameExtension.cs index d799c1871..9ca7be21b 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Extensions/X509SubjectAltNameExtension.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Extensions/X509SubjectAltNameExtension.cs @@ -118,7 +118,23 @@ public X509SubjectAltNameExtension( { Oid = new Oid(SubjectAltName2Oid, kFriendlyName); Critical = false; - Initialize(applicationUri, domainNames); + Initialize(new string[] { applicationUri }, domainNames); + RawData = Encode(); + m_decoded = true; + } + + /// + /// Build the Subject Alternative name extension (for OPC UA application certs). + /// + /// The application Uri + /// The domain names. DNS Hostnames, IPv4 or IPv6 addresses + public X509SubjectAltNameExtension( + IEnumerable applicationUris, + IEnumerable domainNames) + { + Oid = new Oid(SubjectAltName2Oid, kFriendlyName); + Critical = false; + Initialize(applicationUris, domainNames); RawData = Encode(); m_decoded = true; } @@ -402,14 +418,17 @@ private void Decode(byte[] data) /// /// Initialize the Subject Alternative name extension. /// - /// The application Uri + /// The application Uris /// The general names. DNS Hostnames, IPv4 or IPv6 addresses - private void Initialize(string applicationUri, IEnumerable generalNames) + private void Initialize(IEnumerable applicationUris, IEnumerable generalNames) { var uris = new List(); var domainNames = new List(); var ipAddresses = new List(); - uris.Add(applicationUri); + foreach (string applicationUri in applicationUris) + { + uris.Add(applicationUri); + } foreach (string generalName in generalNames) { switch (Uri.CheckHostName(generalName)) diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index a5d19b836..575bcfb78 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -380,23 +380,34 @@ public override ResponseHeader CreateSession( if (context.SecurityPolicyUri != SecurityPolicies.None) { - string certificateApplicationUri = X509Utils.GetApplicationUriFromCertificate(parsedClientCertificate); - - // verify if applicationUri from ApplicationDescription matches the applicationUri in the client certificate. - if (!String.IsNullOrEmpty(certificateApplicationUri) && - !String.IsNullOrEmpty(clientDescription.ApplicationUri) && - certificateApplicationUri != clientDescription.ApplicationUri) + // verify if applicationUri from ApplicationDescription matches the applicationUris in the client certificate. + if (!String.IsNullOrEmpty(clientDescription.ApplicationUri)) { - // report the AuditCertificateDataMismatch event for invalid uri - ServerInternal?.ReportAuditCertificateDataMismatchEvent(parsedClientCertificate, null, clientDescription.ApplicationUri, StatusCodes.BadCertificateUriInvalid); + bool noMatch = true; + var certificateApplicationUris = X509Utils.GetApplicationUrisFromCertificate(parsedClientCertificate); + foreach (var certificateApplicationUri in certificateApplicationUris) + { + if (!String.IsNullOrEmpty(certificateApplicationUri) && + certificateApplicationUri == clientDescription.ApplicationUri) + { + noMatch = false; + break; + } + } - throw ServiceResultException.Create( - StatusCodes.BadCertificateUriInvalid, - "The URI specified in the ApplicationDescription {0} does not match the URI in the Certificate: {1}.", - clientDescription.ApplicationUri, certificateApplicationUri); - } + if (noMatch) + { + // report the AuditCertificateDataMismatch event for invalid uri + ServerInternal?.ReportAuditCertificateDataMismatchEvent(parsedClientCertificate, null, clientDescription.ApplicationUri, StatusCodes.BadCertificateUriInvalid); + + throw ServiceResultException.Create( + StatusCodes.BadCertificateUriInvalid, + "The URI specified in the ApplicationDescription {0} does not match the URIs in the Certificate.", + clientDescription.ApplicationUri); + } - CertificateValidator.Validate(clientCertificateChain); + CertificateValidator.Validate(clientCertificateChain); + } } } catch (Exception e) @@ -2327,7 +2338,7 @@ public bool RegisterWithDiscoveryServer() { client.RegisterServer(requestHeader, m_registrationInfo); } - + m_registeredWithDiscoveryServer = m_registrationInfo.IsOnline; return true; } @@ -3082,7 +3093,7 @@ protected override void OnServerStopping() // attempt graceful shutdown the server. try { - + if (m_maxRegistrationInterval > 0 && m_registeredWithDiscoveryServer) { // unregister from Discovery Server if registered before diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index 2a2fde212..1a145fc77 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -319,10 +319,10 @@ public static byte[] CreateSigningRequest( } } - string applicationUri = X509Utils.GetApplicationUriFromCertificate(certificate); + var applicationUris = X509Utils.GetApplicationUrisFromCertificate(certificate); // Subject Alternative Name - var subjectAltName = new X509SubjectAltNameExtension(applicationUri, domainNames); + var subjectAltName = new X509SubjectAltNameExtension(applicationUris, domainNames); request.CertificateExtensions.Add(new X509Extension(subjectAltName, false)); using (RSA rsa = certificate.GetRSAPrivateKey()) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index e47f1a4c2..9c0a02372 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -126,6 +126,7 @@ public static int GetRSAPublicKeySize(X509Certificate2 certificate) /// /// The certificate. /// The application URI. + [Obsolete("Use GetApplicationUrisFromCertificate instead. The certificate may contain more than one Uri.")] public static string GetApplicationUriFromCertificate(X509Certificate2 certificate) { // extract the alternate domains from the subject alternate name extension. @@ -140,6 +141,25 @@ public static string GetApplicationUriFromCertificate(X509Certificate2 certifica return string.Empty; } + /// + /// Extracts the application URIs specified in the certificate. + /// + /// The certificate. + /// The application URIs. + public static IReadOnlyList GetApplicationUrisFromCertificate(X509Certificate2 certificate) + { + // extract the alternate domains from the subject alternate name extension. + X509SubjectAltNameExtension alternateName = X509Extensions.FindExtension(certificate); + + // get the application uris. + if (alternateName != null && alternateName.Uris != null) + { + return alternateName.Uris; + } + + return new List(); + } + /// /// Check if certificate has an application urn. /// diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index 9d75de2b0..15804de89 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -1405,7 +1405,7 @@ protected virtual void OnServerStarting(ApplicationConfiguration configuration) // assign a unique identifier if none specified. if (String.IsNullOrEmpty(configuration.ApplicationUri)) { - configuration.ApplicationUri = X509Utils.GetApplicationUriFromCertificate(InstanceCertificate); + configuration.ApplicationUri = X509Utils.GetApplicationUrisFromCertificate(InstanceCertificate).FirstOrDefault(); if (String.IsNullOrEmpty(configuration.ApplicationUri)) { diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 6816af4e8..550a01e14 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -2529,7 +2529,7 @@ public static void UpdateExtension(ref XmlElementCollection extensions, XmlQu extensions.Add(document.DocumentElement); } } -#endregion + #endregion #region Reflection Helper Functions /// diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs index b6e28a798..16273c14e 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs @@ -435,8 +435,8 @@ public static void VerifyApplicationCert(ApplicationTestData testApp, X509Certif Assert.True(domainNames.Contains(domainName, StringComparer.OrdinalIgnoreCase)); } Assert.True(subjectAlternateName.Uris.Count == 1); - var applicationUri = X509Utils.GetApplicationUriFromCertificate(cert); - TestContext.Out.WriteLine("ApplicationUri: "); + var applicationUri = X509Utils.GetApplicationUrisFromCertificate(cert).FirstOrDefault(); + TestContext.Out.WriteLine("ApplicationUris: "); TestContext.Out.WriteLine(applicationUri); Assert.AreEqual(testApp.ApplicationUri, applicationUri); } diff --git a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs index e61ad47a1..1b0dc51e8 100644 --- a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs +++ b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs @@ -156,7 +156,7 @@ public static void VerifySignedApplicationCert(ApplicationTestData testApp, byte Assert.True(domainNames.Contains(domainName, StringComparer.OrdinalIgnoreCase)); } Assert.True(subjectAlternateName.Uris.Count == 1); - var applicationUri = X509Utils.GetApplicationUriFromCertificate(signedCert); + var applicationUri = X509Utils.GetApplicationUrisFromCertificate(signedCert).FirstOrDefault(); Assert.True(testApp.ApplicationRecord.ApplicationUri == applicationUri); } }