Всем привет. Представлюсь - меня зовут Евгений Думчев и я Team Lead .NET разработки в DDPlanet.

В какой-то момент в моей практике появилась задача по интеграции с внешним API. Для взаимодействия требовалось применять предоставленный публичный доверенный сертификат сервера .cer и клиентский .pfx сертификат. Особенность в том, что .pfx сертификат был выпущен через CryptoPro CSP - а это вносит свои тонкости в процесс интеграции.

В этой статье я расскажу, как интегрировать .NET-приложение с внешним API, требующему двусторонней TLS-аутентификации по национальным криптографическим стандартам (ГОСТ) с использованием сертификатов, выпущенных через CryptoPro CSP.

Содержание

  1. Безопасность и сферы применения двустороннего TLS с CryptoPro

  2. Особенности взаимодействия с API, использующим сертификат CryptoPro

  3. Решение для .NET приложений в Windows

  4. Решение для .NET приложений в Linux

  5. Решение с помощью NGINX

  6. Как безопасно хранить сертификаты и пароли к ним

  7. Заключение

Безопасность и сферы применения двустороннего TLS с CryptoPro

При работе с государственными или банковскими API в РФ часто предъявляются требования к применению национальных криптографических стандартов. В таких случаях сертификаты формируются через CryptoPro CSP и содержат ГОСТ-алгоритмы, которые отличают эти сертификаты от обычных X.509 сертификатов, применяемых в международной практике.

Более того, для повышения уровня безопасности при взаимодействии по API может применяться двусторонняя TLS-аутентификация (mTLS), которая обеспечивает повышенный уровень доверия между клиентом и сервером. Это означает, что:

  1. Сервер предоставляет свой сертификат - клиент проверяет, доверять ли серверу.

  2. Клиент предъявляет свой сертификат (client certificate) - сервер проверяет его подлинность.

Работает это по следующему принципу:

  • При вызове внешнего API, клиент (в нашем случае .NET Web API приложение) выполняет запрос соединения с сервером.

  • В процессе TLS-рукопожатия сервер отсылает клиенту свой публичный сертификат.

  • Клиент проверяет валидность публичного сертификата, сравнивая его с заранее известным и доверенным сертификатом сервера (например, .cer файлом) или по цепочке доверия к корневому УЦ. Если сертификат соответствует ожидаемому, то можно доверять этому серверу.

  • Так как на стороне сервера включена двусторонняя аутентификация - он отправляет запрос о необходимости предоставления клиентского сертификата.

  • Клиент отправляет публичный сертификат (и при необходимости цепочку) из контейнера .pfx и подтверждает владение приватным ключом из контейнера .pfx.

  • Сервер проверяет подпись и цепочку доверия, а также сверяет, выдан ли сертификат от доверенного УЦ.

  • Если все проверки пройдены успешно - устанавливается защищенное соединение - API становится доступным и клиенту можно выполнять обработку запросов.

Сертификат .pfx (Personal Information Exchange) применяется из соображений безопасности. Это стандартизированный двоичный формат контейнера сертификатов PKCS #12 (Public-Key Cryptography Standards). Он представляет собой единый файл, который включает в себя закрытый ключ  (private key), открытый ключ / сертификат (public key) и цепочку доверия (промежуточные и корневые сертификаты). К особенностям можно отнести то, что содержимое .pfx-файла шифруется и защищается паролем.

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

Получаем следующую схему взаимодействия:

Рис.1. Схема взаимодействия клиента и сервера с применением mTLS
Рис.1. Схема взаимодействия клиента и сервера с применением mTLS

С таким уровнем безопасности с применением CryptoPro чаще всего можно столкнуться при взаимодействии с отечественным ПО в следующих сферах:

  • Системы электронного документооборота (ЭДО).

  • Порталы государственных услуг.

  • Банковские и финансовые API.

  • Торговые площадки, работающие с ЭЦП (электронной подписью).

  • Юридически значимые интеграции - в которых важно соблюдение требований к квалифицированной электронной подписи (КЭП).

