Как я познакомился с BouncyCastle в .NET 7

от автора

Введение

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

Данная работа написана исключительно в рамках моих рабочих будней и не является профессиональным гайдом.

Как я к этому пришел

Мой проект занимается медициной и отправкой документов на подпись онлайн (для выдачи рецептов). Недавно наш старый сертификат почти закончил свой срок жизни и нам прислали новый. Однако при замене сертификата и приватного ключа оказалось, что на Azure энвах мы ловим тайм ауты, в то время как с локал хоста все запросы отправляются и приходят 200 в ответе.

Начало ресерча

Первое что приходит в голову — может неправильные конфиги на Azure? Как оказалось нет, так как если отправить с контейнера курл запрос — всегда приходило 200 и никаких проблем не наблюдалось (причем быстро очень). Тогда закрались опасения насчет того, насколько быстро работает наш HttpClient. Ниже приведет код регистрации его в сервис провайдере:

builder.Services.AddHttpClient<SigningClient>((serviceProvider, client) =>   {       var options = serviceProvider.GetService<IOptions<SigningSettings>>()?.Value;       if (options?.Url != null)       {           client.BaseAddress = new Uri(options.Url);           client.Timeout = TimeSpan.FromSeconds(options.ConnectionTimeout);       }          client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(         MediaTypeNames.Application.Json));   })   .ConfigurePrimaryHttpMessageHandler(serviceProvider =>   {       var options = serviceProvider.GetService<IOptions<SigningSettings>>()?.Value;          var httpClientHandler = new HttpClientHandler       {           ClientCertificates =           {               GetClientCertificate(options)           }       };          return httpClientHandler;   });

В целом не увидев никаких проблем в регистрации — я написал свой MyHttpClientHandler и добавил парочку логов, чтобы потенциально увидеть проблему

public class MyHttpClientHandler : HttpClientHandler {     public MyHttpClientHandler()     {         ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation;     }      protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,  CancellationToken cancellationToken)     {         Log.Information("-- Information about call -- " +                                "\nClientCertificateOptions: " +                                "{ClientCertificateOptions},\n" +                                "ClientCertificates:\n{ClientCertificates}",                                ClientCertificateOptions.ToString(),                                ClientCertificates[0].ToString());          var response = base.SendAsync(request, cancellationToken);          var content = response.GetAwaiter().GetResult()           .Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();          Log.Information("Response: {Content}\nStatusCode: {StatusCode}",            content, response.Result.StatusCode);          return response;     }      private static bool ServerCertificateCustomValidation(HttpRequestMessage        requestMessage, X509Certificate2 certificate, X509Chain chain,       SslPolicyErrors sslErrors)     {         // Based on the custom logic it is possible to          // decide whether the client considers certificate valid or not         Log.Information($"Errors: {sslErrors}");         return sslErrors == SslPolicyErrors.None;     } }

В целом, после деплоя как вы думаете что я увидел? Абсолютно ничего. Метод .SendAsync стал ботлнек без всяких объяснений! Использовав старый сертификат было обнаружено, что все работаем в штатном режиме и очень быстро.

Продолжение ресерча

Просмотрев что я имею на данный момент, я решил посмотреть как мы работаем с сертификатом. На входе у нас есть ClientCert (сертификат) и ClientCertKey (приватный ключ). Ниже код, который обрабатываем данные значения.

var provider = new RSACryptoServiceProvider(2048); provider.ImportFromPem(Encoding.UTF8.GetString(   Convert.FromBase64String(_settings.ClientCertKey)));  var certificate = new X509Certificate2(   Convert.FromBase64String(_settings.ClientCert)); var certificateWithPrivateKey = certificate.CopyWithPrivateKey(provider);  var cert = new X509Certificate2(   certificateWithPrivateKey.Export(X509ContentType.Pfx));

В целом никакой проблеме в коде я не увидел. Возможно, знатоки криптографии и смогут упростить этот код, но на тот момент мне ничего явного и РАБОЧЕГО в голову не пришло (так как пару раз я попробовал переписать этот код и ничего не вышло)

После этого я решил написать тест, который выполняет данный код и потом шлет его на наш сервис. В целом, код выглядел вот так:

[Fact] public async Task OldApproach() {     var provider = new RSACryptoServiceProvider(2048);     provider.ImportFromPem(Encoding.UTF8.GetString(       Convert.FromBase64String(_settings.ClientCertKey)));      var certificate = new X509Certificate2(       Convert.FromBase64String(_settings.ClientCert));     var certificateWithPrivateKey = certificate.CopyWithPrivateKey(provider);      var cert = new X509Certificate2(       certificateWithPrivateKey.Export(X509ContentType.Pfx));      var result = await GetResponse(cert);      result.StatusCode.Should().Be(HttpStatusCode.OK); }

Много останавливаться на методе GetResponse(X509Certificate2 cert) останавливаться не будем, просто скажу что он имеет такой вид:

