Usando un certificado autofirmado con HttpWebRequest / Response de .NET

80

Estoy intentando conectarme a una API que utiliza un certificado SSL autofirmado. Lo estoy haciendo usando los objetos HttpWebRequest y HttpWebResponse de .NET. Y obtengo una excepción que:

La conexión subyacente se cerró: no se pudo establecer una relación de confianza para el canal seguro SSL / TLS.

Entiendo lo que esto significa. Y entiendo por qué .NET siente que debería advertirme y cerrar la conexión. Pero en este caso, me gustaría conectarme a la API de todos modos, al diablo con los ataques man-in-the-middle.

Entonces, ¿cómo puedo agregar una excepción para este certificado autofirmado? ¿O el enfoque es decirle a HttpWebRequest / Response que no valide el certificado en absoluto? ¿Como podría hacerlo?

Dominic Scheirlinck
fuente

Respuestas:

81

@Domster: eso funciona, pero es posible que desee aplicar un poco de seguridad al verificar si el hash del certificado coincide con lo que espera. Entonces, una versión expandida se parece un poco a esto (basado en un código en vivo que estamos usando):

static readonly byte[] apiCertHash = { 0xZZ, 0xYY, ....};

/// <summary>
/// Somewhere in your application's startup/init sequence...
/// </summary>
void InitPhase()
{
    // Override automatic validation of SSL server certificates.
    ServicePointManager.ServerCertificateValidationCallback =
           ValidateServerCertficate;
}

/// <summary>
/// Validates the SSL server certificate.
/// </summary>
/// <param name="sender">An object that contains state information for this
/// validation.</param>
/// <param name="cert">The certificate used to authenticate the remote party.</param>
/// <param name="chain">The chain of certificate authorities associated with the
/// remote certificate.</param>
/// <param name="sslPolicyErrors">One or more errors associated with the remote
/// certificate.</param>
/// <returns>Returns a boolean value that determines whether the specified
/// certificate is accepted for authentication; true to accept or false to
/// reject.</returns>
private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        // Good certificate.
        return true;
    }

    log.DebugFormat("SSL certificate error: {0}", sslPolicyErrors);

    bool certMatch = false; // Assume failure
    byte[] certHash = cert.GetCertHash();
    if (certHash.Length == apiCertHash.Length)
    {
        certMatch = true; // Now assume success.
        for (int idx = 0; idx < certHash.Length; idx++)
        {
            if (certHash[idx] != apiCertHash[idx])
            {
                certMatch = false; // No match
                break;
            }
        }
    }

    // Return true => allow unauthenticated server,
    //        false => disallow unauthenticated server.
    return certMatch;
}
devstuff
fuente
Probablemente alguien que prefirió la forma correcta a continuación. De todos modos, este truco funciona en un apuro, pero probablemente no debería estar codificando este tipo de excepciones en ... o simplemente deshabilite la verificación por completo (a través de la sugerencia directamente a continuación) o indique a su computadora que confíe en el certificado ... .
BrainSlugs83
3
@ BrainSlugs83: La desactivación también es ciertamente una opción, pero solo los administradores pueden agregar el certificado al almacén de autoridades raíz a nivel de máquina. Mi solución funciona de cualquier manera.
devstuff
Y lo entiendo completamente, pero preguntaste, y esa sigue siendo mi suposición de por qué alguien rechazó tu respuesta. E independientemente de que sea más trabajo, en mi humilde opinión, la respuesta de wgthom a continuación sigue siendo la más correcta.
BrainSlugs83
Por cierto, tenga cuidado, creo que ServerCertificateValidationCallback es ESTÁTICO, y ni siquiera threadlocal. si no me equivoco, una vez configurado, permanece configurado hasta que lo borre. si desea utilizarlo para una sola conexión y no en todas las demás, tenga mucho cuidado con las solicitudes paralelas ..
quetzalcoatl
3
Esta es la mejor forma de hacer esto. Si elimina la comprobación de sslPolicyErrors, puede asegurarse de que el certificado de API sea siempre el esperado. Una cosa a tener en cuenta es que la huella digital del certificado en el código anterior es una matriz de bytes constante. Esto no se compilará como está escrito. En su lugar, intente una matriz de bytes de solo lectura estática. El compilador se ahoga con esto porque requiere el operador new ().
Centijo
92

Resulta que, si solo desea deshabilitar la validación de certificados por completo, puede cambiar ServerCertificateValidationCallback en ServicePointManager, así:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

Esto validará todos los certificados (incluidos los inválidos, vencidos o autofirmados).

