Quale versione TLS è stata negoziata?

Feb 02 2018

Ho la mia app in esecuzione in .NET 4.7. Per impostazione predefinita, proverà a utilizzare TLS1.2. È possibile sapere quale versione TLS è stata negoziata durante l'esecuzione, ad esempio, di una richiesta HTTP come di seguito?

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

Ho bisogno di queste informazioni solo per scopi di registrazione / debug, quindi non è importante disporre di queste informazioni prima di scrivere nel flusso di richiesta o di ricevere la risposta. Non desidero analizzare i registri di tracciamento della rete per queste informazioni e non voglio nemmeno creare una seconda connessione (usando SslStream o simili).

Risposte

23 Jimi Feb 08 2018 at 00:19

È possibile utilizzare Reflection per ottenere il TlsStream->SslState->SslProtocolvalore della proprietà.
Queste informazioni possono essere estratte dal flusso restituito da entrambi HttpWebRequest.GetRequestStream()e HttpWebRequest.GetResponseStream().

La ExtractSslProtocol()gestisce anche la compresso GzipStreamo DeflateStreamche vengono restituiti quando WebRequest l'AutomaticDecompression è attivato.

La convalida avverrà nel ServerCertificateValidationCallback, che viene chiamato quando la richiesta viene inizializzata conrequest.GetRequestStream()

Nota : SecurityProtocolType.Tls13è incluso in .Net Framework 4.8+e .Net Core 3.0+.

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

//(...)
// Allow all, to then check what the Handshake will agree upon
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | 
                                       SecurityProtocolType.Tls | 
                                       SecurityProtocolType.Tls11 | 
                                       SecurityProtocolType.Tls12 | 
                                       SecurityProtocolType.Tls13;

// Handle the Server certificate exchange, to inspect the certificates received
ServicePointManager.ServerCertificateValidationCallback += TlsValidationCallback;

Uri requestUri = new Uri("https://somesite.com");
var request = WebRequest.CreateHttp(requestUri);

request.Method = WebRequestMethods.Http.Post;
request.ServicePoint.Expect100Continue = false;
request.AllowAutoRedirect = true;
request.CookieContainer = new CookieContainer();

request.ContentType = "application/x-www-form-urlencoded";
var postdata = Encoding.UTF8.GetBytes("Some postdata here");
request.ContentLength = postdata.Length;

request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident / 7.0; rv: 11.0) like Gecko";
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip, deflate;q=0.8");
request.Headers.Add(HttpRequestHeader.CacheControl, "no-cache");

using (var 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);
}

Il RemoteCertificateValidationCallbackdispone di alcune informazioni utili sui protocolli di sicurezza utilizzati. (vedere: Parametri Transport Layer Security (TLS) (IANA) e RFC 5246 ).
I tipi di protocolli di sicurezza utilizzati possono essere abbastanza informativi, poiché ogni versione del protocollo supporta un sottoinsieme di algoritmi di hash e crittografia.
Tls 1.2, introduce HMAC-SHA256e deprecate IDEAe DEScifrari (tutte le varianti sono elencate nei documenti collegati).

Qui ho inserito un OIDExtractor, che elenca gli algoritmi in uso.
Nota che sia TcpClient () che WebRequest () verranno qui.

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

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


AGGIORNAMENTO 2:
Il metodo secur32.dll-> QueryContextAttributesW(), permette di interrogare il Contesto di Sicurezza della Connessione di uno Stream inizializzato.

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

Come puoi vedere dalla documentazione, questo metodo restituisce un void* bufferche fa riferimento a una SecPkgContext_ConnectionInfostruttura:

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

Il SchProtocols dwProtocolmembro è SslProtocol.

Qual è il trucco.
Il TlsStream.Context.m_SecurityContext._handleriferimento all'handle del contesto di connessione non è pubblico.
Pertanto, puoi ottenerlo, ancora una volta, solo tramite reflection o tramite le System.Net.Security.AuthenticatedStreamclassi derivate ( System.Net.Security.SslStreame System.Net.Security.NegotiateStream) restituite da TcpClient.GetStream().

