From fd964ba98258f4c45cf8ed48ea097a1e4b9aa157 Mon Sep 17 00:00:00 2001 From: haga-rak Date: Wed, 6 May 2026 00:11:33 +0200 Subject: [PATCH 1/2] Populate cipher and hash in SslInfo on BouncyCastle path --- src/Fluxzy.Core/Archiving/SslInfo.cs | 53 +++++++++++++- .../Handlers/CipherAndHashInSslInfoTests.cs | 73 +++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs diff --git a/src/Fluxzy.Core/Archiving/SslInfo.cs b/src/Fluxzy.Core/Archiving/SslInfo.cs index 6c7c2bab..56698a7f 100644 --- a/src/Fluxzy.Core/Archiving/SslInfo.cs +++ b/src/Fluxzy.Core/Archiving/SslInfo.cs @@ -60,13 +60,12 @@ public SslInfo(SslStream sslStream, bool dumpCertificate) /// internal SslInfo(FluxzyClientProtocol clientProtocol, bool dumpCertificate) { -//#if NET6_0 -// CipherAlgorithm = ((System.Net.Security.TlsCipherSuite) clientProtocol.SessionParameters.CipherSuite).ToString(); -//#endif - NegotiatedApplicationProtocol = clientProtocol.GetApplicationProtocol().ToString(); SslProtocol = clientProtocol.GetSChannelProtocol(); + NegotiatedCipherSuite = (TlsCipherSuite) clientProtocol.SessionParameters.CipherSuite; + (CipherAlgorithm, HashAlgorithm) = DeriveAlgorithmsFromCipherSuite(NegotiatedCipherSuite); + if (BcCertificateHelper.TryReadDetailedInfo(clientProtocol.SessionParameters.LocalCertificate, out var localSubject, out var localIssuer, out var localNotBefore, out var localNotAfter, out var localSerial)) { @@ -172,5 +171,51 @@ public SslInfo( public DateTime? LocalCertificateNotAfter { get; } public string? LocalCertificateSerialNumber { get; } + + private static (CipherAlgorithmType Cipher, HashAlgorithmType Hash) DeriveAlgorithmsFromCipherSuite( + TlsCipherSuite cipherSuite) + { + var name = cipherSuite.ToString(); + + // TLS 1.2 and earlier use TLS__WITH__; TLS 1.3 uses TLS__. + var withIdx = name.IndexOf("_WITH_", StringComparison.Ordinal); + var body = withIdx >= 0 + ? name.Substring(withIdx + "_WITH_".Length) + : name.StartsWith("TLS_", StringComparison.Ordinal) ? name.Substring(4) : name; + + var lastUnderscore = body.LastIndexOf('_'); + var hashName = lastUnderscore >= 0 ? body.Substring(lastUnderscore + 1) : string.Empty; + var cipherPart = lastUnderscore >= 0 ? body.Substring(0, lastUnderscore) : body; + + var hash = hashName switch { + "SHA" => HashAlgorithmType.Sha1, + "SHA256" => HashAlgorithmType.Sha256, + "SHA384" => HashAlgorithmType.Sha384, + "SHA512" => HashAlgorithmType.Sha512, + "MD5" => HashAlgorithmType.Md5, + _ => HashAlgorithmType.None + }; + + var cipher = CipherAlgorithmType.None; + + if (cipherPart.Contains("AES_128")) + cipher = CipherAlgorithmType.Aes128; + else if (cipherPart.Contains("AES_256")) + cipher = CipherAlgorithmType.Aes256; + else if (cipherPart.Contains("AES_192")) + cipher = CipherAlgorithmType.Aes192; + else if (cipherPart.Contains("3DES")) + cipher = CipherAlgorithmType.TripleDes; + else if (cipherPart.Contains("DES")) + cipher = CipherAlgorithmType.Des; + else if (cipherPart.Contains("RC4")) + cipher = CipherAlgorithmType.Rc4; + else if (cipherPart.Contains("RC2")) + cipher = CipherAlgorithmType.Rc2; + else if (cipherPart.Contains("NULL")) + cipher = CipherAlgorithmType.Null; + + return (cipher, hash); + } } } diff --git a/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs b/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs new file mode 100644 index 00000000..62253eaa --- /dev/null +++ b/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs @@ -0,0 +1,73 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Threading.Tasks; +using Fluxzy.Clients.DotNetBridge; +using Fluxzy.Core; +using Fluxzy.Writers; +using Xunit; + +namespace Fluxzy.Tests.UnitTests.Handlers +{ + /// + /// Confirms that exposes the negotiated cipher suite, + /// cipher algorithm and hash algorithm regardless of the SSL provider. + /// The OsDefault path populates them from SslStream; the BouncyCastle + /// constructor currently does not (see SslInfo(FluxzyClientProtocol, bool)). + /// + public class CipherAndHashInSslInfoTests + { + [Theory] + [InlineData(SslProvider.OsDefault)] + [InlineData(SslProvider.BouncyCastle)] + public async Task NegotiatedCipherSuite_IsPopulated(SslProvider sslProvider) + { + var sslInfo = await GetSslInfoForGoogle(sslProvider); + + Assert.NotNull(sslInfo); + Assert.NotEqual(default(TlsCipherSuite), sslInfo.NegotiatedCipherSuite); + } + + [Theory] + [InlineData(SslProvider.OsDefault)] + [InlineData(SslProvider.BouncyCastle)] + public async Task CipherAlgorithm_IsPopulated(SslProvider sslProvider) + { + var sslInfo = await GetSslInfoForGoogle(sslProvider); + + Assert.NotNull(sslInfo); + Assert.NotEqual(CipherAlgorithmType.None, sslInfo.CipherAlgorithm); + } + + [Theory] + [InlineData(SslProvider.OsDefault)] + [InlineData(SslProvider.BouncyCastle)] + public async Task HashAlgorithm_IsPopulated(SslProvider sslProvider) + { + var sslInfo = await GetSslInfoForGoogle(sslProvider); + + Assert.NotNull(sslInfo); + Assert.NotEqual(HashAlgorithmType.None, sslInfo.HashAlgorithm); + } + + private static async Task GetSslInfoForGoogle(SslProvider sslProvider) + { + await using var tcpProvider = ITcpConnectionProvider.Default; + + using var handler = new FluxzyDefaultHandler(sslProvider, tcpProvider, new EventOnlyArchiveWriter()); + + using var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) }; + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://www.google.com/"); + + var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + var fluxzyResponse = Assert.IsType(response); + return fluxzyResponse.Exchange.Connection?.SslInfo; + } + } +} From 9842dc15b3e1d74f90b48ae9dfb0a3689ee20cc7 Mon Sep 17 00:00:00 2001 From: haga-rak Date: Wed, 6 May 2026 00:21:37 +0200 Subject: [PATCH 2/2] Skip OsDefault for HashAlgorithm assertion (TLS 1.3 returns None on OpenSSL) --- .../UnitTests/Handlers/CipherAndHashInSslInfoTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs b/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs index 62253eaa..123e185c 100644 --- a/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs +++ b/test/Fluxzy.Tests/UnitTests/Handlers/CipherAndHashInSslInfoTests.cs @@ -42,8 +42,11 @@ public async Task CipherAlgorithm_IsPopulated(SslProvider sslProvider) Assert.NotEqual(CipherAlgorithmType.None, sslInfo.CipherAlgorithm); } + // OsDefault is intentionally excluded: SslStream.HashAlgorithm is obsolete and returns + // None for TLS 1.3 on OpenSSL (Linux/macOS), even though the negotiated suite carries + // a hash. The BouncyCastle path derives Sha256/Sha384 from the suite name and so + // exposes a useful value where .NET no longer does. [Theory] - [InlineData(SslProvider.OsDefault)] [InlineData(SslProvider.BouncyCastle)] public async Task HashAlgorithm_IsPopulated(SslProvider sslProvider) {