ssl web api c#
¿Qué versión de TLS se negoció? (4)
Al reunir algunas ideas aquí y allá, hice un método simple para probar cada protocolo disponible, forzando un tipo específico de conexión en cada intento. Al final, obtengo una lista con los resultados para usar según lo necesito.
Ps: La prueba es válida solo si sabe que el sitio web está en línea; puede hacer una prueba previa para verificar esto.
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;
}
Espero que esto sea útil
Tengo mi aplicación ejecutándose en .NET 4.7. Por defecto, intentará usar TLS1.2. ¿Es posible saber qué versión de TLS se negoció al realizar, por ejemplo, una solicitud HTTP como se muestra a continuación?
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(decodedUri);
if (requestPayload.Length > 0)
{
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(requestPayload, 0, requestPayload.Length);
}
}
Solo necesito esta información para fines de registro / depuración, por lo que no es importante tener esta información antes de escribir en la secuencia de solicitud o recibir la respuesta. No deseo analizar los registros de seguimiento de red para esta información, y tampoco quiero crear una segunda conexión (usando SslStream o similar).
La única forma en que puedo entender es usar
SslStream
para hacer una conexión de prueba, luego verificar la propiedad
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();
He comprobado que
sslStream.SslProtocl
siempre será igual que el
TlsStream.m_worker.SslProtocol
que usó
HttpWebRequest
''s
Connection
.
La solución a continuación es sin duda un "truco" en el sentido de que utiliza la reflexión, pero actualmente cubre la mayoría de las situaciones en las que podría encontrarse con una HttpWebRequest. Volverá nulo si no se pudo determinar la versión de Tls. También verifica la versión de Tls en la misma solicitud, antes de que haya escrito algo en la secuencia de solicitud. Si aún no se ha producido el protocolo de enlace Tls cuando se llama al método, lo activará.
Su uso de muestra se vería así:
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);
}
}
Y el 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;
}
Puede usar Reflection para llegar al valor de la propiedad
TlsStream->SslState->SslProtocol
.
Esta información se puede extraer del flujo devuelto por
HttpWebRequest.GetRequestStream()
y
HttpWebRequest.GetResponseStream()
.
ExtractSslProtocol()
también maneja el
GzipStream
o
DeflateStream
comprimido que se devuelven cuando se activa
WebRequest
AutomaticDecompression
.
La validación ocurrirá en
ServerCertificateValidationCallback
, que se llama cuando la solicitud se inicializa con
request.GetRequestStream()
Nota
:
SecurityProtocolType.Tls13
se incluye en .Net Framework
4.8+
y .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);
}
RemoteCertificateValidationCallback
tiene información útil sobre los protocolos de seguridad utilizados.
(consulte:
Parámetros de seguridad de la capa de transporte (TLS) (IANA)
y
RFC 5246
).
Los tipos de protocolos de seguridad utilizados pueden ser lo suficientemente informativos, ya que cada versión de protocolo admite un subconjunto de algoritmos de Hashing y Cifrado.
Tls 1.2, presenta
HMAC-SHA256
y desprecia los cifrados
IDEA
y
DES
(todas las variantes se enumeran en los documentos vinculados).
Aquí,
OIDExtractor
un
OIDExtractor
, que enumera los algoritmos en uso.
Tenga en cuenta que tanto TcpClient () como WebRequest () llegarán aquí.
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;
}
ACTUALIZACIÓN 2:
El
secur32.dll
->
QueryContextAttributesW()
, permite consultar el contexto de seguridad de conexión de una secuencia inicializada.
[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 puede ver en la documentación, este método devuelve un
void* buffer
que hace referencia a una estructura
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;
}
El miembro
SchProtocols dwProtocol
es el SslProtocol.
Cuál es el truco.
El
TlsStream.Context.m_SecurityContext._handle
que hace referencia al controlador de contexto de conexión no es público.
Por lo tanto, puede obtenerlo, nuevamente, solo a través de la reflexión o a través de las clases derivadas
System.Net.Security.SslStream
(
System.Net.Security.SslStream
y
System.Net.Security.NegotiateStream
) devueltas por
TcpClient.GetStream()
.
Desafortunadamente, el Stream devuelto por WebRequest / WebResponse no se puede transmitir a estas clases.
Los tipos de conexiones y secuencias solo se mencionan a través de propiedades y campos no públicos.
Estoy publicando la documentación reunida, tal vez te ayude a encontrar otro camino para llegar a ese controlador de contexto.
Las declaraciones, estructuras, listas de enumeradores están en
QueryContextAttributesW (PASTEBIN)
.
Microsoft TechNet
Estructuras de autenticación
MSDN
Crear una conexión segura usando Schannel
Obteniendo información sobre las conexiones de Schannel
Consulta de los atributos de un contexto de canal
QueryContextAttributes (Schannel)
Base de código (parcial)
estructura interna SSPIHandle {}
enumeración interna ContextAttribute {}
ACTUALIZACIÓN 1:
Vi en su comentario a otra respuesta que la solución que usa
TcpClient()
no es aceptable para usted. Lo dejo aquí de todos modos, así que los comentarios de Ben Voigt en este serán útiles para cualquier persona interesada. Además, 3 posibles soluciones son mejores que 2.
Algunos detalles de implementación sobre el uso de
TcpClient()
SslStream
en el contexto proporcionado.
Si se requiere información de protocolo antes de inicializar una WebRequest, se puede establecer una conexión TcpClient () en el mismo contexto utilizando las mismas herramientas requeridas para una conexión TLS.
A saber, el
ServicePointManager.SecurityProtocol
para definir los protocolos compatibles y el
ServicePointManager.ServerCertificateValidationCallback
para validar el certificado del servidor.
Tanto TcpClient () como WebRequest pueden usar esta configuración:
- habilite todos los protocolos y deje que Tls Handshake determine cuál se usará.
- defina un delegado
RemoteCertificateValidationCallback()
para validar los
X509Certificates
el servidor pasa en una
X509Chain
.
En la práctica, el Apretón de manos de Tls es el mismo cuando se establece una conexión TcpClient o WebRequest.
Este enfoque le permite saber qué protocolo Tls negociará su HttpWebRequest con el mismo servidor.
Configure un
TcpClient()
para recibir y evaluar el
SslStream
.
El indicador
checkCertificateRevocation
está establecido en
false
, por lo que el proceso no perderá tiempo buscando la lista de revocación.
La devolución de llamada de validación del certificado es la misma especificada en
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 clase
TlsInfo
recopila cierta información sobre la conexión segura establecida:
-
Versión del protocolo Tls
- Algoritmos de cifrado y hash
- El certificado del servidor utilizado en el protocolo de enlace 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; }
}