Рекомендации по протоколу TLS/SSL

TLS (транспортный уровень безопасности) — это криптографический протокол, предназначенный для защиты связи между двумя компьютерами через Интернет. Протокол TLS предоставляется в .NET через SslStream класс.

В этой статье представлены рекомендации по настройке безопасного взаимодействия между клиентом и сервером и предполагается использование .NET. Рекомендации по использованию .NET Framework см. в рекомендациях по протоколу TLS с помощью .NET Framework.

Выбор версии TLS

Хотя можно указать версию протокола TLS для использования с помощью EnabledSslProtocols свойства, рекомендуется отложить параметры операционной системы с помощью None значения (это значение по умолчанию).

Отложение решения на ОС автоматически использует последнюю версию TLS и позволяет приложению выполнять изменения после обновления ОС. Операционная система также может предотвратить использование версий TLS, которые больше не считаются безопасными.

Выбор наборов шифров

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

Замечание

CipherSuitesPolicy не поддерживается в Windows, и попытка создать его экземпляр приведет к возникновению NotSupportedException.

Указание локального сертификата

При аутентификации в качестве сервера SslStream всегда требует сертификат. При аутентификации в качестве клиента вы также предоставляете сертификат, если сервер запрашивает его для взаимной TLS-аутентификации (mTLS). В обеих ролях сертификат должен быть экземпляром X509Certificate2 , содержащим закрытый ключ.

В последних версиях .NET серверная и клиентская стороны обрабатываются симметрично, поэтому приведённые ниже рекомендации применимы независимо от того, проходит ли приложение проверку подлинности как сервер, клиент или в обеих ролях.

Вы можете предоставить сертификат SslStream несколькими способами.

При аутентификации как сервер:

При аутентификации в качестве клиента:

Замечание

Свойство ClientCertificateContext доступно начиная с .NET 8.

Для повышения производительности используйте свойство контекста сертификата (ServerCertificateContext или ClientCertificateContext). Если сертификат предоставляется одним из других способов, SslStream внутренне создает SslStreamCertificateContext. При создании контекста создается X509Chain, а это ресурсоемкая для ЦП операция, поэтому эффективнее создать контекст один раз и повторно использовать его в нескольких экземплярах SslStream.

Повторное использование экземпляра SslStreamCertificateContext также включает дополнительные функции, такие как возобновление сеанса TLS на серверах Linux.

Отправить промежуточные сертификаты другой стороне

Когда промежуточный центр сертификации выдает локальный сертификат, одноранговый узел может не создавать полную цепочку сертификатов, если подтверждение не включает промежуточные сертификаты. Чтобы отправить эти промежуточные сертификаты, создайте SslStreamCertificateContext с помощью метода Create и передайте промежуточные сертификаты в параметре additionalCertificates:

X509Certificate2 leafCertificate = GetLeafCertificate();
X509Certificate2Collection intermediates = GetIntermediateCertificates();

SslStreamCertificateContext certificateContext =
    SslStreamCertificateContext.Create(leafCertificate, intermediates);

// When you authenticate as a server.
serverOptions.ServerCertificateContext = certificateContext;

// When you authenticate as a client for mutual TLS.
clientOptions.ClientCertificateContext = certificateContext;

Контекст работает одинаково для обеих ролей. На стороне клиента контекст сертификата является рекомендуемым способом передачи промежуточных сертификатов, поскольку альтернатива — добавление промежуточных сертификатов в хранилище сертификатов компьютера или пользователя — затрагивает все приложения в системе.

Настраиваемая X509Certificate проверка

Существуют определенные сценарии, в которых процедура проверки сертификатов по умолчанию не является достаточной, и требуется определенная пользовательская логика проверки. Части логики проверки можно настроить, указав SslClientAuthenticationOptions.CertificateChainPolicy или SslServerAuthenticationOptions.CertificateChainPolicy. Кроме того, полностью настраиваемую логику можно предоставить с помощью <свойства System.Net.Security.SslClientAuthenticationOptions.RemoteCertificateValidationCallback> . Дополнительные сведения см. в разделе "Доверие пользовательских сертификатов".

Доверие пользовательских сертификатов

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

