web-dev-qa-db-fra.com

Utilisation d'un certificat auto-signé avec HttpWebRequest / Response de .NET

J'essaie de me connecter à une API qui utilise un certificat SSL auto-signé. Je le fais en utilisant les objets HttpWebRequest et HttpWebResponse de .NET. Et je reçois une exception qui:

La connexion sous-jacente a été fermée: impossible d'établir une relation d'approbation pour le canal sécurisé SSL/TLS.

Je comprends ce que cela signifie. Et je comprends pourquoi .NET pense qu'il devrait m'avertir et fermer la connexion. Mais dans ce cas, je voudrais simplement me connecter à l'API de toute façon, les attaques de l'homme du milieu soient damnées.

Alors, comment puis-je ajouter une exception pour ce certificat auto-signé? Ou l'approche consiste à dire à HttpWebRequest/Response de ne pas valider le certificat du tout? Comment ferais-je ça?

77
Dominic Scheirlinck

@Domster: cela fonctionne, mais vous voudrez peut-être renforcer la sécurité en vérifiant si le hachage du certificat correspond à ce que vous attendez. Donc, une version étendue ressemble un peu à ceci (basée sur du code en direct que nous utilisons):

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

Il s'avère que si vous souhaitez simplement désactiver complètement la validation de certificat, vous pouvez modifier le ServerCertificateValidationCallback sur le ServicePointManager, comme suit:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

Cela validera tous les certificats (y compris ceux invalides, expirés ou auto-signés).

87
Dominic Scheirlinck

Notez que dans .NET 4.5, vous pouvez remplacer la validation SSL par HttpWebRequest lui-même (et non via un délégué global qui affecte toutes les demandes):

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

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.ServerCertificateValidationCallback = delegate { return true; };
44
user2117074

Ajoutez le certificat auto-signé aux autorités de certification racine de confiance de l'ordinateur local

Vous pouvez importer le certificat en exécutant le MMC en tant qu'administrateur.

Comment: afficher les certificats avec le composant logiciel enfichable MMC

42
wgthom

La portée du rappel de validation utilisé dans réponse de Domster peut être limitée à une demande spécifique en utilisant le paramètre sender sur le délégué ServerCertificateValidationCallback. La classe d'étendue simple suivante utilise cette technique pour câbler temporairement un rappel de validation qui ne s'exécute que pour un objet de requête donné.

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 classe ci-dessus peut être utilisée pour ignorer toutes les erreurs de certificat pour une demande spécifique comme suit:

var request = WebRequest.Create(uri);
using (new ServerCertificateValidationScope(request, delegate { return true; }))
{
    request.GetResponse();
}
33
Nathan Baulch

Pour ajouter une aide possible à quelqu'un d'autre ... Si vous voulez qu'il invite l'utilisateur à installer le certificat auto-signé, vous pouvez utiliser ce code (modifié à partir du dessus).

Ne nécessite pas de droits d'administrateur, installe sur les profils de confiance des utilisateurs locaux:

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

Cela semble bien fonctionner pour notre application, et si l'utilisateur appuie sur non, la communication ne fonctionnera pas.

Mise à jour: 2015-12-11 - Changé StoreName.Root en StoreName.My - My s'installera dans le magasin d'utilisateurs local, au lieu de Root. Root sur certains systèmes ne fonctionnera pas, même si vous "exécutez en tant qu'administrateur"

3
TravisWhidden

S'appuyant uniquement sur la réponse de devstuff pour inclure le sujet et l'émetteur ... commentaires bienvenus ...

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

Je rencontrais le même problème que l'OP où la demande Web lèverait cette exception exacte. Je pensais que tout était correctement configuré, le certificat était installé, je pouvais le localiser très bien dans le magasin de machines et le joindre à la demande Web, et j'avais désactivé la vérification des certificats dans le contexte de la demande.

Il s'est avéré que je fonctionnais sous mon compte d'utilisateur et que le certificat a été installé dans le magasin de machines. Cela a provoqué la demande Web de lever cette exception. Pour résoudre le problème, j'ai dû exécuter en tant qu'administrateur ou installer le certificat dans le magasin d'utilisateurs et le lire à partir de là.

Il semblerait que C # soit capable de trouver le certificat dans le magasin de machines même s'il ne peut pas être utilisé avec une demande Web, et que cela entraîne la levée de l'exception de l'OP une fois la demande Web émise.

1
Simon Ejsing

Une chose à garder à l'esprit est que le fait d'avoir ServicePointManager.ServerCertificateValidationCallback ne semble pas signifier que la vérification de la liste de révocation de certificats et la validation du nom de serveur ne sont pas effectuées, cela ne fournit qu'un moyen de remplacer leur résultat. Votre service peut donc prendre un certain temps pour obtenir une liste de révocation de certificats, vous ne saurez qu'après avoir échoué à certains contrôles.

1
nicki