diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index b9457b067a097c..7fbfc0b032b983 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.ExceptionServices; @@ -24,6 +23,9 @@ public partial class SslStream private object _handshakeLock => _sslAuthenticationOptions; private volatile TaskCompletionSource? _handshakeWaiter; + private const int HandshakeTypeOffsetSsl2 = 2; // Offset of HelloType in Sslv2 and Unified frames + private const int HandshakeTypeOffsetTls = 5; // Offset of HelloType in Sslv3 and TLS frames + private bool _receivedEOF; // Used by Telemetry to ensure we log connection close exactly once @@ -376,8 +378,10 @@ private async ValueTask ReceiveBlobAsync(Cancellation } break; case TlsContentType.Handshake: - if (!_isRenego && _buffer.EncryptedReadOnlySpan[TlsFrameHelper.HeaderSize] == (byte)TlsHandshakeType.ClientHello && +#pragma warning disable CS0618 + if (!_isRenego && _buffer.EncryptedReadOnlySpan[_lastFrame.Header.Version == SslProtocols.Ssl2 ? HandshakeTypeOffsetSsl2 : HandshakeTypeOffsetTls] == (byte)TlsHandshakeType.ClientHello && _sslAuthenticationOptions!.IsServer) // guard against malicious endpoints. We should not see ClientHello on client. +#pragma warning restore CS0618 { TlsFrameHelper.ProcessingOptions options = NetEventSource.Log.IsEnabled() ? TlsFrameHelper.ProcessingOptions.All : @@ -445,7 +449,7 @@ private ProtocolToken ProcessBlob(int frameSize) break; } - frameSize = nextHeader.Length + TlsFrameHelper.HeaderSize; + frameSize = nextHeader.Length; // Can process more handshake frames in single step or during TLS1.3 post-handshake auth, but we should // avoid processing too much so as to preserve API boundary between handshake and I/O. @@ -974,7 +978,7 @@ private int GetFrameSize(ReadOnlySpan buffer) throw new AuthenticationException(SR.net_frame_read_size); } - return _lastFrame.Header.Length + TlsFrameHelper.HeaderSize; + return _lastFrame.Header.Length; } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs index a5ddab5067c510..4a455736379677 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs @@ -90,11 +90,6 @@ internal struct TlsFrameHeader public int Length; public override string ToString() => $"{Version}:{Type}[{Length}]"; - - public int GetFrameSize() - { - return Length + TlsFrameHelper.HeaderSize; - } } internal static class TlsFrameHelper @@ -173,44 +168,52 @@ public override string ToString() public static bool TryGetFrameHeader(ReadOnlySpan frame, ref TlsFrameHeader header) { - bool result = frame.Length > 4; - - if (frame.Length >= 1) + if (frame.Length < HeaderSize) { - header.Type = (TlsContentType)frame[0]; + header.Length= -1; + return false; + } - if (frame.Length >= 3) - { - // SSLv3, TLS or later - if (frame[1] == 3) - { - if (frame.Length > 4) - { - header.Length = ((frame[3] << 8) | frame[4]); - } + header.Type = (TlsContentType)frame[0]; - header.Version = TlsMinorVersionToProtocol(frame[2]); - } - else - { - header.Length = -1; - header.Version = SslProtocols.None; - } - } + // SSLv3, TLS or later + if (frame[1] == 3) + { + header.Length = ((frame[3] << 8) | frame[4]) + HeaderSize; + header.Version = TlsMinorVersionToProtocol(frame[2]); } + else if (frame[2] == (byte)TlsHandshakeType.ClientHello && + frame[3] == 3) // SSL3 or above + { + int length; + if ((frame[0] & 0x80) != 0) + { + // Two bytes + length = (((frame[0] & 0x7f) << 8) | frame[1]) + 2; + } + else + { + // Three bytes + length = (((frame[0] & 0x3f) << 8) | frame[1]) + 3; + } - return result; - } - // Returns frame size e.g. header + content - public static int GetFrameSize(ReadOnlySpan frame) - { - if (frame.Length < 5 || frame[1] < 3) + // max frame for SSLv2 is 32767. + // However, we expect something reasonable for initial HELLO + // We don't have enough logic to verify full validity, + // the limits bellow are queses. +#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete + header.Version = SslProtocols.Ssl2; +#pragma warning restore CS0618 + header.Length = length; + header.Type = TlsContentType.Handshake; + } + else { - return -1; + header.Length = -1; } - return ((frame[3] << 8) | frame[4]) + HeaderSize; + return true; } // This function will try to parse TLS hello frame and fill details in provided info structure. @@ -252,9 +255,21 @@ public static bool TryGetFrameInfo(ReadOnlySpan frame, ref TlsFrameInfo in } info.HandshakeType = (TlsHandshakeType)frame[HandshakeTypeOffset]; +#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete + if (info.Header.Version == SslProtocols.Ssl2) + { + // This is safe. We would not get here if the length is too small. + info.SupportedVersions |= TlsMinorVersionToProtocol(frame[4]); + // We only recognize Unified ClientHello at the moment. + // This is needed to trigger certificate selection callback in SslStream. + info.HandshakeType = TlsHandshakeType.ClientHello; + // There is no more parsing for old protocols. + return true; + } +#pragma warning restore CS0618 // Check if we have full frame. - bool isComplete = frame.Length >= HeaderSize + info.Header.Length; + bool isComplete = frame.Length >= info.Header.Length; #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete if (((int)info.Header.Version >= (int)SslProtocols.Tls) && @@ -412,10 +427,10 @@ private static bool TryParseClientHello(ReadOnlySpan clientHello, ref TlsF // Skip compression methods (max size 2^8-1 => size fits in 1 byte) p = SkipOpaqueType1(p); - // is invalid structure or no extensions? + // no extensions if (p.IsEmpty) { - return false; + return true; } // client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs index b7809488712f82..481101ba88bf90 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs @@ -1017,6 +1017,71 @@ public async Task SslStream_RandomSizeWrites_OK(int bufferSize, int readBufferSi } + [Theory] + [InlineData(true)] + [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows)] + public async Task SslStream_UnifiedHello_Ok(bool useOptionCallback) + { + (Stream client, Stream server) = TestHelper.GetConnectedTcpStreams(); + SslStream ssl = new SslStream(server); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TestConfiguration.PassingTestTimeout); + bool callbackCalled = false; + Task serverTask; + + var options = new SslServerAuthenticationOptions(); + if (useOptionCallback) + { + serverTask = ssl.AuthenticateAsServerAsync((ssl, info, o, ct) => + { + callbackCalled = true; + options.ServerCertificate = Configuration.Certificates.GetServerCertificate(); + return new ValueTask(options); + }, null, cts.Token); + + } + else + { + options.ServerCertificateSelectionCallback = (o, name) => + { + callbackCalled = true; + return Configuration.Certificates.GetServerCertificate(); + }; + + serverTask = ssl.AuthenticateAsServerAsync(options, cts.Token); + } + + Task.WaitAny(client.WriteAsync(TlsFrameHelperTests.s_UnifiedHello).AsTask(), serverTask); + if (serverTask.IsCompleted) + { + // Something failed. Raise exception. + await serverTask; + } + + byte[] buffer = new byte[1024]; + Task readTask = client.ReadAsync(buffer, cts.Token).AsTask(); + Task.WaitAny(readTask, serverTask); + if (serverTask.IsCompleted) + { + // Something failed. Raise exception. + await serverTask; + } + + int readLength = await readTask; + // We should get back ServerHello + Assert.True(readLength > 0); + Assert.True(callbackCalled); + Assert.Equal(22, buffer[0]); // Handshake Protocol + Assert.Equal(2, buffer[5]); // ServerHello + + // Handshake should not be finished at this point. + Assert.False(serverTask.IsCompleted); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => serverTask); + } + private static bool ValidateServerCertificate( object sender, X509Certificate retrievedServerPublicCertificate, diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs index 83b28777431bc8..be14f2e9ca45b2 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsFrameHelperTests.cs @@ -46,7 +46,7 @@ public void TlsFrameHelper_ValidData_Ok() Assert.True(TlsFrameHelper.TryGetFrameInfo(s_validClientHello, ref info)); Assert.Equal(SslProtocols.Tls12, info.Header.Version); - Assert.Equal(203, info.Header.Length); + Assert.Equal(208, info.Header.Length); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); } @@ -88,6 +88,37 @@ public void TlsFrameHelper_Tls12ServerHello_Ok() Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols); } + [Fact] + public void TlsFrameHelper_UnifiedClientHello_Ok() + { + TlsFrameHelper.TlsFrameInfo info = default; + Assert.True(TlsFrameHelper.TryGetFrameHeader(s_UnifiedHello, ref info.Header)); + Assert.Equal(75, info.Header.Length); + + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_UnifiedHello, ref info)); +#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete +#pragma warning disable SYSLIB0039 // Tls is obsolete + Assert.Equal(SslProtocols.Ssl2, info.Header.Version); + Assert.Equal(SslProtocols.Ssl2 | SslProtocols.Tls, info.SupportedVersions); +#pragma warning restore CS0618 +#pragma warning restore SYSLIB0039 + Assert.Equal(TlsContentType.Handshake, info.Header.Type); + Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); + Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType); + } + + [Fact] + public void TlsFrameHelper_TlsClientHelloNoExtensions_Ok() + { + TlsFrameHelper.TlsFrameInfo info = default; + Assert.True(TlsFrameHelper.TryGetFrameInfo(s_TlsClientHelloNoExtensions, ref info)); + Assert.Equal(SslProtocols.Tls12, info.Header.Version); + Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); + Assert.Equal(TlsContentType.Handshake, info.Header.Type); + Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); + Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType); + } + public static IEnumerable InvalidClientHelloData() { int id = 0; @@ -381,6 +412,36 @@ public static IEnumerable InvalidClientHelloDataTruncatedBytes() 0x00, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32, }; + internal static byte[] s_UnifiedHello = new byte[] + { + // Length + 0x80, 0x49, + // ClientHello + 0x01, + // Version + 0x03, 0x01, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x2F, 0x00, 0x00, 0x35, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x05, 0x00, 0x00, 0x0A, 0x01, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0x03, 0x00, 0x80, 0x00, 0x00, + 0x09, 0x06, 0x00, 0x40, 0x00, 0x00, 0x64, 0x00, + 0x00, 0x62, 0x00, 0x00, 0x03, 0x00, 0x00, 0x06, + 0x02, 0x00, 0x80, 0x04, 0x00, 0x80, 0x5B, 0x0B, + 0xA1, 0xEB, 0xBF, 0x2D, 0x57, 0xF5, 0xD1, 0x0F, + 0x52, 0x3B, 0x12, 0x9C, 0xF8, 0xD4, + }; + + private static byte[] s_TlsClientHelloNoExtensions = new byte[] { + 0x16, 0x03, 0x03, 0x00, 0x39, 0x01, 0x00, 0x00, + 0x35, 0x03, 0x03, 0x62, 0x5d, 0x50, 0x2a, 0x41, + 0x2f, 0xd8, 0xc3, 0x65, 0x35, 0xea, 0x01, 0x70, + 0x03, 0x7e, 0x7e, 0x2d, 0xd4, 0xfe, 0x93, 0x39, + 0xa4, 0x04, 0x66, 0xbb, 0x46, 0x91, 0x41, 0xc3, + 0x48, 0x87, 0x3d, 0x00, 0x00, 0x0e, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x0a, 0x00, 0x35, 0x00, 0x2f, + 0x00, 0x05, 0x00, 0x04, 0x01, 0x00 + }; + private static IEnumerable InvalidClientHello() { // This test covers following test cases: