diff --git a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs index 4d0d041d..711bd6be 100644 --- a/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs +++ b/source/Halibut.Tests/Support/BackwardsCompatibility/LatestClientAndPreviousServiceVersionBuilder.cs @@ -1,12 +1,15 @@ using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using Halibut.Diagnostics; using Halibut.Diagnostics.LogCreators; using Halibut.Logging; using Halibut.TestProxy; using Halibut.Tests.Support.Logging; using Halibut.Transport.Proxy; using Octopus.TestPortForwarder; +using ILog = Halibut.Diagnostics.ILog; namespace Halibut.Tests.Support.BackwardsCompatibility { @@ -21,6 +24,7 @@ public class LatestClientAndPreviousServiceVersionBuilder : IClientAndServiceBui ProxyFactory? proxyFactory; Reference? proxyServiceReference; LogLevel halibutLogLevel = LogLevel.Trace; + ConcurrentDictionary? clientInMemoryLoggers; readonly OldServiceAvailableServices availableServices = new(false, false); LatestClientAndPreviousServiceVersionBuilder(ServiceConnectionType serviceConnectionType, CertAndThumbprint serviceCertAndThumbprint) @@ -146,7 +150,14 @@ async Task IClientAndServiceBuilder.Build(CancellationToken c { return await Build(cancellationToken); } - + + public LatestClientAndPreviousServiceVersionBuilder RecordingClientLogs(out ConcurrentDictionary inMemoryLoggers) + { + inMemoryLoggers = new ConcurrentDictionary(); + this.clientInMemoryLoggers = inMemoryLoggers; + return this; + } + public async Task Build(CancellationToken cancellationToken) { var logger = new SerilogLoggerBuilder().Build().ForContext(); @@ -158,7 +169,7 @@ public async Task Build(CancellationToken cancellationToken) var clientBuilder = new HalibutRuntimeBuilder() .WithServerCertificate(clientCertAndThumbprint.Certificate2) - .WithLogFactory(new TestContextLogCreator("Client", halibutLogLevel).ToCachingLogFactory()) + .WithLogFactory(BuildClientLogger()) .WithHalibutTimeoutsAndLimits(new HalibutTimeoutsAndLimitsForTestsBuilder().Build()); var client = clientBuilder.Build(); @@ -338,5 +349,24 @@ public async ValueTask DisposeAsync() Try.CatchingError(() => cancellationTokenSource.Dispose(), LogError); } } + + ILogFactory BuildClientLogger() + { + if (clientInMemoryLoggers == null) + { + return new TestContextLogCreator("Client", halibutLogLevel).ToCachingLogFactory(); + } + + return new AggregateLogWriterLogCreator( + new TestContextLogCreator("Client", halibutLogLevel), + s => + { + var logger = new InMemoryLogWriter(); + clientInMemoryLoggers[s] = logger; + return new[] {logger}; + } + ) + .ToCachingLogFactory(); + } } } diff --git a/source/Halibut.Tests/Support/ClientAndServiceBuilderExtensionMethods.cs b/source/Halibut.Tests/Support/ClientAndServiceBuilderExtensionMethods.cs index 01c1e127..def312ac 100644 --- a/source/Halibut.Tests/Support/ClientAndServiceBuilderExtensionMethods.cs +++ b/source/Halibut.Tests/Support/ClientAndServiceBuilderExtensionMethods.cs @@ -10,12 +10,17 @@ public static LatestClientAndLatestServiceBuilder AsLatestClientAndLatestService { return (LatestClientAndLatestServiceBuilder) clientAndServiceBuilder; } - + public static PreviousClientVersionAndLatestServiceBuilder AsPreviousClientVersionAndLatestServiceBuilder(this IClientAndServiceBuilder clientAndServiceBuilder) { return (PreviousClientVersionAndLatestServiceBuilder) clientAndServiceBuilder; } + public static LatestClientAndPreviousServiceVersionBuilder AsLatestClientAndPreviousServiceVersionBuilder(this IClientAndServiceBuilder clientAndServiceBuilder) + { + return (LatestClientAndPreviousServiceVersionBuilder) clientAndServiceBuilder; + } + public static IClientAndServiceBuilder WithAsyncService(this IClientAndServiceBuilder clientAndServiceBuilder, Func implementation) { if (clientAndServiceBuilder is LatestClientAndLatestServiceBuilder) diff --git a/source/Halibut.Tests/TlsFixture.cs b/source/Halibut.Tests/TlsFixture.cs new file mode 100644 index 00000000..b7aff138 --- /dev/null +++ b/source/Halibut.Tests/TlsFixture.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using System.Threading.Tasks; +using FluentAssertions; +using Halibut.Tests.Support; +using Halibut.Tests.Support.TestAttributes; +using Halibut.Tests.Support.TestCases; +using Halibut.Tests.TestServices.Async; +using Halibut.TestUtils.Contracts; +using NUnit.Framework; + +namespace Halibut.Tests +{ + public class TlsFixture : BaseTest + { + [Test] + [LatestClientAndLatestServiceTestCases(testWebSocket: false, testNetworkConditions: false)] + public async Task LatestClientAndServiceUseBestAvailableSslProtocol(ClientAndServiceTestCase clientAndServiceTestCase) + { + await using (var clientAndService = await clientAndServiceTestCase.CreateTestCaseBuilder() + .WithStandardServices() + .AsLatestClientAndLatestServiceBuilder() + .RecordingClientLogs(out var clientLogs) + .RecordingServiceLogs(out var serviceLogs) + .Build(CancellationToken)) + { + Logger.Information("Platform detection:"); + Logger.Information("Environment.OSVersion.Platform: {EnvironmentOSVersionPlatform}", Environment.OSVersion.Platform); + Logger.Information("Environment.OSVersion.Version: {EnvironmentOSVersionVersion}", Environment.OSVersion.Version); + Logger.Information("Environment.OSVersion.VersionString: {EnvironmentOSVersionVersionString}", Environment.OSVersion.VersionString); + Logger.Information("Environment.OSVersion.ServicePack: {EnvironmentOSVersionServicePack}", Environment.OSVersion.ServicePack); + Logger.Information("RuntimeInformation.OSDescription: {RuntimeInformationOSDescription}", RuntimeInformation.OSDescription); + Logger.Information("RuntimeInformation.FrameworkDescription: {RuntimeInformationFrameworkDescription}", RuntimeInformation.FrameworkDescription); + Logger.Information("RuntimeInformation.ProcessArchitecture: {RuntimeInformationProcessArchitecture}", RuntimeInformation.ProcessArchitecture); + Logger.Information("RuntimeInformation.OSArchitecture: {RuntimeInformationOSArchitecture}", RuntimeInformation.OSArchitecture); + + var echo = clientAndService.CreateAsyncClient(); + await echo.SayHelloAsync("World"); + + var connectionInitiatorLogs = clientAndServiceTestCase.ServiceConnectionType == ServiceConnectionType.Listening + ? clientLogs + : serviceLogs; + + var expectedSslProtocol = GetExpectedSslProtocolForTheCurrentPlatform(); + var expectedLogFragment = $"using protocol {expectedSslProtocol}"; + + connectionInitiatorLogs.Values + .SelectMany(log => log.GetLogs()) + .Should().Contain( + logEvent => logEvent.FormattedMessage.Contains(expectedLogFragment), + $"the OS is \"{RuntimeInformation.OSDescription}\", so we expect {expectedSslProtocol} to be used, and expect log output to contain \"{expectedLogFragment}\" for {clientAndServiceTestCase.ServiceConnectionType} tentacles"); + } + } + + [Test] + [LatestClientAndPreviousServiceVersionsTestCases(testWebSocket: false, testNetworkConditions: false)] + public async Task LatestClientAndPreviousServiceFallBackOnTls12(ClientAndServiceTestCase clientAndServiceTestCase) + { + await using (var clientAndService = await clientAndServiceTestCase.CreateTestCaseBuilder() + .WithStandardServices() + .AsLatestClientAndPreviousServiceVersionBuilder() + .RecordingClientLogs(out var clientLogs) + .Build(CancellationToken)) + { + var echo = clientAndService.CreateAsyncClient(); + await echo.SayHelloAsync("World"); + + var expectedLogMessage = clientAndServiceTestCase.ServiceConnectionType == ServiceConnectionType.Listening + ? $"using protocol {SslProtocols.Tls12}" + : $"client connected with {SslProtocols.Tls12}"; + + clientLogs.Values + .SelectMany(log => log.GetLogs()) + .Should().Contain(logEvent => logEvent.FormattedMessage.Contains(expectedLogMessage)); + } + } + + SslProtocols GetExpectedSslProtocolForTheCurrentPlatform() + { + // All linux platforms we test against support TLS 1.3. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return SslProtocols.Tls13; + } + + // We test against old versions of Windows which do not support TLS 1.3. + // TLS 1.3 is supported since Windows Server 2022 which has build number 20348, and Windows 11 which has higher build numbers. + // TLS 1.3 is partially supported in Windows 10, which can have lower build numbers, but we don't test against that so it is ignored here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + const int WindowsServer2022OSBuild = 20348; + return Environment.OSVersion.Version.Build >= WindowsServer2022OSBuild + ? SslProtocols.Tls13 + : SslProtocols.Tls12; + } + + // .NET does not support TLS 1.3 on Mac OS yet. + // https://github.com/dotnet/runtime/issues/1979 + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return SslProtocols.Tls12; + } + + throw new NotSupportedException($"Unsupported OS platform: {RuntimeInformation.OSDescription}"); + } + } +} \ No newline at end of file diff --git a/source/Halibut/Transport/DiscoveryClient.cs b/source/Halibut/Transport/DiscoveryClient.cs index d95494d2..79b343ad 100644 --- a/source/Halibut/Transport/DiscoveryClient.cs +++ b/source/Halibut/Transport/DiscoveryClient.cs @@ -42,7 +42,11 @@ public async Task DiscoverAsync(ServiceEndPoint serviceEndpoint #if NETFRAMEWORK // TODO: ASYNC ME UP! // AuthenticateAsClientAsync in .NET 4.8 does not support cancellation tokens. So `cancellationToken` is not respected here. - await ssl.AuthenticateAsClientAsync(serviceEndpoint.BaseUri.Host, new X509Certificate2Collection(), SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false); + await ssl.AuthenticateAsClientAsync( + serviceEndpoint.BaseUri.Host, + new X509Certificate2Collection(), + SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, + false); #else await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(), cancellationToken); #endif diff --git a/source/Halibut/Transport/SecureListener.cs b/source/Halibut/Transport/SecureListener.cs index 9ee69d54..2367c61d 100644 --- a/source/Halibut/Transport/SecureListener.cs +++ b/source/Halibut/Transport/SecureListener.cs @@ -298,11 +298,17 @@ async Task ExecuteRequest(TcpClient client) { log.Write(EventType.SecurityNegotiation, "Performing TLS server handshake"); + await ssl + .AuthenticateAsServerAsync( + serverCertificate, + true, #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 - await ssl.AuthenticateAsServerAsync(serverCertificate, true, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false).ConfigureAwait(false); + // See https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0039 + // TLS 1.0 and 1.1 are obsolete from .NET 7 + SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, #pragma warning restore SYSLIB0039 + false) + .ConfigureAwait(false); log.Write(EventType.SecurityNegotiation, "Secure connection established, client is not yet authenticated, client connected with {0}", ssl.SslProtocol.ToString()); diff --git a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs index 07dad022..e63420b3 100644 --- a/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs +++ b/source/Halibut/Transport/Streams/SslStreamExtensionMethods.cs @@ -26,7 +26,7 @@ internal static async Task AuthenticateAsClientEnforcingTimeout( #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 - EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, + EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, #pragma warning restore SYSLIB0039 CertificateRevocationCheckMode = X509RevocationMode.NoCheck }; diff --git a/source/Halibut/Transport/TcpConnectionFactory.cs b/source/Halibut/Transport/TcpConnectionFactory.cs index 41512bc6..3a13913a 100644 --- a/source/Halibut/Transport/TcpConnectionFactory.cs +++ b/source/Halibut/Transport/TcpConnectionFactory.cs @@ -47,7 +47,11 @@ public async Task EstablishNewConnectionAsync(ExchangeProtocolBuild #if NETFRAMEWORK // TODO: ASYNC ME UP! // AuthenticateAsClientAsync in .NET 4.8 does not support cancellation tokens. So `cancellationToken` is not respected here. - await ssl.AuthenticateAsClientAsync(serviceEndpoint.BaseUri.Host, new X509Certificate2Collection(clientCertificate), SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false); + await ssl.AuthenticateAsClientAsync( + serviceEndpoint.BaseUri.Host, + new X509Certificate2Collection(clientCertificate), + SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, + false); #else await ssl.AuthenticateAsClientEnforcingTimeout(serviceEndpoint, new X509Certificate2Collection(clientCertificate), cancellationToken); #endif