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..1457792b34c63 --- /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('.'); + + 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. + internal static unsafe bool IsValidAddress(string? hostname) + { + if (string.IsNullOrEmpty(hostname)) + { + return false; + } + + ReadOnlySpan ipSpan = hostname.AsSpan(); + + 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..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,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..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 @@ -28,7 +28,7 @@ private readonly struct SslConnectionOptions /// /// Host name send in SNI, set only for outbound/client connections. Configured via . /// - private readonly string? _targetHost; + private readonly string _targetHost; /// /// Always true for outbound/client connections. Configured for inbound/server ones via . /// @@ -47,8 +47,10 @@ private readonly struct SslConnectionOptions /// private readonly X509ChainPolicy? _certificateChainPolicy; + internal string TargetHost => _targetHost; + public SslConnectionOptions(QuicConnection connection, bool isClient, - string? targetHost, bool certificateRequired, X509RevocationMode + string targetHost, bool certificateRequired, X509RevocationMode revocationMode, RemoteCertificateValidationCallback? validationCallback, X509ChainPolicy? certificateChainPolicy) { @@ -118,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.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index fdc06d3163224..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 @@ -149,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. @@ -279,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, @@ -312,7 +324,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); @@ -322,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: null, + targetHost, options.ServerAuthenticationOptions.ClientCertificateRequired, options.ServerAuthenticationOptions.CertificateRevocationCheckMode, options.ServerAuthenticationOptions.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..345aa6d236a58 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -1204,5 +1204,58 @@ public async Task IdleTimeout_ThrowsQuicException() await AssertThrowsQuicExceptionAsync(QuicError.ConnectionIdle, async () => await acceptTask).WaitAsync(TimeSpan.FromSeconds(10)); } } + + private async Task SniTestCore(string hostname, bool shouldSendSni) + { + string expectedHostName = shouldSendSni ? hostname : string.Empty; + + 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(expectedHostName, 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(expectedHostName, clientConnection.TargetHostName); + Assert.Equal(expectedHostName, serverConnection.TargetHostName); + } + } + + [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); } } 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,11 @@ 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; - } - } - } + // 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..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 @@ -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); @@ -1059,7 +1060,7 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot _remoteCertificate, _sslAuthenticationOptions.CheckCertName, _sslAuthenticationOptions.IsServer, - _sslAuthenticationOptions.TargetHost); + TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost)); } if (remoteCertValidationCallback != null) 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 diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 4a0cdf41d837e..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; @@ -44,6 +45,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 +203,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( server.AuthenticateAsServerAsync(serverOptions, default)); Assert.Equal(string.Empty, server.TargetHostName); + Assert.Equal(string.Empty, client.TargetHostName); } [Theory] @@ -232,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")] @@ -260,6 +307,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( await TestHelper.PingPong(client, server, default); Assert.Equal(name, server.TargetHostName); + Assert.Equal(name, client.TargetHostName); } } 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" /> +