Особенности взаимодействия с API, использующим сертификат CryptoPro

При взаимодействии со сторонней API по REST в нашем случае должен использоваться .pfx сертификат, созданный через CryptoPro. Это означает, что алгоритмы и структуры сертификата могут содержать специфичные для ГОСТ и CryptoPro криптографические идентификаторы OID, например, 1.2.840.113549.1.12.1.80, которые не поддерживаются большинством стандартных библиотек (например, OpenSSL, curl). Что требует использования именно CryptoPro или его оберток для корректной работы.

У сторонней API предусмотрена Swagger-документация, но она так просто не доступна в браузере.

Во-первых, для доступа к API из браузера необходимо установить CryptoPro CSP. Этот инструмент позволяет установить на устройство клиентский сертификат .pfx.

Во-вторых, даже после установки сертификата Swagger-документация внешнего API становится доступна только в браузерах Chromium-Gost и Яндекс Браузер. Эти браузеры поддерживают отечественные криптографические алгоритмы ГОСТ, используемые в сертификатах, выпущенных через CryptoPro. И на этих браузерах должен быть установлен КриптоПро ЭЦП Browser Plugin.

Наконец, при открытии url /swagger в подходящем браузере появляется модальное окно CryptoPro CSP для выбора нужного сертификата, применяемого для доступа к ресурсу. И после выбора нужного сертификата - победа, Swagger-документация становится доступна и позволяет успешно отправлять REST запросы на сервер.

Но как же выполнять взаимодействие с API по REST из приложения на .NET? Ведь в случае классической реализации выполнения запросов через HttpClient получаем в ответе HTTP статус код 400 (Bad Request) с ошибкой:

<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx</center>
</body>
</html>

Основным решением является подключение к запросам клиентского сертификата .pfx CryptoPro и публичного доверенного сертификата сервера .cer для организации двусторонней TLS-аутентификации.

Рассмотрим следующие варианты решения:

  • для .NET приложения, запускаемом на Windows;

  • для .NET приложения, запускаемом на Linux;

  • с помощью NGINX.

Решение для .NET приложений в Windows

Самый простой способ подцепить сертификаты (клиентский .pfx и доверенный сертификат сервера .cer) из приложения на .NET под Windows заключается в том, чтобы добавить их в виде файлов в структуру проекта.

Рис.2. Сертификаты в структуре проекта
Рис.2. Сертификаты в структуре проекта

После чего эти файлы можно подгрузить по пути к ним и применить в REST запросах к целевой API.  Для этого необходимо сконфигурировать типизированный HttpClient и добавить к обработке HTTP запросов логику применения сертификатов в HttpClientHandler.

public static IServiceCollection ConfigureApiHttpClient(this IServiceCollection services)
{
    services.AddSingleton(serviceProvider =>
    {
        var options = serviceProvider.GetRequiredService<IOptions<ApiOptions>>().Value;
        var env = serviceProvider.GetRequiredService<IWebHostEnvironment>();

        // Формируем абсолютные пути к сертификатам
        var publicCertPath = Path.Combine(env.ContentRootPath, "Certificates", options.CerCertificateName);
        var privateCertPath = Path.Combine(env.ContentRootPath, "Certificates", options.PfxCertificateName);

        return new ApiCertificates
        {
            //Загружаем серверный публичный сертификат (.cer)
            PublicCert = new X509Certificate2(publicCertPath),
            //Загружаем клиентский сертификат с приватным ключом (.pfx)
            PrivateCert = new X509Certificate2(privateCertPath, options.PfxCertificatePassword)
        };
    });

    services.AddHttpClient<IApiHttpClient, ApiHttpClient>()
        .ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
        {
            var apiCertificates = serviceProvider.GetRequiredService<ApiCertificates>();

            var handler = new HttpClientHandler();

            handler.ClientCertificateOptions = ClientCertificateOption.Manual;
            //клиент будет отправлять свой сертификат при TLS-рукопожатии
            handler.ClientCertificates.Add(apiCertificates.PrivateCert);

            // Опционально: настройка проверки серверного сертификата
            handler.ServerCertificateCustomValidationCallback = (_, cert, _, sslPolicyErrors) =>
            {
                // Проверяем, совпадает ли сертификат с ожидаемым
                if (cert?.Thumbprint?.Equals(apiCertificates.PublicCert.Thumbprint, StringComparison.OrdinalIgnoreCase) == true)
                    return true;

                // Если сертификат не совпадает, можно по желанию разрешить стандартную проверку TLS
                return sslPolicyErrors == SslPolicyErrors.None;
            };

            return handler;
        });

    return services;
}

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

