Qual versão do TLS foi negociada?

17

Eu tenho meu aplicativo em execução no .net 4.7. Por padrão, ele tentará usar o TLS1.2. É possível saber qual versão do TLS foi negociada ao executar, por exemplo, uma solicitação HTTP como abaixo?

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

Eu só preciso dessas informações para fins de registro / depuração, por isso não é importante que eu tenha essas informações antes de gravar no fluxo de solicitação ou receber a resposta. Eu não quero analisar logs de rastreamento de rede para essa informação, e eu também não quero criar uma segunda conexão (usando SslStream ou similar).

    
por Frederic 02.02.2018 в 20:25

3 respostas

9

Você pode usar o Reflection para chegar ao valor da propriedade TlsStream->SslState->SslProtocol .
Essas informações podem ser extraídas do Fluxo retornado por HttpWebRequest.GetRequestStream() e HttpWebRequest.GetResponseStream() .

Atualizar :
O método ExtractSslProtocol() agora detecta os GzipStream ou DeflateStream compactados que podem ser retornados ao ativar o WebRequest AutomaticDecompression .

A validação ocorrerá no TlsValidationCallback , que é chamado quando a solicitação é inicializada com request.GetRequestStream()

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;
    ServicePointManager.ServerCertificateValidationCallback += TlsValidationCallback;

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(decodedUri);
    if (requestPayload.Length > 0)
    {
        using (Stream requestStream = request.GetRequestStream())
        {
            //Here the request stream is already validated
            SslProtocols SslProtocol = ExtractSslProtocol(requestStream);
            requestStream.Write(requestPayload, 0, requestPayload.Length);
        }
    }
    //(...)

private SslProtocols ExtractSslProtocol(Stream stream)
{
    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
    Stream CompressedStream = null;
    if (stream.GetType().BaseType == typeof(GZipStream))
        CompressedStream = (GZipStream)stream;
    else if (stream.GetType().BaseType == typeof(DeflateStream))
        CompressedStream = (DeflateStream)stream;

    var objbaseStream = CompressedStream?.GetType().GetProperty("BaseStream").GetValue(stream);
    if (objbaseStream == null) objbaseStream = stream;

    var objConnection = objbaseStream.GetType().GetField("m_Connection", bindingFlags).GetValue(objbaseStream);
    var objTlsStream = objConnection.GetType().GetProperty("NetworkStream", bindingFlags).GetValue(objConnection);
    var objSslState = objTlsStream.GetType().GetField("m_Worker", bindingFlags).GetValue(objTlsStream);
    return (SslProtocols)objSslState.GetType().GetProperty("SslProtocol", bindingFlags).GetValue(objSslState);
}

O RemoteCertificateValidationCallback tem algumas informações úteis sobre os protocolos de segurança usados. (consulte: Parâmetros de segurança da camada de transporte (TLS) e RFC 5246 ).
Os tipos de protocolos de segurança usados ​​podem ser informativos o suficiente, já que cada versão de protocolo suporta um subconjunto de algoritmos de Hashing e Encryption. Tls 1.2, introduz o HMAC-SHA256 e descarta IDEA e DES ciphers (todas as variantes estão listadas nos documentos vinculados).

Aqui, eu inseri um OIDExtractor , que lista os algoritmos em uso.
Note que tanto o TcpClient () quanto o WebRequest () chegarão aqui.

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();

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

    X509Certificate2 _Certificate = new X509Certificate2(CACert);

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

UPDATE 2:
O secur32.dll - & gt; QueryContextAttributesW() method, permite consultar o contexto de segurança de conexão de um fluxo inicializado.
[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);

Como você pode ver na documentação, esse método retorna um void* buffer que faz referência a uma SecPkgContext_ConnectionInfo structure:

//[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;
}

O membro SchProtocols dwProtocol é o SslProtocol.

