Використання самопідписаного сертифіката за допомогою HttpWebRequest / Response .NET


80

Я намагаюся підключитися до API, який використовує самопідписаний сертифікат SSL. Я роблю це, використовуючи об'єкти HttpWebRequest та HttpWebResponse .NET. І я отримую виняток, який:

Основне з'єднання було закрито: не вдалося встановити відносини довіри для захищеного каналу SSL / TLS.

Я розумію, що це означає. І я розумію, чому .NET вважає, що він повинен попередити мене та розірвати зв’язок. Але в цьому випадку я хотів би просто підключитися до API в будь-якому випадку, атаки "людина посередині" будуть прокляті.

Отже, як мені додати виняток для цього самопідписаного сертифіката? Або підхід сказати HttpWebRequest / Response взагалі не перевіряти сертифікат? Як би я це зробив?

Відповіді:


81

@Domster: це працює, але, можливо, ви захочете застосувати трохи безпеки, перевіривши, чи відповідає хеш сертифіката тому, що ви очікуєте. Тож розширена версія виглядає приблизно так (на основі деякого коду, який ми використовуємо):

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

Можливо, хтось, хто віддав перевагу правильному шляху нижче. У будь-якому випадку, цей хак працює вкрай мало, але вам, мабуть, не слід кодувати такі види винятків у ... або просто відключити перевірку всіх разом (за допомогою пропозиції безпосередньо нижче), або фактично доручити своєму комп’ютеру довіряти сертифікату .. .
BrainSlugs83

4
@ BrainSlugs83: Вимкнення, безумовно, теж є варіантом, але додавати сертифікат до сховища кореневих повноважень на рівні машини можуть лише адміністратори. Моє рішення працює в будь-якому випадку.
devstuff

І я це повністю розумію, але ти запитав, і це все ще моє припущення щодо того, чому хтось проголосував за твою відповідь. І незважаючи на те, що це більше роботи, відповідь IMHO wgthom, як і раніше, є найбільш правильною.
BrainSlugs83,

До речі, будьте обережні, я думаю, що ServerCertificateValidationCallback є СТАТИЧНИМ, і навіть не локальним. якщо я не помиляюся, то після встановлення воно залишається встановленим, доки ви його не очистите. якщо ви хочете використовувати його лише для одного підключення, а не для всіх інших, будьте дуже обережні з паралельними запитами ..
quetzalcoatl

3
Це найкращий спосіб зробити це. Якщо ви видалите перевірку щодо sslPolicyErrors, ви можете переконатися, що сертифікат API завжди є очікуваним. Зауважимо одне, що відбиток пальця сертифіката у наведеному вище коді є масивом байтів const. Це не буде скомпільовано як написано. Натомість спробуйте статичний байтовий масив. Компілятор задихається цим, оскільки для цього потрібен оператор new ().
Centijo

92

Виявляється, якщо ви просто хочете взагалі відключити перевірку сертифіката, ви можете змінити ServerCertificateValidationCallback на ServicePointManager, наприклад:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

Це підтвердить усі сертифікати (включаючи недійсні, прострочені чи самопідписані).


2
Ідеально підходить для швидкого тестування на розробці машин. Дякую.
Nate

2
На який обсяг це впливає - на все в домені програми? все в apppool? все на машині?
codeulike

29
Але будь обережним! Досвід RL показує, що хаки цієї розробки часто потрапляють у продукт випуску: Найнебезпечніший код у світі
Doomjunky

4
Це хак, корисний у розробці, тому розміщення навколо нього #if DEBUG #endif - це найменше, що ви повинні зробити, щоб зробити це безпечнішим і зупинити це закінчення у виробництві.
AndyD

3
Якщо цей хлопець не видалить цю відповідь, ми побачимо дивний факт, що за неправильну відповідь набирається набагато більше голосів, ніж за правильну.
Lex Li

47

Зверніть увагу, що в .NET 4.5 ви можете замінити перевірку SSL на кожен HttpWebRequest (а не через глобальний делегат, який впливає на всі запити):

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

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

1
Проголосуйте за це; це варто оновити до 4,5!
Лінн Крумблінг,

1
@FlorianWinter Так, вам потрібно прийняти логіку від користувача devstuff
літній час,

43

Додайте самопідписаний сертифікат до Локальних комп’ютерних надійних кореневих органів сертифікації

Ви можете імпортувати сертифікат, запустивши MMC від імені адміністратора.

Як: переглядати сертифікати за допомогою оснастки MMC


4
ІМХО це найбільш правильний спосіб; люди просто ліниві, тому кодують спеціальні винятки для речей, яких їм, мабуть, не слід.
BrainSlugs83,

4
Чи працює цей метод для Windows Mobile 6.5? Як щодо 7? У моєму випадку я не хотів додавати локальний сертифікат до кожного мобільного пристрою, на якому я планував запустити розробну версію. В цьому випадку хороший виняток полегшує розгортання. Лінощі чи оперативність, скажи ти мені.
Домінік Шейрлінк,

3
@domster Ви використовуєте сертифікати SSL з причини - для перевірки кінцевих точок. Якщо ви розробляєте код, який спеціально працює навколо цього, ви не тестуєте його належним чином і ризикуєте просочити цей код в середовище реального часу. Якщо встановлення сертифіката на клієнт - це справді занадто багато роботи, чому б просто не заплатити за сертифікат від емітента, якому довіряють усі пристрої?
Basic