Dominic Scheirlinck
fuente
2
Perfecto para algunas pruebas rápidas contra máquinas de desarrollo. Gracias.
Nate
2
¿A qué alcance afecta esto? ¿Todo en el dominio de aplicación? todo en el grupo de aplicaciones? todo en la máquina?
codeulike
29
¡Pero ten cuidado! La experiencia de RL muestra que estos trucos de desarrollo a menudo se
abren
4
Este es un truco útil en el desarrollo, por lo que poner una declaración #if DEBUG #endif es lo mínimo que debe hacer para que esto sea más seguro y evitar que termine en producción.
AndyD
3
A menos que este tipo elimine esta respuesta, veremos un hecho curioso de que una respuesta incorrecta recibe muchos más votos que la correcta.
Lex Li
47

Tenga en cuenta que en .NET 4.5 puede anular la validación SSL por HttpWebRequest en sí (y no a través del delegado global que afecta a todas las solicitudes):

http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.servercertificatevalidationcallback.aspx

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.ServerCertificateValidationCallback = delegate { return true; };
usuario2117074
fuente
1
Por favor vota esto; ¡Vale la pena actualizarlo a 4.5!
Lynn Crumbling
1
@FlorianWinter Sí, tienes que adoptar la lógica del usuario devstuff
Horario de verano
43

Agregue el certificado autofirmado a las autoridades de certificación raíz de confianza del equipo local

Puede importar el certificado ejecutando MMC como administrador.

Cómo: ver certificados con el complemento MMC

wgthom
fuente
4
En mi humilde opinión, esta es la forma más correcta; las personas son demasiado vagas, por lo que codifican excepciones especiales para cosas que probablemente no deberían.
BrainSlugs83
4
¿Ese método funciona para Windows Mobile 6.5? ¿Qué tal 7? En mi caso, no quería tener que agregar un certificado local a cada dispositivo móvil en el que planeaba ejecutar una versión de desarrollo. Una buena excepción, en este caso, facilita enormemente la implementación. Pereza o eficiencia, me dices.
Dominic Scheirlinck
3
@domster Está utilizando certificados SSL por una razón: verificar los puntos finales. Si desarrolla código que funcione específicamente en torno a eso, no lo está probando correctamente y corre el riesgo de filtrar ese código a un entorno en vivo. Si instalar un certificado en el cliente es realmente demasiado trabajo, ¿por qué no pagar por un certificado de un emisor en el que confían todos los dispositivos?
Básico
1
@Basic Si recuerdo este caso específico, habría necesitado varios certificados comodín (había media docena de TLD a los que se estaba conectando, todos bajo nuestro control). Es un costo difícil de justificar para un entorno de desarrollo. En este caso, el único código que se "soluciona" y no se prueba es que no se lanza una excepción donde estaría de otro modo. Debería probar esa ruta de excepción específica independientemente de si está utilizando esta solución alternativa. Y, finalmente, si no puede mantener el código de desarrollo fuera de producción, tiene problemas mucho mayores que la validación SSL.
Dominic Scheirlinck
para aplicaciones web, asegúrese de reciclar su grupo de aplicaciones o reiniciar su sitio web. personalmente, acabo de volver a compilar y luego funcionó. para nuestro material wsdl, la verificación del certificado parece ocurrir en la inicialización y en la caché.
sonjz
34

El alcance de la devolución de llamada de validación utilizada en la respuesta de Domster se puede limitar a una solicitud específica utilizando el parámetro del remitente en el ServerCertificateValidationCallbackdelegado. La siguiente clase de alcance simple utiliza esta técnica para conectar temporalmente una devolución de llamada de validación que solo se ejecuta para un objeto de solicitud determinado.

public class ServerCertificateValidationScope : IDisposable
{
    private readonly RemoteCertificateValidationCallback _callback;

    public ServerCertificateValidationScope(object request,
        RemoteCertificateValidationCallback callback)
    {
        var previous = ServicePointManager.ServerCertificateValidationCallback;
        _callback = (sender, certificate, chain, errors) =>
            {
                if (sender == request)
                {
                    return callback(sender, certificate, chain, errors);
                }
                if (previous != null)
                {
                    return previous(sender, certificate, chain, errors);
                }
                return errors == SslPolicyErrors.None;
            };
        ServicePointManager.ServerCertificateValidationCallback += _callback;
    }

    public void Dispose()
    {
        ServicePointManager.ServerCertificateValidationCallback -= _callback;
    }
}

La clase anterior se puede utilizar para ignorar todos los errores de certificado para una solicitud específica de la siguiente manera:

