c# - with - Qual versão do TLS foi negociada?




web api 2 client certificate authentication (3)

A única maneira de descobrir é usar o 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 se o sslStream.SslProtocl sempre será o mesmo que o TlsStream.m_worker.SslProtocol usado pela Connection HttpWebRequest .

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, portanto, 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).


A solução abaixo é certamente um "hack" em que ele 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á acionado.

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

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 ambos HttpWebRequest.GetRequestStream() e HttpWebRequest.GetResponseStream() .

O ExtractSslProtocol() também lida com o GzipStream ou DeflateStream comprimido que são retornados quando o WebRequest AutomaticDecompression é ativado.

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

Nota : SecurityProtocolType.Tls13 é incluído no .Net Framework 4.8+ e no .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);
}

O RemoteCertificateValidationCallback possui algumas informações úteis sobre os protocolos de segurança usados. (consulte: Parâmetros TLS (Transport Layer Security) e IANA ( 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 as cifras IDEA e DES (todas as variantes sã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();
    // 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;
}

ATUALIZAÇÃO 2:
O secur32.dll -> QueryContextAttributesW() , 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 estrutura 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;
}

O membro SchProtocols dwProtocol é o SslProtocol.

Qual é o truque?
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 por meio de reflexão ou através das classes derivadas System.Net.Security.SslStream ( 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, 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

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 .NET

Internals.cs

estrutura interna SSPIHandle {}

enum interno ContextAttribute {}

ATUALIZAÇÃO 1:

Eu vi em seu comentário para outra resposta que a solução usando TcpClient() não é aceitável para você. De qualquer forma, vou deixar aqui, para que os comentários de Ben Voigt sejam úteis para qualquer outra pessoa interessada. Além disso, 3 soluções possíveis são melhores que 2.

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

Se informações de protocolo forem necessárias antes de inicializar um WebRequest, uma conexão TcpClient () pode 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 delegado RemoteCertificateValidationCallback() para validar os X509Certificates o Servidor passa em um 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 seu HttpWebRequest negociará com o mesmo servidor.

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