Qual é o problema?
O TlsStream.Context.m_SecurityContext._handle que referencia o identificador de contexto de conexão não é público.
Assim, você pode obtê-lo, novamente, somente através da reflexão ou através das classes System.Net.Security.AuthenticatedStream derivadas ( System.Net.Security.SslStream e System.Net.Security.NegotiateStream ) retornadas por TcpClient.GetStream() .

Infelizmente, o Stream retornado pelo WebRequest / WebResponse não pode ser convertido para essas classes. Os tipos de conexões e fluxos são referenciados apenas por propriedades e campos não públicos.

Estou publicando a documentação montada, isso talvez ajude você a descobrir outro caminho para chegar ao Context Handle.

As declarações, estruturas, listas de enumeradores estão em QueryContextAttributesW (PASTEBIN) .

Microsoft TechNet Estruturas de autenticação a>

MSDN
Criando uma conexão segura usando o Schannel

Obtendo informações sobre conexões Schannel

Consultando os atributos de um contexto Schannel

QueryContextAttributes (Schannel)

Base de código (parcial)

Fonte de referência do .NET

Internals.cs

estrutura interna SSPIHandle {}

enum interno ContextAttribute {}

UPDATE 1:
  

Eu vi no seu comentário para outra resposta que a solução usando    TcpClient() não é aceitável para você. Eu estou deixando aqui de qualquer maneira, então   os comentários de Ben Voigt neste site serão úteis para qualquer pessoa interessada. Além disso, 3 soluções possíveis são melhores que 2.

Alguns detalhes de implementação no TcpClient ( ) SslStream uso no contexto fornecido.

Se as informações do protocolo forem necessárias antes de inicializar um WebRequest, uma conexão TcpClient () poderá ser estabelecida no mesmo contexto, usando as mesmas ferramentas necessárias para uma conexão TLS. Ou seja, o ServicePointManager.SecurityProtocol para definir os protocolos suportados e o ServicePointManager.ServerCertificateValidationCallback para validar o certificado do servidor.

Tanto o TcpClient () quanto o WebRequest podem usar estas configurações:
- ative todos os protocolos e deixe o Tls Handshake determinar qual deles será usado.
- define um RemoteCertificateValidationCallback() delegate para validar o X509Certificates que o servidor passa em X509Chain .

Na prática, o Tls Handshake é o mesmo ao estabelecer uma conexão TcpClient ou WebRequest.
Essa abordagem permite que você saiba qual protocolo Tls o seu HttpWebRequest irá negociar com o mesmo servidor.

Configure um TcpClient() para receber e avaliar o SslStream .
O sinalizador checkCertificateRevocation está definido como false , portanto, o processo não perderá tempo procurando a lista de revogação.
A validação do certificado de retorno de chamada é a mesma especificada em 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);

//(...)

A classe TlsInfo coleta algumas informações sobre a conexão segura estabelecida:
- versão do protocolo Tls
- Algoritmos de codificação e hash
- O certificado do servidor usado no handshake do Ssl

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; }
}
    
por Jimi 08.02.2018 / 01:19
1

A solução abaixo é certamente um "truque", pois usa reflexão, mas atualmente cobre a maioria das situações em que você poderia estar com um HttpWebRequest. Ele retornará null se a versão do Tls não puder ser determinada. Ele também verifica a versão do Tls na mesma solicitação, antes que você tenha escrito alguma coisa no fluxo de solicitação. Se o handshake do Tls do fluxo ainda não tiver ocorrido quando você chamar o método, ele será disparado.

Seu uso de amostra ficaria assim:

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);
    }
}

E o método:

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;
}
    
por caesay 09.02.2018 / 14:35
0

A única maneira de descobrir é usar SslStream para fazer uma conexão de teste e, em seguida, verificar a propriedade 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();

Verifiquei que sslStream.SslProtocl será sempre igual ao TlsStream.m_worker.SslProtocol usado por HttpWebRequest ' Connection .

    
por Alex.Wei 07.02.2018 / 12:32