Рис.3. Окна КриптоПро CSP для ввода учетных данных от контейнера .pfx
Рис.3. Окна КриптоПро CSP для ввода учетных данных от контейнера .pfx

После указания учетных данных запросы успешно доходят до целевого API.

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

Можно воспользоваться альтернативным вариантом и предварительно установить все эти сертификаты на устройство и искать целевые сертификаты из хранилища сертификатов.

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

Тогда получим следующий код:

private static X509Certificate2? FindCertInStore(string thumbprint, StoreName storeName, StoreLocation storeLocation)
{
    using var store = new X509Store(storeName, storeLocation);
    store.Open(OpenFlags.ReadOnly);

    return store.Certificates
        .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
        .FirstOrDefault();
}

public static IServiceCollection ConfigureApiHttpClient(this IServiceCollection services)
{
    services.AddSingleton(serviceProvider =>
    {
        var options = serviceProvider.GetRequiredService<IOptions<ApiOptions>>().Value;

        return new ApiCertificates
        {
            //Загружаем серверный публичный сертификат (.cer)
            PublicCert = FindCertInStore(options.CerCertificateThumbprint, StoreName.My, StoreLocation.CurrentUser)!,
            //Загружаем клиентский сертификат с приватным ключом (.pfx)
            PrivateCert = FindCertInStore(options.PfxCertificateThumbprint, StoreName.My, StoreLocation.CurrentUser)!
        };
    });

    services.AddHttpClient<IApiHttpClient, ApiHttpClient>()
        .ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
        {
            var apiCertificates = serviceProvider.GetRequiredService<ApiCertificates>();

            var handler = new HttpClientHandler();

            handler.ClientCertificateOptions = ClientCertificateOption.Manual;
            //клиент будет отправлять свой сертификат при TLS-рукопожатии
            handler.ClientCertificates.Add(apiCertificates.PrivateCert);

            // Опционально: настройка проверки серверного сертификата
            handler.ServerCertificateCustomValidationCallback = (_, cert, _, sslPolicyErrors) =>
            {
                // Проверяем, совпадает ли сертификат с ожидаемым
                if (cert?.Thumbprint?.Equals(apiCertificates.PublicCert.Thumbprint, StringComparison.OrdinalIgnoreCase) == true)
                    return true;

                // Если сертификат не совпадает, можно по желанию разрешить стандартную проверку TLS
                return sslPolicyErrors == SslPolicyErrors.None;
            };

            return handler;
        });

    return services;
}

В данном случае сертификат CryptoPro успешно достается из хранилища сертификатов Windows и применяется в запросах на сервер без всплывающего окна и запроса пароля. Это работает за счет того, что криптопровайдер CryptoPro интегрируется в инфраструктуру Windows как один из поддерживаемых провайдеров. Соответственно, сертификаты, установленные через CryptoPro, регистрируются в стандартных хранилищах Windows - например, в StoreName.My и StoreLocation.CurrentUser или LocalMachine и доступны через X509Store.

Решение для .NET приложений в Linux

Для запуска приложения в Linux соберем и запустим docker образ.

# этап сборки проекта
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish
WORKDIR /src
COPY . .