Альтернативное решение заключается в указании пользовательских доверенных корневых сертификатов с помощью X509ChainPolicy. Чтобы указать настраиваемый список доверия, который будет использоваться вместо списка доверия системы во время проверки, рассмотрим следующий пример:

SslClientAuthenticationOptions clientOptions = new();

clientOptions.CertificateChainPolicy = new X509ChainPolicy()
{
    TrustMode = X509ChainTrustMode.CustomRootTrust,
    CustomTrustStore =
    {
        customIssuerCert
    }
};

Клиенты, настроенные с предыдущей политикой, будут принимать только доверенные customIssuerCertсертификаты.

Игнорировать определенные ошибки проверки

Рассмотрим устройство Интернета вещей без постоянных часов. После включения часы устройства будут устанавливаться на много лет назад, и, следовательно, все сертификаты будут считаться "еще не действительными". Рассмотрим следующий код, показывающий реализацию обратного вызова проверки, игнорирующую нарушения срока действия.

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // Anything that would have been accepted by default is OK
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }

    // If there is something wrong other than a chain processing error, don't trust it.
    if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
    {
        return false;
    }

    Debug.Assert(chain is not null);

    // If the reason for RemoteCertificateChainError is that the chain built empty, don't trust it.
    if (chain.ChainStatus.Length == 0)
    {
        return false;
    }

    foreach (X509ChainStatus status in chain.ChainStatus)
    {
        // If an error other than `NotTimeValid` (or `NoError`) is present, don't trust it.
        if ((status.Status & ~X509ChainStatusFlags.NotTimeValid) != X509ChainStatusFlags.NoError)
        {
            return false;
        }
    }

    return true;
}

Привязка к сертификату

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

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // If there is something wrong other than a chain processing error, don't trust it.
    if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
    {
        return false;
    }

    Debug.Assert(certificate is not null);

    const string ExpectedPublicKey =
        "3082010A0282010100C204ECF88CEE04C2B3D850D57058CC9318EB5C" +
        "A86849B022B5F9959EB12B2C763E6CC04B604C4CEAB2B4C00F80B6B0" +
        "F972C98602F95C415D132B7F71C44BBCE9942E5037A6671C618CF641" +
        "42C546D31687279F74EB0A9D11522621736C844C7955E4D16BE8063D" +
        "481552ADB328DBAAFF6EFF60954A776B39F124D131B6DD4DC0C4FC53" +
        "B96D42ADB57CFEAEF515D23348E72271C7C2147A6C28EA374ADFEA6C" +
        "B572B47E5AA216DC69B15744DB0A12ABDEC30F47745C4122E19AF91B" +
        "93E6AD2206292EB1BA491C0C279EA3FB8BF7407200AC9208D98C5784" +
        "538105CBE6FE6B5498402785C710BB7370EF6918410745557CF9643F" +
        "3D2CC3A97CEB931A4C86D1CA850203010001";

    return certificate.GetPublicKeyString().Equals(ExpectedPublicKey);
}

Рекомендации по проверке сертификата клиента

Серверные приложения должны быть осторожны при необходимости и проверке сертификатов клиента. Сертификаты могут содержать расширение AIA (доступ к данным центра), указывающее, где можно скачать сертификат издателя. Поэтому сервер может попытаться скачать сертификат издателя с внешнего сервера при создании X509Chain сертификата клиента. Аналогичным образом, серверы могут обратиться к внешним серверам, чтобы убедиться, что сертификат клиента не был отозван.

Необходимость обращения к внешним серверам при создании и проверке X509Chain может подвергнуть приложение атакам типа "отказ в обслуживании", если внешние серверы медленно реагируют. Поэтому серверные приложения должны настроить поведение сборки X509Chain с помощью CertificateChainPolicy.

Замечание

Начиная с .NET 11, SslStream отключает скачивание сертификатов AIA по умолчанию при проверке сертификатов клиента в качестве сервера. Если CertificateChainPolicy не предоставлен, сервер не попытается получить промежуточные отсутствующие сертификаты через AIA. Дополнительные сведения см. в статье SslStream, загрузка сертификатов AIA на стороне сервера отключена по умолчанию.