Korzystanie z certyfikatu z podpisem własnym z HttpWebRequest / Response platformy .NET

80

Próbuję połączyć się z interfejsem API, który używa certyfikatu SSL z podpisem własnym. Robię to przy użyciu obiektów HttpWebRequest i HttpWebResponse platformy .NET. Otrzymuję wyjątek, który:

Połączenie bazowe zostało zamknięte: nie można ustanowić relacji zaufania dla bezpiecznego kanału SSL / TLS.

Rozumiem, co to oznacza. I rozumiem, dlaczego .NET uważa, że ​​powinien mnie ostrzec i zamknąć połączenie. Ale w tym przypadku i tak chciałbym po prostu połączyć się z API, przeklęte ataki typu man-in-the-middle.

Jak więc dodać wyjątek dla tego certyfikatu z podpisem własnym? Czy jest podejściem nakazującym HttpWebRequest / Response, aby w ogóle nie sprawdzał poprawności certyfikatu? Jak miałbym to zrobić?

Dominic Scheirlinck
źródło

Odpowiedzi:

81

@Domster: to działa, ale możesz chcieć wymusić trochę bezpieczeństwa, sprawdzając, czy hash certyfikatu jest zgodny z oczekiwaniami. Tak więc rozszerzona wersja wygląda trochę tak (w oparciu o kod na żywo, którego używamy):

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
źródło
Prawdopodobnie ktoś, kto wolał właściwy sposób poniżej. W każdym razie ten hack działa w mgnieniu oka, ale prawdopodobnie nie powinieneś kodować tego rodzaju wyjątków w ... albo po prostu wyłącz sprawdzanie razem (za pomocą sugestii bezpośrednio poniżej) lub faktycznie poinstruuj komputer, aby zaufał certyfikatowi. ,
BrainSlugs83
4
@ BrainSlugs83: Wyłączenie jest z pewnością również opcją, ale dodanie certyfikatu do magazynu uprawnień głównych na poziomie komputera może być wykonane tylko przez administratorów. Moje rozwiązanie działa w obu przypadkach.
devstuff,
W pełni to rozumiem, ale zapytałeś, a to wciąż moje przypuszczenie, dlaczego ktoś przegłosował twoją odpowiedź. I niezależnie od tego, że jest to więcej pracy, odpowiedź IMHO wgthom poniżej jest nadal najbardziej poprawna.
BrainSlugs83
btw, bądź ostrożny, myślę, że ServerCertificateValidationCallback jest STATIC, a nawet nie jest wątkowy. jeśli się nie mylę, to po ustawieniu pozostaje ustawione, dopóki go nie usuniesz. jeśli chcesz użyć go do jednego połączenia i nie we wszystkich innych, być bardzo ostrożnym z równoległych żądań ..
Quetzalcoatl
3
To najlepszy sposób na zrobienie tego. Jeśli usuniesz sprawdzanie błędów sslPolicyErrors, możesz faktycznie upewnić się, że certyfikat API jest zawsze taki, jakiego oczekujesz. Należy zauważyć, że odcisk cyfrowy certyfikatu w powyższym kodzie jest tablicą bajtów stałych. To nie skompiluje się tak, jak napisano. Zamiast tego spróbuj statycznej tablicy bajtów tylko do odczytu. Kompilator dławi się tym, ponieważ wymaga operatora new ().
Centijo
92

Okazuje się, że jeśli chcesz po prostu całkowicie wyłączyć weryfikację certyfikatu, możesz zmienić ServerCertificateValidationCallback w ServicePointManager, na przykład:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

Spowoduje to zweryfikowanie wszystkich certyfikatów (w tym nieważnych, wygasłych lub z podpisem własnym).

Dominic Scheirlinck
źródło
2
Idealny do szybkich testów na maszynach deweloperskich. Dzięki.
Nate
2
Jaki zakres ma to wpływ - wszystko w domenie aplikacji? wszystko w Apppool? wszystko na maszynie?
codeulike
29
Ale uważaj! Doświadczenie RL pokazuje, że te hacki rozwojowe często trafiają do wydania produktu: Najniebezpieczniejszy kod na świecie
Doomjunky
4
Jest to hack przydatny w programowaniu, więc umieszczenie wokół niego instrukcji #if DEBUG #endif jest najmniejszą rzeczą, którą powinieneś zrobić, aby uczynić to bezpieczniejszym i powstrzymać proces kończący się w produkcji.
AndyD
3
Jeśli ten gość nie usunie tej odpowiedzi, zobaczymy zabawny fakt, że zła odpowiedź otrzyma znacznie więcej głosów niż poprawna.
Lex Li
47

