Quale versione TLS è stata negoziata?
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
È 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)
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; }
}
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;
}
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
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.