    private async Task<HttpResponseMessage> GetResponse(X509Certificate2 cert)     {         _handler.ClientCertificates.Add(cert);         _client = new HttpClient(_handler);         return await _client.SendAsync(GetRequest());     }

Как вы думаете, какой результат показал мне код? Среднее время выполнения было от 10.9 до 12.4 секунд (где-то 10-15 колов было сделано). Это показалось мне долгим и я попробовал старый сертификат — среднее время выполнения от 1.8 до 2.5 секунд.

Откуда такая разница во времени?

Я не знаю. Может у кого есть свои догадки? Буду раз обсудить!

Ну пора решать проблему, а то бизнес ждет!

Вообщем, первое что приходит в голову когда что‑то долго работает — использовать другой нугет. В этот раз так и вышло, мой взгляд сразу упал на BouncyCastle, как на главный исходник всех остальных либ.

Первая проблема с которой я столкнулся — очень мало актуальной информации. Я прям часами сидел, чтобы найти какую‑то полезную инфу, но мой максимум — какие‑то вопросы 3–4 летней давности.

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

byte[] certData = Convert.FromBase64String(options.ClientCert); string encodedPrivateKey = Encoding.UTF8.GetString   Convert.FromBase64String(options.ClientCertKey));  var certParser = new X509CertificateParser(); var certStructure = certParser.ReadCertificate(certData);  var certGenerator = new X509V3CertificateGenerator();  // Set the certificate's serial number, issuer, and subject certGenerator.SetSerialNumber(certStructure.SerialNumber); certGenerator.SetIssuerDN(certStructure.IssuerDN); certGenerator.SetSubjectDN(certStructure.SubjectDN);  // Set the certificate's validity period certGenerator.SetNotBefore(certStructure.NotBefore); certGenerator.SetNotAfter(certStructure.NotAfter);  // Set Public Key var publicKey = certStructure.GetPublicKey(); certGenerator.SetPublicKey(publicKey);  // Private key reading AsymmetricKeyParameter privateKey; using (var stringReader = new StringReader(encodedPrivateKey)) {     var pemReader = new PemReader(stringReader);     var pemObject = pemReader.ReadObject();     privateKey = ((AsymmetricCipherKeyPair)pemObject).Private; }  // Generate the certificate ISignatureFactory signatureFactory = new Asn1SignatureFactory(   "SHA256WITHRSA", privateKey, new SecureRandom()); Org.BouncyCastle.X509.X509Certificate certificate = certGenerator   .Generate(signatureFactory);  // Convert the BouncyCastle X509Certificate to .NET X509Certificate2 var dotNetCertificate = new X509Certificate2(certificate.GetEncoded());

В целом что делает этот код в каждый момент времени и так расписано, но если кратко

  • Декодим и читаем наш сертификат

  • Копируем из него все полезные данные

  • Читаем приватный ключ (эту часть я вообще еле откопал)

  • Создаем сертификат

  • Переводим его в X509Certificate2

В целом, под данный код был также написан тест точно такого же плана, как и под старый код:

[Fact] public async Task NewApproach() {     // Код с геренарацией опущен, он написан выше      // Convert the BouncyCastle X509Certificate to .NET X509Certificate2     var dotNetCertificate = new X509Certificate2(certificate.GetEncoded());      var result = await GetResponse(dotNetCertificate);      result.StatusCode.Should().Be(HttpStatusCode.OK); }

Как вы думаете, какие результаты показал данный код?

Результаты

Старый сертификат

Новый сертификат

Старый код

1.8-2.5 s

10.9-12.4 s

Новый код

340-400 ms

430-480 ms

Результаты показались мне очень удивительными, особенно как человеку который на словах только знает что такое криптография и сертификаты. Но при этом это интересный кейс и возможно кому‑то он тоже поможет, когда новый сертификат станет настолько «тяжелым».

Буду рад любым фидбекам, так как в этой теме я новичок и это был мой первый опыт с проблемами сертификатов

В любом случае всем спасибо кто дочитал до конца!


ссылка на оригинал статьи https://habr.com/ru/articles/744462/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *