diff --git a/source/Halibut.Tests.DotMemory/MemoryFixture.cs b/source/Halibut.Tests.DotMemory/MemoryFixture.cs index ca55418ab..78bc0e18e 100644 --- a/source/Halibut.Tests.DotMemory/MemoryFixture.cs +++ b/source/Halibut.Tests.DotMemory/MemoryFixture.cs @@ -59,11 +59,26 @@ public void TcpClientsAreDisposedCorrectly() .WriteTo.NUnitOutput() .CreateLogger(); + // Two separate HalibutRuntime instances are used to avoid an SChannel session cache + // collision on Windows (.NET Framework / SslProtocols.None). SChannel's TLS session + // cache is per-process and keyed on certificate + host. If a single runtime acts as + // both a TLS server (accepting inbound connections) and a TLS client (making outbound + // polling connections) using the same certificate, SChannel can incorrectly reuse a + // server-side session for a client-side handshake, causing SSPI/TLS failures. + // + // - server: pure TLS server — accepts inbound connections from listening tentacles. + // Uses Certificates.Octopus. + // - pollingServer: pure TLS client — only makes outbound polling connections to tentacles. + // Must use a DIFFERENT certificate (Certificates.TentacleListening) so + // that SChannel never sees the same cert in both server and client roles + // within this process. HalibutRuntime? server = null; + HalibutRuntime? pollingServer = null; try { server = RunServer(Certificates.Octopus, out var port); + pollingServer = RunPollingServer(Certificates.TentacleListening); var expectedTcpClientCount = 1; //server listen = 1 tcpclient //valid requests @@ -75,13 +90,15 @@ public void TcpClientsAreDisposedCorrectly() for (var i = 0; i < NumberOfClients; i++) { expectedTcpClientCount++; // each time the server polls, it keeps a tcpclient (as we dont have support to say StopPolling) - RunPollingClient(server, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint).GetAwaiter().GetResult(); + RunPollingClient(pollingServer, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint).GetAwaiter().GetResult(); } #if SUPPORTS_WEB_SOCKET_CLIENT + //setup polling websocket + AddSslCertToLocalStoreAndRegisterFor("0.0.0.0:8434"); for (var i = 0; i < NumberOfClients; i++) { - RunWebSocketPollingClient(server, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint, Certificates.OctopusPublicThumbprint).GetAwaiter().GetResult(); + RunWebSocketPollingClient(pollingServer, Certificates.TentaclePolling, Certificates.TentaclePollingPublicThumbprint, Certificates.TentacleListeningPublicThumbprint).GetAwaiter().GetResult(); } #endif @@ -106,6 +123,7 @@ public void TcpClientsAreDisposedCorrectly() finally { server?.DisposeAsync().GetAwaiter().GetResult(); + pollingServer?.DisposeAsync().GetAwaiter().GetResult(); } } @@ -142,16 +160,24 @@ static HalibutRuntime RunServer(X509Certificate2 serverCertificate, out int port .WithLogFactory(new TestContextLogFactory("client", LogLevel.Info)) .Build(); - //set up listening server.Trust(Certificates.TentacleListeningPublicThumbprint); port = server.Listen(); - //setup polling websocket - AddSslCertToLocalStoreAndRegisterFor("0.0.0.0:8434"); - return server; } + static HalibutRuntime RunPollingServer(X509Certificate2 serverCertificate) + { + var services = new DelegateServiceFactory(); + services.Register(() => new AsyncCalculatorService()); + + return new HalibutRuntimeBuilder() + .WithServerCertificate(serverCertificate) + .WithServiceFactory(services) + .WithLogFactory(new TestContextLogFactory("polling-server", LogLevel.Info)) + .Build(); + } + static async Task RunListeningClient(X509Certificate2 clientCertificate, int port, string remoteThumbprint, bool expectSuccess = true) { await using (var runtime = new HalibutRuntimeBuilder().WithServerCertificate(clientCertificate).Build()) @@ -169,9 +195,13 @@ static async Task RunPollingClient(HalibutRuntime server, X509Certificate2 clien .Build()) { runtime.Listen(new IPEndPoint(IPAddress.IPv6Any, 8433)); - runtime.Trust(Certificates.OctopusPublicThumbprint); + // Trust the thumbprint of pollingServer's certificate (TentacleListening), which is + // the cert pollingServer presents when it dials in to establish the polling connection. + runtime.Trust(Certificates.TentacleListeningPublicThumbprint); //setup polling + // The remote thumbprint here is this runtime's own certificate (TentaclePolling), + // which pollingServer verifies when it connects to port 8433. var serverEndpoint = new ServiceEndPoint(new Uri("https://localhost:8433"), Certificates.TentaclePollingPublicThumbprint, runtime.TimeoutsAndLimits) { TcpClientConnectTimeout = TimeSpan.FromSeconds(5) diff --git a/source/Halibut.Tests/BadCertificatesTests.cs b/source/Halibut.Tests/BadCertificatesTests.cs index 00ee8e3aa..4801bf7b0 100644 --- a/source/Halibut.Tests/BadCertificatesTests.cs +++ b/source/Halibut.Tests/BadCertificatesTests.cs @@ -30,6 +30,7 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI var clientTrustProvider = new DefaultTrustProvider(); var unauthorizedThumbprint = ""; var firstCall = true; + var serviceThumbprint = ""; var unauthorizedClientHasConnected = new TaskCompletionSource(); CancellationToken.Register(() => unauthorizedClientHasConnected.TrySetCanceled()); // backup to fail the test in case it never connects @@ -43,7 +44,7 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI { if (firstCall) { - clientTrustProvider.IsTrusted(CertAndThumbprint.TentaclePolling.Thumbprint).Should().BeFalse(); + clientTrustProvider.IsTrusted(serviceThumbprint).Should().BeFalse(); firstCall = false; } @@ -53,6 +54,8 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI }) .Build(CancellationToken)) { + serviceThumbprint = clientAndBuilder.ServiceThumbprint; + // Act var clientCountingService = clientAndBuilder.CreateAsyncClient(); await clientCountingService.IncrementAsync(); @@ -62,8 +65,8 @@ public async Task SucceedsWhenPollingServicePresentsWrongCertificate_ButServiceI // Assert countingService.CurrentValue().Should().Be(1); - clientTrustProvider.IsTrusted(CertAndThumbprint.TentaclePolling.Thumbprint).Should().BeTrue(); - unauthorizedThumbprint.Should().Be(CertAndThumbprint.TentaclePolling.Thumbprint); + clientTrustProvider.IsTrusted(serviceThumbprint).Should().BeTrue(); + unauthorizedThumbprint.Should().Be(serviceThumbprint); } } @@ -93,6 +96,8 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon }) .Build(CancellationToken)) { + var serviceThumbprint = clientAndBuilder.ServiceThumbprint; + using var cts = new CancellationTokenSource(); var clientCountingService = clientAndBuilder.CreateAsyncClient(point => { @@ -105,7 +110,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon // Interestingly the message exchange error is logged to a non polling looking URL, perhaps because it has not been identified? Wait.UntilActionSucceeds(() => { AllLogs(serviceLoggers).Select(l => l.FormattedMessage).ToArray() - .Should().Contain(s => s.Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint '4098EC3A2FC2B92B97339D3831BA230CC1DD590F' which is not in the list of thumbprints that we trust")); + .Should().Contain(s => s.Contains($"and attempted a message exchange, but it presented a client certificate with the thumbprint '{serviceThumbprint}' which is not in the list of thumbprints that we trust")); }, TimeSpan.FromSeconds(10), Logger, @@ -124,7 +129,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate_ButServiceIsCon // Assert countingService.CurrentValue().Should().Be(0, "With a bad certificate the request never should have been made"); - unauthorizedThumbprint.Should().Be(CertAndThumbprint.TentaclePolling.Thumbprint); + unauthorizedThumbprint.Should().Be(serviceThumbprint); } } @@ -195,8 +200,8 @@ public async Task FailWhenClientPresentsWrongCertificateToListeningService(Clien serviceLoggers[serviceLoggers.Keys.First(x => x != nameof(MessageSerializer))].GetLogs().Should() .Contain(log => log.FormattedMessage - .Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint " + - "'76225C0717A16C1D0BA4A7FFA76519D286D8A248' which is not in the list of thumbprints that we trust")); + .Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint") + && log.FormattedMessage.Contains("which is not in the list of thumbprints that we trust")); } } @@ -254,11 +259,13 @@ public async Task FailWhenListeningServicePresentsWrongCertificate(ClientAndServ .WithCountingService(countingService) .Build(CancellationToken)) { + var serviceThumbprint = clientAndBuilder.ServiceThumbprint; + var clientCountingService = clientAndBuilder.CreateAsyncClient(); (await AssertionExtensions.Should(() => clientCountingService.IncrementAsync()).ThrowAsync()) .And.Message.Should().Contain("" + "We expected the server to present a certificate with the thumbprint 'EC32122053C6BFF582F8246F5697633D06F0F97F'. " + - "Instead, it presented a certificate with a thumbprint of '36F35047CE8B000CF4C671819A2DD1AFCDE3403D'"); + $"Instead, it presented a certificate with a thumbprint of '{serviceThumbprint}'"); countingService.CurrentValue().Should().Be(0, "With a bad certificate the request never should have been made"); } } @@ -275,6 +282,8 @@ public async Task FailWhenPollingServicePresentsWrongCertificate(ClientAndServic .RecordingClientLogs(out var serviceLoggers) .Build(CancellationToken)) { + var serviceThumbprint = clientAndBuilder.ServiceThumbprint; + using var cts = new CancellationTokenSource(); var clientCountingService = clientAndBuilder.CreateAsyncClient(point => { @@ -285,7 +294,7 @@ public async Task FailWhenPollingServicePresentsWrongCertificate(ClientAndServic // Interestingly the message exchange error is logged to a non polling looking URL, perhaps because it has not been identified? Wait.UntilActionSucceeds(() => { AllLogs(serviceLoggers).Select(l => l.FormattedMessage).ToArray() - .Should().Contain(s => s.Contains("and attempted a message exchange, but it presented a client certificate with the thumbprint '4098EC3A2FC2B92B97339D3831BA230CC1DD590F' which is not in the list of thumbprints that we trust")); }, + .Should().Contain(s => s.Contains($"and attempted a message exchange, but it presented a client certificate with the thumbprint '{serviceThumbprint}' which is not in the list of thumbprints that we trust")); }, TimeSpan.FromSeconds(10), Logger, CancellationToken); diff --git a/source/Halibut.Tests/ClientServerLifecycleTests.cs b/source/Halibut.Tests/ClientServerLifecycleTests.cs index 730e4bf98..bfd03141b 100644 --- a/source/Halibut.Tests/ClientServerLifecycleTests.cs +++ b/source/Halibut.Tests/ClientServerLifecycleTests.cs @@ -25,13 +25,33 @@ namespace Halibut.Tests { public class ClientServerLifecycleTests : BaseTest { + DisposableCollection disposables = null!; + ICertAndThumbprint serverCert = null!; + ICertAndThumbprint listenerCert = null!; + ICertAndThumbprint pollerCert = null!; + + [SetUp] + public void SetUpCerts() + { + disposables = new DisposableCollection(); + serverCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + listenerCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, disposedBy: disposables); + pollerCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, disposedBy: disposables); + } + + [TearDown] + public void TearDownCerts() + { + disposables?.Dispose(); + } + [Test] public async Task ListeningConfiguration() { await using var server = RunServer(out var serverPort); await using var runtime = CreateRuntimeForListener(); - var client = CreateClient(runtime, serverPort); + var client = CreateClient(runtime, serverPort, serverCert); var result = await client.AddAsync(2, 2); result.Should().Be(4); } @@ -56,7 +76,7 @@ public async Task ListeningThenPollingConfiguration() HalibutRuntime CreateRuntimeForListener() { var runtime = new HalibutRuntimeBuilder() - .WithServerCertificate(Certificates.TentacleListening) + .WithServerCertificate(listenerCert.Certificate2) .WithLogFactory(new TestLogFactory(HalibutLog)) .Build(); return runtime; @@ -65,15 +85,15 @@ HalibutRuntime CreateRuntimeForListener() HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncClientCalculatorService client) { var runtime = new HalibutRuntimeBuilder() - .WithServerCertificate(Certificates.TentaclePolling) + .WithServerCertificate(pollerCert.Certificate2) .WithLogFactory(new TestLogFactory(HalibutLog)) .Build(); var port = runtime.Listen(); - runtime.Trust(Certificates.OctopusPublicThumbprint); + runtime.Trust(serverCert.Thumbprint); var pollEndpoint = new ServiceEndPoint( baseUri: new Uri($"https://localhost:{port}/"), - remoteThumbprint: Certificates.TentaclePollingPublicThumbprint, + remoteThumbprint: pollerCert.Thumbprint, halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits ) { @@ -83,7 +103,7 @@ HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncCl serverRuntime.Poll(pollingUri, pollEndpoint, CancellationToken); var clientEndpoint = new ServiceEndPoint( baseUri: pollingUri, - remoteThumbprint: Certificates.OctopusPublicThumbprint, + remoteThumbprint: serverCert.Thumbprint, halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits ); client = runtime.CreateAsyncClient(clientEndpoint); @@ -91,11 +111,11 @@ HalibutRuntime CreateRuntimeForPoller(HalibutRuntime serverRuntime, out IAsyncCl return runtime; } - static IAsyncClientCalculatorService CreateClient(HalibutRuntime runtime, int port) + static IAsyncClientCalculatorService CreateClient(HalibutRuntime runtime, int port, ICertAndThumbprint serverCertAndThumbprint) { var endpoint = new ServiceEndPoint( baseUri: $"https://localhost:{port}", - remoteThumbprint: Certificates.OctopusPublicThumbprint, + remoteThumbprint: serverCertAndThumbprint.Thumbprint, halibutTimeoutsAndLimits: runtime.TimeoutsAndLimits ); var client = runtime @@ -115,13 +135,13 @@ HalibutRuntime RunServer(out int port) var services = CreateServiceFactory(); var runtime = new HalibutRuntimeBuilder() - .WithServerCertificate(Certificates.Octopus) + .WithServerCertificate(serverCert.Certificate2) .WithServiceFactory(services) .WithLogFactory(new TestLogFactory(HalibutLog)) .Build(); - runtime.Trust(Certificates.TentacleListeningPublicThumbprint); - runtime.Trust(Certificates.TentaclePollingPublicThumbprint); + runtime.Trust(listenerCert.Thumbprint); + runtime.Trust(pollerCert.Thumbprint); port = runtime.Listen(); return runtime; diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs index 3860e7173..b7a0675ce 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryPath.cs @@ -9,15 +9,25 @@ public class HalibutTestBinaryPath public string BinPath(string version) { var onDiskVersion = version.Replace(".", "_"); + var projectName = $"Halibut.TestUtils.CompatBinary.v{onDiskVersion}"; + return ResolveProjectBinPath(projectName); + } + + public string SchannelProbeBinPath() + { + return ResolveProjectBinPath("Halibut.TestUtils.CompatBinary.SchannelProbe"); + } + + string ResolveProjectBinPath(string projectName) + { var assemblyDir = new DirectoryInfo(Path.GetDirectoryName(typeof(HalibutTestBinaryRunner).Assembly.Location)!); var upAt = assemblyDir.Parent!.Parent!.Parent!.Parent!; - var projectName = $"Halibut.TestUtils.CompatBinary.v{onDiskVersion}"; var executable = Path.Combine(upAt.FullName, projectName, assemblyDir.Parent.Parent.Name, assemblyDir.Parent.Name, assemblyDir.Name, projectName); executable = AddExeForWindows(executable); if (!File.Exists(executable)) { - throw new Exception("Could not executable at path:\n" + + throw new Exception("Could not find executable at path:\n" + executable + "\n" + $"Did you forget to update the csproj to depend on {projectName}\n" + "If testing a previously untested version of Halibut a new project may be required."); @@ -25,7 +35,7 @@ public string BinPath(string version) return executable; } - + string AddExeForWindows(string path) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return path + ".exe"; diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryRunner.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryRunner.cs index fb79b9616..35cdaa459 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryRunner.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/HalibutTestBinaryRunner.cs @@ -17,8 +17,8 @@ public class HalibutTestBinaryRunner // The port the binary should poll. readonly ServiceConnectionType serviceConnectionType; readonly int? clientServicePort; - readonly CertAndThumbprint clientCertAndThumbprint; - readonly CertAndThumbprint serviceCertAndThumbprint; + readonly ICertAndThumbprint clientCertAndThumbprint; + readonly ICertAndThumbprint serviceCertAndThumbprint; readonly string? version; readonly ProxyDetails? proxyDetails; readonly LogLevel halibutLogLevel; @@ -28,8 +28,8 @@ public class HalibutTestBinaryRunner public HalibutTestBinaryRunner( ServiceConnectionType serviceConnectionType, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, string? version, ProxyDetails? proxyDetails, LogLevel halibutLogLevel, @@ -48,8 +48,8 @@ public HalibutTestBinaryRunner( public HalibutTestBinaryRunner( ServiceConnectionType serviceConnectionType, int? clientServicePort, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, string? version, ProxyDetails proxyDetails, LogLevel halibutLoggingLevel, @@ -63,8 +63,8 @@ public HalibutTestBinaryRunner( public HalibutTestBinaryRunner( ServiceConnectionType serviceConnectionType, Uri webSocketServiceEndpointUri, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, string? version, ProxyDetails? proxyDetails, LogLevel halibutLoggingLevel, diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs index 563ec4133..f63f79d3c 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs @@ -16,8 +16,9 @@ namespace Halibut.Tests.Support.BackwardsCompatibility public class LatestClientAndPreviousServiceVersionBuilder : IClientAndServiceBuilder { readonly ServiceConnectionType serviceConnectionType; - CertAndThumbprint serviceCertAndThumbprint; - CertAndThumbprint clientCertAndThumbprint = CertAndThumbprint.Octopus; + ICertAndThumbprint serviceCertAndThumbprint; + ICertAndThumbprint clientCertAndThumbprint; + DisposableCollection disposables; Version? version; Func? portForwarderFactory; Reference? portForwarderReference; @@ -27,15 +28,21 @@ public class LatestClientAndPreviousServiceVersionBuilder : IClientAndServiceBui ConcurrentDictionary? clientInMemoryLoggers; readonly OldServiceAvailableServices availableServices = new(false, false); - LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint) + LatestClientAndPreviousServiceVersionBuilder( + ServiceConnectionType serviceConnectionType, + ICertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + DisposableCollection disposables) { this.serviceConnectionType = serviceConnectionType; this.serviceCertAndThumbprint = serviceCertAndThumbprint; + this.clientCertAndThumbprint = clientCertAndThumbprint; + this.disposables = disposables; } public LatestClientAndPreviousServiceVersionBuilder WithCertificates( - CertAndThumbprint serviceCertAndThumbprint, - CertAndThumbprint clientCertAndThumbprint) + ICertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint) { this.serviceCertAndThumbprint = serviceCertAndThumbprint; this.clientCertAndThumbprint = clientCertAndThumbprint; @@ -44,17 +51,23 @@ public LatestClientAndPreviousServiceVersionBuilder WithCertificates( public static LatestClientAndPreviousServiceVersionBuilder WithPollingService() { - return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling, clientCert, disposables); } public static LatestClientAndPreviousServiceVersionBuilder WithPollingOverWebSocketService() { - return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling, clientCert, disposables); } public static LatestClientAndPreviousServiceVersionBuilder WithListeningService() { - return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening, clientCert, disposables); } public static LatestClientAndPreviousServiceVersionBuilder ForServiceConnectionType(ServiceConnectionType connectionType) @@ -186,6 +199,7 @@ public async Task Build(CancellationToken cancellationToken) HalibutTestBinaryRunner.RunningOldHalibutBinary? runningOldHalibutBinary = null; var disposableCollection = new DisposableCollection(); + disposableCollection.Add(disposables); PortForwarder? portForwarder = null; var proxy = proxyFactory?.Build(); @@ -294,7 +308,7 @@ public class ClientAndService : IClientAndService { readonly HalibutTestBinaryRunner.RunningOldHalibutBinary? runningOldHalibutBinary; readonly Uri serviceUri; - readonly CertAndThumbprint serviceCertAndThumbprint; // for creating a client + readonly ICertAndThumbprint serviceCertAndThumbprint; // for creating a client readonly DisposableCollection disposableCollection; readonly ProxyDetails? proxyDetails; readonly CancellationTokenSource cancellationTokenSource; @@ -305,7 +319,7 @@ public ClientAndService( HalibutRuntime client, HalibutTestBinaryRunner.RunningOldHalibutBinary? runningOldHalibutBinary, Uri serviceUri, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, PortForwarder? portForwarder, DisposableCollection disposableCollection, HttpProxyService? httpProxy, diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs index ab03a3a01..1db2732b2 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/PreviousClientVersionAndLatestServiceBuilder.cs @@ -27,8 +27,9 @@ public class PreviousClientVersionAndLatestServiceBuilder: IClientAndServiceBuil readonly ServiceConnectionType serviceConnectionType; readonly ServiceFactoryBuilder serviceFactoryBuilder = new(); - readonly CertAndThumbprint serviceCertAndThumbprint; - readonly CertAndThumbprint clientCertAndThumbprint = CertAndThumbprint.Octopus; + readonly ICertAndThumbprint serviceCertAndThumbprint; + readonly ICertAndThumbprint clientCertAndThumbprint; + DisposableCollection disposables; Version? version; ProxyFactory? proxyFactory; Reference? proxyServiceReference; @@ -36,25 +37,37 @@ public class PreviousClientVersionAndLatestServiceBuilder: IClientAndServiceBuil Reference? portForwarderReference; LogLevel halibutLogLevel = LogLevel.Trace; - PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint) + PreviousClientVersionAndLatestServiceBuilder( + ServiceConnectionType serviceConnectionType, + ICertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + DisposableCollection disposables) { this.serviceConnectionType = serviceConnectionType; this.serviceCertAndThumbprint = serviceCertAndThumbprint; + this.clientCertAndThumbprint = clientCertAndThumbprint; + this.disposables = disposables; } public static PreviousClientVersionAndLatestServiceBuilder WithPollingService() { - return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.TentaclePolling, clientCert, disposables); } public static PreviousClientVersionAndLatestServiceBuilder WithPollingOverWebSocketsService() { - return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.TentaclePolling, clientCert, disposables); } public static PreviousClientVersionAndLatestServiceBuilder WithListeningService() { - return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new PreviousClientVersionAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.TentacleListening, clientCert, disposables); } public static PreviousClientVersionAndLatestServiceBuilder ForServiceConnectionType(ServiceConnectionType connectionType) @@ -202,6 +215,7 @@ public async Task Build(CancellationToken cancellationToken) PortForwarder? portForwarder = null; var disposableCollection = new DisposableCollection(); + disposableCollection.Add(disposables); ProxyHalibutTestBinaryRunner.RoundTripRunningOldHalibutBinary runningOldHalibutBinary; Uri serviceUri; var httpProxy = proxyFactory?.Build(); @@ -319,7 +333,7 @@ public class ClientAndService : IClientAndService { readonly ProxyHalibutTestBinaryRunner.RoundTripRunningOldHalibutBinary runningOldHalibutBinary; readonly Uri serviceUri; - readonly CertAndThumbprint serviceCertAndThumbprint; // for creating a client + readonly ICertAndThumbprint serviceCertAndThumbprint; // for creating a client readonly HalibutRuntime service; readonly DisposableCollection disposableCollection; readonly CancellationTokenSource cancellationTokenSource; @@ -331,7 +345,7 @@ public ClientAndService( HalibutRuntime proxyClient, ProxyHalibutTestBinaryRunner.RoundTripRunningOldHalibutBinary runningOldHalibutBinary, Uri serviceUri, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, HalibutRuntime service, DisposableCollection disposableCollection, CancellationTokenSource cancellationTokenSource, diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/ProxyHalibutTestBinaryRunner.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/ProxyHalibutTestBinaryRunner.cs index 66cc9b340..0fd3a98a2 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/ProxyHalibutTestBinaryRunner.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/ProxyHalibutTestBinaryRunner.cs @@ -15,8 +15,8 @@ public class ProxyHalibutTestBinaryRunner { readonly ServiceConnectionType serviceConnectionType; readonly int? proxyClientListeningPort; - readonly CertAndThumbprint clientCertAndThumbprint; - readonly CertAndThumbprint serviceCertAndThumbprint; + readonly ICertAndThumbprint clientCertAndThumbprint; + readonly ICertAndThumbprint serviceCertAndThumbprint; readonly string? version; readonly ProxyDetails? proxyDetails; readonly string? webSocketPath; @@ -27,8 +27,8 @@ public class ProxyHalibutTestBinaryRunner public ProxyHalibutTestBinaryRunner( ServiceConnectionType serviceConnectionType, int? proxyClientListeningPort, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, Uri? realServiceListenAddress, string? version, ProxyDetails? proxyDetails, diff --git a/source/Halibut.Tests/Support/CertAndThumbprint.cs b/source/Halibut.Tests/Support/CertAndThumbprint.cs index 0eb72da5e..e5f5c1c47 100644 --- a/source/Halibut.Tests/Support/CertAndThumbprint.cs +++ b/source/Halibut.Tests/Support/CertAndThumbprint.cs @@ -3,7 +3,8 @@ namespace Halibut.Tests.Support { - public class CertAndThumbprint + public sealed class CertAndThumbprint + : ICertAndThumbprint { /// /// CN=Halibut Alice @@ -36,7 +37,7 @@ public class CertAndThumbprint /// public static CertAndThumbprint Ssl = new(Certificates.sslPfxPath, Certificates.Ssl); - public CertAndThumbprint(string certificatePfxPath, X509Certificate2 certificate2) + CertAndThumbprint(string certificatePfxPath, X509Certificate2 certificate2) { Certificate2 = certificate2; CertificatePfxPath = certificatePfxPath; diff --git a/source/Halibut/Transport/DefaultSslConfigurationProvider.cs b/source/Halibut.Tests/Support/ICertAndThumbprint.cs similarity index 64% rename from source/Halibut/Transport/DefaultSslConfigurationProvider.cs rename to source/Halibut.Tests/Support/ICertAndThumbprint.cs index ad354eddc..0fa9b9d53 100644 --- a/source/Halibut/Transport/DefaultSslConfigurationProvider.cs +++ b/source/Halibut.Tests/Support/ICertAndThumbprint.cs @@ -13,15 +13,14 @@ // limitations under the License. using System; -using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; -namespace Halibut.Transport +namespace Halibut.Tests.Support { - /// - /// Provides a default implementation of ISslConfigurationProvider that uses the system defaults. - /// - public class DefaultSslConfigurationProvider : ISslConfigurationProvider + public interface ICertAndThumbprint { - public SslProtocols SupportedProtocols => SslProtocols.None; + X509Certificate2 Certificate2 { get; } + string CertificatePfxPath { get; } + string Thumbprint { get; } } } \ No newline at end of file diff --git a/source/Halibut.Tests/Support/IClient.cs b/source/Halibut.Tests/Support/IClient.cs index c0c1b3824..c11ee8061 100644 --- a/source/Halibut.Tests/Support/IClient.cs +++ b/source/Halibut.Tests/Support/IClient.cs @@ -6,6 +6,7 @@ public interface IClient : IAsyncDisposable { HalibutRuntime Client { get; } Uri? ListeningUri { get; } + string ClientThumbprint { get; } TAsyncClientService CreateClient(Uri serviceEndPoint); TAsyncClientService CreateClientWithoutService(); TAsyncClientService CreateClientWithoutService(Action modifyServiceEndpoint); diff --git a/source/Halibut/Transport/ISslConfigurationProvider.cs b/source/Halibut.Tests/Support/IDisposableCertAndThumbprint.cs similarity index 78% rename from source/Halibut/Transport/ISslConfigurationProvider.cs rename to source/Halibut.Tests/Support/IDisposableCertAndThumbprint.cs index 952b45e51..e8bd25573 100644 --- a/source/Halibut/Transport/ISslConfigurationProvider.cs +++ b/source/Halibut.Tests/Support/IDisposableCertAndThumbprint.cs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Security.Authentication; +using System; -namespace Halibut.Transport +namespace Halibut.Tests.Support { - public interface ISslConfigurationProvider + public interface IDisposableCertAndThumbprint : ICertAndThumbprint, IDisposable { - public SslProtocols SupportedProtocols { get; } } } \ No newline at end of file diff --git a/source/Halibut.Tests/Support/LatestClient.cs b/source/Halibut.Tests/Support/LatestClient.cs index d632e10f2..05994e170 100644 --- a/source/Halibut.Tests/Support/LatestClient.cs +++ b/source/Halibut.Tests/Support/LatestClient.cs @@ -20,6 +20,7 @@ public LatestClient( HalibutRuntime client, Uri? listeningUri, string thumbprint, + string clientThumbprint, PortForwarder? portForwarder, ProxyDetails? proxyDetails, ServiceConnectionType serviceConnectionType, @@ -28,6 +29,7 @@ public LatestClient( Client = client; ListeningUri = listeningUri; this.thumbprint = thumbprint; + ClientThumbprint = clientThumbprint; this.portForwarder = portForwarder; this.proxyDetails = proxyDetails; this.serviceConnectionType = serviceConnectionType; @@ -37,6 +39,9 @@ public LatestClient( public HalibutRuntime Client { get; } public Uri? ListeningUri { get; } + /// The thumbprint of this client's own certificate (what a polling service must trust). + public string ClientThumbprint { get; } + public TAsyncClientService CreateClient(Uri serviceUri) { var serviceEndPoint = GetServiceEndPoint(serviceUri); diff --git a/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs b/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs index b9f043b71..670811758 100644 --- a/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs +++ b/source/Halibut.Tests/Support/LatestClientAndLatestServiceBuilder.cs @@ -30,6 +30,7 @@ public class LatestClientAndLatestServiceBuilder : IClientAndServiceBuilder readonly LatestClientBuilder clientBuilder; readonly LatestServiceBuilder serviceBuilder; + DisposableCollection disposables; ProxyFactory? proxyFactory; Reference? proxyServiceReference; @@ -38,31 +39,44 @@ public class LatestClientAndLatestServiceBuilder : IClientAndServiceBuilder Reference? servicePortForwarderReference; Reference? portForwarderReference; - public LatestClientAndLatestServiceBuilder( + LatestClientAndLatestServiceBuilder( ServiceConnectionType serviceConnectionType, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, - PollingQueueTestCase? pollingQueueTestCase) + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, + PollingQueueTestCase? pollingQueueTestCase, + DisposableCollection disposables) { ServiceConnectionType = serviceConnectionType; + this.disposables = disposables; clientBuilder = new LatestClientBuilder(serviceConnectionType, clientCertAndThumbprint, serviceCertAndThumbprint, pollingQueueTestCase); serviceBuilder = new LatestServiceBuilder(serviceConnectionType, clientCertAndThumbprint, serviceCertAndThumbprint); } public static LatestClientAndLatestServiceBuilder Polling(PollingQueueTestCase pollingQueueTestCase) { - return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Polling, CertAndThumbprint.Octopus, CertAndThumbprint.TentaclePolling, pollingQueueTestCase); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, disposedBy: disposables); + return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Polling, clientCert, serviceCert, pollingQueueTestCase, disposables); } public static LatestClientAndLatestServiceBuilder PollingOverWebSocket(PollingQueueTestCase pollingQueueTestCase) { - return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.Ssl, CertAndThumbprint.TentaclePolling, pollingQueueTestCase); + // For WebSocket, the client cert must be CertAndThumbprint.Ssl because it is bound to the port + // via netsh http add sslcert and must match the cert registered in the Windows local machine cert store. + var disposables = new DisposableCollection(); + var clientCert = CertAndThumbprint.Ssl; + var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentaclePolling, disposedBy: disposables); + return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.PollingOverWebSocket, clientCert, serviceCert, pollingQueueTestCase, disposables); } public static LatestClientAndLatestServiceBuilder Listening() { - return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Listening, CertAndThumbprint.Octopus, CertAndThumbprint.TentacleListening, null); + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + var serviceCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, disposedBy: disposables); + return new LatestClientAndLatestServiceBuilder(ServiceConnectionType.Listening, clientCert, serviceCert, null, disposables); } public static LatestClientAndLatestServiceBuilder ForServiceConnectionType(ServiceConnectionType serviceConnectionType, PollingQueueTestCase? pollingQueueTestCase = null) @@ -81,8 +95,8 @@ public static LatestClientAndLatestServiceBuilder ForServiceConnectionType(Servi } public LatestClientAndLatestServiceBuilder WithCertificates( - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint) + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint) { clientBuilder.WithCertificate(clientCertAndThumbprint); clientBuilder.WithTrustedThumbprint(serviceCertAndThumbprint.Thumbprint); @@ -366,7 +380,7 @@ public async Task Build(CancellationToken cancellationToken) portForwarderReference.Value = portForwarder; } } - return new ClientAndService(client, service, httpProxy); + return new ClientAndService(client, service, httpProxy, disposables, serviceBuilder.ServiceCertAndThumbprint.Thumbprint); } public class ClientAndService : IClientAndService @@ -374,14 +388,19 @@ public class ClientAndService : IClientAndService readonly LatestClient client; readonly LatestService service; readonly HttpProxyService? httpProxy; + readonly DisposableCollection disposables; public ClientAndService( LatestClient client, LatestService service, - HttpProxyService? proxy) + HttpProxyService? proxy, + DisposableCollection disposables, + string serviceThumbprint) { this.client = client; this.service = service; + this.disposables = disposables; + ServiceThumbprint = serviceThumbprint; httpProxy = proxy; } @@ -390,6 +409,14 @@ public ClientAndService( public HalibutRuntime Client => client.Client; public HalibutRuntime Service => service.Service; + /// + /// The actual thumbprint of the certificate the service is presenting. + /// Use this instead of .RemoteThumbprint when verifying + /// the service cert, as RemoteThumbprint reflects what the client is configured to trust + /// (which may differ, e.g. in bad-certificate tests). + /// + public string ServiceThumbprint { get; } + public ServiceEndPoint GetServiceEndPoint() { return client.GetServiceEndPoint(ServiceUri); @@ -419,6 +446,7 @@ public async ValueTask DisposeAsync() void LogError(Exception e) => logger.Warning(e, "Ignoring error in dispose"); Try.CatchingError(() => httpProxy?.Dispose(), LogError); + Try.CatchingError(() => disposables.Dispose(), LogError); } } } diff --git a/source/Halibut.Tests/Support/LatestClientBuilder.cs b/source/Halibut.Tests/Support/LatestClientBuilder.cs index 1de31111f..1f3fc3b3c 100644 --- a/source/Halibut.Tests/Support/LatestClientBuilder.cs +++ b/source/Halibut.Tests/Support/LatestClientBuilder.cs @@ -21,9 +21,11 @@ public class LatestClientBuilder : IClientBuilder { readonly ServiceConnectionType serviceConnectionType; - CertAndThumbprint clientCertAndThumbprint; + ICertAndThumbprint clientCertAndThumbprint; readonly PollingQueueTestCase? pollingQueueTestCase; + DisposableCollection disposables; + string clientTrustsThumbprint; bool clientTrustsNoThumbprints; IRpcObserver? clientRpcObserver; @@ -43,13 +45,29 @@ public class LatestClientBuilder : IClientBuilder public LatestClientBuilder( ServiceConnectionType serviceConnectionType, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint, - PollingQueueTestCase? pollingQueueTestCase) + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, + PollingQueueTestCase? pollingQueueTestCase + ) : this( + serviceConnectionType, + clientCertAndThumbprint, + serviceCertAndThumbprint, + pollingQueueTestCase, + new DisposableCollection()) + { + } + + LatestClientBuilder( + ServiceConnectionType serviceConnectionType, + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint, + PollingQueueTestCase? pollingQueueTestCase, + DisposableCollection disposables) { this.serviceConnectionType = serviceConnectionType; this.clientCertAndThumbprint = clientCertAndThumbprint; this.pollingQueueTestCase = pollingQueueTestCase; + this.disposables = disposables; clientTrustsThumbprint = serviceCertAndThumbprint.Thumbprint; if (serviceConnectionType is ServiceConnectionType.Polling or ServiceConnectionType.PollingOverWebSocket) { @@ -65,19 +83,29 @@ public static LatestClientBuilder ForServiceConnectionType(ServiceConnectionType switch (serviceConnectionType) { case ServiceConnectionType.Polling: - return new LatestClientBuilder(ServiceConnectionType.Polling, CertAndThumbprint.Octopus, CertAndThumbprint.TentaclePolling, pollingQueueTestCase); + { + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new LatestClientBuilder(ServiceConnectionType.Polling, clientCert, CertAndThumbprint.TentaclePolling, pollingQueueTestCase, disposables); + } case ServiceConnectionType.Listening: - return new LatestClientBuilder(ServiceConnectionType.Listening, CertAndThumbprint.Octopus, CertAndThumbprint.TentacleListening, pollingQueueTestCase); + { + var disposables = new DisposableCollection(); + var clientCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + return new LatestClientBuilder(ServiceConnectionType.Listening, clientCert, CertAndThumbprint.TentacleListening, pollingQueueTestCase); + } case ServiceConnectionType.PollingOverWebSocket: + // For WebSocket, the client cert must be CertAndThumbprint.Ssl because it is bound to the port + // via netsh http add sslcert and must match the cert registered in the Windows local machine cert store. return new LatestClientBuilder(ServiceConnectionType.PollingOverWebSocket, CertAndThumbprint.Ssl, CertAndThumbprint.TentaclePolling, pollingQueueTestCase); default: throw new ArgumentOutOfRangeException(nameof(serviceConnectionType), serviceConnectionType, null); } } - public LatestClientBuilder WithCertificate(CertAndThumbprint clientCertAndThumprint) + public LatestClientBuilder WithCertificate(ICertAndThumbprint clientCertAndThumbprint) { - this.clientCertAndThumbprint = clientCertAndThumprint; + this.clientCertAndThumbprint = clientCertAndThumbprint; return this; } @@ -228,6 +256,7 @@ public async Task Build(CancellationToken cancellationToken) } var disposableCollection = new DisposableCollection(); + PortForwarder? portForwarder = null; Uri? clientListeningUri = null; @@ -267,7 +296,7 @@ public async Task Build(CancellationToken cancellationToken) portForwarderReference.Value = portForwarder; } - return new LatestClient(client, clientListeningUri, clientTrustsThumbprint, portForwarder, proxyDetails, serviceConnectionType, disposableCollection); + return new LatestClient(client, clientListeningUri, clientTrustsThumbprint, clientCertAndThumbprint.Thumbprint, portForwarder, proxyDetails, serviceConnectionType, disposableCollection); } IPendingRequestQueueFactory CreatePendingRequestQueueFactory(QueueMessageSerializer queueMessageSerializer, ILogFactory octopusLogFactory) diff --git a/source/Halibut.Tests/Support/LatestServiceBuilder.cs b/source/Halibut.Tests/Support/LatestServiceBuilder.cs index 19caa44b5..343167294 100644 --- a/source/Halibut.Tests/Support/LatestServiceBuilder.cs +++ b/source/Halibut.Tests/Support/LatestServiceBuilder.cs @@ -26,12 +26,12 @@ public class LatestServiceBuilder : IServiceBuilder readonly ServiceConnectionType serviceConnectionType; readonly ServiceFactoryBuilder serviceFactoryBuilder = new(); - CertAndThumbprint serviceCertAndThumbprint; + ICertAndThumbprint serviceCertAndThumbprint; IServiceFactory? serviceFactory; string serviceTrustsThumbprint; - readonly List listeningClientUris = new(); + readonly List<(Uri ListeningUri, string? Thumbprint)> listeningClients = new(); Func? portForwarderFactory; Reference? portForwarderReference; Func? pollingReconnectRetryPolicy; @@ -46,8 +46,8 @@ public class LatestServiceBuilder : IServiceBuilder public LatestServiceBuilder( ServiceConnectionType serviceConnectionType, - CertAndThumbprint clientCertAndThumbprint, - CertAndThumbprint serviceCertAndThumbprint) + ICertAndThumbprint clientCertAndThumbprint, + ICertAndThumbprint serviceCertAndThumbprint) { this.serviceConnectionType = serviceConnectionType; this.serviceCertAndThumbprint = serviceCertAndThumbprint; @@ -69,21 +69,28 @@ public static LatestServiceBuilder ForServiceConnectionType(ServiceConnectionTyp } } - public LatestServiceBuilder WithListeningClient(Uri listeningClient) + public LatestServiceBuilder WithListeningClient(Uri listeningClientUri) { - listeningClientUris.Add(listeningClient); + // No explicit thumbprint: the service dials this client using its configured + // serviceTrustsThumbprint (which the wrong-certificate test helpers manipulate). + listeningClients.Add((listeningClientUri, null)); return this; } - public LatestServiceBuilder WithListeningClients(IEnumerable listeningClientUris) + public LatestServiceBuilder WithListeningClients(IEnumerable<(Uri ListeningUri, string Thumbprint)> listeningClients) { - this.listeningClientUris.AddRange(listeningClientUris); + foreach (var listeningClient in listeningClients) + { + this.listeningClients.Add((listeningClient.ListeningUri, listeningClient.Thumbprint)); + } return this; } - public LatestServiceBuilder WithCertificate(CertAndThumbprint serviceCertAndThumbprint) + public ICertAndThumbprint ServiceCertAndThumbprint => serviceCertAndThumbprint; + + public LatestServiceBuilder WithCertificate(ICertAndThumbprint serviceCertAndThumbprint) { this.serviceCertAndThumbprint = serviceCertAndThumbprint; return this; @@ -220,13 +227,13 @@ public async Task Build(CancellationToken cancellationToken) { serviceUri = PollingTentacleServiceUri; - foreach (var listeningClientUri in listeningClientUris) + foreach (var (listeningClientUri, clientThumbprint) in listeningClients) { for (var i = 0; i < pollingConnectionCount; i++) { service.Poll( serviceUri, - new ServiceEndPoint(listeningClientUri, serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits), + new ServiceEndPoint(listeningClientUri, clientThumbprint ?? serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits), cancellationToken); } } @@ -235,11 +242,11 @@ public async Task Build(CancellationToken cancellationToken) { serviceUri = PollingOverWebSocketTentacleServiceUri; - foreach (var listeningClientUri in listeningClientUris) + foreach (var (listeningClientUri, clientThumbprint) in listeningClients) { service.Poll( serviceUri, - new ServiceEndPoint(listeningClientUri, serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits), + new ServiceEndPoint(listeningClientUri, clientThumbprint ?? serviceTrustsThumbprint, proxyDetails, service.TimeoutsAndLimits), cancellationToken); } } diff --git a/source/Halibut.Tests/Support/TempDisposableCertAndThumbprint.cs b/source/Halibut.Tests/Support/TempDisposableCertAndThumbprint.cs new file mode 100644 index 000000000..2c952fec0 --- /dev/null +++ b/source/Halibut.Tests/Support/TempDisposableCertAndThumbprint.cs @@ -0,0 +1,57 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Halibut.Tests.Support +{ + public sealed class TempDisposableCertAndThumbprint : IDisposableCertAndThumbprint + { + readonly TemporaryDirectory tempDirectory; + TempDisposableCertAndThumbprint(TemporaryDirectory directory, string certificatePfxPath, X509Certificate2 certificate2) + { + tempDirectory = directory; + Certificate2 = certificate2; + CertificatePfxPath = certificatePfxPath; + } + + public X509Certificate2 Certificate2 { get; } + public string CertificatePfxPath { get; } + public string Thumbprint => Certificate2.Thumbprint; + + void IDisposable.Dispose() + { + Certificate2.Dispose(); + tempDirectory.Dispose(); + } + + public static ICertAndThumbprint CreateSelfSigned(DisposableCollection disposedBy) + { + var tempDir = new TemporaryDirectory(); + var name = Guid.NewGuid().ToString(); + using var rsa = RSA.Create(2048); + var request = new CertificateRequest($"CN={name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); + var bytes = certificate.Export(X509ContentType.Pfx); + var filePath = Path.Combine(tempDir.DirectoryPath, $"{name}.pfx"); + File.WriteAllBytes(filePath, bytes); + var disposableCert = new TempDisposableCertAndThumbprint(tempDir, filePath, new X509Certificate2(bytes)); + disposedBy.Add(disposableCert); + return disposableCert; + } + } +} \ No newline at end of file diff --git a/source/Halibut.Tests/Support/TestCertificates.cs b/source/Halibut.Tests/Support/TestCertificates.cs new file mode 100644 index 000000000..696b12db4 --- /dev/null +++ b/source/Halibut.Tests/Support/TestCertificates.cs @@ -0,0 +1,33 @@ +namespace Halibut.Tests.Support +{ + /// + /// Decides which certificate a test builder should use, based on the target framework. + /// + /// On .NET Framework 4.8, causes Windows + /// SChannel to use a per-process TLS session cache keyed on certificate + hostname. Reusing the same static + /// certificates across tests in one process causes incorrect session reuse when connecting to localhost, + /// producing AuthenticationExceptions. To avoid this we generate a fresh, unique certificate per test. + /// + /// On other frameworks the static certificates are safe to reuse and, crucially, sharing them enables + /// SChannel TLS session resumption (fast resumed handshakes). Generating unique certs there would defeat + /// resumption and slow down every handshake, which can break tests that enforce short receive timeouts. + /// + /// All of the #if NETFRAMEWORK logic lives here so call sites can route through a single helper. + /// + public static class TestCertificates + { + /// + /// On .NET Framework, generates a fresh unique self-signed certificate into . + /// On other frameworks, returns the supplied so static certificates are + /// shared (enabling TLS session resumption). + /// + public static ICertAndThumbprint CertFor(CertAndThumbprint staticCert, DisposableCollection disposedBy) + { +#if NETFRAMEWORK + return TempDisposableCertAndThumbprint.CreateSelfSigned(disposedBy); +#else + return staticCert; +#endif + } + } +} diff --git a/source/Halibut.Tests/Support/WebSocketSslCertificateBuilder.cs b/source/Halibut.Tests/Support/WebSocketSslCertificateBuilder.cs index 83b3ce3e9..94b2281dd 100644 --- a/source/Halibut.Tests/Support/WebSocketSslCertificateBuilder.cs +++ b/source/Halibut.Tests/Support/WebSocketSslCertificateBuilder.cs @@ -6,14 +6,14 @@ namespace Halibut.Tests.Support class WebSocketSslCertificateBuilder { readonly string bindingAddress; - CertAndThumbprint certAndThumbprint = CertAndThumbprint.Ssl; + ICertAndThumbprint certAndThumbprint = CertAndThumbprint.Ssl; public WebSocketSslCertificateBuilder(string bindingAddress) { this.bindingAddress = bindingAddress; } - public WebSocketSslCertificateBuilder WithCertificate(CertAndThumbprint certAndThumbprint) + public WebSocketSslCertificateBuilder WithCertificate(ICertAndThumbprint certAndThumbprint) { this.certAndThumbprint = certAndThumbprint; return this; diff --git a/source/Halibut.Tests/Support/WebSocketSslCertificateHelper.cs b/source/Halibut.Tests/Support/WebSocketSslCertificateHelper.cs index f157ea536..467bab845 100644 --- a/source/Halibut.Tests/Support/WebSocketSslCertificateHelper.cs +++ b/source/Halibut.Tests/Support/WebSocketSslCertificateHelper.cs @@ -16,7 +16,7 @@ internal static void AddSslCertToLocalStore() store.Close(); } - internal static void AddSslCertBindingFor(string address, CertAndThumbprint certAndThumbprint) + internal static void AddSslCertBindingFor(string address, ICertAndThumbprint certAndThumbprint) { if (certAndThumbprint.Thumbprint != CertAndThumbprint.Ssl.Thumbprint) { diff --git a/source/Halibut.Tests/TlsFixture.cs b/source/Halibut.Tests/TlsFixture.cs index 3c3053a13..433fc274c 100644 --- a/source/Halibut.Tests/TlsFixture.cs +++ b/source/Halibut.Tests/TlsFixture.cs @@ -8,7 +8,6 @@ using Halibut.Tests.Support.TestAttributes; using Halibut.Tests.Support.TestCases; using Halibut.Tests.TestServices.Async; -using Halibut.Tests.Util; using Halibut.TestUtils.Contracts; using NUnit.Framework; @@ -22,9 +21,9 @@ public async Task LatestClientAndServiceUseBestAvailableSslProtocol(ClientAndSer { // We need to avoid the use of cached SSL sessions to ensure that correct SSL protocol is chosen, so we use // unique certificates for each test. - using var tmpDirectory = new TmpDirectory(); - var clientCertAndThumbprint = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath); - var serviceCertAndThumbprint = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath); + using var disposables = new DisposableCollection(); + var clientCertAndThumbprint = TempDisposableCertAndThumbprint.CreateSelfSigned(disposedBy: disposables); + var serviceCertAndThumbprint = TempDisposableCertAndThumbprint.CreateSelfSigned(disposedBy: disposables); await using var clientAndService = await clientAndServiceTestCase.CreateTestCaseBuilder() .WithStandardServices() @@ -57,9 +56,9 @@ public async Task LatestClientAndPreviousServiceFallBackOnTls12(ClientAndService { // We need to avoid the use of cached SSL sessions to ensure that correct SSL protocol is chosen, so we use // unique certificates for each test. - using var tmpDirectory = new TmpDirectory(); - var clientCertAndThumbprint = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath); - var serviceCertAndThumbprint = CertificateGenerator.GenerateSelfSignedCertificate(tmpDirectory.FullPath); + using var disposables = new DisposableCollection(); + var clientCertAndThumbprint = TempDisposableCertAndThumbprint.CreateSelfSigned(disposedBy: disposables); + var serviceCertAndThumbprint = TempDisposableCertAndThumbprint.CreateSelfSigned(disposedBy: disposables); await using var clientAndService = await clientAndServiceTestCase.CreateTestCaseBuilder() .WithStandardServices() diff --git a/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs b/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs index b395aa909..0bf512b60 100644 --- a/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs +++ b/source/Halibut.Tests/Transport/DiscoveryClientFixture.cs @@ -51,7 +51,7 @@ public async Task OctopusCanDiscoverTentacle(ClientAndServiceTestCase clientAndS { var info = await clientAndService.Client.DiscoverAsync(clientAndService.ServiceUri, CancellationToken); - info.RemoteThumbprint.Should().Be(Certificates.TentacleListeningPublicThumbprint); + info.RemoteThumbprint.Should().Be(clientAndService.GetServiceEndPoint().RemoteThumbprint); } } diff --git a/source/Halibut.Tests/Transport/SecureClientFixture.cs b/source/Halibut.Tests/Transport/SecureClientFixture.cs index d6df4e782..0df6e4398 100644 --- a/source/Halibut.Tests/Transport/SecureClientFixture.cs +++ b/source/Halibut.Tests/Transport/SecureClientFixture.cs @@ -27,21 +27,28 @@ public class SecureClientFixture : IAsyncDisposable ServiceEndPoint endpoint; HalibutRuntime tentacle; ILog log; + DisposableCollection disposables; + ICertAndThumbprint tentacleCert; + ICertAndThumbprint octopusCert; #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. [SetUp] public void SetUp() { + disposables = new DisposableCollection(); + tentacleCert = TestCertificates.CertFor(CertAndThumbprint.TentacleListening, disposedBy: disposables); + octopusCert = TestCertificates.CertFor(CertAndThumbprint.Octopus, disposedBy: disposables); + var services = new DelegateServiceFactory(); services.Register(() => new AsyncEchoService()); tentacle = new HalibutRuntimeBuilder() - .WithServerCertificate(Certificates.TentacleListening) + .WithServerCertificate(tentacleCert.Certificate2) .WithServiceFactory(services) .WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build()) .Build(); var tentaclePort = tentacle.Listen(); - tentacle.Trust(Certificates.OctopusPublicThumbprint); - endpoint = new ServiceEndPoint("https://localhost:" + tentaclePort, Certificates.TentacleListeningPublicThumbprint, tentacle.TimeoutsAndLimits) + tentacle.Trust(octopusCert.Thumbprint); + endpoint = new ServiceEndPoint("https://localhost:" + tentaclePort, tentacleCert.Thumbprint, tentacle.TimeoutsAndLimits) { ConnectionErrorRetryTimeout = TimeSpan.MaxValue }; @@ -51,6 +58,7 @@ public void SetUp() public async ValueTask DisposeAsync() { await tentacle.DisposeAsync(); + disposables?.Dispose(); } [Test] @@ -81,13 +89,12 @@ public async Task SecureClientClearsPoolWhenAllConnectionsCorrupt() }; var tcpConnectionFactory = new TcpConnectionFactory( - Certificates.Octopus, + octopusCert.Certificate2, halibutTimeoutsAndLimits, new StreamFactory(), - NoOpSecureConnectionObserver.Instance, - SslConfiguration.Default + NoOpSecureConnectionObserver.Instance ); - var secureClient = new SecureListeningClient(GetProtocol, endpoint, Certificates.Octopus, log, connectionManager, tcpConnectionFactory); + var secureClient = new SecureListeningClient(GetProtocol, endpoint, octopusCert.Certificate2, log, connectionManager, tcpConnectionFactory); ResponseMessage response = null!; await secureClient.ExecuteTransactionAsync(async (mep, ct) => response = await mep.ExchangeAsClientAsync(request, ct), CancellationToken.None); diff --git a/source/Halibut.Tests/Transport/SecureListenerFixture.cs b/source/Halibut.Tests/Transport/SecureListenerFixture.cs index 314daf5ed..69775b77e 100644 --- a/source/Halibut.Tests/Transport/SecureListenerFixture.cs +++ b/source/Halibut.Tests/Transport/SecureListenerFixture.cs @@ -74,8 +74,7 @@ public async Task SecureListenerDoesNotCreateHundredsOfIoEventsPerSecondOnWindow timeoutsAndLimits, new StreamFactory(), NoOpConnectionsObserver.Instance, - NoOpSecureConnectionObserver.Instance, - SslConfiguration.Default + NoOpSecureConnectionObserver.Instance ); var idleAverage = CollectCounterValues(opsPerSec) diff --git a/source/Halibut.Tests/Util/CertificateGenerator.cs b/source/Halibut.Tests/Util/CertificateGenerator.cs deleted file mode 100644 index 5c56c8596..000000000 --- a/source/Halibut.Tests/Util/CertificateGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Halibut.Tests.Support; - -namespace Halibut.Tests.Util -{ - public static class CertificateGenerator - { - public static CertAndThumbprint GenerateSelfSignedCertificate(string folderPath) - { - var name = Guid.NewGuid().ToString(); - using var rsa = RSA.Create(2048); - var request = new CertificateRequest($"CN={name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); - var bytes = certificate.Export(X509ContentType.Pfx); - var filePath = Path.Combine(folderPath, $"{name}.pfx"); - File.WriteAllBytes(filePath, bytes); - return new CertAndThumbprint(filePath, new X509Certificate2(bytes)); - } - } -} \ No newline at end of file diff --git a/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs b/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs index 86d1dc895..a4c60f2b5 100644 --- a/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs +++ b/source/Halibut.Tests/WhenPollingMultipleClientsWithOneService.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using FluentAssertions; using Halibut.Tests.Support; @@ -23,8 +22,8 @@ public async Task RequestsShouldBeTakenFromAnyClient(ClientAndServiceTestCase cl { var clients = new[] { - clientOnly1.ListeningUri!, - clientOnly2.ListeningUri! + (clientOnly1.ListeningUri!, clientOnly1.ClientThumbprint), + (clientOnly2.ListeningUri!, clientOnly2.ClientThumbprint) }; await using (var service = await clientAndServiceTestCase.CreateServiceOnlyTestCaseBuilder() diff --git a/source/Halibut/HalibutRuntime.cs b/source/Halibut/HalibutRuntime.cs index 7233b29ec..1588239e0 100644 --- a/source/Halibut/HalibutRuntime.cs +++ b/source/Halibut/HalibutRuntime.cs @@ -47,7 +47,6 @@ public class HalibutRuntime : IHalibutRuntime readonly ISecureConnectionObserver secureConnectionObserver; readonly IActiveTcpConnectionsLimiter activeTcpConnectionsLimiter; readonly IControlMessageObserver controlMessageObserver; - readonly ISslConfigurationProvider sslConfigurationProvider; internal HalibutRuntime( IServiceFactory serviceFactory, @@ -63,8 +62,7 @@ internal HalibutRuntime( IRpcObserver rpcObserver, IConnectionsObserver connectionsObserver, IControlMessageObserver controlMessageObserver, - ISecureConnectionObserver secureConnectionObserver, - ISslConfigurationProvider sslConfigurationProvider + ISecureConnectionObserver secureConnectionObserver ) { this.serverCertificate = serverCertificate; @@ -81,10 +79,9 @@ ISslConfigurationProvider sslConfigurationProvider this.connectionsObserver = connectionsObserver; this.secureConnectionObserver = secureConnectionObserver; this.controlMessageObserver = controlMessageObserver; - this.sslConfigurationProvider = sslConfigurationProvider; connectionManager = new ConnectionManagerAsync(); - tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory, secureConnectionObserver, sslConfigurationProvider); + tcpConnectionFactory = new TcpConnectionFactory(serverCertificate, TimeoutsAndLimits, streamFactory, secureConnectionObserver); activeTcpConnectionsLimiter = new ActiveTcpConnectionsLimiter(TimeoutsAndLimits); } @@ -139,8 +136,7 @@ public int Listen(IPEndPoint endpoint) TimeoutsAndLimits, streamFactory, connectionsObserver, - secureConnectionObserver, - sslConfigurationProvider + secureConnectionObserver ); listeners.DoWithExclusiveAccess(l => @@ -256,7 +252,7 @@ public async Task DiscoverAsync(Uri uri, CancellationToken canc public async Task DiscoverAsync(ServiceEndPoint endpoint, CancellationToken cancellationToken) { - var client = new DiscoveryClient(streamFactory, sslConfigurationProvider); + var client = new DiscoveryClient(streamFactory); return await client.DiscoverAsync(endpoint, TimeoutsAndLimits, cancellationToken); } diff --git a/source/Halibut/HalibutRuntimeBuilder.cs b/source/Halibut/HalibutRuntimeBuilder.cs index acd85cc65..6bb3e7313 100644 --- a/source/Halibut/HalibutRuntimeBuilder.cs +++ b/source/Halibut/HalibutRuntimeBuilder.cs @@ -32,7 +32,6 @@ public class HalibutRuntimeBuilder ISecureConnectionObserver? secureConnectionObserver; IControlMessageObserver? controlMessageObserver; MessageStreamWrappers queueMessageStreamWrappers = new(); - ISslConfigurationProvider? sslConfigurationProvider; public HalibutRuntimeBuilder WithQueueMessageStreamWrappers(MessageStreamWrappers queueMessageStreamWrappers) { @@ -52,12 +51,6 @@ public HalibutRuntimeBuilder WithSecureConnectionObserver(ISecureConnectionObser return this; } - public HalibutRuntimeBuilder WithSslConfigurationProvider(ISslConfigurationProvider sslConfigurationProvider) - { - this.sslConfigurationProvider = sslConfigurationProvider; - return this; - } - internal HalibutRuntimeBuilder WithStreamFactory(IStreamFactory streamFactory) { this.streamFactory = streamFactory; @@ -193,7 +186,6 @@ public HalibutRuntime Build() var secureConnectionObserver = this.secureConnectionObserver ?? NoOpSecureConnectionObserver.Instance; var rpcObserver = this.rpcObserver ?? new NoRpcObserver(); var controlMessageObserver = this.controlMessageObserver ?? new NoOpControlMessageObserver(); - var sslConfigurationProvider = this.sslConfigurationProvider ?? SslConfiguration.Default; var halibutRuntime = new HalibutRuntime( serviceFactory, @@ -209,8 +201,7 @@ public HalibutRuntime Build() rpcObserver, connectionsObserver, controlMessageObserver, - secureConnectionObserver, - sslConfigurationProvider + secureConnectionObserver ); if (onUnauthorizedClientConnect is not null) diff --git a/source/Halibut/Transport/DiscoveryClient.cs b/source/Halibut/Transport/DiscoveryClient.cs index a631c32ec..c92280702 100644 --- a/source/Halibut/Transport/DiscoveryClient.cs +++ b/source/Halibut/Transport/DiscoveryClient.cs @@ -16,17 +16,10 @@ public class DiscoveryClient readonly LogFactory logs = new (); readonly IStreamFactory streamFactory; - readonly ISslConfigurationProvider sslConfigurationProvider; public DiscoveryClient(IStreamFactory streamFactory) - : this(streamFactory, SslConfiguration.Default) - { - } - - public DiscoveryClient(IStreamFactory streamFactory, ISslConfigurationProvider sslConfigurationProvider) { this.streamFactory = streamFactory; - this.sslConfigurationProvider = sslConfigurationProvider; } public async Task DiscoverAsync(ServiceEndPoint serviceEndpoint, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, CancellationToken cancellationToken) @@ -52,13 +45,12 @@ public async Task DiscoverAsync(ServiceEndPoint serviceEndpoint await ssl.AuthenticateAsClientAsync( serviceEndpoint.BaseUri.Host, new X509Certificate2Collection(), - sslConfigurationProvider.SupportedProtocols, + SslConfiguration.SupportedProtocols, false); #else await ssl.AuthenticateAsClientEnforcingTimeout( serviceEndpoint, new X509Certificate2Collection(), - sslConfigurationProvider, cancellationToken ); #endif diff --git a/source/Halibut/Transport/LegacySslConfigurationProvider.cs b/source/Halibut/Transport/LegacySslConfigurationProvider.cs deleted file mode 100644 index 67c92fa90..000000000 --- a/source/Halibut/Transport/LegacySslConfigurationProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2012-2013 Octopus Deploy Pty. Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Security.Authentication; - -namespace Halibut.Transport -{ - /// - /// An implementation of ISslConfigurationProvider that uses legacy TLS protocols (1.0 and 1.1) - /// in addition to modern ones. Protocols are explicitly specified rather than using system - /// defaults. - /// - public class LegacySslConfigurationProvider : ISslConfigurationProvider - { -#pragma warning disable SYSLIB0039 - // See https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0039 - // TLS 1.0 and 1.1 are obsolete from .NET 7 - public SslProtocols SupportedProtocols => SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; -#pragma warning restore SYSLIB0039 - } -} \ No newline at end of file diff --git a/source/Halibut/Transport/SecureListener.cs b/source/Halibut/Transport/SecureListener.cs index 6a48bde2a..5132e1a4e 100644 --- a/source/Halibut/Transport/SecureListener.cs +++ b/source/Halibut/Transport/SecureListener.cs @@ -49,7 +49,6 @@ public class SecureListener : IAsyncDisposable readonly IStreamFactory streamFactory; readonly IConnectionsObserver connectionsObserver; readonly ISecureConnectionObserver secureConnectionObserver; - readonly ISslConfigurationProvider sslConfigurationProvider; ILog log; TcpListener listener; Thread? backgroundThread; @@ -70,8 +69,7 @@ public SecureListener( HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, IConnectionsObserver connectionsObserver, - ISecureConnectionObserver secureConnectionObserver, - ISslConfigurationProvider sslConfigurationProvider + ISecureConnectionObserver secureConnectionObserver ) { this.endPoint = endPoint; @@ -87,7 +85,6 @@ ISslConfigurationProvider sslConfigurationProvider this.streamFactory = streamFactory; this.connectionsObserver = connectionsObserver; this.secureConnectionObserver = secureConnectionObserver; - this.sslConfigurationProvider = sslConfigurationProvider; this.cts = new CancellationTokenSource(); this.cancellationToken = cts.Token; @@ -312,7 +309,7 @@ await ssl .AuthenticateAsServerAsync( serverCertificate, true, - sslConfigurationProvider.SupportedProtocols, + SslConfiguration.SupportedProtocols, false) .ConfigureAwait(false); diff --git a/source/Halibut/Transport/SslConfiguration.cs b/source/Halibut/Transport/SslConfiguration.cs index 254a7c41c..0626651bd 100644 --- a/source/Halibut/Transport/SslConfiguration.cs +++ b/source/Halibut/Transport/SslConfiguration.cs @@ -1,12 +1,9 @@ +using System.Security.Authentication; + namespace Halibut.Transport { public static class SslConfiguration { - public static ISslConfigurationProvider Default { get; } -#if NETFRAMEWORK // .NET4.8 exhibited inconsistent behavior when using the default configuration - = new LegacySslConfigurationProvider(); -#else - = new DefaultSslConfigurationProvider(); -#endif + public static SslProtocols SupportedProtocols => SslProtocols.None; // None means system defaults } } \ No newline at end of file diff --git a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs index aceddd438..857899c09 100644 --- a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs +++ b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs @@ -13,7 +13,6 @@ internal static async Task AuthenticateAsClientEnforcingTimeout( this SslStream ssl, ServiceEndPoint serviceEndpoint, X509Certificate2Collection clientCertificates, - ISslConfigurationProvider sslConfigurationProvider, CancellationToken cancellationToken) { using var timeoutCts = new CancellationTokenSource(ssl.ReadTimeout); @@ -23,7 +22,7 @@ internal static async Task AuthenticateAsClientEnforcingTimeout( { TargetHost = serviceEndpoint.BaseUri.Host, ClientCertificates = clientCertificates, - EnabledSslProtocols = sslConfigurationProvider.SupportedProtocols, + EnabledSslProtocols = SslConfiguration.SupportedProtocols, CertificateRevocationCheckMode = X509RevocationMode.NoCheck }; diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index b61e190e5..10750ee97 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -22,21 +22,18 @@ public class TcpConnectionFactory : IConnectionFactory readonly HalibutTimeoutsAndLimits halibutTimeoutsAndLimits; readonly IStreamFactory streamFactory; readonly ISecureConnectionObserver secureConnectionObserver; - readonly ISslConfigurationProvider sslConfigurationProvider; public TcpConnectionFactory( X509Certificate2 clientCertificate, HalibutTimeoutsAndLimits halibutTimeoutsAndLimits, IStreamFactory streamFactory, - ISecureConnectionObserver secureConnectionObserver, - ISslConfigurationProvider sslConfigurationProvider + ISecureConnectionObserver secureConnectionObserver ) { this.clientCertificate = clientCertificate; this.halibutTimeoutsAndLimits = halibutTimeoutsAndLimits; this.streamFactory = streamFactory; this.secureConnectionObserver = secureConnectionObserver; - this.sslConfigurationProvider = sslConfigurationProvider; } public async Task EstablishNewConnectionAsync(ExchangeProtocolBuilder exchangeProtocolBuilder, ServiceEndPoint serviceEndpoint, ILog log, CancellationToken cancellationToken) @@ -61,10 +58,10 @@ public async Task EstablishNewConnectionAsync(ExchangeProtocolBuild await ssl.AuthenticateAsClientAsync( serviceEndpoint.BaseUri.Host, new X509Certificate2Collection(clientCertificate), - sslConfigurationProvider.SupportedProtocols, + SslConfiguration.SupportedProtocols, false); #else - await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(clientCertificate), sslConfigurationProvider, cancellationToken); + await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(clientCertificate), cancellationToken); #endif await ssl.WriteAsync(MxLine, 0, MxLine.Length, cancellationToken);