web-dev-qa-db-fra.com

Quelle version de TLS a été négociée?

J'ai mon application en cours d'exécution dans .NET 4.7. Par défaut, il essaiera d'utiliser TLS1.2 . Est-il possible de savoir quelle version de TLS a été négociée lors de l'exécution, par exemple, d'une requête HTTP comme ci-dessous?

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

J'ai besoin de ces informations uniquement à des fins de journalisation/débogage. Il n'est donc pas important que je dispose de ces informations avant d'écrire dans le flux de demandes ou de recevoir la réponse. Je ne souhaite pas analyser les journaux de suivi du réseau pour ces informations, et je ne souhaite pas non plus créer une deuxième connexion (à l'aide de SslStream ou similaire).

19
Frederic

Vous pouvez utiliser Reflection pour obtenir la valeur de la propriété TlsStream->SslState->SslProtocol.
Ces informations peuvent être extraites du flux renvoyé par HttpWebRequest.GetRequestStream() et HttpWebRequest.GetResponseStream().

Mettre à jour:
La méthode ExtractSslProtocol() détecte maintenant la GzipStream ou DeflateStream compressée qui pourrait être renvoyée lors de l'activation de la WebRequestAutomaticDecompression .

La validation aura lieu dans la TlsValidationCallback, qui est appelée lorsque la demande est initialisée avec 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);
}

La RemoteCertificateValidationCallback contient des informations utiles sur les protocoles de sécurité utilisés. (voir: Paramètres IANA (TLS) (Transport Layer Security) } _ et RFC 5246 ).
Les types de protocoles de sécurité utilisés peuvent être suffisamment informatifs, car chaque version de protocole prend en charge un sous-ensemble d’algorithmes de hachage et de chiffrement.
Tls 1.2, introduit HMAC-SHA256 et déconseille les chiffrements IDEA et DES (toutes les variantes sont répertoriées dans les documents liés).

Ici, j’ai inséré une OIDExtractor, qui répertorie les algorithmes utilisés.
Notez que TcpClient () et WebRequest () arriveront tous les deux.

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:
La méthode secur32.dll -> QueryContextAttributesW() permet d’interroger le contexte de sécurité de connexion d’un flux initialisé.

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

Comme vous pouvez le constater dans la documentation, cette méthode renvoie un void* buffer qui fait référence à une structure 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;
}

Le membre SchProtocols dwProtocol est le protocole SslProtocol.

Quel est le piège.
Le TlsStream.Context.m_SecurityContext._handle qui fait référence au descripteur de contexte de connexion n’est pas public.
Ainsi, vous ne pouvez l'obtenir, encore une fois, que par réflexion ou par les classes dérivées System.Net.Security.AuthenticatedStream (System.Net.Security.SslStream et System.Net.Security.NegotiateStream) renvoyées par TcpClient.GetStream().

Malheureusement, le flux renvoyé par WebRequest/WebResponse ne peut pas être converti en ces classes. Les types de connexions et de flux ne sont référencés que par le biais de propriétés et de champs non publics.

Je publie la documentation assemblée, cela vous aidera peut-être à trouver un autre chemin pour accéder à ce descripteur de contexte.

Les déclarations, les structures et les listes d'énumérateurs sont dans QueryContextAttributesW (Pastebin) }.

Microsoft TechNet
Structures d'authentification

MSDN
Création d'une connexion sécurisée à l'aide de Schannel

_ { Obtention d'informations sur les connexions Schannel } _

(Interrogation des attributs d'un contexte Schannel } _

(QueryContextAttributes (Schannel))

Base de code (partielle)

.NET Reference Source

Internals.cs

structure interne SSPIHandle {}

enum interne ContextAttribute {}


UPDATE 1:

J'ai vu dans votre commentaire à une autre réponse que la solution utilisant TcpClient() n'est pas acceptable pour vous. Je le laisse ici de toute façon alors les commentaires de Ben Voigt dans celui-ci seront utiles à toute autre personne intéressée. De plus, 3 solutions possibles valent mieux que 2.

Quelques détails d’implémentation sur l’utilisation de TcpClient () _ { SslStream _ _ dans le contexte fourni.

Si des informations de protocole sont requises avant d'initialiser WebRequest, une connexion TcpClient () peut être établie dans le même contexte à l'aide des mêmes outils que ceux requis pour une connexion TLS. À savoir, le ServicePointManager.SecurityProtocol pour définir les protocoles pris en charge et le ServicePointManager.ServerCertificateValidationCallback pour valider le certificat du serveur.

TcpClient () et WebRequest peuvent utiliser ces paramètres:
- activer tous les protocoles et laisser le Tls Handshake déterminer lequel sera utilisé.
- Définissez un délégué RemoteCertificateValidationCallback() pour valider le X509Certificates que le serveur transmet dans un X509Chain.

En pratique, le Tls Handshake est identique lors de l'établissement d'une connexion TcpClient ou WebRequest.
Cette approche vous permet de savoir quels protocoles Tls votre HttpWebRequest va négocier avec le même serveur.

Configurez un TcpClient() pour recevoir et évaluer la SslStream.
L'indicateur checkCertificateRevocation est défini sur false, de sorte que le processus ne perd pas de temps à rechercher la liste de révocation.
Le rappel de validation du certificat est le même que celui spécifié dans 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);

//(...)

La classe TlsInfo collecte des informations sur la connexion sécurisée établie:
- version du protocole Tls
- Algorithmes de chiffrement et de hachage
- Le certificat de serveur utilisé dans la poignée de main 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; }
}
10
Jimi

La solution ci-dessous est très certainement un "hack" dans le sens où elle utilise la réflexion, mais elle couvre actuellement la plupart des situations dans lesquelles vous pourriez être avec un HttpWebRequest. Il retournera null si la version Tls n'a pas pu être déterminée. Il vérifie également la version de Tls dans la même demande avant d’écrire quoi que ce soit dans le flux de demandes. Si la poignée de main Tls du flux ne s'est pas encore produite lorsque vous appelez la méthode, elle sera déclenchée.

Votre exemple d'utilisation ressemblerait à ceci:

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

Et la méthode:

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

En rassemblant quelques idées ici et là, j'ai mis au point une méthode simple pour tester chaque protocole disponible, en forçant un type spécifique de connexion à chaque essai . À la fin, je reçois une liste avec les résultats à utiliser selon mes besoins.

Ps: Le test n’est valide que si vous savez que le site Web est en ligne - vous pouvez effectuer un test préalable pour le vérifier.

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

J'espère que cela vous sera utile

0
Eduardo Gadotti

La seule façon de comprendre est d'utiliser SslStream pour établir une connexion test, puis de vérifier la propriété 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();

J'ai vérifié que sslStream.SslProtocl sera toujours identique au TlsStream.m_worker.SslProtocol utilisé par HttpWebRequest 's Connection.

0
Alex.Wei