WORKDIR "/src/Cert.Client.Api"

RUN dotnet restore "Cert.Client.Api.csproj" --verbosity Minimal --use-current-runtime 
RUN dotnet publish "Cert.Client.Api.csproj" --no-restore -c Release -o /app/publish /p:UseAppHost=false


# этап подготовки финального образа
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final

RUN sed -i '/\[openssl_init\]/a ssl_conf = ssl_sect' /etc/ssl/openssl.cnf
RUN printf "\n[ssl_sect]\nsystem_default = system_default_sect\n" >> /etc/ssl/openssl.cnf
RUN printf "\n[system_default_sect]\nMinProtocol = TLSv1.2\nCipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf

USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

ENV ASPNETCORE_ENVIRONMENT=Development

WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Cert.Client.Api.dll"]

Если воспользоваться логикой подключения сертификатов напрямую из структуры проекта, то при запуске получим ошибку:

System.Security.Cryptography.CryptographicException: The certificate data cannot be read with the provided password, the password may be incorrect.
  ---> System.Security.Cryptography.CryptographicException: The EncryptedPrivateKeyInfo structure was decoded but was not successfully interpreted, the password may be incorrect.
  ---> System.Security.Cryptography.CryptographicException: The algorithm identified by '1.2.840.113549.1.12.1.80' is unknown, not valid for the requested usage, or was not handled.
  at System.Security.Cryptography.X509Certificates.OpenSslX509CertificateReader.FromFile(String fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)

Ошибка возникает из-за того, что OpenSSL не может корректно прочитать сертификат, поскольку в нем используется алгоритм шифрования от CryptoPro, который не поддерживается в стандартной сборке OpenSSL. То есть в Linux такой сертификат не типизировать в X509Certificate2, как это работало для Windows «из коробки».

Установить сертификат на Linux с помощью команд OpenSSL не получится - формат PFX сертификатов не поддерживается в операционной системе Linux. Однако есть возможность конвертировать его в формат PEM с помощью OpenSSL:

# Конвертация .pfx в .pem
openssl pkcs12 -in private-cert.pfx -out private-cert.pem -nodes

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

# Извлечь ключ
openssl pkcs12 -in private-cert.pfx -nocerts -nodes -out private.key

# Извлечь сертификат
openssl pkcs12 -in private-cert.pfx -clcerts -nokeys -out certificate.crt

# Извлечь CA-цепочку
openssl pkcs12 -in private-cert.pfx -cacerts -nokeys -out ca-chain.crt

Все это не сработает для .pfx сертификата CryptoPro по причине неизвестного алгоритма шифрования.

Error outputting keys and certificates
C8650000:error:03000079:digital envelope routines:EVP_PBE_CipherInit_ex:unknown pbe algorithm:crypto\evp\evp_pbe.c:116:TYPE=1.2.840.113549.1.12.1.80

Одним из вариантов решения является установка модифицированной версии OpenSSL, например, gost-engine. Но я бы не рекомендовал подобный вариант, так как сторонние реализации не гарантируют поддержку и безопасность, а также могут не поддерживать актуальные сертификаты или ключи.

Правильным вариантом решения является использование сертифицированного CryptoPro CSP для Linux. И установить его в docker-образ. Скачать необходимый дистрибутив можно по ссылке. В нашем случае используется образ mcr.microsoft.com/dotnet/aspnet:8.0, который базируется на debian образе, поэтому нужно установить дистрибутив linux-amd64_deb.tgz.

