Какую версию TLS обсуждали?

У меня есть приложение, работающее в .NET 4.7. По умолчанию он попытается использовать TLS1.2. Возможно ли узнать, какая версия TLS была согласована при выполнении, например, HTTP-запроса, как показано ниже?

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(decodedUri);
if (requestPayload.Length > 0)
{
    using (Stream requestStream = request.GetRequestStream())
    {
        requestStream.Write(requestPayload, 0, requestPayload.Length);
    }
}

Мне нужна эта информация только для ведения журнала/отладки, поэтому не важно, чтобы у меня была эта информация, прежде чем писать в поток запросов или получать ответ. Я не хочу разбирать журналы трассировки сети для этой информации, и я также не хочу создавать второе соединение (используя SslStream или подобное).

Ответ 1

Вы можете использовать Reflection, чтобы получить значение TlsStream->SslState->SslProtocol.
Эта информация может быть извлечена из потока, возвращаемого как HttpWebRequest.GetRequestStream() и HttpWebRequest.GetResponseStream().

ExtractSslProtocol() также обрабатывает сжатые GzipStream или DeflateStream, которые возвращаются при активации автоматического восстановления WebRequest.

Проверка будет происходить в ServerCertificateValidationCallback, который вызывается, когда запрос инициализируется с помощью request.GetRequestStream()

Примечание. SecurityProtocolType.Tls13 включен в .Net Framework 4.8+ и .Net Core 3.0+.

using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

    //(...)
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | 
                                           SecurityProtocolType.Tls | 
                                           SecurityProtocolType.Tls11 | 
                                           SecurityProtocolType.Tls12 | 
                                           SecurityProtocolType.Tls13;
    ServicePointManager.ServerCertificateValidationCallback += TlsValidationCallback;

    HttpWebRequest request = WebRequest.CreateHttp(decodedUri);
    using (Stream requestStream = request.GetRequestStream()) {
        //Here the request stream is already validated
        SslProtocols sslProtocol = ExtractSslProtocol(requestStream);
        if (sslProtocol < SslProtocols.Tls12)
        {
            // Refuse/close the connection
        }
    }
    //(...)

private SslProtocols ExtractSslProtocol(Stream stream)
{
        if (stream is null) return SslProtocols.None;
        BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
        Stream metaStream = stream;
        if (stream.GetType().BaseType == typeof(GZipStream)) {
            metaStream = (stream as GZipStream).BaseStream;
        }
        else if (stream.GetType().BaseType == typeof(DeflateStream)) {
            metaStream = (stream as DeflateStream).BaseStream;
        }

        var connection = metaStream.GetType().GetProperty("Connection", bindingFlags).GetValue(metaStream);
        if (!(bool)connection.GetType().GetProperty("UsingSecureStream", bindingFlags).GetValue(connection)) {
            // Not a Https connection
            return SslProtocols.None;
        }
        var tlsStream = connection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(connection);
        var tlsState = tlsStream.GetType().GetField("m_Worker", bindingFlags).GetValue(tlsStream);
        return (SslProtocols)tlsState.GetType().GetProperty("SslProtocol", bindingFlags).GetValue(tlsState);
}

RemoteCertificateValidationCallback содержит полезную информацию об используемых протоколах безопасности. (см. Параметры безопасности транспортного уровня (TLS) (IANA) и RFC 5246).
Используемые типы протоколов безопасности могут быть достаточно информативными, поскольку каждая версия протокола поддерживает подмножество алгоритмов хеширования и шифрования.
Tls 1.2 представляет HMAC-SHA256 и не поддерживает шифры IDEA и DES (все варианты перечислены в связанных документах).

Здесь я вставил OIDExtractor, который перечисляет используемые алгоритмы.
Обратите внимание, что и TcpClient(), и WebRequest() попадут сюда.

private bool TlsValidationCallback(object sender, X509Certificate CACert, X509Chain CAChain, SslPolicyErrors sslPolicyErrors)
{
    List<Oid> oidExtractor = CAChain
                             .ChainElements
                             .Cast<X509ChainElement>()
                             .Select(x509 => new Oid(x509.Certificate.SignatureAlgorithm.Value))
                             .ToList();
    // Inspect the oidExtractor list

    if (sslPolicyErrors == SslPolicyErrors.None) 
        return true;

    X509Certificate2 certificate = new X509Certificate2(CACert);

    //If you needed/have to pass a certificate, add it here.
    //X509Certificate2 cert = new X509Certificate2(@"[localstorage]/[ca.cert]");
    //CAChain.ChainPolicy.ExtraStore.Add(cert);
    CAChain.Build(certificate);
    foreach (X509ChainStatus CACStatus in CAChain.ChainStatus)
    {
        if ((CACStatus.Status != X509ChainStatusFlags.NoError) &
            (CACStatus.Status != X509ChainStatusFlags.UntrustedRoot))
            return false;
    }
    return true;
}


