diff --git a/src/libraries/System.Net.Security/src/Resources/Strings.resx b/src/libraries/System.Net.Security/src/Resources/Strings.resx
index 7270ea80faefdc..8af05ebe134c8d 100644
--- a/src/libraries/System.Net.Security/src/Resources/Strings.resx
+++ b/src/libraries/System.Net.Security/src/Resources/Strings.resx
@@ -212,6 +212,9 @@
Authentication failed because the remote party has closed the transport stream.
+
+ Authentication failed because the remote party sent a TLS alert: '{0}'.
+
Authentication failed on the remote side (the stream might still be available for additional authentication attempts).
diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj
index 868faddb5727a8..067df9b00fcf6c 100644
--- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj
+++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj
@@ -34,6 +34,7 @@
+
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs
index 82aab9fe951968..b770ef26fb78a6 100644
--- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs
+++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs
@@ -625,7 +625,7 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint)
//
// Acquire Server Side Certificate information and set it on the class.
//
- private bool AcquireServerCredentials(ref byte[]? thumbPrint, ReadOnlySpan clientHello)
+ private bool AcquireServerCredentials(ref byte[]? thumbPrint)
{
if (NetEventSource.IsEnabled)
NetEventSource.Enter(this);
@@ -639,13 +639,13 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint, ReadOnlySpan
// with .NET Framework), and if neither is set we fall back to using ServerCertificate.
if (_sslAuthenticationOptions.ServerCertSelectionDelegate != null)
{
- string? serverIdentity = SniHelper.GetServerName(clientHello);
- localCertificate = _sslAuthenticationOptions.ServerCertSelectionDelegate(serverIdentity);
-
+ localCertificate = _sslAuthenticationOptions.ServerCertSelectionDelegate(_sslAuthenticationOptions.TargetHost);
if (localCertificate == null)
{
throw new AuthenticationException(SR.net_ssl_io_no_server_cert);
}
+ if (NetEventSource.IsEnabled)
+ NetEventSource.Info(this, "Use delegate selected Cert");
}
else if (_sslAuthenticationOptions.CertSelectionDelegate != null)
{
@@ -784,7 +784,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte
if (_refreshCredentialNeeded)
{
cachedCreds = _sslAuthenticationOptions.IsServer
- ? AcquireServerCredentials(ref thumbPrint, inputBuffer)
+ ? AcquireServerCredentials(ref thumbPrint)
: AcquireClientCredentials(ref thumbPrint);
}
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SniHelper.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SniHelper.cs
index ffbf730e7d9712..db682188997e1b 100644
--- a/src/libraries/System.Net.Security/src/System/Net/Security/SniHelper.cs
+++ b/src/libraries/System.Net.Security/src/System/Net/Security/SniHelper.cs
@@ -26,13 +26,13 @@ internal class SniHelper
// opaque fragment[SSLPlaintext.length];
// } SSLPlaintext;
const int ContentTypeOffset = 0;
- const int ProtocolVersionOffset = ContentTypeOffset + sizeof(ContentType);
+ const int ProtocolVersionOffset = ContentTypeOffset + sizeof(TlsContentType);
const int LengthOffset = ProtocolVersionOffset + ProtocolVersionSize;
const int HandshakeOffset = LengthOffset + sizeof(ushort);
// SSL v2's ContentType has 0x80 bit set.
// We do not care about SSL v2 here because it does not support client hello extensions
- if (sslPlainText.Length < HandshakeOffset || (ContentType)sslPlainText[ContentTypeOffset] != ContentType.Handshake)
+ if (sslPlainText.Length < HandshakeOffset || (TlsContentType)sslPlainText[ContentTypeOffset] != TlsContentType.Handshake)
{
return null;
}
@@ -62,10 +62,10 @@ internal class SniHelper
// } body;
// } Handshake;
const int HandshakeTypeOffset = 0;
- const int ClientHelloLengthOffset = HandshakeTypeOffset + sizeof(HandshakeType);
+ const int ClientHelloLengthOffset = HandshakeTypeOffset + sizeof(TlsHandshakeType);
const int ClientHelloOffset = ClientHelloLengthOffset + UInt24Size;
- if (sslHandshake.Length < ClientHelloOffset || (HandshakeType)sslHandshake[HandshakeTypeOffset] != HandshakeType.ClientHello)
+ if (sslHandshake.Length < ClientHelloOffset || (TlsHandshakeType)sslHandshake[HandshakeTypeOffset] != TlsHandshakeType.ClientHello)
{
return null;
}
@@ -363,16 +363,6 @@ private static Encoding CreateEncoding()
return Encoding.GetEncoding("utf-8", new EncoderExceptionFallback(), new DecoderExceptionFallback());
}
- private enum ContentType : byte
- {
- Handshake = 0x16
- }
-
- private enum HandshakeType : byte
- {
- ClientHello = 0x01
- }
-
private enum ExtensionType : ushort
{
ServerName = 0x00
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs
index 600163be36290a..d9ff52dfeba8f3 100644
--- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs
+++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs
@@ -34,14 +34,8 @@ private enum Framing
// This is set on the first packet to figure out the framing style.
private Framing _framing = Framing.Unknown;
- // SSL3/TLS protocol frames definitions.
- private enum FrameType : byte
- {
- ChangeCipherSpec = 20,
- Alert = 21,
- Handshake = 22,
- AppData = 23
- }
+ private TlsAlertDescription _lastAlertDescription;
+ private TlsFrameHandshakeInfo _lastFrame;
private readonly object _handshakeLock = new object();
private volatile TaskCompletionSource? _handshakeWaiter;
@@ -274,7 +268,6 @@ private async Task ForceAuthenticationAsync(TIOAdapter adapter, bool
{
// get ready to receive first frame
_handshakeBuffer = new ArrayBuffer(InitialHandshakeBufferSize);
- _framing = Framing.Unknown;
}
while (!handshakeCompleted)
@@ -288,6 +281,19 @@ private async Task ForceAuthenticationAsync(TIOAdapter adapter, bool
if (message.Failed)
{
+ if (_lastFrame.Header.Type == TlsContentType.Handshake && message.Size == 0)
+ {
+ // If we failed without OS sending out alert, inject one here to be consistent across platforms.
+ byte[] alert = TlsFrameHelper.CreateAlertFrame(_lastFrame.Header.Version, TlsAlertDescription.ProtocolVersion);
+ await adapter.WriteAsync(alert, 0, alert.Length).ConfigureAwait(false);
+ }
+ else if (_lastFrame.Header.Type == TlsContentType.Alert && _lastAlertDescription != TlsAlertDescription.CloseNotify &&
+ message.Status.ErrorCode == SecurityStatusPalErrorCode.IllegalMessage)
+ {
+ // Improve generic message and show details if we failed because of TLS Alert.
+ throw new AuthenticationException(SR.Format(SR.net_auth_tls_alert, _lastAlertDescription.ToString()), message.GetException());
+ }
+
throw new AuthenticationException(SR.net_auth_SSPI, message.GetException());
}
else if (message.Status.ErrorCode == SecurityStatusPalErrorCode.OK)
@@ -346,17 +352,49 @@ private async ValueTask ReceiveBlobAsync(TIOAdapter a
_framing = DetectFraming(_handshakeBuffer.ActiveReadOnlySpan);
}
- int frameSize = GetFrameSize(_handshakeBuffer.ActiveReadOnlySpan);
- if (frameSize < 0)
+ if (_framing == Framing.BeforeSSL3)
+ {
+#pragma warning disable 0618
+ _lastFrame.Header.Version = SslProtocols.Ssl2;
+#pragma warning restore 0618
+ _lastFrame.Header.Length = GetFrameSize(_handshakeBuffer.ActiveReadOnlySpan);
+ }
+ else
+ {
+ TlsFrameHelper.TryGetFrameHeader(_handshakeBuffer.ActiveReadOnlySpan, ref _lastFrame.Header);
+ }
+
+ if (_lastFrame.Header.Length < 0)
{
throw new IOException(SR.net_frame_read_size);
}
+ // Header length is content only so we must add header size as well.
+ int frameSize = _lastFrame.Header.Length + TlsFrameHelper.HeaderSize;
if (_handshakeBuffer.ActiveLength < frameSize)
{
await FillHandshakeBufferAsync(adapter, frameSize).ConfigureAwait(false);
}
+
// At this point, we have at least one TLS frame.
+ if (_lastFrame.Header.Type == TlsContentType.Alert)
+ {
+ TlsAlertLevel level = 0;
+ if (TlsFrameHelper.TryGetAlertInfo(_handshakeBuffer.ActiveReadOnlySpan, ref level, ref _lastAlertDescription))
+ {
+ if (NetEventSource.IsEnabled && _lastAlertDescription != TlsAlertDescription.CloseNotify) NetEventSource.Fail(this, $"Received TLS alert {_lastAlertDescription}");
+ }
+ }
+ else if (_lastFrame.Header.Type == TlsContentType.Handshake)
+ {
+ if (_handshakeBuffer.ActiveReadOnlySpan[TlsFrameHelper.HeaderSize] == (byte)TlsHandshakeType.ClientHello &&
+ _sslAuthenticationOptions!.ServerCertSelectionDelegate != null)
+ {
+ // Process SNI from Client Hello message
+ TlsFrameHelper.TryGetHandshakeInfo(_handshakeBuffer.ActiveReadOnlySpan, ref _lastFrame);
+ _sslAuthenticationOptions.TargetHost = _lastFrame.TargetName;
+ }
+ }
return ProcessBlob(frameSize);
}
@@ -372,23 +410,24 @@ private ProtocolToken ProcessBlob(int frameSize)
_handshakeBuffer.Discard(frameSize);
// Often more TLS messages fit into same packet. Get as many complete frames as we can.
- while (_handshakeBuffer.ActiveLength > SecureChannel.ReadHeaderSize)
+ while (_handshakeBuffer.ActiveLength > TlsFrameHelper.HeaderSize)
{
- ReadOnlySpan remainingData = _handshakeBuffer.ActiveReadOnlySpan;
- if (remainingData[0] >= (int)FrameType.AppData)
+ TlsFrameHeader nextHeader = default;
+
+ if (!TlsFrameHelper.TryGetFrameHeader(_handshakeBuffer.ActiveReadOnlySpan, ref nextHeader))
{
break;
}
- frameSize = GetFrameSize(remainingData);
- if (_handshakeBuffer.ActiveLength >= frameSize)
+ frameSize = nextHeader.Length + TlsFrameHelper.HeaderSize;
+ if (nextHeader.Type == TlsContentType.AppData || frameSize > _handshakeBuffer.ActiveLength)
{
- chunkSize += frameSize;
- _handshakeBuffer.Discard(frameSize);
- continue;
+ // We don't have full frame left or we already have app data which needs to be processed by decrypt.
+ break;
}
- break;
+ chunkSize += frameSize;
+ _handshakeBuffer.Discard(frameSize);
}
return _context!.NextMessage(availableData.Slice(0, chunkSize));
@@ -645,7 +684,7 @@ private async ValueTask ReadAsyncInternal(TIOAdapter adapter, M
Debug.Assert(_internalBufferCount >= SecureChannel.ReadHeaderSize);
// Parse the frame header to determine the payload size (which includes the header size).
- int payloadBytes = GetFrameSize(_internalBuffer.AsSpan(_internalOffset));
+ int payloadBytes = TlsFrameHelper.GetFrameSize(_internalBuffer.AsSpan(_internalOffset));
if (payloadBytes < 0)
{
throw new IOException(SR.net_frame_read_size);
@@ -913,6 +952,7 @@ private static byte[] EnsureBufferSize(byte[] buffer, int copyCount, int size)
Buffer.BlockCopy(saved, 0, buffer, 0, copyCount);
}
}
+
return buffer;
}
@@ -1003,8 +1043,8 @@ private Framing DetectFraming(ReadOnlySpan bytes)
}
// If the first byte is SSL3 HandShake, then check if we have a SSLv3 Type3 client hello.
- if (bytes[0] == (byte)FrameType.Handshake || bytes[0] == (byte)FrameType.AppData
- || bytes[0] == (byte)FrameType.Alert)
+ if (bytes[0] == (byte)TlsContentType.Handshake || bytes[0] == (byte)TlsContentType.AppData
+ || bytes[0] == (byte)TlsContentType.Alert)
{
if (bytes.Length < 3)
{
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs
index b72e549355fdcc..3cf26e783a9d06 100644
--- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs
+++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs
@@ -237,9 +237,8 @@ private static SecurityStatusPal HandshakeInternal(
sslContext = new SafeDeleteSslContext((credential as SafeFreeSslCredentials)!, sslAuthenticationOptions);
context = sslContext;
- if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost))
+ if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !sslAuthenticationOptions.IsServer)
{
- Debug.Assert(!sslAuthenticationOptions.IsServer, "targetName should not be set for server-side handshakes");
Interop.AppleCrypto.SslSetTargetName(sslContext.SslContext, sslAuthenticationOptions.TargetHost);
}
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
new file mode 100644
index 00000000000000..ec18be1b8e374b
--- /dev/null
+++ b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs
@@ -0,0 +1,235 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Security.Authentication;
+
+namespace System.Net.Security
+{
+ // SSL3/TLS protocol frames definitions.
+ internal enum TlsContentType : byte
+ {
+ ChangeCipherSpec = 20,
+ Alert = 21,
+ Handshake = 22,
+ AppData = 23
+ }
+
+ internal enum TlsHandshakeType : byte
+ {
+ HelloRequest = 0,
+ ClientHello = 1,
+ ServerHello = 2,
+ NewSessionTicket = 4,
+ EndOfEarlyData = 5,
+ EncryptedExtensions = 8,
+ Certificate = 11,
+ ServerKeyExchange = 12,
+ CertificateRequest = 13,
+ ServerHelloDone = 14,
+ CertificateVerify = 15,
+ ClientKeyExchange = 16,
+ Finished = 20,
+ KeyEpdate = 24,
+ MessageHash = 254
+ }
+
+ internal enum TlsAlertLevel : byte
+ {
+ Warning = 1,
+ Fatal = 2,
+ }
+
+ internal enum TlsAlertDescription : byte
+ {
+ CloseNotify = 0, // warning
+ UnexpectedMessage = 10, // error
+ BadRecordMac = 20, // error
+ DecryptionFailed = 21, // reserved
+ RecordOverflow = 22, // error
+ DecompressionFail = 30, // error
+ HandshakeFailure = 40, // error
+ BadCertificate = 42, // warning or error
+ UnsupportedCert = 43, // warning or error
+ CertificateRevoked = 44, // warning or error
+ CertificateExpired = 45, // warning or error
+ CertificateUnknown = 46, // warning or error
+ IllegalParameter = 47, // error
+ UnknownCA = 48, // error
+ AccessDenied = 49, // error
+ DecodeError = 50, // error
+ DecryptError = 51, // error
+ ExportRestriction = 60, // reserved
+ ProtocolVersion = 70, // error
+ InsuffientSecurity = 71, // error
+ InternalError = 80, // error
+ UserCanceled = 90, // warning or error
+ NoRenegotiation = 100, // warning
+ UnsupportedExt = 110, // error
+ }
+
+ internal struct TlsFrameHeader
+ {
+ public TlsContentType Type;
+ public SslProtocols Version;
+ public int Length;
+ }
+
+ internal struct TlsFrameHandshakeInfo
+ {
+ public TlsFrameHeader Header;
+ public TlsHandshakeType HandshakeType;
+ public SslProtocols SupportedVersions;
+ public string? TargetName;
+ }
+
+ internal class TlsFrameHelper
+ {
+ public const int HeaderSize = 5;
+
+ private static byte[] s_protocolMismatch13 = new byte[] { (byte)TlsContentType.Alert, 3, 4, 0, 2, 2, 70 };
+ private static byte[] s_protocolMismatch12 = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, 70 };
+ private static byte[] s_protocolMismatch11 = new byte[] { (byte)TlsContentType.Alert, 3, 2, 0, 2, 2, 70 };
+ private static byte[] s_protocolMismatch10 = new byte[] { (byte)TlsContentType.Alert, 3, 1, 0, 2, 2, 70 };
+
+ public static bool TryGetFrameHeader(ReadOnlySpan frame, ref TlsFrameHeader header)
+ {
+ bool result = frame.Length > 4;
+
+ if (frame.Length >= 1)
+ {
+ header.Type = (TlsContentType)frame[0];
+
+ if (frame.Length >= 3)
+ {
+ // SSLv3, TLS or later
+ if (frame[1] == 3)
+ {
+ if (frame.Length > 4)
+ {
+ header.Length = ((frame[3] << 8) | frame[4]);
+ }
+
+ switch (frame[2])
+ {
+ case 4:
+ header.Version = SslProtocols.Tls13;
+ break;
+ case 3:
+ header.Version = SslProtocols.Tls12;
+ break;
+ case 2:
+ header.Version = SslProtocols.Tls11;
+ break;
+ case 1:
+ header.Version = SslProtocols.Tls;
+ break;
+ case 0:
+#pragma warning disable 0618
+ header.Version = SslProtocols.Ssl3;
+#pragma warning restore 0618
+ break;
+ default:
+ header.Version = SslProtocols.None;
+ break;
+ }
+ }
+ else
+ {
+ header.Length = -1;
+ header.Version = SslProtocols.None;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Returns frame size e.g. header + content
+ public static int GetFrameSize(ReadOnlySpan frame)
+ {
+ if (frame.Length < 5 || frame[1] < 3)
+ {
+ return - 1;
+ }
+
+ return ((frame[3] << 8) | frame[4]) + HeaderSize;
+ }
+
+ public static bool TryGetHandshakeInfo(ReadOnlySpan frame, ref TlsFrameHandshakeInfo info)
+ {
+ if (frame.Length < 6 || frame[0] != (byte)TlsContentType.Handshake)
+ {
+ return false;
+ }
+
+ // This will not fail since we have enough data.
+ bool gotHeader = TryGetFrameHeader(frame, ref info.Header);
+ Debug.Assert(gotHeader);
+
+ info.SupportedVersions = info.Header.Version;
+
+ info.HandshakeType = (TlsHandshakeType)frame[5];
+
+ if (info.HandshakeType == TlsHandshakeType.ClientHello)
+ {
+ info.TargetName = SniHelper.GetServerName(frame);
+ }
+
+ return true;
+ }
+
+ public static bool TryGetAlertInfo(ReadOnlySpan frame, ref TlsAlertLevel level, ref TlsAlertDescription description)
+ {
+ if (frame.Length < 7 || frame[0] != (byte)TlsContentType.Alert)
+ {
+ return false;
+ }
+
+ level = (TlsAlertLevel)frame[5];
+ description = (TlsAlertDescription)frame[6];
+
+ return true;
+ }
+
+ private static byte[] CreateProtocolVersionAlert(SslProtocols version) =>
+ version switch
+ {
+ SslProtocols.Tls13 => s_protocolMismatch13,
+ SslProtocols.Tls12 => s_protocolMismatch12,
+ SslProtocols.Tls11 => s_protocolMismatch11,
+ SslProtocols.Tls => s_protocolMismatch10,
+ _ => Array.Empty(),
+ };
+
+ public static byte[] CreateAlertFrame(SslProtocols version, TlsAlertDescription reason)
+ {
+ if (reason == TlsAlertDescription.ProtocolVersion)
+ {
+ return CreateProtocolVersionAlert(version);
+ }
+ else if ((int)version > (int)SslProtocols.Tls)
+ {
+ // Create TLS1.2 alert
+ byte[] buffer = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, (byte)reason };
+ switch (version)
+ {
+ case SslProtocols.Tls13:
+ buffer[2] = 4;
+ break;
+ case SslProtocols.Tls11:
+ buffer[2] = 2;
+ break;
+ case SslProtocols.Tls:
+ buffer[2] = 1;
+ break;
+ }
+
+ return buffer;
+ }
+
+ return Array.Empty();
+ }
+ }
+}
diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/ClientAsyncAuthenticateTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/ClientAsyncAuthenticateTest.cs
index 69c96735809da4..5c227192f2448c 100644
--- a/src/libraries/System.Net.Security/tests/FunctionalTests/ClientAsyncAuthenticateTest.cs
+++ b/src/libraries/System.Net.Security/tests/FunctionalTests/ClientAsyncAuthenticateTest.cs
@@ -44,11 +44,7 @@ public async Task ClientAsyncAuthenticate_ServerRequireEncryption_ConnectWithEnc
public async Task ClientAsyncAuthenticate_ServerNoEncryption_NoConnect()
{
// Don't use Tls13 since we are trying to use NullEncryption
- Type expectedExceptionType = TestConfiguration.SupportsHandshakeAlerts && TestConfiguration.SupportsNullEncryption ?
- typeof(AuthenticationException) :
- typeof(IOException);
-
- await Assert.ThrowsAsync(expectedExceptionType,
+ await Assert.ThrowsAsync(
() => ClientAsyncSslHelper(
EncryptionPolicy.NoEncryption,
SslProtocolSupport.DefaultSslProtocols, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 ));
@@ -120,12 +116,12 @@ public static IEnumerable