Zauważ, że w .NET 4.5 możesz przesłonić walidację SSL dla samego HttpWebRequest (a nie przez delegata globalnego, który wpływa na wszystkie żądania):

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

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.ServerCertificateValidationCallback = delegate { return true; };
user2117074
źródło
1
Proszę, zagłosuj na to; warto to zaktualizować do 4,5 za!
Lynn Crumbling
1
@FlorianWinter Tak, musisz przyjąć logikę z user devstuff
Summer-Time
43

Dodaj certyfikat z podpisem własnym do urzędów certyfikacji zaufanych głównych komputerów lokalnych

Certyfikat można zaimportować, uruchamiając program MMC jako administrator.

Instrukcje: wyświetlanie certyfikatów za pomocą przystawki MMC

wgthom
źródło
4
IMHO to jest najbardziej poprawny sposób; ludzie są po prostu zbyt leniwi, więc kodują w specjalnych wyjątkach dla rzeczy, których prawdopodobnie nie powinni.
BrainSlugs83
4
Czy ta metoda działa w przypadku systemu Windows Mobile 6.5? A co powiesz na 7? W moim przypadku nie chciałem dodawać lokalnego certyfikatu do każdego urządzenia mobilnego, na którym planowałem uruchomić wersję rozwojową. Dobry wyjątek w tym przypadku znacznie ułatwia rozmieszczenie. Mówisz mi, że lenistwo lub efektywność.
Dominic Scheirlinck,
3
@domster Nie bez powodu używasz certyfikatów SSL - aby zweryfikować punkty końcowe. Jeśli opracujesz kod, który konkretnie obejdzie ten problem, nie testujesz go poprawnie i ryzykujesz przeciekanie tego kodu do środowiska na żywo. Jeśli instalacja certyfikatu na kliencie to naprawdę zbyt dużo pracy, dlaczego nie zapłacić za certyfikat od wystawcy, któremu ufają wszystkie urządzenia?
Podstawowy
1
@Basic Gdybym pamiętał ten konkretny przypadek, potrzebowałbym kilku certyfikatów typu wildcard (istniało pół tuzina TLD, z którymi się łączył, wszystkie pod naszą kontrolą). To trudny do uzasadnienia koszt dla środowiska programistycznego. W tym przypadku jedyny kod, który jest „omijany” i nie jest testowany, polega na tym, że wyjątek nie jest generowany w innym miejscu. Powinieneś testować tę konkretną ścieżkę wyjątku, niezależnie od tego, czy używasz tego obejścia. I wreszcie, jeśli nie możesz zatrzymać kodu programistycznego poza produkcją, masz znacznie większe problemy niż walidacja SSL.
Dominic Scheirlinck,
w przypadku aplikacji internetowych pamiętaj, aby poddać recyklingowi swoją pulę aplikacji lub ponownie uruchomić witrynę. osobiście właśnie ponownie skompilowałem i zadziałało. w przypadku naszych elementów wsdl weryfikacja certyfikatu wydaje się następować podczas inicjowania i buforowania.
sonjz
35

Zakres wywołania zwrotnego walidacji używanego w odpowiedzi Domstera można ograniczyć do konkretnego żądania przy użyciu parametru nadawcy w ServerCertificateValidationCallbackdelegacie. Poniższa prosta klasa zakresu używa tej techniki do tymczasowego połączenia wywołania zwrotnego walidacji, które jest wykonywane tylko dla danego obiektu żądania.

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

Powyższa klasa może służyć do ignorowania wszystkich błędów certyfikatu dla określonego żądania w następujący sposób:

var request = WebRequest.Create(uri);
using (new ServerCertificateValidationScope(request, delegate { return true; }))
{
    request.GetResponse();
}
Nathan Baulch
źródło
6
Ta odpowiedź wymaga większej liczby głosów pozytywnych :) Najbardziej rozsądną odpowiedzią jest pominięcie sprawdzania poprawności certyfikatu dla pojedynczego żądania przy użyciu obiektu HttpWebRequest.
MikeJansen
Dodałem to i nadal otrzymuję. Żądanie zostało przerwane: nie można utworzyć bezpiecznego kanału SSL / TLS.
vikingben
7
To naprawdę nie rozwiązuje problemu w środowisku wielowątkowym.
Hans
1
Maaan !!!, 5-letni post ratuj mój dzień, mam problem z połączeniem ze starym modemem satelitarnym z nieważnym certyfikatem !! Dziękuję Ci!!
WindyHen
Jestem zdezorientowany / lekko zmartwiony! Czy powrót SslPolicyErrors.Nonew przypadku, gdy nie było poprzedniego wywołania zwrotnego, oznacza, że ​​w końcu nadpisujemy domyślną zasadę zasadą „zaakceptuj wszystko”? por. to pytanie i różne odpowiedzi: stackoverflow.com/q/9058096 . Byłbym bardzo szczęśliwy, gdyby powiedziano mi, dlaczego się mylę, a ten kod jest w porządku!
MikeBeaton
3