ОБНОВЛЕНИЕ 2:
Метод secur32.dllQueryContextAttributesW() позволяет запрашивать контекст безопасности подключения инициализированного потока.
[DllImport("secur32.dll", CharSet = CharSet.Auto, ExactSpelling=true, SetLastError=false)]
private static extern int QueryContextAttributesW(SSPIHandle contextHandle,
                                                  [In] ContextAttribute attribute,
                                                  [In] [Out] ref SecPkgContext_ConnectionInfo ConnectionInfo);

Как видно из документации, этот метод возвращает void* buffer который ссылается на структуру SecPkgContext_ConnectionInfo:

//[SuppressUnmanagedCodeSecurity]
private struct SecPkgContext_ConnectionInfo
{
    public SchProtocols dwProtocol;
    public ALG_ID aiCipher;
    public int dwCipherStrength;
    public ALG_ID aiHash;
    public int dwHashStrength;
    public ALG_ID aiExch;
    public int dwExchStrength;
}

SchProtocols dwProtocol является SslProtocol.

В чем подвох.
TlsStream.Context.m_SecurityContext._handle который ссылается на TlsStream.Context.m_SecurityContext._handle контекста соединения, не является общедоступным.
Таким образом, вы можете получить его, опять же, только через отражение или через System.Net.Security.AuthenticatedStream производных классов (System.Net.Security.SslStream и System.Net.Security.NegotiateStream) возвращаемые TcpClient.GetStream().

К сожалению, поток, возвращаемый WebRequest/WebResponse, не может быть приведен к этим классам. Типы соединений и потоков доступны только через непубличные свойства и поля.

Я публикую собранную документацию, возможно, она поможет вам найти другой путь, чтобы добраться до этого дескриптора контекста.

Объявления, структуры, списки перечислителей находятся в QueryContextAttributesW (PASTEBIN).

Microsoft TechNet
Структуры аутентификации

MSDN
Создание безопасного соединения с использованием Schannel

Получение информации о соединениях Schannel

Запрос атрибутов контекста канала

QueryContextAttributes (Schannel)

Кодовая база (частичная)

Справочный источник .NET

Internals.cs

внутренняя структура SSPIHandle {}

внутреннее перечисление ContextAttribute {}


ОБНОВЛЕНИЕ 1:

В вашем комментарии к другому ответу я увидел, что решение с использованием TcpClient() неприемлемо для вас. В любом случае я оставляю это здесь, так что комментарии Бена Фойгта в этом будут полезны всем, кто заинтересован. Кроме того, 3 возможных решения лучше, чем 2.

Предоставлены некоторые подробности реализации использования TcpClient() SslStream в контексте.

Если перед инициализацией WebRequest требуются данные протокола, соединение TcpClient() может быть установлено в том же контексте с использованием тех же инструментов, которые необходимы для соединения TLS. А именно, ServicePointManager.SecurityProtocol для определения поддерживаемых протоколов и ServicePointManager.ServerCertificateValidationCallback для проверки сертификата сервера.

И TcpClient(), и WebRequest могут использовать эти параметры:
- включить все протоколы и позволить Tls Handshake определить, какой из них будет использоваться.
- определить RemoteCertificateValidationCallback() для проверки X509Certificates Сервер передает в X509Chain.

На практике Tls Handshake одинакова при установлении соединения TcpClient или WebRequest.
Этот подход позволяет узнать, какой протокол Tls ваш HttpWebRequest будет согласовывать с тем же сервером.

Установите TcpClient() для получения и оценки SslStream.
Флаг checkCertificateRevocation имеет значение false, поэтому процесс не будет тратить время на поиск списка отзыва.
Обратный вызов проверки сертификата такой же, как указано в ServicePointManager

TlsInfo TLSInfo;
IPHostEntry DnsHost = await Dns.GetHostEntryAsync(HostURI.Host);
using (TcpClient client = new TcpClient(DnsHost.HostName, 443))
{
    using (SslStream sslstream = new SslStream(client.GetStream(), false, 
                                               TlsValidationCallback, null))
    {
        sslstream.AuthenticateAsClient(DnsHost.HostName, null, 
                                      (SslProtocols)ServicePointManager.SecurityProtocol, false);
        TLSInfo = new TlsInfo(sslstream);
    }
}

//The HttpWebRequest goes on from here.
HttpWebRequest httpRequest = WebRequest.CreateHttp(HostURI);

//(...)

Класс TlsInfo собирает некоторую информацию об установленном безопасном соединении:
- версия протокола Tls
- Алгоритмы шифрования и хэширования
- Сертификат сервера, используемый в Ssl Handshake

