From 40bce0203fe31ec78353e982d017fa63749d10a5 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 18 Apr 2023 12:42:00 +0200 Subject: [PATCH 01/12] Add TargetHostName to QuicConnection Fixes #80508 --- .../Net/Security/TargetHostNameHelper.cs | 83 +++++++++++++++++++ .../System.Net.Quic/ref/System.Net.Quic.cs | 2 + .../src/System.Net.Quic.csproj | 4 + .../QuicConnection.SslConnectionOptions.cs | 5 +- .../src/System/Net/Quic/QuicConnection.cs | 12 ++- .../FunctionalTests/QuicConnectionTests.cs | 5 +- .../src/System.Net.Security.csproj | 2 + .../Net/Security/SslAuthenticationOptions.cs | 69 +-------------- .../SslAuthenticationOptionsTest.cs | 1 - 9 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs new file mode 100644 index 0000000000000..fc7474f98825a --- /dev/null +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace System.Net.Security +{ + internal static class TargetHostNameHelper + { + private static readonly IdnMapping s_idnMapping = new IdnMapping(); + private static readonly IndexOfAnyValues s_safeDnsChars = + IndexOfAnyValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"); + + private static bool IsSafeDnsString(ReadOnlySpan name) => + name.IndexOfAnyExcept(s_safeDnsChars) < 0; + + internal static string NormalizeHostName(string? targetHost) + { + if (string.IsNullOrEmpty(targetHost)) + { + return string.Empty; + } + + // RFC 6066 section 3 says to exclude trailing dot from fully qualified DNS hostname + targetHost = targetHost.TrimEnd('.'); + + // RFC 6066 forbids IP literals + if (IsValidAddress(targetHost)) + { + return targetHost; + } + + try + { + return s_idnMapping.GetAscii(targetHost); + } + catch (ArgumentException) when (IsSafeDnsString(targetHost)) + { + // Seems like name that does not confrom to IDN but apers somewhat valid according to original DNS rfc. + } + + return targetHost; + } + + // Simplified version of IPAddressParser.Parse to avoid allocations and dependencies. + // It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id. + private static unsafe bool IsValidAddress(ReadOnlySpan ipSpan) + { + int end = ipSpan.Length; + + if (ipSpan.Contains(':')) + { + // The address is parsed as IPv6 if and only if it contains a colon. This is valid because + // we don't support/parse a port specification at the end of an IPv4 address. + Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; + + fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); + } + } + else if (char.IsDigit(ipSpan[0])) + { + long tmpAddr; + + fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) + { + tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); + } + + if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) + { + return true; + } + } + + return false; + } + + } +} diff --git a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs index dccd0daf4c8e3..932fdc36382dc 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs @@ -28,6 +28,7 @@ internal QuicConnection() { } public System.Net.Security.SslApplicationProtocol NegotiatedApplicationProtocol { get { throw null; } } public System.Security.Cryptography.X509Certificates.X509Certificate? RemoteCertificate { get { throw null; } } public System.Net.IPEndPoint RemoteEndPoint { get { throw null; } } + public string? TargetHostName { get { throw null; } } public System.Threading.Tasks.ValueTask AcceptInboundStreamAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.ValueTask CloseAsync(long errorCode, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask ConnectAsync(System.Net.Quic.QuicClientConnectionOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -122,6 +123,7 @@ public override void Flush() { } public override int ReadByte() { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } + public override string ToString() { throw null; } public override void Write(byte[] buffer, int offset, int count) { } public override void Write(System.ReadOnlySpan buffer) { } public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index d3864b6c24cb7..c3b0772f0b2a0 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -32,6 +32,10 @@ + + + + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs index d376a5a3079d6..145f399b4a6ed 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs @@ -28,7 +28,8 @@ private readonly struct SslConnectionOptions /// /// Host name send in SNI, set only for outbound/client connections. Configured via . /// - private readonly string? _targetHost; + internal string TargetHost => _targetHost; + private readonly string _targetHost; /// /// Always true for outbound/client connections. Configured for inbound/server ones via . /// @@ -48,7 +49,7 @@ private readonly struct SslConnectionOptions private readonly X509ChainPolicy? _certificateChainPolicy; public SslConnectionOptions(QuicConnection connection, bool isClient, - string? targetHost, bool certificateRequired, X509RevocationMode + string targetHost, bool certificateRequired, X509RevocationMode revocationMode, RemoteCertificateValidationCallback? validationCallback, X509ChainPolicy? certificateChainPolicy) { diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index fdc06d3163224..68e7beebe795e 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -46,6 +46,12 @@ public sealed partial class QuicConnection : IAsyncDisposable /// public static bool IsSupported => MsQuicApi.IsQuicSupported; + /// + /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address. + /// + /// The name of the server the client is trying to connect to. + public string? TargetHostName => _sslConnectionOptions.TargetHost; + /// /// Creates a new and connects it to the peer. /// @@ -282,7 +288,7 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, _sslConnectionOptions = new SslConnectionOptions( this, isClient: true, - options.ClientAuthenticationOptions.TargetHost, + TargetHostNameHelper.NormalizeHostName(options.ClientAuthenticationOptions.TargetHost), certificateRequired: true, options.ClientAuthenticationOptions.CertificateRevocationCheckMode, options.ClientAuthenticationOptions.RemoteCertificateValidationCallback, @@ -312,7 +318,7 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, await valueTask.ConfigureAwait(false); } - internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string? targetHost, CancellationToken cancellationToken = default) + internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string targetHost, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed == 1, this); @@ -325,7 +331,7 @@ internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, str _sslConnectionOptions = new SslConnectionOptions( this, isClient: false, - targetHost: null, + targetHost: targetHost, options.ServerAuthenticationOptions.ClientCertificateRequired, options.ServerAuthenticationOptions.CertificateRevocationCheckMode, options.ServerAuthenticationOptions.RemoteCertificateValidationCallback, diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs index a12a14e8c9eb0..f2c34ebda10b5 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs @@ -22,7 +22,8 @@ public async Task TestConnect() { await using QuicListener listener = await CreateQuicListener(); - ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); + var options = CreateQuicClientOptions(listener.LocalEndPoint); + ValueTask connectTask = CreateQuicConnection(options); ValueTask acceptTask = listener.AcceptConnectionAsync(); await new Task[] { connectTask.AsTask(), acceptTask.AsTask() }.WhenAllOrAnyFailed(PassingTestTimeoutMilliseconds); @@ -34,6 +35,8 @@ public async Task TestConnect() Assert.Equal(clientConnection.LocalEndPoint, serverConnection.RemoteEndPoint); Assert.Equal(ApplicationProtocol.ToString(), clientConnection.NegotiatedApplicationProtocol.ToString()); Assert.Equal(ApplicationProtocol.ToString(), serverConnection.NegotiatedApplicationProtocol.ToString()); + Assert.Equal(options.ClientAuthenticationOptions.TargetHost, clientConnection.TargetHostName); + Assert.Equal(options.ClientAuthenticationOptions.TargetHost, serverConnection.TargetHostName); } private static async Task OpenAndUseStreamAsync(QuicConnection c) 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 54f8c9feeb4d6..2de2f39ee186b 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -92,6 +92,8 @@ Link="Common\System\NotImplemented.cs" /> + ipSpan) - { - int end = ipSpan.Length; - - if (ipSpan.Contains(':')) - { - // The address is parsed as IPv6 if and only if it contains a colon. This is valid because - // we don't support/parse a port specification at the end of an IPv4 address. - Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; - - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end); - } - } - else if (char.IsDigit(ipSpan[0])) - { - long tmpAddr; - - fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan)) - { - tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true); - } - - if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length) - { - return true; - } - } - - return false; - } - - private static readonly IndexOfAnyValues s_safeDnsChars = - IndexOfAnyValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"); - - private static bool IsSafeDnsString(ReadOnlySpan name) => - name.IndexOfAnyExcept(s_safeDnsChars) < 0; - internal SslAuthenticationOptions() { TargetHost = string.Empty; @@ -93,29 +48,7 @@ internal void UpdateOptions(SslClientAuthenticationOptions sslClientAuthenticati IsServer = false; RemoteCertRequired = true; CertificateContext = sslClientAuthenticationOptions.ClientCertificateContext; - if (!string.IsNullOrEmpty(sslClientAuthenticationOptions.TargetHost)) - { - // RFC 6066 section 3 says to exclude trailing dot from fully qualified DNS hostname - string targetHost = sslClientAuthenticationOptions.TargetHost.TrimEnd('.'); - - // RFC 6066 forbids IP literals - if (IsValidAddress(targetHost)) - { - TargetHost = string.Empty; - } - else - { - try - { - TargetHost = s_idnMapping.GetAscii(targetHost); - } - catch (ArgumentException) when (IsSafeDnsString(targetHost)) - { - // Seems like name that does not confrom to IDN but apers somewhat valid according to orogional DNS rfc. - TargetHost = targetHost; - } - } - } + TargetHost = TargetHostNameHelper.NormalizeHostName(sslClientAuthenticationOptions.TargetHost); // Client specific options. CertificateRevocationCheckMode = sslClientAuthenticationOptions.CertificateRevocationCheckMode; diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs index 0029de46bfc3b..0ec5374b895c8 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs @@ -101,7 +101,6 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() Assert.Same(clientLocalCallback, clientOptions.LocalCertificateSelectionCallback); Assert.Same(clientRemoteCallback, clientOptions.RemoteCertificateValidationCallback); Assert.Same(clientHost, clientOptions.TargetHost); - Assert.Same(clientHost, clientOptions.TargetHost); Assert.Same(policy, clientOptions.CertificateChainPolicy); // Validate that server options are unchanged From d6bd50e0d852e09fd17d8f083ffd4d93f1c4d66f Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 18 Apr 2023 12:46:40 +0200 Subject: [PATCH 02/12] Make TargetHostName not nullable --- src/libraries/System.Net.Quic/ref/System.Net.Quic.cs | 2 +- .../System.Net.Quic/src/System/Net/Quic/QuicConnection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs index 932fdc36382dc..932596828d755 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs @@ -28,7 +28,7 @@ internal QuicConnection() { } public System.Net.Security.SslApplicationProtocol NegotiatedApplicationProtocol { get { throw null; } } public System.Security.Cryptography.X509Certificates.X509Certificate? RemoteCertificate { get { throw null; } } public System.Net.IPEndPoint RemoteEndPoint { get { throw null; } } - public string? TargetHostName { get { throw null; } } + public string TargetHostName { get { throw null; } } public System.Threading.Tasks.ValueTask AcceptInboundStreamAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.ValueTask CloseAsync(long errorCode, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask ConnectAsync(System.Net.Quic.QuicClientConnectionOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 68e7beebe795e..d5e3cb97f5992 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -50,7 +50,7 @@ public sealed partial class QuicConnection : IAsyncDisposable /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address. /// /// The name of the server the client is trying to connect to. - public string? TargetHostName => _sslConnectionOptions.TargetHost; + public string TargetHostName => _sslConnectionOptions?.TargetHost ?? string.Empty; /// /// Creates a new and connects it to the peer. From 40af6e31d880efdc101268ade988e3b7d28f9dec Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 18 Apr 2023 12:55:05 +0200 Subject: [PATCH 03/12] Fix build --- .../System.Net.Quic/src/System/Net/Quic/QuicConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index d5e3cb97f5992..1aae6264a6d40 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -50,7 +50,7 @@ public sealed partial class QuicConnection : IAsyncDisposable /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address. /// /// The name of the server the client is trying to connect to. - public string TargetHostName => _sslConnectionOptions?.TargetHost ?? string.Empty; + public string TargetHostName => _sslConnectionOptions.TargetHost ?? string.Empty; /// /// Creates a new and connects it to the peer. From f34abe2b35375976a321cb36385346cb15dee298 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 18 Apr 2023 17:35:52 +0200 Subject: [PATCH 04/12] Fix build of tests --- .../tests/UnitTests/System.Net.Security.Unit.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index 6618a8d6fc113..81dc725b0af91 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -50,6 +50,8 @@ Link="Common\System\Net\Security\RC4.cs" /> + Date: Tue, 18 Apr 2023 21:00:40 +0200 Subject: [PATCH 05/12] Fix failing tests --- .../Common/src/System/Net/Security/TargetHostNameHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index fc7474f98825a..e3ed63a4bac17 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -29,7 +29,7 @@ internal static string NormalizeHostName(string? targetHost) // RFC 6066 forbids IP literals if (IsValidAddress(targetHost)) { - return targetHost; + return string.Empty; } try From 04dcff6fec4d686920db01a5ee4937cb5f2d87d9 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 20 Apr 2023 20:55:12 +0200 Subject: [PATCH 06/12] Code review feedback --- .../Net/Security/TargetHostNameHelper.cs | 1 - .../src/System.Net.Quic.csproj | 4 -- .../QuicConnection.SslConnectionOptions.cs | 3 +- .../src/System/Net/Quic/QuicConnection.cs | 14 +++--- .../tests/FunctionalTests/MsQuicTests.cs | 48 +++++++++++++++++++ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index e3ed63a4bac17..341a01e42874f 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -78,6 +78,5 @@ private static unsafe bool IsValidAddress(ReadOnlySpan ipSpan) return false; } - } } diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index c3b0772f0b2a0..d3864b6c24cb7 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -32,10 +32,6 @@ - - - - diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs index 145f399b4a6ed..47b9118bab60a 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs @@ -28,7 +28,6 @@ private readonly struct SslConnectionOptions /// /// Host name send in SNI, set only for outbound/client connections. Configured via . /// - internal string TargetHost => _targetHost; private readonly string _targetHost; /// /// Always true for outbound/client connections. Configured for inbound/server ones via . @@ -48,6 +47,8 @@ private readonly struct SslConnectionOptions /// private readonly X509ChainPolicy? _certificateChainPolicy; + internal string TargetHost => _targetHost; + public SslConnectionOptions(QuicConnection connection, bool isClient, string targetHost, bool certificateRequired, X509RevocationMode revocationMode, RemoteCertificateValidationCallback? validationCallback, diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 1aae6264a6d40..39b288e1faae6 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -46,12 +46,6 @@ public sealed partial class QuicConnection : IAsyncDisposable /// public static bool IsSupported => MsQuicApi.IsQuicSupported; - /// - /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address. - /// - /// The name of the server the client is trying to connect to. - public string TargetHostName => _sslConnectionOptions.TargetHost ?? string.Empty; - /// /// Creates a new and connects it to the peer. /// @@ -155,6 +149,12 @@ public static async ValueTask ConnectAsync(QuicClientConnectionO /// public IPEndPoint LocalEndPoint => _localEndPoint; + /// + /// Gets the name of the server the client is trying to connect to. That name is used for server certificate validation. It can be a DNS name or an IP address. + /// + /// The name of the server the client is trying to connect to. + public string TargetHostName => _sslConnectionOptions.TargetHost ?? string.Empty; + /// /// The certificate provided by the peer. /// For an outbound/client connection will always have the peer's (server) certificate; for an inbound/server one, only if the connection requested and the peer (client) provided one. @@ -288,7 +288,7 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, _sslConnectionOptions = new SslConnectionOptions( this, isClient: true, - TargetHostNameHelper.NormalizeHostName(options.ClientAuthenticationOptions.TargetHost), + options.ClientAuthenticationOptions.TargetHost ?? "", certificateRequired: true, options.ClientAuthenticationOptions.CertificateRevocationCheckMode, options.ClientAuthenticationOptions.RemoteCertificateValidationCallback, diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 5812a9b69c17d..4e24bcadfdd0d 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -1204,5 +1204,53 @@ public async Task IdleTimeout_ThrowsQuicException() await AssertThrowsQuicExceptionAsync(QuicError.ConnectionIdle, async () => await acceptTask).WaitAsync(TimeSpan.FromSeconds(10)); } } + + [Theory] + [MemberData(nameof(HostNameData))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] + public async Task ClientSendsSniServerReceives_Ok(string hostName) + { + using X509Certificate serverCert = Configuration.Certificates.GetSelfSignedServerCertificate(); + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificateContext = null; + serverOptions.ServerAuthenticationOptions.ServerCertificate = null; + serverOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, actualHostName) => + { + Assert.Equal(hostName, actualHostName); + return serverCert; + }; + return ValueTask.FromResult(serverOptions); + } + }; + + // Use whatever endpoint, it'll get overwritten in CreateConnectedQuicConnection. + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listenerOptions.ListenEndPoint); + clientOptions.ClientAuthenticationOptions.TargetHost = hostName; + clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; }; + + + (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(clientOptions, listenerOptions); + await using (clientConnection) + await using (serverConnection) + { + Assert.Equal(clientConnection.TargetHostName, hostName); + Assert.Equal(serverConnection.TargetHostName, hostName); + } + } + + public static IEnumerable HostNameData() + { + yield return new object[] { "a" }; + yield return new object[] { "test" }; + // max allowed hostname length is 63 + yield return new object[] { new string('a', 63) }; + yield return new object[] { "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 ja\u017A\u0144. \u7EA2\u70E7. \u7167\u308A\u713C\u304D" }; + } } } From a5c124bfa45e523d2f62deb89de0ea6e28bf23f7 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 21 Apr 2023 10:26:20 +0200 Subject: [PATCH 07/12] Use unencoded hostname in user-facing properties/params --- .../System/Net/Security/TargetHostNameHelper.cs | 15 ++++++++------- .../Net/Security/SslAuthenticationOptions.cs | 6 +++++- .../src/System/Net/Security/SslStream.Protocol.cs | 5 +++-- .../tests/FunctionalTests/SslStreamSniTest.cs | 9 ++++++--- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs index 341a01e42874f..1457792b34c63 100644 --- a/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs +++ b/src/libraries/Common/src/System/Net/Security/TargetHostNameHelper.cs @@ -26,12 +26,6 @@ internal static string NormalizeHostName(string? targetHost) // RFC 6066 section 3 says to exclude trailing dot from fully qualified DNS hostname targetHost = targetHost.TrimEnd('.'); - // RFC 6066 forbids IP literals - if (IsValidAddress(targetHost)) - { - return string.Empty; - } - try { return s_idnMapping.GetAscii(targetHost); @@ -46,8 +40,15 @@ internal static string NormalizeHostName(string? targetHost) // Simplified version of IPAddressParser.Parse to avoid allocations and dependencies. // It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id. - private static unsafe bool IsValidAddress(ReadOnlySpan ipSpan) + internal static unsafe bool IsValidAddress(string? hostname) { + if (string.IsNullOrEmpty(hostname)) + { + return false; + } + + ReadOnlySpan ipSpan = hostname.AsSpan(); + int end = ipSpan.Length; if (ipSpan.Contains(':')) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs index 63fce9d7d9515..36a3cbbad331e 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs @@ -48,7 +48,11 @@ internal void UpdateOptions(SslClientAuthenticationOptions sslClientAuthenticati IsServer = false; RemoteCertRequired = true; CertificateContext = sslClientAuthenticationOptions.ClientCertificateContext; - TargetHost = TargetHostNameHelper.NormalizeHostName(sslClientAuthenticationOptions.TargetHost); + + // RFC 6066 forbids IP literals + TargetHost = TargetHostNameHelper.IsValidAddress(sslClientAuthenticationOptions.TargetHost) + ? string.Empty + : sslClientAuthenticationOptions.TargetHost ?? string.Empty; // Client specific options. CertificateRevocationCheckMode = sslClientAuthenticationOptions.CertificateRevocationCheckMode; diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 6ca7dcf5a9d95..e9446f6ad1b9a 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -844,10 +844,11 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte } else { + string hostName = TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost); status = SslStreamPal.InitializeSecurityContext( ref _credentialsHandle!, ref _securityContext, - _sslAuthenticationOptions.TargetHost, + hostName, inputBuffer, ref result, _sslAuthenticationOptions); @@ -863,7 +864,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte status = SslStreamPal.InitializeSecurityContext( ref _credentialsHandle!, ref _securityContext, - _sslAuthenticationOptions.TargetHost, + hostName, ReadOnlySpan.Empty, ref result, _sslAuthenticationOptions); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 4a0cdf41d837e..50f742a05b5fb 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs @@ -44,6 +44,8 @@ await WithVirtualConnection(async (server, client) => await TaskTimeoutExtensions.WhenAllOrAnyFailed(new[] { clientJob, server.AuthenticateAsServerAsync(options, CancellationToken.None) }); Assert.Equal(1, timesCallbackCalled); + Assert.Equal(hostName, server.TargetHostName); + Assert.Equal(hostName, client.TargetHostName); }, (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => { @@ -200,6 +202,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( server.AuthenticateAsServerAsync(serverOptions, default)); Assert.Equal(string.Empty, server.TargetHostName); + Assert.Equal(string.Empty, client.TargetHostName); } [Theory] @@ -320,10 +323,10 @@ private async Task WithVirtualConnection(Func server public static IEnumerable HostNameData() { - yield return new object[] { "a" }; - yield return new object[] { "test" }; + // yield return new object[] { "a" }; + // yield return new object[] { "test" }; // max allowed hostname length is 63 - yield return new object[] { new string('a', 63) }; + // yield return new object[] { new string('a', 63) }; yield return new object[] { "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 ja\u017A\u0144. \u7EA2\u70E7. \u7167\u308A\u713C\u304D" }; } } From aae6f4807b898364822fe33e1e58909888f33e89 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 21 Apr 2023 10:26:32 +0200 Subject: [PATCH 08/12] Fix failing tests --- .../src/System.Net.Quic.csproj | 4 ++ .../src/System/Net/Quic/QuicConnection.cs | 8 +++- .../tests/FunctionalTests/MsQuicTests.cs | 37 +++++++++++-------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index d3864b6c24cb7..c3b0772f0b2a0 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -32,6 +32,10 @@ + + + + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 39b288e1faae6..696245482a6fe 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -285,10 +285,16 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, MsQuicHelpers.SetMsQuicParameter(_handle, QUIC_PARAM_CONN_LOCAL_ADDRESS, quicAddress); } + // RFC 6066 forbids IP literals + // DNI mapping is handled by MsQuic + var hostname = TargetHostNameHelper.IsValidAddress(options.ClientAuthenticationOptions.TargetHost) + ? string.Empty + : options.ClientAuthenticationOptions.TargetHost ?? string.Empty; + _sslConnectionOptions = new SslConnectionOptions( this, isClient: true, - options.ClientAuthenticationOptions.TargetHost ?? "", + hostname, certificateRequired: true, options.ClientAuthenticationOptions.CertificateRevocationCheckMode, options.ClientAuthenticationOptions.RemoteCertificateValidationCallback, diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 4e24bcadfdd0d..345aa6d236a58 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -1205,11 +1205,10 @@ public async Task IdleTimeout_ThrowsQuicException() } } - [Theory] - [MemberData(nameof(HostNameData))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] - public async Task ClientSendsSniServerReceives_Ok(string hostName) + private async Task SniTestCore(string hostname, bool shouldSendSni) { + string expectedHostName = shouldSendSni ? hostname : string.Empty; + using X509Certificate serverCert = Configuration.Certificates.GetSelfSignedServerCertificate(); var listenerOptions = new QuicListenerOptions() { @@ -1222,7 +1221,7 @@ public async Task ClientSendsSniServerReceives_Ok(string hostName) serverOptions.ServerAuthenticationOptions.ServerCertificate = null; serverOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, actualHostName) => { - Assert.Equal(hostName, actualHostName); + Assert.Equal(expectedHostName, actualHostName); return serverCert; }; return ValueTask.FromResult(serverOptions); @@ -1231,7 +1230,7 @@ public async Task ClientSendsSniServerReceives_Ok(string hostName) // Use whatever endpoint, it'll get overwritten in CreateConnectedQuicConnection. QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listenerOptions.ListenEndPoint); - clientOptions.ClientAuthenticationOptions.TargetHost = hostName; + clientOptions.ClientAuthenticationOptions.TargetHost = hostname; clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; }; @@ -1239,18 +1238,24 @@ public async Task ClientSendsSniServerReceives_Ok(string hostName) await using (clientConnection) await using (serverConnection) { - Assert.Equal(clientConnection.TargetHostName, hostName); - Assert.Equal(serverConnection.TargetHostName, hostName); + Assert.Equal(expectedHostName, clientConnection.TargetHostName); + Assert.Equal(expectedHostName, serverConnection.TargetHostName); } } - public static IEnumerable HostNameData() - { - yield return new object[] { "a" }; - yield return new object[] { "test" }; - // max allowed hostname length is 63 - yield return new object[] { new string('a', 63) }; - yield return new object[] { "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 ja\u017A\u0144. \u7EA2\u70E7. \u7167\u308A\u713C\u304D" }; - } + [Theory] + [InlineData("a")] + [InlineData("test")] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] // max allowed hostname length is 63 + [InlineData("\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 ja\u017A\u0144. \u7EA2\u70E7. \u7167\u308A\u713C\u304D")] + public Task ClientSendsSniServerReceives_Ok(string hostname) => SniTestCore(hostname, true); + + [Theory] + [InlineData("127.0.0.1")] + [InlineData("::1")] + [InlineData("2001:11:22::1")] + [InlineData("fe80::9c3a:b64d:6249:1de8%2")] + [InlineData("fe80::9c3a:b64d:6249:1de8")] + public Task DoesNotSendIPAsSni(string target) => SniTestCore(target, false); } } From 2fb014a6c0405d11d064b155b47d1e36e9905d58 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 21 Apr 2023 10:27:12 +0200 Subject: [PATCH 09/12] Revert unwanted changes --- .../tests/FunctionalTests/SslStreamSniTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 50f742a05b5fb..507bc3a8f70c9 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs @@ -323,10 +323,10 @@ private async Task WithVirtualConnection(Func server public static IEnumerable HostNameData() { - // yield return new object[] { "a" }; - // yield return new object[] { "test" }; + yield return new object[] { "a" }; + yield return new object[] { "test" }; // max allowed hostname length is 63 - // yield return new object[] { new string('a', 63) }; + yield return new object[] { new string('a', 63) }; yield return new object[] { "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 ja\u017A\u0144. \u7EA2\u70E7. \u7167\u308A\u713C\u304D" }; } } From 98b827b31a2751ad63e52c1ef4afccdbd184f417 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 21 Apr 2023 11:00:12 +0200 Subject: [PATCH 10/12] Add test for IDN cert validation --- .../tests/FunctionalTests/SslStreamSniTest.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 507bc3a8f70c9..8862f324ee2c6 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Test.Common; +using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -235,6 +236,49 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } + [Fact] + public async Task UnencodedHostName_ValidatesCertificate() + { + string rawHostname = "räksmörgås.josefsson.org"; + string punycodeHostname = "xn--rksmrgs-5wao1o.josefsson.org"; + + var (serverCert, serverChain) = TestHelper.GenerateCertificates(punycodeHostname); + try + { + SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions() + { + ServerCertificateContext = SslStreamCertificateContext.Create(serverCert, serverChain), + }; + + SslClientAuthenticationOptions clientOptions = new () + { + TargetHost = rawHostname, + CertificateChainPolicy = new X509ChainPolicy() + { + RevocationMode = X509RevocationMode.NoCheck, + TrustMode = X509ChainTrustMode.CustomRootTrust, + CustomTrustStore = { serverChain[serverChain.Count - 1] } + } + }; + + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions, default), + server.AuthenticateAsServerAsync(serverOptions, default)); + + await TestHelper.PingPong(client, server, default); + Assert.Equal(rawHostname, server.TargetHostName); + Assert.Equal(rawHostname, client.TargetHostName); + } + finally + { + serverCert.Dispose(); + foreach (var c in serverChain) c.Dispose(); + TestHelper.CleanupCertificates(rawHostname); + } + } + [Theory] [InlineData("www-.volal.cz")] [InlineData("www-.colorhexa.com")] @@ -263,6 +307,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( await TestHelper.PingPong(client, server, default); Assert.Equal(name, server.TargetHostName); + Assert.Equal(name, client.TargetHostName); } } From 15aa758996a7506a2e6249ae2cd14257382dc520 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Fri, 21 Apr 2023 13:04:09 +0200 Subject: [PATCH 11/12] Fix test again --- .../System.Net.Quic/src/System/Net/Quic/QuicConnection.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 696245482a6fe..432d5fde2b907 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -334,10 +334,16 @@ internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, str _defaultStreamErrorCode = options.DefaultStreamErrorCode; _defaultCloseErrorCode = options.DefaultCloseErrorCode; + // RFC 6066 forbids IP literals, avoid setting IP address here for consistency with SslStream + if (TargetHostNameHelper.IsValidAddress(targetHost)) + { + targetHost = string.Empty; + } + _sslConnectionOptions = new SslConnectionOptions( this, isClient: false, - targetHost: targetHost, + targetHost, options.ServerAuthenticationOptions.ClientCertificateRequired, options.ServerAuthenticationOptions.CertificateRevocationCheckMode, options.ServerAuthenticationOptions.RemoteCertificateValidationCallback, From 995949a3216c3d72c862410795de93c521fff2c1 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Mon, 24 Apr 2023 17:54:54 +0200 Subject: [PATCH 12/12] Fix trailing dot in hostname --- .../src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs | 2 +- .../src/System/Net/Security/SslStream.Protocol.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs index 47b9118bab60a..333ef433bb993 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs @@ -120,7 +120,7 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* if (result is not null) { bool checkCertName = !chain!.ChainPolicy!.VerificationFlags.HasFlag(X509VerificationFlags.IgnoreInvalidName); - sslPolicyErrors |= CertificateValidation.BuildChainAndVerifyProperties(chain!, result, checkCertName, !_isClient, _targetHost, certificateBuffer, certificateLength); + sslPolicyErrors |= CertificateValidation.BuildChainAndVerifyProperties(chain!, result, checkCertName, !_isClient, TargetHostNameHelper.NormalizeHostName(_targetHost), certificateBuffer, certificateLength); } else if (_certificateRequired) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index e9446f6ad1b9a..e295d83153058 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -1060,7 +1060,7 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot _remoteCertificate, _sslAuthenticationOptions.CheckCertName, _sslAuthenticationOptions.IsServer, - _sslAuthenticationOptions.TargetHost); + TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost)); } if (remoteCertValidationCallback != null)