CryptoPro CSP является набором инструментов по работе с криптографией, предназначенных для реализации российских алгоритмов ГОСТ, создания и проверки электронной подписи, шифрования, управления сертификатами и контейнерами закрытых ключей. Часть полезных инструментов, содержащихся в сборке linux-amd64_deb.tgz:

  • csptest - инструмент для тестирования функций CSP: проверка алгоритмов, ключей, сертификатов, подписей и шифрования.

  • certmgt - утилита управления сертификатами: установка, удаление, экспорт и просмотр сертификатов из хранилищ CSP.

  • cprodiag - диагностический инструмент для сбора информации о конфигурации CryptoPro CSP, лицензии, хранилищах и возможных ошибках.

  • cpverify - проверка целостности и корректности установки CryptoPro CSP: сверка контрольных сумм и доступности компонентов.

  • cryptcp - утилита для подписания, шифрования и проверки данных.

  • cpconfig - утилита настройки компонентов CryptoPro CSP: используется для активации и просмотра лицензий, управления параметрами криптопровайдера и системными настройками.

  • cpnginx - модифицированная версия NGINX с поддержкой ГОСТ TLS через интерфейс SSPI, встроенная в CryptoPro CSP.

Внесем изменения в Dockerfile - добавим команды по установке CryptoPro CSP. Для соблюдения правил использования необходимо указать лицензионный ключ. И с помощью инструментария CryptoPro CSP нужно установить сертификаты .pfx и .cer. Скорректированный Dockerfile:

# этап сборки проекта
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish
WORKDIR /src
COPY . .

WORKDIR "/src/Cert.Client.Api"

RUN dotnet restore "Cert.Client.Api.csproj" --verbosity Minimal --use-current-runtime 
RUN dotnet publish "Cert.Client.Api.csproj" --no-restore -c Release -o /app/publish /p:UseAppHost=false


# этап подготовки финального образа
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final

# установка CryptoPro
WORKDIR /cryptopro
ADD Cert.Client.Api/CryptoPro/linux-amd64_deb.tgz .
RUN ./linux-amd64_deb/install.sh

# добавление лицензионного ключа CryptoPro
ARG CRYPTO_PRO_LICENSE
RUN /opt/cprocsp/sbin/amd64/cpconfig -license -set ${CRYPTO_PRO_LICENSE}

# копирование сертификатов CryptoPro
ADD Cert.Client.Api/Certificates ./certificates

# установка доверенного сертификата сервера .cer в CryptoPro
RUN /opt/cprocsp/bin/amd64/certmgr -install -file ./certificates/public-cert.cer -silent

# установка клиентского сертификата .pfx в CryptoPro
ARG PRIVATE_CERT_PASS
RUN /opt/cprocsp/bin/amd64/certmgr -install -pfx -file ./certificates/private-cert.pfx -pin ${PRIVATE_CERT_PASS} -silent

# установка сертификатов из контейнеров в CryptoPro
RUN /opt/cprocsp/bin/amd64/csptest -absorb -certs -autoprov

WORKDIR /app
RUN rm -R /cryptopro


RUN sed -i '/\[openssl_init\]/a ssl_conf = ssl_sect' /etc/ssl/openssl.cnf
RUN printf "\n[ssl_sect]\nsystem_default = system_default_sect\n" >> /etc/ssl/openssl.cnf
RUN printf "\n[system_default_sect]\nMinProtocol = TLSv1.2\nCipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf

USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

ENV ASPNETCORE_ENVIRONMENT=Development

WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Cert.Client.Api.dll"]

После выполнения команды установки сертификата .pfx его содержимое импортируется в систему CryptoPro и преобразуется - разбивается на отдельные компоненты, сохраненные в формате с расширением .key:

Рис.4. Структура сертификата в Linux
Рис.4. Структура сертификата в Linux

В основном хранилище ключей keys появляется наш преобразованный .pfx контейнер в формате cert.000 с шестью файлами:

  • primary.key, primary2.key - основные ключи.

  • name.key - содержит имя или идентификатор контейнера.

  • masks.key, masks2.key - используется для маскировки/шифрования ключей.

  • header.key - содержит метаинформацию (заголовок, версию и т.п.).