var request = WebRequest.Create(uri);
using (new ServerCertificateValidationScope(request, delegate { return true; }))
{
    request.GetResponse();
}
Nathan Baulch
fuente
6
Esta respuesta necesita más votos positivos :) Es la respuesta más razonable para omitir la validación del certificado para una sola solicitud utilizando un objeto HttpWebRequest.
MikeJansen
Agregué esto y sigo recibiendo La solicitud fue cancelada: No se pudo crear un canal seguro SSL / TLS.
vikingben
7
Esto realmente no resuelve el problema en un entorno de subprocesos múltiples.
Hans
1
maaan !!!, una publicación de 5 años me salvó el día, tengo problemas para conectarme a un dispositivo de módem de satélite antiguo con un certificado no válido. ¡¡Gracias!!
WindyHen
3

Basándome en la respuesta de devstuff para incluir el asunto y el emisor ... comentarios bienvenidos ...

public class SelfSignedCertificateValidator
{
    private class CertificateAttributes
    {
        public string Subject { get; private set; }
        public string Issuer { get; private set; }
        public string Thumbprint { get; private set; }

        public CertificateAttributes(string subject, string issuer, string thumbprint)
        {
            Subject = subject;
            Issuer = issuer;                
            Thumbprint = thumbprint.Trim(
                new char[] { '\u200e', '\u200f' } // strip any lrt and rlt markers from copy/paste
                ); 
        }

        public bool IsMatch(X509Certificate cert)
        {
            bool subjectMatches = Subject.Replace(" ", "").Equals(cert.Subject.Replace(" ", ""), StringComparison.InvariantCulture);
            bool issuerMatches = Issuer.Replace(" ", "").Equals(cert.Issuer.Replace(" ", ""), StringComparison.InvariantCulture);
            bool thumbprintMatches = Thumbprint == String.Join(" ", cert.GetCertHash().Select(h => h.ToString("x2")));
            return subjectMatches && issuerMatches && thumbprintMatches; 
        }
    }

    private readonly List<CertificateAttributes> __knownSelfSignedCertificates = new List<CertificateAttributes> {
        new CertificateAttributes(  // can paste values from "view cert" dialog
            "CN = subject.company.int", 
            "CN = issuer.company.int", 
            "f6 23 16 3d 5a d8 e5 1e 13 58 85 0a 34 9f d6 d3 c8 23 a8 f4") 
    };       

    private static bool __createdSingleton = false;

    public SelfSignedCertificateValidator()
    {
        lock (this)
        {
            if (__createdSingleton)
                throw new Exception("Only a single instance can be instanciated.");

            // Hook in validation of SSL server certificates.  
            ServicePointManager.ServerCertificateValidationCallback += ValidateServerCertficate;

            __createdSingleton = true;
        }
    }

    /// <summary>
    /// Validates the SSL server certificate.
    /// </summary>
    /// <param name="sender">An object that contains state information for this
    /// validation.</param>
    /// <param name="cert">The certificate used to authenticate the remote party.</param>
    /// <param name="chain">The chain of certificate authorities associated with the
    /// remote certificate.</param>
    /// <param name="sslPolicyErrors">One or more errors associated with the remote
    /// certificate.</param>
    /// <returns>Returns a boolean value that determines whether the specified
    /// certificate is accepted for authentication; true to accept or false to
    /// reject.</returns>
    private bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    {
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;   // Good certificate.

        Dbg.WriteLine("SSL certificate error: {0}", sslPolicyErrors);
        return __knownSelfSignedCertificates.Any(c => c.IsMatch(cert));            
    }
}
crokusek
fuente
3

Para agregar como posible ayuda a otra persona ... Si desea que le solicite al usuario que instale el certificado autofirmado, puede usar este código (modificado desde arriba).

No requiere derechos de administrador, se instala en los perfiles de confianza de los usuarios locales:

    private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    {
        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            // Good certificate.
            return true;
        }

        Common.Helpers.Logger.Log.Error(string.Format("SSL certificate error: {0}", sslPolicyErrors));
        try
        {
            using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
            {
                store.Open(OpenFlags.ReadWrite);
                store.Add(new X509Certificate2(cert));
                store.Close();
            }
            return true;
        }
        catch (Exception ex)
        {
            Common.Helpers.Logger.Log.Error(string.Format("SSL certificate add Error: {0}", ex.Message));
        }

        return false;
    }

Esto parece funcionar bien para nuestra aplicación, y si el usuario presiona no, la comunicación no funcionará.

Actualización: 2015-12-11 - Se cambió StoreName.Root a StoreName.My: My se instalará en la tienda de usuarios locales, en lugar de Root. La raíz en algunos sistemas no funcionará, incluso si "ejecuta como administrador"

TravisWhidden
fuente
Esto sería fantástico si funcionara en Compact Framework winCE. store.Add (..) no está disponible.
Dawit
1