public class TlsInfo
{
    public TlsInfo(SslStream SecureStream)
    {
        this.ProtocolVersion = SecureStream.SslProtocol;
        this.CipherAlgorithm = SecureStream.CipherAlgorithm;
        this.HashAlgorithm = SecureStream.HashAlgorithm;
        this.RemoteCertificate = SecureStream.RemoteCertificate;
    }

    public SslProtocols ProtocolVersion { get; set; }
    public CipherAlgorithmType CipherAlgorithm { get; set; }
    public HashAlgorithmType HashAlgorithm { get; set; }
    public X509Certificate RemoteCertificate { get; set; }
}

Ответ 2

Нижеприведенное решение, безусловно, является "взломом" в том смысле, что оно использует рефлексию, но в настоящее время оно охватывает большинство ситуаций, с которыми вы могли бы столкнуться с HttpWebRequest. Он вернет значение null, если версия Tls не может быть определена. Он также проверяет версию Tls в том же запросе, прежде чем писать что-либо в поток запросов. Если при вызове метода еще не возникло рукопожатие потока Tls, оно вызывает его.

Ваша выборка будет выглядеть так:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create("...");
request.Method = "POST";
if (requestPayload.Length > 0)
{
    using (Stream requestStream = request.GetRequestStream())
    {
        SslProtocols? protocol = GetSslProtocol(requestStream);
        requestStream.Write(requestPayload, 0, requestPayload.Length);
    }
}

И метод:

public static SslProtocols? GetSslProtocol(Stream stream)
{
    if (stream == null)
        return null;

    if (typeof(SslStream).IsAssignableFrom(stream.GetType()))
    {
        var ssl = stream as SslStream;
        return ssl.SslProtocol;
    }

    var flags = BindingFlags.NonPublic | BindingFlags.Instance;

    if (stream.GetType().FullName == "System.Net.ConnectStream")
    {
        var connection = stream.GetType().GetProperty("Connection", flags).GetValue(stream);
        var netStream = connection.GetType().GetProperty("NetworkStream", flags).GetValue(connection) as Stream;
        return GetSslProtocol(netStream);
    }

    if (stream.GetType().FullName == "System.Net.TlsStream")
    {
        // type SslState
        var ssl = stream.GetType().GetField("m_Worker", flags).GetValue(stream);

        if (ssl.GetType().GetProperty("IsAuthenticated", flags).GetValue(ssl) as bool? != true)
        {
            // we're not authenticated yet. see: https://referencesource.microsoft.com/#System/net/System/Net/_TLSstream.cs,115
            var processAuthMethod = stream.GetType().GetMethod("ProcessAuthentication", flags);
            processAuthMethod.Invoke(stream, new object[] { null });
        }

        var protocol = ssl.GetType().GetProperty("SslProtocol", flags).GetValue(ssl) as SslProtocols?;
        return protocol;
    }

    return null;
}

Ответ 3

Единственный способ, которым я могу понять, это использовать SslStream для создания тестового соединения, а затем проверить свойство SslProtocol.

TcpClient client = new TcpClient(decodedUri.DnsSafeHost, 443);
SslStream sslStream = new SslStream(client.GetStream());

// use this overload to ensure SslStream has the same scope of enabled protocol as HttpWebRequest
sslStream.AuthenticateAsClient(decodedUri.Host, null,
    (SslProtocols)ServicePointManager.SecurityProtocol, true);

// Check sslStream.SslProtocol here

client.Close();
sslStream.Close();

Я проверил, что sslStream.SslProtocl всегда будет таким же, как TlsStream.m_worker.SslProtocol, который используется HttpWebRequest Connection.

Ответ 4

Собрав воедино некоторые идеи, я разработал простой метод для проверки каждого доступного протокола, заставляя один конкретный тип подключения при каждой попытке. В конце я получаю список результатов, которые нужно использовать по мере необходимости.

PS: Тест действителен только если вы знаете, что веб-сайт онлайн - вы можете сделать предварительный тест, чтобы проверить это.

    public static IEnumerable<T> GetValues<T>()
    {
        return Enum.GetValues(typeof(T)).Cast<T>();
    }

    private Dictionary<SecurityProtocolType, bool> ProcessProtocols(string address)
    {   
        var protocolResultList = new Dictionary<SecurityProtocolType, bool>();
        var defaultProtocol = ServicePointManager.SecurityProtocol;

        ServicePointManager.Expect100Continue = true;
        foreach (var protocol in GetValues<SecurityProtocolType>())
        {
            try
            {
                ServicePointManager.SecurityProtocol = protocol;

                var request = WebRequest.Create(address);
                var response = request.GetResponse();

                protocolResultList.Add(protocol, true);
            }
            catch
            {
                protocolResultList.Add(protocol, false);
            }
        }

        ServicePointManager.SecurityProtocol = defaultProtocol;

        return protocolResultList;
    }

Надеюсь, это будет полезно