Поскольку в Linux .pfx сертификат CryptoPro не типизировать в X509Certificate2, то ни вариант с загрузкой сертификата напрямую из структуры проекта, ни вариант с получением из хранилища сертификатов X509Store не сработает - нам не удастся найти целевой .pfx сертификат и, соответственно, не получится выполнить TLS-рукопожатие. Получим следующую ошибку:

InvokeGetRequest Error.
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
  ---> System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
  at System.Net.Security.SslStream.ReceiveHandshakeFrameAsync[TIOAdapter](CancellationToken cancellationToken)
  at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
  at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)

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

Решение включает в себя четыре NuGet-пакета, которые необходимо подключить к проекту. Это можно сделать одним из следующих способов:

  • Локальное подключение пакетов.

  • Загрузка пакетов в онлайн-репозиторий, например Nexus, Azure DevOps и другие.

В .csproj нужно добавить все эти пакеты:

<PackageReference Include="CryptoPro.Net.Security" Version="2025.4.17" />
<PackageReference Include="CryptoPro.Security.Cryptography" Version="2025.4.17" />
<PackageReference Include="CryptoPro.Security.Cryptography.Pkcs" Version="2025.4.17" />
<PackageReference Include="CryptoPro.Security.Cryptography.Xml" Version="2025.4.17" />

Теперь с помощью библиотек CryptoPro можно получить сертификаты с типом CpX509Certificate2 из хранилища сертификатов CpX509Store. Получим следующий код:

private static CpX509Certificate2? FindCertInCryptoProStore(string thumbprint, StoreName storeName, StoreLocation storeLocation)
{
    using var store = new CpX509Store(StoreName.My, StoreLocation.CurrentUser);

    store.Open(OpenFlags.ReadOnly);

    return store.Certificates
        .Find(X509FindType.FindByThumbprint, thumbprint, false)
        .FirstOrDefault();
}

public static IServiceCollection ConfigureApiHttpClient(this IServiceCollection services)
{
    services.AddSingleton(serviceProvider =>
    {
        var options = serviceProvider.GetRequiredService<IOptions<ApiOptions>>().Value;

        return new ApiCertificates
        {
            //Загружаем серверный публичный сертификат (.cer)
            PublicCert = FindCertInCryptoProStore(options.CerCertificateThumbprint, StoreName.My, StoreLocation.CurrentUser)!,
            //Загружаем клиентский сертификат с приватным ключом (.pfx)
            PrivateCert = FindCertInCryptoProStore(options.PfxCertificateThumbprint, StoreName.My, StoreLocation.CurrentUser)!
        };
    });

    services.AddHttpClient<IApiHttpClient, ApiHttpClient>()
        .ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
        {
            var apiCertificates = serviceProvider.GetRequiredService<ApiCertificates>();

            return new CpHttpHandler
            {
                SslOptions = new()
                {
                    ClientCertificates = [apiCertificates.PrivateCert],
                    RemoteCertificateValidationCallback = (_, cert, _, sslPolicyErrors) => 
                    {
                        // Проверяем, совпадает ли сертификат с ожидаемым
                        if (cert?.Thumbprint?.Equals(apiCertificates.PublicCert.Thumbprint, StringComparison.OrdinalIgnoreCase) == true)
                            return true;

                        // Если сертификат не совпадает, можно по желанию разрешить стандартную проверку TLS
                        return sslPolicyErrors == SslPolicyErrors.None;
                    }
                }
            };
        });

    return services;
}

В данном случае сертификат CryptoPro успешно достается из хранилища сертификатов CryptoPro и применяется в запросах на сервер. Вдобавок это решение работает и для Windows тоже.

Решение с помощью NGINX

Вместо интеграции ГОСТ TLS напрямую в .NET-приложение, можно вынести TLS-обвязку на уровень NGINX. Это снимает с .NET приложения (или приложения на любом другом языке программирования) необходимость подключения CryptoPro-сертификатов и реализации TLS взаимодействия, что упрощает код, повышает кроссплатформенность и масштабируемость решения.