Una cosa a tener en cuenta es que tener ServicePointManager.ServerCertificateValidationCallback no parece significar que la verificación de CRL y la validación del nombre del servidor no se hayan realizado, solo proporciona un medio para anular su resultado. Por lo tanto, es posible que su servicio aún tarde un poco en obtener una CRL, solo después sabrá que falló algunas verificaciones.

Nicki
fuente
1

Me estaba encontrando con el mismo problema que el OP donde la solicitud web arrojaría esa excepción exacta. Pensé que tenía todo configurado correctamente, el certificado estaba instalado, podía ubicarlo en la tienda de la máquina sin problemas y adjuntarlo a la solicitud web, y había deshabilitado la verificación de certificados en el contexto de la solicitud.

Resultó que estaba ejecutando mi cuenta de usuario y que el certificado estaba instalado en la tienda de máquinas. Esto provocó que la solicitud web lanzara esta excepción. Para resolver el problema, tenía que ejecutarme como administrador o instalar el certificado en la tienda de usuarios y leerlo desde allí.

Parecería que C # puede encontrar el certificado en la tienda de la máquina aunque no se pueda usar con una solicitud web, y esto da como resultado que se lance la excepción del OP una vez que se emite la solicitud web.

Simon Ejsing
fuente
Para los servicios de Windows, puede configurar configuraciones de certificado independientes para cada servicio. Si no está escribiendo una aplicación de escritorio sino un servicio, el certificado de CA se puede importar en MMC para el demonio de servicio específicamente. ¿Cuál es la diferencia entre cuenta de usuario y cuenta de máquina? Pensé que todo en la cuenta de la máquina se aplica al usuario automáticamente.
ArticIceJuice
1

En primer lugar, me disculpo porque he utilizado la solución descrita por @devstuff. Sin embargo, he encontrado algunas formas de mejorarlo.

  • agregar manejo de certificados autofirmados
  • comparación por los datos brutos de los certificados
  • validación real de la autoridad certificadora
  • algunos comentarios y mejoras adicionales

Aquí está mi modificación:

private static X509Certificate2 caCertificate2 = null;

/// <summary>
/// Validates the SSL server certificate.
/// </summary>
/// <param name="sender">An object that contains state information for this validation.</param>
/// <param name="cert">The certificate used to authenticate the remote party.</param>
/// <param name="chain">The chain of certificate authorities associated with the remote certificate.</param>
/// <param name="sslPolicyErrors">One or more errors associated with the remote certificate.</param>
/// <returns>Returns a boolean value that determines whether the specified certificate is accepted for authentication; true to accept or false to reject.</returns>
private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        // Good certificate.
        return true;
    }

    // If the following line is not added, then for the self-signed cert an error will be (not tested with let's encrypt!):
    // "A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider. (UntrustedRoot)"
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

    // convert old-style cert to new-style cert
    var returnedServerCert2 = new X509Certificate2(cert);

    // This part is very important. Adding known root here. It doesn't have to be in the computer store at all. Neither do certificates.
    chain.ChainPolicy.ExtraStore.Add(caCertificate2);

    // 1. Checks if ff the certs are OK (not expired/revoked/etc) 
    // 2. X509VerificationFlags.AllowUnknownCertificateAuthority will make sure that untrusted certs are OK
    // 3. IMPORTANT: here, if the chain contains the wrong CA - the validation will fail, as the chain is wrong!
    bool isChainValid = chain.Build(returnedServerCert2);
    if (!isChainValid)
    {
        string[] errors = chain.ChainStatus
            .Select(x => String.Format("{0} ({1})", x.StatusInformation.Trim(), x.Status))
            .ToArray();

        string certificateErrorsString = "Unknown errors.";

        if (errors != null && errors.Length > 0)
        {
            certificateErrorsString = String.Join(", ", errors);
        }

        Log.Error("Trust chain did not complete to the known authority anchor. Errors: " + certificateErrorsString);
        return false;
    }

    // This piece makes sure it actually matches your known root
    bool isValid = chain.ChainElements
        .Cast<X509ChainElement>()
        .Any(x => x.Certificate.RawData.SequenceEqual(caCertificate2.GetRawCertData()));

    if (!isValid)
    {
        Log.Error("Trust chain did not complete to the known authority anchor. Thumbprints did not match.");
    }

    return isValid;
}

establecimiento de certificados:

caCertificate2 = new X509Certificate2("auth/ca.crt", "");
var clientCertificate2 = new X509Certificate2("auth/client.pfx", "");

paso método delegado

ServerCertificateValidationCallback(ValidateServerCertficate)

client.pfx se genera con KEY y CERT como tal:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx
Alex
fuente