未验证 提交 c3df339c 编写于 作者: T Tomas Weinfurt 提交者: GitHub

Support sites with invalid IDN in SslStream (#82934)

* initial test

* 'update'

* feedback from review

* android
上级 86175a9e
......@@ -5,7 +5,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Security;
......@@ -25,7 +24,6 @@ internal static partial class OpenSsl
private const string TlsCacheSizeCtxName = "System.Net.Security.TlsCacheSize";
private const string TlsCacheSizeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_TLSCACHESIZE";
private const SslProtocols FakeAlpnSslProtocol = (SslProtocols)1; // used to distinguish server sessions with ALPN
private static readonly IdnMapping s_idnMapping = new IdnMapping();
private static readonly ConcurrentDictionary<SslProtocols, SafeSslContextHandle> s_clientSslContexts = new ConcurrentDictionary<SslProtocols, SafeSslContextHandle>();
#region internal methods
......@@ -385,21 +383,22 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
if (sslAuthenticationOptions.IsClient)
{
// The IdnMapping converts unicode input into the IDNA punycode sequence.
string punyCode = string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) ? string.Empty : s_idnMapping.GetAscii(sslAuthenticationOptions.TargetHost!);
// Similar to windows behavior, set SNI on openssl by default for client context, ignore errors.
if (!Ssl.SslSetTlsExtHostName(sslHandle, punyCode))
if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost))
{
Crypto.ErrClearError();
}
// Similar to windows behavior, set SNI on openssl by default for client context, ignore errors.
if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost))
{
Crypto.ErrClearError();
}
if (cacheSslContext && !string.IsNullOrEmpty(punyCode))
{
sslCtxHandle.TrySetSession(sslHandle, punyCode);
bool ignored = false;
sslCtxHandle.DangerousAddRef(ref ignored);
sslHandle.SslContextHandle = sslCtxHandle;
if (cacheSslContext)
{
sslCtxHandle.TrySetSession(sslHandle, sslAuthenticationOptions.TargetHost);
bool ignored = false;
sslCtxHandle.DangerousAddRef(ref ignored);
sslHandle.SslContextHandle = sslCtxHandle;
}
}
// relevant to TLS 1.3 only: if user supplied a client cert or cert callback,
......@@ -745,16 +744,18 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session)
Debug.Assert(session != IntPtr.Zero);
IntPtr ptr = Ssl.SslGetData(ssl);
Debug.Assert(ptr != IntPtr.Zero);
GCHandle gch = GCHandle.FromIntPtr(ptr);
SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle;
// There is no relation between SafeSslContextHandle and SafeSslHandle so the handle
// may be released while the ssl session is still active.
if (ctxHandle != null && ctxHandle.TryAddSession(Ssl.SslGetServerName(ssl), session))
if (ptr != IntPtr.Zero)
{
// offered session was stored in our cache.
return 1;
GCHandle gch = GCHandle.FromIntPtr(ptr);
SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle;
// There is no relation between SafeSslContextHandle and SafeSslHandle so the handle
// may be released while the ssl session is still active.
if (ctxHandle != null && ctxHandle.TryAddSession(Ssl.SslGetServerName(ssl), session))
{
// offered session was stored in our cache.
return 1;
}
}
// OpenSSL will destroy session.
......
......@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography.X509Certificates;
......@@ -334,9 +333,6 @@ internal abstract partial class SafeDeleteContext : DebugSafeHandle
internal abstract partial class SafeDeleteContext : SafeHandle
{
#endif
private const string dummyStr = " ";
private static readonly IdnMapping s_idnMapping = new IdnMapping();
protected SafeFreeCredentials? _EffectiveCredential;
//-------------------------------------------------------------------
......@@ -453,18 +449,12 @@ internal abstract partial class SafeDeleteContext : SafeHandle
}
}
if (targetName == null || targetName.Length == 0)
{
targetName = dummyStr;
}
string punyCode = s_idnMapping.GetAscii(targetName);
fixed (char* namePtr = punyCode)
fixed (char* namePtr = targetName)
{
errorCode = MustRunInitializeSecurityContext(
ref inCredentials,
isContextAbsent,
(byte*)(((object)targetName == (object)dummyStr) ? null : namePtr),
(byte*)namePtr,
inFlags,
endianness,
&inSecurityBufferDescriptor,
......@@ -514,7 +504,7 @@ internal abstract partial class SafeDeleteContext : SafeHandle
errorCode = MustRunInitializeSecurityContext(
ref inCredentials,
isContextAbsent,
(byte*)(((object)targetName == (object)dummyStr) ? null : namePtr),
(byte*)namePtr,
inFlags,
endianness,
&inSecurityBufferDescriptor,
......
// 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.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
......@@ -11,6 +13,8 @@ namespace System.Net.Security
{
internal sealed class SslAuthenticationOptions
{
private static readonly IdnMapping s_idnMapping = new IdnMapping();
// 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<char> ipSpan)
......@@ -46,6 +50,12 @@ private static unsafe bool IsValidAddress(ReadOnlySpan<char> ipSpan)
return false;
}
private static readonly IndexOfAnyValues<char> s_safeDnsChars =
IndexOfAnyValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz");
private static bool IsSafeDnsString(ReadOnlySpan<char> name) =>
name.IndexOfAnyExcept(s_safeDnsChars) < 0;
internal SslAuthenticationOptions()
{
TargetHost = string.Empty;
......@@ -86,13 +96,25 @@ internal void UpdateOptions(SslClientAuthenticationOptions sslClientAuthenticati
if (!string.IsNullOrEmpty(sslClientAuthenticationOptions.TargetHost))
{
// RFC 6066 section 3 says to exclude trailing dot from fully qualified DNS hostname
TargetHost = sslClientAuthenticationOptions.TargetHost.TrimEnd('.');
string targetHost = sslClientAuthenticationOptions.TargetHost.TrimEnd('.');
// RFC 6066 forbids IP literals
if (IsValidAddress(TargetHost))
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;
}
}
}
// Client specific options.
......
......@@ -202,6 +202,88 @@ public async Task SslStream_IpLiteral_NotSend(string target)
Assert.Equal(string.Empty, server.TargetHostName);
}
[Theory]
[InlineData("\u00E1b\u00E7d\u00EB.com")]
[InlineData("\u05D1\u05F1.com")]
[InlineData("\u30B6\u30C7\u30D8.com")]
public async Task SslStream_ValidIdn_Success(string name)
{
(SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
using (client)
using (server)
{
using X509Certificate2 serverCertificate = Configuration.Certificates.GetServerCertificate();
using X509Certificate2 clientCertificate = Configuration.Certificates.GetClientCertificate();
SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions() { ServerCertificate = serverCertificate };
SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions()
{
TargetHost = name,
CertificateChainPolicy = new X509ChainPolicy() { VerificationFlags = X509VerificationFlags.IgnoreInvalidName },
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
};
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
client.AuthenticateAsClientAsync(clientOptions, default),
server.AuthenticateAsServerAsync(serverOptions, default));
await TestHelper.PingPong(client, server, default);
Assert.Equal(name, server.TargetHostName);
}
}
[Theory]
[InlineData("www-.volal.cz")]
[InlineData("www-.colorhexa.com")]
[InlineData("xn--www-7m0a.thegratuit.com")]
[ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)]
public async Task SslStream_SafeInvalidIdn_Success(string name)
{
(SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
using (client)
using (server)
{
using X509Certificate2 serverCertificate = Configuration.Certificates.GetServerCertificate();
using X509Certificate2 clientCertificate = Configuration.Certificates.GetClientCertificate();
SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions() { ServerCertificate = serverCertificate };
SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions()
{
TargetHost = name,
CertificateChainPolicy = new X509ChainPolicy() { VerificationFlags = X509VerificationFlags.IgnoreInvalidName },
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
};
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
client.AuthenticateAsClientAsync(clientOptions, default),
server.AuthenticateAsServerAsync(serverOptions, default));
await TestHelper.PingPong(client, server, default);
Assert.Equal(name, server.TargetHostName);
}
}
[Theory]
[InlineData("\u0000\u00E7d\u00EB.com")]
public async Task SslStream_UnsafeInvalidIdn_Throws(string name)
{
(SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
using (client)
using (server)
{
using X509Certificate2 serverCertificate = Configuration.Certificates.GetServerCertificate();
SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions()
{
TargetHost = name,
CertificateChainPolicy = new X509ChainPolicy() { VerificationFlags = X509VerificationFlags.IgnoreInvalidName },
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
};
await Assert.ThrowsAsync<ArgumentException>(() => client.AuthenticateAsClientAsync(clientOptions, default));
}
}
private static Func<Task> WithAggregateExceptionUnwrapping(Func<Task> a)
{
return async () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册