Чтобы это работало с сертификатом CryptoPro необходимо воспользоваться cpnginx - специальной версией NGINX от CryptoPro с поддержкой ГОСТ TLS. Это решение содержится в linux-дистрибутивах, доступных на официальной странице загрузки CryptoPro CSP.

Получаем следующую схему взаимодействия с внешним API:

Рис.5. Схема взаимодействия с API через Nginx
Рис.5. Схема взаимодействия с API через Nginx

Таким образом:

  • .NET-приложение посылает обычные HTTP-запросы на cpnginx;

  • cpnginx выступает в роли клиента TLS, обрабатывая криптографию через CryptoPro CSP;

  • cpnginx проксирует запрос на сервер и возвращает ответ приложению.

Для настройки CryptoPro Nginx необходимо создать внешний конфигурационный файл api-tls.conf. Этот конфиг необходимо поместить в директорию /etc/opt/cprocsp/cpnginx/conf.d/api-tls.conf. Все *.conf-файлы из этой директории автоматически подключаются к основному конфигурационному файлу cpnginx.

server {
    listen 8080;  # cpnginx слушает обычный HTTP-порт 8080
    
    #listen 443 sspi; # если нужен https
    #sspi_certificate 0x7C00013D42201F55870674C594000100013D421; # Сертификат сервера NGINX (если https)
    
    server_name localhost; # Имя сервера
    sspi_protocols TLSv1.2; # Используемый TLS-протокол

    location / {
        proxy_pass https://external-api.ru; # API на который проксировать запрос
        proxy_http_version 1.1; # Используем HTTP/1.1 при проксировании
		
        proxy_sspi on; # Включаем использование SSPI - ГОСТ TLS при подключении
        proxy_ssl_verify on; # Включаем проверку подлинности TLS-сертификата сервера API
        proxy_ssl_server_name on; # Включаем передачу SNI (Server Name Indication) при TLS

        proxy_ssl_certificate 0x0552A7B13510B2E8AE4C322F78CAB170BC; # Серийный номер клиентского сертификата .pfx
        proxy_ssl_trusted_certificate Root; # Указываем хранилище, откуда cpnginx возьмет корневой (доверенный) сертификат для валидации сервера
        proxy_ssl_verify_local_crl_only on; # Проверка на отзыв сертификата производится только по локальному CRL
    }
}

Для запуска cpnginx приложения в Linux соберем и запустим docker образ:

# Используем базовый образ Debian
FROM debian:bullseye

# Обновляем систему и устанавливаем необходимые пакеты
RUN apt-get update && apt-get install -y \
    wget curl unzip gnupg lsb-release sudo \
    libcap2-bin ca-certificates systemd libssl1.1 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Создаем пользователя cpnginx
RUN useradd -r -m -d /var/opt/cprocsp/cpnginx -s /bin/bash cpnginx

# Копируем дистрибутив CryptoPro в контейнер
COPY ./crypto-pro/linux-amd64_deb.tgz /tmp/linux-amd64_deb.tgz

# Распаковка и установка дистрибутива CryptoPro с cpnginx
WORKDIR /tmp
RUN tar -xvzf linux-amd64_deb.tgz && \
    cd linux-amd64_deb && \
    ./install.sh cprocsp-nginx && \
    rm -rf /tmp/linux-amd64_deb*

# Копируем сертификаты в контейнер
COPY ./certs/private-cert.pfx /certs/private-cert.pfx
COPY ./certs/public-cert.cer /certs/public-cert.cer

# добавление лицензионного ключа CryptoPro
ARG CRYPTO_PRO_LICENSE
RUN /opt/cprocsp/sbin/amd64/cpconfig -license -set ${CRYPTO_PRO_LICENSE}

#Установка тестового .cer (только открытая часть) в CryptoPro
RUN echo o | sudo -u cpnginx /opt/cprocsp/bin/amd64/certmgr -install -store uRoot -file /certs/public-cert.cer