1
@Basic Якщо я пам'ятаю цей конкретний випадок, мені знадобилося б декілька сертифікатів підстановки (було півдюжини доменів верхнього рівня, до яких він підключався, всі під нашим контролем). Це важко виправдана вартість для середовища розробки. У цьому випадку єдиним кодом, який «обробляється» і не тестується, є те, що виняток не створюється там, де він міг би бути в іншому випадку. Вам слід протестувати цей конкретний шлях винятків незалежно від того, чи використовуєте ви цей обхідний шлях. І, нарешті, якщо ви не можете уникнути виробництва коду розробки, у вас є набагато більші проблеми, ніж перевірка SSL.
Домінік Шейрлінк,

для веб-додатків обов’язково переробіть свій пул додатків або перезапустіть веб-сайт. особисто я просто перекомпілював, і тоді це спрацювало. для наших матеріалів wsdl перевірка сертифіката, здається, відбувається під час ініціалізації та кешування.
sonjz

35

Обсяг зворотного виклику перевірки, що використовується у відповіді Domster, може бути обмежений конкретним запитом, використовуючи параметр відправника на ServerCertificateValidationCallbackделегатові. Наступний простий клас області використовує цю техніку для тимчасового з'єднання зворотного виклику перевірки, який виконується лише для даного об'єкта запиту.

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

Вищевказаний клас можна використовувати для ігнорування всіх помилок сертифіката для конкретного запиту наступним чином:

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

6
Ця відповідь потребує більшої кількості голосів :) Це найбільш розумна відповідь, щоб пропустити перевірку сертифіката для одного запиту за допомогою об'єкта HttpWebRequest.
MikeJansen,

Я додав це і все ще отримую Запит скасовано: Не вдалося створити захищений канал SSL / TLS.
vikingben

7
Це насправді не вирішує проблему в багатопотоковому середовищі.
Ганс,

1
maaan !!!, 5-річний пост, збережіть мій день, у мене проблема з підключенням до старого супутникового модему-приладу з недійсним сертифікатом !! Дякую!!
WindyHen

Я розгублений / трохи переживаю! Чи не означає повернення SslPolicyErrors.Noneу випадку, коли попереднього зворотного виклику не було, що ми в кінцевому підсумку замінюємо політику за замовчуванням політикою "прийняти всіх"? пор. це питання та різні відповіді на нього: stackoverflow.com/q/9058096 . Мені було б дуже приємно сказати, чому я помиляюся, і цей код чудовий!
MikeBeaton

3

Просто спираючись на відповідь devstuff, щоб включити тему та видавця ... коментарі вітаються ...

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

3

Щоб додати як можливу допомогу комусь іншому ... Якщо ви хочете, щоб це запропонувало користувачеві встановити самопідписаний сертифікат, ви можете використовувати цей код (змінений зверху).

Не вимагає прав адміністратора, встановлює довіреним профілям локальних користувачів:

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

Здається, це добре працює для нашого додатку, і якщо користувач натисне «ні», зв’язок не буде працювати.

Оновлення: 11.12.2015 - Змінено StoreName.Root на StoreName.My - My встановить у локальний магазин користувачів, а не в Root. Рут на деяких системах не буде працювати, навіть якщо ви "працюєте від імені адміністратора"


Це було б чудово, якби це працювало на Compact Framework winCE. store.Add (..) недоступний.
Dawit

1

Майте на увазі одне, що наявність ServicePointManager.ServerCertificateValidationCallback, здається, не означає, що перевірка CRL та перевірка імені сервера не виконуються, він лише надає можливість замінити їх результат. Отже, ваша служба все ще може зайняти деякий час, щоб отримати CRL, ви лише пізніше дізнаєтесь, що вона не пройшла перевірку.


1

Я стикався з тією ж проблемою, що і OP, де веб-запит видавав саме це виняток. У мене все було налаштовано правильно, я думав, сертифікат був встановлений, я міг його нормально знайти в машинному магазині та приєднати до веб-запиту, і я відключив перевірку сертифікатів у контексті запиту.

Виявилося, що я працював під своїм обліковим записом користувача, і що сертифікат був встановлений до магазину машин. Це спричинило веб-запит на вилучення цього винятку. Щоб вирішити проблему, мені довелося або працювати від імені адміністратора, або встановити сертифікат в магазин користувачів і прочитати його звідти.

Здавалося б, що C # може знайти сертифікат у машинному магазині, хоча його не можна використовувати з веб-запитом, і це призводить до того, що виняток OP буде видалено після видачі веб-запиту.


Для служб Windows ви можете встановити окремі конфігурації сертифікатів для кожної служби. Якщо ви пишете не настільну програму, а службу, сертифікат ЦС можна імпортувати в MMC спеціально для демона служби. Яка різниця між обліковим записом користувача та обліковим записом машини? Я думав, що все в машинному обліковому записі застосовується до користувача автоматично.
ArticIceJuice

1

Перш за все - я перепрошую, тому що я використав рішення, описане @devstuff. Однак я знайшов кілька способів його вдосконалити.

  • додавання самостійно підписаних сертифікатів
  • порівняння за необробленими даними сертифікатів
  • фактична перевірка центру сертифікації
  • деякі додаткові коментарі та вдосконалення

Ось моя модифікація:

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

встановлення сертифікатів:

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

передача методу делегатів

ServerCertificateValidationCallback(ValidateServerCertficate)

client.pfx генерується за допомогою KEY та CERT як таких:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.