Po prostu opierając się na odpowiedzi od devstuff, aby uwzględnić temat i wydawcę ... komentarze mile widziane ...

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
źródło
3

Aby dodać jako ewentualną pomoc komuś innemu ... Jeśli chcesz, aby monitował użytkownika o zainstalowanie certyfikatu z podpisem własnym, możesz użyć tego kodu (zmodyfikowanego powyżej).

Nie wymaga uprawnień administratora, instaluje się na zaufanych profilach użytkowników lokalnych:

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

Wydaje się, że działa to dobrze w przypadku naszej aplikacji i jeśli użytkownik naciśnie „nie”, komunikacja nie będzie działać.

Aktualizacja: 2015-12-11 - Zmieniono StoreName.Root na StoreName.My - Moja instalacja zostanie zainstalowana w lokalnym sklepie użytkowników zamiast Root. Root w niektórych systemach nie będzie działał, nawet jeśli „uruchomisz jako administrator”

TravisWhidden
źródło
Byłoby wspaniale, gdyby działało na Compact Framework winCE. store.Add (..) jest niedostępny.
Dawit
1

Jedną z rzeczy, o których należy pamiętać, jest to, że posiadanie ServicePointManager.ServerCertificateValidationCallback nie wydaje się oznaczać, że sprawdzanie listy CRL i walidacja nazwy serwera nie zostały zakończone, a jedynie zapewnia możliwość zastąpienia ich wyniku. Uzyskanie listy CRL może zająć Twojej usłudze jeszcze trochę czasu, dopiero potem będziesz wiedział, że niektóre testy nie powiodły się.

nicki
źródło
1

Miałem ten sam problem, co w przypadku OP, w którym żądanie sieciowe wyrzuciło dokładnie ten wyjątek. Pomyślałem, że wszystko zostało skonfigurowane poprawnie, certyfikat został zainstalowany, mogłem go bez problemu zlokalizować w magazynie komputera i dołączyć do żądania internetowego, a także wyłączyłem weryfikację certyfikatów w kontekście żądania.

Okazało się, że działam na koncie użytkownika, a certyfikat został zainstalowany w magazynie maszyny. To spowodowało, że żądanie sieci Web zgłosiło ten wyjątek. Aby rozwiązać problem, musiałem albo działać jako administrator, albo zainstalować certyfikat w magazynie użytkownika i odczytać go z tego miejsca.

Wydawałoby się, że C # jest w stanie znaleźć certyfikat w magazynie komputera, mimo że nie można go użyć z żądaniem sieci Web, a to powoduje, że wyjątek OP jest generowany po wysłaniu żądania internetowego.

Simon Ejsing
źródło
W przypadku usług systemu Windows można skonfigurować oddzielne konfiguracje certyfikatów dla każdej usługi. Jeśli piszesz nie aplikację komputerową, ale usługę, certyfikat CA można zaimportować do programu MMC specjalnie dla demona usługi. Jaka jest różnica między kontem użytkownika a kontem komputera? Myślałem, że wszystko na koncie komputera odnosi się do użytkownika automatycznie.
ArticIceJuice
1

Po pierwsze - przepraszam, bo korzystałem z rozwiązania, które opisał @devstuff. Jednak znalazłem kilka sposobów, aby to poprawić.

  • dodanie obsługi certyfikatów z podpisem własnym
  • porównanie według danych surowych certyfikatów
  • faktyczna walidacja urzędu certyfikacji
  • kilka dodatkowych komentarzy i ulepszeń

Oto moja modyfikacja:

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

ustawianie certyfikatów:

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

przekazywanie metody delegata

ServerCertificateValidationCallback(ValidateServerCertficate)

client.pfx jest generowany za pomocą KEY i CERT jako takich:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx
Alex
źródło