ARG PRIVATE_CERT_PASS
#Установка production .pfx (экспортированный ключ с закрытой и открытой частью) в CryptoPro
RUN sudo -u cpnginx /opt/cprocsp/bin/amd64/certmgr -install -pfx -file /certs/private-cert.pfx -pin ${PRIVATE_CERT_PASS} -silent

#Установка сертификатов из контейнеров в CryptoPro
RUN sudo -u cpnginx /opt/cprocsp/bin/amd64/csptest -absorb -certs -autoprov

# Копируем конфигурацию cpnginx
COPY ./config/sspi.conf /etc/opt/cprocsp/cpnginx/conf.d/api-tls.conf

# Устанавливаем права доступа к ключам	
RUN chown -R cpnginx:cpnginx /var/opt/cprocsp/keys/cpnginx && \
    chmod -R go= /var/opt/cprocsp/keys/cpnginx

# Открываем порты
EXPOSE 8080

# Запускаем cpnginx в режиме "foreground"
CMD ["/opt/cprocsp/sbin/amd64/cpnginx", "-g", "daemon off;"]

NGINX успешно запускается и принимает входящие HTTP-запросы на http://localhost:8080 (настраивается) и проксирует их на внешний API с использованием CryptoPro TLS. Все запросы и ошибки логируются в /var/log/cpnginx/access.log и /var/log/cpnginx/error.log.

В приложении .NET конфигурация HttpClient упрощается. Остается только указать адрес nginx - куда посылать запросы:

private static IServiceCollection ConfigureApiHttpClient(this IServiceCollection services)
{
    services.AddHttpClient<IApiHttpClient, ApiHttpClient>(httpClient => 
    {
        httpClient.BaseAddress = new Uri("http://localhost:8080"); //адрес Nginx
    });

    return services;
}

Получаем решение, которое обладает следующими преимуществами:

  • Упрощение .NET-приложения - нет необходимости подключать и настраивать ГОСТ TLS в приложении. -TLS реализован на nginx.

  • Независимость от языка программирования приложения.

  • Изоляция криптографии - CryptoPro CSP используется только внутри контейнера cpnginx.

  • Легкое масштабирование - cpnginx можно масштабировать независимо от самого приложения.

Как безопасно хранить сертификаты и пароли к ним

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

Правильным подходом является хранение сертификатов и секретов вне исходного кода. Кроме того, сертификаты не должны включаться в Docker-образ напрямую. Вместо этого они монтируются во время запуска контейнера через Docker volume или persistent volume в Kubernetes. Пароли к ним считываются в runtime, не попадая в историю образа.

Для исключения чувствительных данных можно воспользоваться следующими безопасными механизмами хранения:

  • Переменные окружения, определенные в CI/CD или на хост-машине при запуске.

  • Secrets-хранилища: например, Azure Key Vault, AWS Secrets Manager, HashiCorp Vault.

  • Docker Secrets / Kubernetes Secrets - для защищенной передачи секретов в контейнеры.

Такой подход позволяет обеспечить высокий уровень безопасности на всех этапах жизненного цикла приложения - от сборки до развертывания в production среде.

Заключение

Интеграция с API, использующим сертификаты CryptoPro и ГОСТ TLS, может показаться непростой задачей, особенно в кроссплатформенных проектах. В статье рассмотрено три рабочих варианта: реализацию для .NET-приложений на Windows и на Linux, а также подход с использованием cpnginx - модифицированного NGINX с поддержкой ГОСТ TLS.

Наиболее универсальным и масштабируемым решением становится cpnginx. Он полностью изолирует криптографическую обвязку от логики приложения, устраняет зависимость от ОС, упрощает сопровождение и снижает риски, связанные с безопасностью. С его помощью .NET-приложение может работать с API как с обычным HTTP-сервисом, а все криптографические операции выполняются внутри защищённого контейнера.

Все решения применимы на практике. Выбор подхода зависит от ваших потребностей, ограничений, инфраструктуры и требований безопасности.

Комментарии (0)