From e156a3c3da7340c455145c077635f63ec4e034c1 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 15 Nov 2017 21:08:43 +0100 Subject: [PATCH] PEM support (#33) --- .../Configuration/ConfigurationNodeManager.cs | 12 +- .../Samples/GDS/ClientTest/ClientTest.cs | 74 +++++- .../ClientTest/GlobalDiscoveryTestClient.cs | 1 - .../ClientTest/GlobalDiscoveryTestServer.cs | 21 ++ .../Samples/GDS/ClientTest/PushTest.cs | 240 +++++++++++++++++- .../ServerCommon/ApplicationsNodeManager.cs | 59 +++-- .../Certificates/CertificateFactory.cs | 34 ++- Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 4 + 8 files changed, 388 insertions(+), 57 deletions(-) diff --git a/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 645ae1410e..4ee82e97a4 100644 --- a/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -430,7 +430,14 @@ namespace Opc.Ua.Server { foreach (var issuer in updateCertificate.IssuerCollection) { - issuerStore.Add(issuer); + try + { + issuerStore.Add(issuer).Wait(); + } + catch (ArgumentException) + { + // ignore error if issuer cert already exists + } } } @@ -451,6 +458,9 @@ namespace Opc.Ua.Server { // update Application certificate await m_configuration.CertificateValidator.Update(m_configuration); + + // TODO: call OnUpdateConfiguration + // force close all sessions foreach (var session in Server.SessionManager.GetSessions()) { diff --git a/SampleApplications/Samples/GDS/ClientTest/ClientTest.cs b/SampleApplications/Samples/GDS/ClientTest/ClientTest.cs index 330f8e06a4..783d23e562 100644 --- a/SampleApplications/Samples/GDS/ClientTest/ClientTest.cs +++ b/SampleApplications/Samples/GDS/ClientTest/ClientTest.cs @@ -37,7 +37,7 @@ using Opc.Ua.Gds; using Opc.Ua.Gds.Client; using Opc.Ua.Gds.Test; using Opc.Ua.Test; -using System.Threading.Tasks; +using System.Security.Cryptography.X509Certificates; namespace NUnit.Opc.Ua.Gds.Test { @@ -81,7 +81,7 @@ namespace NUnit.Opc.Ua.Gds.Test /// /// /// - [TestFixture] + [TestFixture, Category("GDSRegistrationAndPull")] public class ClientTest { #region Test Setup @@ -92,6 +92,7 @@ namespace NUnit.Opc.Ua.Gds.Test protected void OneTimeSetUp() { #if DEBUG + // work around travis issue by selecting different ports on every run const int testPort = 60000; #else const int testPort = 60010; @@ -109,6 +110,9 @@ namespace NUnit.Opc.Ua.Gds.Test // good applications test set _goodApplicationTestSet = ApplicationTestSet(goodApplicationsTestCount, false); _invalidApplicationTestSet = ApplicationTestSet(invalidApplicationsTestCount, true); + + _goodRegistrationOk = false; + _invalidRegistrationOk = false; } /// @@ -147,11 +151,13 @@ namespace NUnit.Opc.Ua.Gds.Test Assert.AreEqual(id.IdType, IdType.Guid); application.ApplicationRecord.ApplicationId = id; } + _goodRegistrationOk = true; } [Test, Order(101)] public void RegisterGoodApplicationsAuditEvents() { + AssertIgnoreTestWithoutGoodRegistration(); foreach (var application in _goodApplicationTestSet) { // TODO @@ -166,11 +172,13 @@ namespace NUnit.Opc.Ua.Gds.Test { Assert.That(() => { NodeId id = _gdsClient.GDSClient.RegisterApplication(application.ApplicationRecord); }, Throws.Exception); } + _invalidRegistrationOk = true; } [Test, Order(111)] public void RegisterInvalidApplicationsAuditEvents() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -181,6 +189,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(120)] public void RegisterApplicationAsUser() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(false); foreach (var application in _invalidApplicationTestSet) { @@ -191,6 +200,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(200)] public void UpdateGoodApplications() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) { @@ -207,6 +217,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(201)] public void UpdateGoodApplicationsAuditEvents() { + AssertIgnoreTestWithoutGoodRegistration(); foreach (var application in _goodApplicationTestSet) { // TODO @@ -216,6 +227,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(210)] public void UpdateGoodApplicationsWithNewGuid() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) { @@ -228,6 +240,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(211)] public void UpdateGoodApplicationsWithNewGuidAuditEvents() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) { @@ -238,6 +251,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(220)] public void UpdateInvalidApplications() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -248,6 +262,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(221)] public void UpdateInvalidApplicationsAuditEvents() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -258,6 +273,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(400)] public void FindGoodApplications() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(false); foreach (var application in _goodApplicationTestSet) { @@ -270,6 +286,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(400)] public void FindInvalidApplications() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -282,6 +299,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(400)] public void GetGoodApplications() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(false); foreach (var application in _goodApplicationTestSet) { @@ -294,6 +312,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(400)] public void GetInvalidApplications() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -304,6 +323,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(410)] public void QueryGoodServers() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(false); // get all servers var allServers = _gdsClient.GDSClient.QueryServers(0, "", "", "", null); @@ -370,6 +390,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(500)] public void StartGoodNewKeyPairRequests() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) { @@ -390,6 +411,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(501)] public void StartInvalidNewKeyPairRequests() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); foreach (var application in _invalidApplicationTestSet) { @@ -411,6 +433,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(510)] public void FinishGoodNewKeyPairRequests() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); bool requestBusy; do @@ -455,8 +478,8 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(511)] public void FinishInvalidNewKeyPairRequests() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); - foreach (var application in _invalidApplicationTestSet) { Assert.That(() => @@ -474,11 +497,20 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(520)] public void StartGoodSigningRequests() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) { Assert.Null(application.CertificateRequestId); - var csrCertificate = CertificateFactory.CreateCertificateFromPKCS12(application.PrivateKey, application.PrivateKeyPassword); + X509Certificate2 csrCertificate; + if (application.PrivateKeyFormat == "PFX") + { + csrCertificate = CertificateFactory.CreateCertificateFromPKCS12(application.PrivateKey, application.PrivateKeyPassword); + } + else + { + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); NodeId requestId = _gdsClient.GDSClient.StartSigningRequest( application.ApplicationRecord.ApplicationId, @@ -493,8 +525,8 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(521)] public void FinishGoodSigningRequests() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); - bool requestBusy; do { @@ -538,6 +570,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(600)] public void GetGoodCertificateGroupsNullTests() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); Assert.That(() => @@ -567,8 +600,8 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(601)] public void GetInvalidCertificateGroupsNullTests() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); - Assert.That(() => { _gdsClient.GDSClient.GetCertificateGroups(null); @@ -598,6 +631,7 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(610)] public void GetGoodCertificateGroupsAndTrustLists() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); foreach (var application in _goodApplicationTestSet) @@ -615,8 +649,8 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(690)] public void GetGoodCertificateStatus() { + AssertIgnoreTestWithoutGoodRegistration(); ConnectGDS(true); - foreach (var application in _goodApplicationTestSet) { var certificateStatus = _gdsClient.GDSClient.GetCertificateStatus(application.ApplicationRecord.ApplicationId, null, null); @@ -626,8 +660,8 @@ namespace NUnit.Opc.Ua.Gds.Test [Test, Order(691)] public void GetInvalidCertificateStatus() { + AssertIgnoreTestWithoutInvalidRegistration(); ConnectGDS(true); - foreach (var application in _invalidApplicationTestSet) { Assert.That(() => @@ -641,7 +675,6 @@ namespace NUnit.Opc.Ua.Gds.Test public void UnregisterGoodApplications() { ConnectGDS(true); - foreach (var application in _goodApplicationTestSet) { _gdsClient.GDSClient.UnregisterApplication(application.ApplicationRecord.ApplicationId); @@ -748,6 +781,7 @@ namespace NUnit.Opc.Ua.Gds.Test string appName = "UA " + pureAppName; StringCollection domainNames = RandomDomainNames(); string localhost = domainNames[0]; + string privateKeyFormat = _randomSource.NextInt32(1) == 0 ? "PEM" : "PFX"; string appUri = ("urn:localhost:opcfoundation.org:" + pureAppUri.ToLower()).Replace("localhost", localhost); string prodUri = "http://opcfoundation.org/UA/" + pureAppUri; StringCollection discoveryUrls = new StringCollection(); @@ -779,7 +813,8 @@ namespace NUnit.Opc.Ua.Gds.Test ServerCapabilities = serverCapabilities }, DomainNames = domainNames, - Subject = String.Format("CN={0},DC={1},O=OPC Foundation", appName, localhost) + Subject = String.Format("CN={0},DC={1},O=OPC Foundation", appName, localhost), + PrivateKeyFormat = privateKeyFormat }; return testData; } @@ -847,7 +882,7 @@ namespace NUnit.Opc.Ua.Gds.Test private void ConnectGDS(bool admin) { - _gdsClient.GDSClient.AdminCredentials = new UserIdentity(admin?"appadmin":"appuser", "demo"); + _gdsClient.GDSClient.AdminCredentials = new UserIdentity(admin ? "appadmin" : "appuser", "demo"); _gdsClient.GDSClient.Connect(_gdsClient.GDSClient.EndpointUrl).Wait(); } @@ -856,6 +891,21 @@ namespace NUnit.Opc.Ua.Gds.Test _gdsClient.GDSClient.Disconnect(); } + private void AssertIgnoreTestWithoutGoodRegistration() + { + if (!_goodRegistrationOk) + { + Assert.Ignore("Test requires good application registrations."); + } + } + + private void AssertIgnoreTestWithoutInvalidRegistration() + { + if (!_invalidRegistrationOk) + { + Assert.Ignore("Test requires invalid application registration."); + } + } #endregion #region Private Fields private const int goodApplicationsTestCount = 10; @@ -868,6 +918,8 @@ namespace NUnit.Opc.Ua.Gds.Test private IList _goodApplicationTestSet; private IList _invalidApplicationTestSet; private ServerCapabilities _serverCapabilities; + private bool _goodRegistrationOk; + private bool _invalidRegistrationOk; #endregion } diff --git a/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestClient.cs b/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestClient.cs index dd35686f9e..30697edab5 100644 --- a/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestClient.cs +++ b/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestClient.cs @@ -68,7 +68,6 @@ namespace Opc.Ua.Gds.Test config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); - // use same server configuration for gds and push tests GlobalDiscoveryClientConfiguration gdsClientConfiguration = application.ApplicationConfiguration.ParseExtension(); _client = new GlobalDiscoveryServerClient(application, gdsClientConfiguration); _client.EndpointUrl = TestUtils.PatchEndpointUrlPort(gdsClientConfiguration.GlobalDiscoveryServerUrl, port); diff --git a/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestServer.cs b/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestServer.cs index e7aa3face4..34448ca905 100644 --- a/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestServer.cs +++ b/SampleApplications/Samples/GDS/ClientTest/GlobalDiscoveryTestServer.cs @@ -78,6 +78,27 @@ namespace Opc.Ua.Gds.Test TestUtils.CleanupTrustList(config.SecurityConfiguration.RejectedCertificateStore.OpenStore()); } + if (clean) + { + string thumbprint = config.SecurityConfiguration.ApplicationCertificate.Thumbprint; + if (thumbprint != null) + { + using (var store = config.SecurityConfiguration.ApplicationCertificate.OpenStore()) + { + await store.Delete(thumbprint); + } + } + + // always start with clean cert store + TestUtils.CleanupTrustList(config.SecurityConfiguration.ApplicationCertificate.OpenStore()); + TestUtils.CleanupTrustList(config.SecurityConfiguration.TrustedIssuerCertificates.OpenStore()); + TestUtils.CleanupTrustList(config.SecurityConfiguration.TrustedPeerCertificates.OpenStore()); + TestUtils.CleanupTrustList(config.SecurityConfiguration.RejectedCertificateStore.OpenStore()); + config = await application.LoadApplicationConfiguration(false); + } + + TestUtils.PatchBaseAddressesPorts(config, basePort); + // check the application certificate. bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); if (!haveAppCertificate) diff --git a/SampleApplications/Samples/GDS/ClientTest/PushTest.cs b/SampleApplications/Samples/GDS/ClientTest/PushTest.cs index 2063e1bb28..31f54442e5 100644 --- a/SampleApplications/Samples/GDS/ClientTest/PushTest.cs +++ b/SampleApplications/Samples/GDS/ClientTest/PushTest.cs @@ -27,20 +27,24 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using NUnit.Framework; using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Gds; using Opc.Ua.Gds.Client; using Opc.Ua.Gds.Test; using Opc.Ua.Test; +using NUnit.Framework; using System; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Linq; namespace NUnit.Opc.Ua.Gds.Test { - [TestFixture, Category("GDS")] + [TestFixture, Category("GDSPush")] public class PushTest { #region Test Setup @@ -60,7 +64,7 @@ namespace NUnit.Opc.Ua.Gds.Test _randomSource = new RandomSource(randomStart); _dataGenerator = new DataGenerator(_randomSource); _server = new GlobalDiscoveryTestServer(true); - await _server.StartServer(false, testPort); + await _server.StartServer(true, testPort); await Task.Delay(1000); // load clients @@ -72,6 +76,13 @@ namespace NUnit.Opc.Ua.Gds.Test // connect once await _gdsClient.GDSClient.Connect(_gdsClient.GDSClient.EndpointUrl); await _pushClient.PushClient.Connect(_pushClient.PushClient.EndpointUrl); + + ConnectGDSClient(true); + RegisterPushServerApplication(_pushClient.PushClient.EndpointUrl); + + _selfSignedServerCert = new X509Certificate2(_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); + _domainNames = Utils.GetDomainsFromCertficate(_selfSignedServerCert).ToArray(); + } /// @@ -80,6 +91,8 @@ namespace NUnit.Opc.Ua.Gds.Test [OneTimeTearDown] protected void OneTimeTearDown() { + ConnectGDSClient(true); + UnRegisterPushServerApplication(); _gdsClient.DisconnectClient(); _gdsClient = null; _pushClient.DisconnectClient(); @@ -96,8 +109,8 @@ namespace NUnit.Opc.Ua.Gds.Test DisconnectPushClient(); } -#endregion -#region Test Methods + #endregion + #region Test Methods [Test, Order(100)] public void GetSupportedKeyFormats() { @@ -175,7 +188,15 @@ namespace NUnit.Opc.Ua.Gds.Test Assert.That(() => { _pushClient.PushClient.CreateCertificateRequest(invalidCertGroup, invalidCertType, null, false, null); }, Throws.Exception); byte[] csr = _pushClient.PushClient.CreateCertificateRequest(null, null, null, false, null); Assert.IsNotNull(csr); + } + [Test, Order(400)] + public void CreateCertificateRequestWithNewPrivateKey() + { + ConnectPushClient(true); + ConnectGDSClient(true); + byte[] csr = _pushClient.PushClient.CreateCertificateRequest(null, null, null, true, Encoding.ASCII.GetBytes("OPCTest")); + Assert.IsNotNull(csr); } [Test, Order(500)] @@ -207,24 +228,173 @@ namespace NUnit.Opc.Ua.Gds.Test { _pushClient.PushClient.ApplyChanges(); } + VerifyNewPushServerCert(serverCert.RawData); } [Test, Order(510)] public void UpdateCertificateCASigned() { ConnectPushClient(true); + ConnectGDSClient(true); + byte[] csr = _pushClient.PushClient.CreateCertificateRequest(null, null, null, false, null); + Assert.IsNotNull(csr); + NodeId requestId = _gdsClient.GDSClient.StartSigningRequest( + _applicationRecord.ApplicationId, + null, + null, + csr); + Assert.NotNull(requestId); + byte[] privateKey; + byte[] certificate; + byte[][] issuerCertificates; + int i = 0; + do + { + Thread.Sleep(500); + certificate = _gdsClient.GDSClient.FinishRequest( + _applicationRecord.ApplicationId, + requestId, + out privateKey, + out issuerCertificates); + Assert.LessOrEqual(i++, 5); + } while (certificate == null); + Assert.NotNull(issuerCertificates); + Assert.IsNull(privateKey); + + var success = _pushClient.PushClient.UpdateCertificate(null, null, certificate, null, null, issuerCertificates); + if (success) + { + _pushClient.PushClient.ApplyChanges(); + } + VerifyNewPushServerCert(certificate); } + [Test, Order(520)] - public void UpdateCertificateSelfSignedWithPFXKey() + public void UpdateCertificateSelfSignedPFX() { - ConnectPushClient(true); + UpdateCertificateSelfSigned("PFX"); } [Test, Order(530)] - public void UpdateCertificateSelfSignedWithPEMKey() + public void UpdateCertificateSelfSignedPEM() + { + UpdateCertificateSelfSigned("PEM"); + } + + public void UpdateCertificateSelfSigned(string keyFormat) { ConnectPushClient(true); + var keyFormats = _pushClient.PushClient.GetSupportedKeyFormats(); + if (!keyFormats.Contains(keyFormat)) + { + Assert.Ignore("Push server doesn't support {0} key update", keyFormat); + } + + X509Certificate2 newCert = CertificateFactory.CreateCertificate( + null, + null, + null, + _applicationRecord.ApplicationUri, + _applicationRecord.ApplicationNames[0].Text, + _selfSignedServerCert.Subject, + null, + CertificateFactory.defaultKeySize, + DateTime.UtcNow, + CertificateFactory.defaultLifeTime, + CertificateFactory.defaultHashSize); + + byte[] privateKey = null; + if (keyFormat == "PFX") + { + Assert.IsTrue(newCert.HasPrivateKey); + privateKey = newCert.Export(X509ContentType.Pfx); + } + else if (keyFormat == "PEM") + { + Assert.IsTrue(newCert.HasPrivateKey); + privateKey = CertificateFactory.ExportPrivateKeyAsPEM(newCert); + } + else + { + Assert.Fail("Testing unsupported key format {0}.", keyFormat); + } + + var success = _pushClient.PushClient.UpdateCertificate( + null, + null, + newCert.RawData, + keyFormat, + privateKey, + null); + + if (success) + { + _pushClient.PushClient.ApplyChanges(); + } + VerifyNewPushServerCert(newCert.RawData); + } + + [Test, Order(540)] + public void UpdateCertificateNewKeyPairPFX() + { + UpdateCertificateWithNewKeyPair("PFX"); + } + + [Test, Order(550)] + public void UpdateCertificateNewKeyPairPEM() + { + UpdateCertificateWithNewKeyPair("PEM"); + } + + public void UpdateCertificateWithNewKeyPair(string keyFormat) + { + ConnectPushClient(true); + var keyFormats = _pushClient.PushClient.GetSupportedKeyFormats(); + if (!keyFormats.Contains(keyFormat)) + { + Assert.Ignore("Push server doesn't support {0} key update", keyFormat); + } + + NodeId requestId = _gdsClient.GDSClient.StartNewKeyPairRequest( + _applicationRecord.ApplicationId, + null, + null, + _selfSignedServerCert.Subject, + _domainNames, + keyFormat, + null); + + Assert.NotNull(requestId); + byte[] privateKey; + byte[] certificate; + byte[][] issuerCertificates; + int i = 0; + do + { + Thread.Sleep(500); + certificate = _gdsClient.GDSClient.FinishRequest( + _applicationRecord.ApplicationId, + requestId, + out privateKey, + out issuerCertificates); + Assert.LessOrEqual(i++, 5); + } while (certificate == null); + Assert.NotNull(issuerCertificates); + Assert.NotNull(privateKey); + + var success = _pushClient.PushClient.UpdateCertificate( + null, + null, + certificate, + keyFormat, + privateKey, + issuerCertificates); + if (success) + { + _pushClient.PushClient.ApplyChanges(); + } + VerifyNewPushServerCert(certificate); } [Test, Order(600)] @@ -234,7 +404,6 @@ namespace NUnit.Opc.Ua.Gds.Test var collection = _pushClient.PushClient.GetRejectedList(); } - [Test, Order(700)] public void ApplyChanges() { @@ -248,13 +417,12 @@ namespace NUnit.Opc.Ua.Gds.Test ConnectPushClient(false); Assert.That(() => { _pushClient.PushClient.ApplyChanges(); }, Throws.Exception); Assert.That(() => { _pushClient.PushClient.GetRejectedList(); }, Throws.Exception); - X509Certificate2 serverCert = new X509Certificate2(_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); - Assert.That(() => { _pushClient.PushClient.UpdateCertificate(null, null, serverCert.RawData, null, null, null); }, Throws.Exception); + Assert.That(() => { _pushClient.PushClient.UpdateCertificate(null, null, _selfSignedServerCert.RawData, null, null, null); }, Throws.Exception); Assert.That(() => { _pushClient.PushClient.CreateCertificateRequest(null, null, null, false, null); }, Throws.Exception); Assert.That(() => { _pushClient.PushClient.ReadTrustList(); }, Throws.Exception); } -#endregion -#region Private Methods + #endregion + #region Private Methods private void ConnectPushClient(bool sysAdmin) { _pushClient.PushClient.AdminCredentials = new UserIdentity(sysAdmin ? "sysadmin" : "appuser", "demo"); @@ -286,6 +454,49 @@ namespace NUnit.Opc.Ua.Gds.Test } return result; } + + private void RegisterPushServerApplication(string discoveryUrl) + { + if (_applicationRecord == null && discoveryUrl != null) + { + EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(discoveryUrl, true); + ApplicationDescription description = endpointDescription.Server; + _applicationRecord = new ApplicationRecordDataType + { + ApplicationNames = new LocalizedTextCollection { description.ApplicationName }, + ApplicationUri = description.ApplicationUri, + ApplicationType = description.ApplicationType, + ProductUri = description.ProductUri, + DiscoveryUrls = description.DiscoveryUrls, + ServerCapabilities = new StringCollection { "NA" }, + }; + } + Assert.IsNotNull(_applicationRecord); + Assert.IsNull(_applicationRecord.ApplicationId); + NodeId id = _gdsClient.GDSClient.RegisterApplication(_applicationRecord); + Assert.IsNotNull(id); + _applicationRecord.ApplicationId = id; + } + + private void UnRegisterPushServerApplication() + { + _gdsClient.GDSClient.UnregisterApplication(_applicationRecord.ApplicationId); + _applicationRecord.ApplicationId = null; + } + + private void VerifyNewPushServerCert(byte[] certificate) + { + DisconnectPushClient(); + Thread.Sleep(500); + _gdsClient.GDSClient.AdminCredentials = new UserIdentity("appadmin", "demo"); + _pushClient.PushClient.Connect(_pushClient.PushClient.EndpointUrl).Wait(); +#if TODO + Assert.AreEqual( + certificate, + _pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate + ); +#endif + } #endregion #region Private Fields @@ -296,6 +507,9 @@ namespace NUnit.Opc.Ua.Gds.Test private GlobalDiscoveryTestClient _gdsClient; private ServerConfigurationPushTestClient _pushClient; private ServerCapabilities _serverCapabilities; + private ApplicationRecordDataType _applicationRecord; + private X509Certificate2 _selfSignedServerCert; + private string[] _domainNames; #endregion } } \ No newline at end of file diff --git a/SampleApplications/Samples/GDS/ServerCommon/ApplicationsNodeManager.cs b/SampleApplications/Samples/GDS/ServerCommon/ApplicationsNodeManager.cs index 91322d004d..67b4ecff47 100644 --- a/SampleApplications/Samples/GDS/ServerCommon/ApplicationsNodeManager.cs +++ b/SampleApplications/Samples/GDS/ServerCommon/ApplicationsNodeManager.cs @@ -909,8 +909,8 @@ namespace Opc.Ua.Gds.Server try { newCertificate = CertificateFactory.CreateCertificate( - CertificateStoreType.Directory, - m_configuration.ApplicationCertificatesStorePath, + null, + null, privateKeyPassword, application.ApplicationUri ?? "urn:ApplicationURI", application.ApplicationNames.Count > 0 ? application.ApplicationNames[0].Text : "ApplicationName", @@ -935,27 +935,32 @@ namespace Opc.Ua.Gds.Server return new ServiceResult(StatusCodes.BadConfigurationError, error.ToString()); } - // since CreateCert() automatically stores the new cert in our cert store, - // delete the private key but keep the cert + byte[] privateKey; + if (privateKeyFormat == "PFX") + { + privateKey = newCertificate.Export(X509ContentType.Pfx, privateKeyPassword); + } + else if (privateKeyFormat == "PEM") + { + privateKey = CertificateFactory.ExportPrivateKeyAsPEM(newCertificate); + } + else + { + return new ServiceResult(StatusCodes.BadInvalidArgument, "Invalid private key format"); + } + + // store only app certificate using (DirectoryCertificateStore store = (DirectoryCertificateStore)CertificateStoreIdentifier.OpenStore(m_configuration.ApplicationCertificatesStorePath)) { - byte[] privateKeyPFX = null; - var privateKeyPath = store.GetPrivateKeyFilePath(newCertificate.Thumbprint); - if (privateKeyPath != null) - { - privateKeyPFX = File.ReadAllBytes(privateKeyPath); - } - - // delete private key file - File.Delete(privateKeyPath); - - requestId = m_database.CreateCertificateRequest( - applicationId, - newCertificate.RawData, - privateKeyPFX, - certificateGroup.Id.Identifier as string); + store.Add(new X509Certificate2(newCertificate.RawData)).Wait(); } + requestId = m_database.CreateCertificateRequest( + applicationId, + newCertificate.RawData, + privateKey, + certificateGroup.Id.Identifier as string); + // immediately approve certificate for now. m_database.ApproveCertificateRequest(requestId, false); @@ -1262,12 +1267,12 @@ namespace Opc.Ua.Gds.Server handle.Validated = true; return handle.Node; } - #endregion +#endregion - #region Overridden Methods - #endregion +#region Overridden Methods +#endregion - #region Private Methods +#region Private Methods /// /// Generates a new node id. /// @@ -1288,14 +1293,14 @@ namespace Opc.Ua.Gds.Server }; return await certIdentifier.LoadPrivateKey(signingKeyPassword); } - #endregion +#endregion - #region Private Fields - private string m_certificatePassword = null; // TODO: placeholder +#region Private Fields + private string m_certificatePassword = ""; private uint m_nextNodeId; private GlobalDiscoveryServerConfiguration m_configuration; private IApplicationsDatabase m_database; private Dictionary m_certificateGroups; - #endregion +#endregion } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index 610d98a375..289de97480 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -93,6 +93,11 @@ public class CertificateFactoryX509Name : X509Name { } + public CertificateFactoryX509Name(bool reverse, string distinguishedName) : + base(reverse, ConvertToX509Name(distinguishedName)) + { + } + private static string ConvertToX509Name(string distinguishedName) { // convert from X509Certificate to bouncy castle DN entries @@ -136,8 +141,9 @@ public class CertificateFactory /// If true a key container is created for a certificate that must be deleted by calling Cleanup. /// The cached certificate. /// - /// This function is necessary because all private keys used for cryptography operations must be in a key conatiner. - /// Private keys stored in a PFX file have no key conatiner by default. + /// This function is necessary because all private keys used for cryptography + /// operations must be in a key container. + /// Private keys stored in a PFX file have no key container by default. /// public static X509Certificate2 Load(X509Certificate2 certificate, bool ensurePrivateKeyAccessible) { @@ -560,7 +566,7 @@ public class CertificateFactory new CrlNumber(crlSerialNumber)); // generate updated CRL - Org.BouncyCastle.X509.X509Crl updatedCrl = crlGen.Generate(signatureFactory); + X509Crl updatedCrl = crlGen.Generate(signatureFactory); // add updated CRL to store X509CRL updatedCRL = new X509CRL(updatedCrl.GetEncoded()); @@ -650,7 +656,7 @@ public class CertificateFactory Pkcs10CertificationRequest pkcs10CertificationRequest = new Pkcs10CertificationRequest( signatureFactory, - new CertificateFactoryX509Name(certificate.Subject), + new CertificateFactoryX509Name(false, certificate.Subject), publicKey, attributes, signingKey); @@ -767,6 +773,25 @@ public class CertificateFactory } } } + /// + /// returns a byte array containing the private key in PEM format. + /// + + public static byte[] ExportPrivateKeyAsPEM( + X509Certificate2 certificate + ) + { + RsaPrivateCrtKeyParameters keyParameter = GetPrivateKeyParameter(certificate); + using (TextWriter textWriter = new StringWriter()) + { + PemWriter pemWriter = new PemWriter(textWriter); + pemWriter.WriteObject(keyParameter); + pemWriter.Writer.Flush(); + return Encoding.ASCII.GetBytes(textWriter.ToString()); + } + } + + private class Password : IPasswordFinder { @@ -1205,6 +1230,7 @@ public class CertificateFactory } return result; } + #endregion private static Dictionary m_certificates = new Dictionary(); diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 0188b6aacc..7fba75ceb4 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -3064,6 +3064,10 @@ namespace Opc.Ua return false; } + // sort to ensure similar entries are compared + fields1.Sort(StringComparer.OrdinalIgnoreCase); + fields2.Sort(StringComparer.OrdinalIgnoreCase); + // compare each. for (int ii = 0; ii < fields1.Count; ii++) {