diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs index 3d219c7378c8a..0558485b6c4aa 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Security.Authentication; using static Interop.SspiCli; namespace System.Net.Security @@ -66,7 +67,17 @@ public void UpdateSslConnectionInfo(SafeDeleteContext securityContext) TlsCipherSuite = cipherSuite; - ApplicationProtocol = GetNegotiatedApplicationProtocol(securityContext); + // In TLS1.3, Schannel may erroneously report empty ALPN after + // receiving resumption ticket (fake Renegotiation). Avoid updating + // ApplicationProtocol in this case if we already have some, TLS1.3 + // does not allow ALPN changes after the initial handshake. + // + // TLS 1.2 and below theoretically support ALPN changes during + // Renegotiation. + if (ApplicationProtocol == null || (Protocol & (int)SslProtocols.Tls13) == 0) + { + ApplicationProtocol = GetNegotiatedApplicationProtocol(securityContext); + } #if DEBUG SecPkgContext_SessionInfo info = default; diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs index a4882469fccc7..c441c8f70d62d 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Sockets; using System.Net.Test.Common; using System.Security.Authentication; @@ -31,21 +32,6 @@ protected SslStreamAlpnTestBase(ITestOutputHelper output) _output = output; } - private async Task DoHandshakeWithOptions(SslStream clientSslStream, SslStream serverSslStream, SslClientAuthenticationOptions clientOptions, SslServerAuthenticationOptions serverOptions) - { - using (X509Certificate2 certificate = Configuration.Certificates.GetServerCertificate()) - { - clientOptions.RemoteCertificateValidationCallback = AllowAnyServerCertificate; - clientOptions.TargetHost = certificate.GetNameInfo(X509NameType.SimpleName, false); - serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(certificate, null); - - Task t1 = clientSslStream.AuthenticateAsClientAsync(TestAuthenticateAsync, clientOptions); - Task t2 = serverSslStream.AuthenticateAsServerAsync(TestAuthenticateAsync, serverOptions); - - await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); - } - } - protected bool AllowAnyServerCertificate( object sender, X509Certificate certificate, @@ -101,28 +87,48 @@ public async Task SslStream_StreamToStream_DuplicateOptions_Throws() [Theory] [MemberData(nameof(Alpn_TestData))] - public async Task SslStream_StreamToStream_Alpn_Success(List clientProtocols, List serverProtocols, SslApplicationProtocol expected) + public async Task SslStream_StreamToStream_Alpn_Success(SslProtocols protocol, List clientProtocols, List serverProtocols, SslApplicationProtocol expected) { - (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); - using (clientStream) - using (serverStream) - using (var client = new SslStream(clientStream, false)) - using (var server = new SslStream(serverStream, false)) + using X509Certificate2 certificate = Configuration.Certificates.GetServerCertificate(); + + SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions { - SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions - { - ApplicationProtocols = clientProtocols, - }; + ApplicationProtocols = clientProtocols, + EnabledSslProtocols = protocol, + RemoteCertificateValidationCallback = delegate { return true; }, + TargetHost = Guid.NewGuid().ToString("N"), + }; - SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions + SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions + { + ApplicationProtocols = serverProtocols, + EnabledSslProtocols = protocol, + ServerCertificateContext = SslStreamCertificateContext.Create(certificate, null) + }; + + // We do multiple loops to also cover credential cache and TLS resume. + for (int i = 0; i < 3; i++) + { + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (var client = new SslStream(clientStream, false)) + using (var server = new SslStream(serverStream, false)) { - ApplicationProtocols = serverProtocols, - }; + Task t1 = client.AuthenticateAsClientAsync(TestAuthenticateAsync, clientOptions); + Task t2 = server.AuthenticateAsServerAsync(TestAuthenticateAsync, serverOptions); - await DoHandshakeWithOptions(client, server, clientOptions, serverOptions); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); + + Assert.Equal(expected, client.NegotiatedApplicationProtocol); + Assert.Equal(expected, server.NegotiatedApplicationProtocol); - Assert.Equal(expected, client.NegotiatedApplicationProtocol); - Assert.Equal(expected, server.NegotiatedApplicationProtocol); + await TestHelper.PingPong(client, server); + await TestHelper.PingPong(server, client); + + Assert.Equal(expected, client.NegotiatedApplicationProtocol); + Assert.Equal(expected, server.NegotiatedApplicationProtocol); + } } } @@ -184,7 +190,7 @@ public async Task SslStream_Http2_Alpn_Success(Uri server) { SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions { - ApplicationProtocols = new List { SslApplicationProtocol.Http2 , SslApplicationProtocol.Http11 }, + ApplicationProtocols = new List { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }, TargetHost = server.Host }; @@ -202,16 +208,37 @@ public async Task SslStream_Http2_Alpn_Success(Uri server) public static IEnumerable Alpn_TestData() { - yield return new object[] { new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, new List { SslApplicationProtocol.Http2 }, BackendSupportsAlpn ? SslApplicationProtocol.Http2 : default }; - yield return new object[] { new List { SslApplicationProtocol.Http11 }, new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, BackendSupportsAlpn ? SslApplicationProtocol.Http11 : default }; - yield return new object[] { new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, BackendSupportsAlpn ? SslApplicationProtocol.Http11 : default }; - yield return new object[] { null, new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, default(SslApplicationProtocol) }; - yield return new object[] { new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, new List(), default(SslApplicationProtocol) }; - yield return new object[] { new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, null, default(SslApplicationProtocol) }; - yield return new object[] { new List(), new List(), default(SslApplicationProtocol) }; - yield return new object[] { null, new List(), default(SslApplicationProtocol) }; - yield return new object[] { new List(), null, default(SslApplicationProtocol) }; - yield return new object[] { null, null, default(SslApplicationProtocol) }; + SslApplicationProtocol h1 = SslApplicationProtocol.Http11; + SslApplicationProtocol h2 = SslApplicationProtocol.Http2; + List list_empty = []; + List list_h1 = [h1]; + List list_h2 = [h2]; + List list_both = [h1, h2]; + + foreach (var protocol in new SslProtocolSupport.SupportedSslProtocolsTestData().Concat(new[] { new object[] { SslProtocols.None } })) + { + var proto = protocol[0]; +#pragma warning disable 0618 // SSL2/3 are deprecated + if (proto.Equals(SslProtocols.Ssl3) || proto.Equals(SslProtocols.Ssl2)) +#pragma warning restore 0618 + { + // ALPN not supported by this protocol + continue; + } + + yield return new object[] { proto, list_both, list_h2, BackendSupportsAlpn ? h2 : default }; + yield return new object[] { proto, list_h1, list_both, BackendSupportsAlpn ? h1 : default }; + + yield return new object[] { proto, list_both, list_both, BackendSupportsAlpn ? h1 : default }; + yield return new object[] { proto, null, list_both, default(SslApplicationProtocol) }; + yield return new object[] { proto, list_both, list_empty, default(SslApplicationProtocol) }; + yield return new object[] { proto, list_both, null, default(SslApplicationProtocol) }; + + yield return new object[] { proto, list_empty, list_empty, default(SslApplicationProtocol) }; + yield return new object[] { proto, null, list_empty, default(SslApplicationProtocol) }; + yield return new object[] { proto, list_empty, null, default(SslApplicationProtocol) }; + yield return new object[] { proto, null, null, default(SslApplicationProtocol) }; + } } } @@ -220,7 +247,7 @@ public sealed class SslStreamAlpnTest_Async : SslStreamAlpnTestBase public override bool TestAuthenticateAsync => true; public SslStreamAlpnTest_Async(ITestOutputHelper output) - : base (output) { } + : base(output) { } } public sealed class SslStreamAlpnTest_Sync : SslStreamAlpnTestBase