Sfortunatamente, il flusso restituito da WebRequest / WebResponse non può essere sottoposto a cast a queste classi. I tipi di connessioni e flussi sono referenziati solo tramite proprietà e campi non pubblici.

Sto pubblicando la documentazione assemblata, forse ti aiuterà a capire un altro percorso per arrivare a quel Context Handle.

Le dichiarazioni, le strutture, gli elenchi di enumeratori si trovano in QueryContextAttributesW (PASTEBIN) .

Strutture di autenticazione di Microsoft TechNet

MSDN
Creazione di una connessione protetta utilizzando Schannel

Recupero di informazioni sulle connessioni Schannel

Interrogare gli attributi di un contesto Schannel

QueryContextAttributes (Schannel)

Base codice (parziale)

.NET Reference Source

Internals.cs

struttura interna SSPIHandle {}

enumerazione interna ContextAttribute {}


AGGIORNAMENTO 1:

Ho visto nel tuo commento a un'altra risposta che la soluzione utilizzata TcpClient()non è accettabile per te. Lo lascio comunque qui, così i commenti di Ben Voigt in questo saranno utili a chiunque sia interessato. Inoltre, 3 possibili soluzioni sono migliori di 2.

Alcuni dettagli di implementazione sull'utilizzo di TcpClient () SslStream nel contesto fornito.

Se sono richieste informazioni sul protocollo prima di inizializzare una WebRequest, è possibile stabilire una connessione TcpClient () nello stesso contesto utilizzando gli stessi strumenti richiesti per una connessione TLS. Vale a dire, ServicePointManager.SecurityProtocolper definire i protocolli supportati e ServicePointManager.ServerCertificateValidationCallbackper convalidare il certificato del server.

Sia TcpClient () che WebRequest possono utilizzare queste impostazioni:
- abilitare tutti i protocolli e lasciare che il TLS Handshake determini quale verrà utilizzato.
- definire un RemoteCertificateValidationCallback()delegato per convalidare i X509Certificatespassaggi del server in un file X509Chain.

In pratica, l'handshake TLS è lo stesso quando si stabilisce una connessione TcpClient o WebRequest.
Questo approccio ti consente di sapere quale protocollo Tls verrà negoziato dalla tua HttpWebRequest con lo stesso server.

Imposta un TcpClient()per ricevere e valutare il file SslStream.
Il checkCertificateRevocationflag è impostato su false, quindi il processo non perderà tempo a cercare l'elenco di revoche.
La richiamata di convalida del certificato è la stessa specificata in ServicePointManager.

TlsInfo tlsInfo = null;
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);

//(...)

La TlsInfoClasse raccoglie alcune informazioni sulla connessione sicura stabilita:
- Versione del protocollo TLS
- Algoritmi di cifratura e hash
- Il certificato del server utilizzato nell'Handshake 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; }
}
2 caesay Feb 09 2018 at 13:35

La soluzione seguente è sicuramente un "trucco" in quanto utilizza la riflessione, ma attualmente copre la maggior parte delle situazioni in cui potresti trovarti con un HttpWebRequest. Restituirà null se non è stato possibile determinare la versione di Tls. Verifica anche la versione di Tls nella stessa richiesta, prima che tu abbia scritto qualcosa nel flusso della richiesta. Se l'handshake di Stream Tls non si è ancora verificato quando chiami il metodo, lo attiverà.

Il tuo utilizzo di esempio sarebbe simile a questo:

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 il metodo:

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;
}
1 EduardoGadotti Jan 31 2019 at 11:56

Mettendo insieme alcune idee qua e là, ho fatto un semplice metodo per testare ogni protocollo disponibile, forzando un tipo specifico di connessione ogni tentativo. Alla fine, ottengo un elenco con i risultati da utilizzare quando necessario.

Ps: il test è valido solo se sai che il sito web è online - puoi fare un test precedente per verificarlo.

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

Spero che questo sia utile

Alex.Wei Feb 07 2018 at 11:32

L'unico modo per capirlo è utilizzare SslStreamper effettuare una connessione di prova, quindi controllare la SslProtocolproprietà.

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

Ho controllato che sslStream.SslProtoclsarà sempre come stessi TlsStream.m_worker.SslProtocolquello utilizzato da HttpWebRequest's Connection.