Использование самозаверяющего сертификата с .NET HttpWebRequest/Response

Я пытаюсь подключиться к API, который использует самоподписанный сертификат SSL. Я делаю это с использованием объектов .NET HttpWebRequest и HttpWebResponse. И я получаю исключение:

Подключенное соединение было закрыто: не удалось установить доверительные отношения для защищенного канала SSL/TLS.

Я понимаю, что это значит. И я понимаю, почему .NET считает, что он должен предупредить меня и закрыть соединение. Но в этом случае я хотел бы просто подключиться к API в любом случае, проклятые атаки "человек-в-середине".

Итак, как мне добавить добавление исключения для этого самозаверяющего сертификата? Или можно сказать, что HttpWebRequest/Response не проверяет сертификат вообще? Как мне это сделать?

Ответ 1

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

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

/// <summary>
/// Somewhere in your application 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;
}

Ответ 2

Оказывается, если вы просто хотите полностью отключить проверку сертификата, вы можете изменить ServerCertificateValidationCallback на ServicePointManager, например:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

Это подтвердит все сертификаты (включая недействительные, истекшие или самозаверяющие).

Ответ 3

Добавить самоподписанный сертификат в доверенные корневые центры сертификации локального компьютера

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

Как просмотреть сертификаты с оснасткой MMC

Ответ 4

Обратите внимание, что в .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; };

Ответ 5

Объем обратного вызова валидации, используемый в ответе 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

Чтобы добавить как возможную помощь кому-то еще... Если вы хотите, чтобы она предложила пользователю установить самозаверяющий сертификат, вы можете использовать этот код (измененный сверху).

Не требует прав администратора, устанавливает для доверенных профилей локальных пользователей:

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

Это, похоже, хорошо работает для нашего приложения, и если пользователь нажимает "нет", связь не будет работать.

Обновление: 2015-12-11 - Изменено имя Store.Root для StoreName.My - My будет устанавливаться в хранилище локальных пользователей вместо Root. Корень на некоторых системах не будет работать, даже если вы "выполняете роль администратора"

Ответ 7

Просто основываясь на ответе 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));            
    }
}

Ответ 8

Следует иметь в виду, что наличие ServicePointManager.ServerCertificateValidationCallback не означает, что проверка CRL и валидация имени сервера не выполняются, это только обеспечивает возможность переопределить их результат. Таким образом, ваша служба может занять некоторое время, чтобы получить CRL, после этого вы узнаете, что не удалось выполнить некоторые проверки.

Ответ 9

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

Оказалось, что я работал под учетной записью пользователя и что сертификат был установлен в хранилище. Это заставило веб-запрос выбросить это исключение. Чтобы решить эту проблему, я должен был либо работать как администратор, либо устанавливать сертификат в хранилище пользователей и читать его оттуда.

Казалось бы, С# может найти сертификат в хранилище машин, даже если он не может использоваться с веб-запросом, и это приводит к тому, что исключение OP вызывается после того, как будет выдан веб-запрос.