AlexMAS / GostCryptography

.NET driver for ViPNet CSP and CryptoPro CSP
MIT License
132 stars 42 forks source link

Взаимодействие с федеральными сервисами. Формирование jwt токена с использованием GOST3410. #49

Closed AlekseyDev closed 8 months ago

AlekseyDev commented 8 months ago

День добрый!

Используем Вашу библиотеку, наверное единственное что удалось найти на просторах интернета для подписи с использованием сертификата GOST 3410 в среде Windows. Задача: осуществить взаимодействие с фед. сервисами. В описании указано:

Требования к формированию токена, подписанного сертификатом ГОСТ Р 34.10-2012

  1. Подпись токена (signature) должна быть сформирована в формате pkcs#7. При этом для подписи необходимо оставить только блок SignerInfo и убрать из нее данные сертификата;
  2. В заголовке токена (Header) в блок “x5c” не нужно помещать всю цепочку сертификатов так как это влечет за собой превышение лимита длины заголовка запроса.

В итоге получаем при любом раскладе ошибку "GW-059: Неверное значение закодированной формы токена". Более детальной информации, к сожалению не удается узнать, что не так в формируемом токене.

Окружение:

За основу взят пример https://github.com/AlexMAS/GostCryptography/blob/master/Source/GostCryptography.Tests/Pkcs/SignedCmsSignTest.cs

Исходный код (для облегчение и сокращения кода, часть кода убрана, суть остается):

using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using System.Text; using GostCryptography.Base; using GostCryptography.Gost_R3410;

var certStore = new X509Store(StoreLocation.CurrentUser); certStore.Open(OpenFlags.ReadOnly); var collection = certStore.Certificates; var searchRes = collection.Find(X509FindType.FindBySerialNumber, serialNumber, false); var cert = searchRes[0];

var alg = string.Empty; switch (cert.PublicKey.Oid.Value) { case "1.2.643.2.2.19": alg = "GOST3410-2001"; break; case "1.2.643.7.1.1.1.1": alg = "ECGOST3410-2012"; break; case "1.2.643.7.1.1.1.2": alg = "ECGOST3410-2012"; break; }

JwtHeader header = new JwtHeader() { { "alg", alg }, { "x5c", Convert.ToBase64String(cert.GetRawCertData()) }, };

JwtPayload payload = new JwtPayload() { { "sub", "CD28DB17-8A5E-4DDC-8C0B-0F6C10C9B041" }, /* произвольный гуид */ { "aud", "http://ya.ru" }, /* произвольный адрес */ { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, { "exp", DateTimeOffset.UtcNow.Add(new TimeSpan(0, 10, 0)).ToUnixTimeSeconds() } };

JwtSecurityToken token = new JwtSecurityToken(header, payload); string message = token.EncodedHeader + "." + token.EncodedPayload;

// Создание объекта для подписи сообщения ContentInfo contentInfo = new ContentInfo(Encoding.UTF8.GetBytes(message)); GostCryptography.Pkcs.GostSignedCms signedCms = new GostCryptography.Pkcs.GostSignedCms(contentInfo); // Создание объект с информацией о подписчике CmsSigner cmsSigner = new CmsSigner(cert); // Включение информации только о конечном сертификате (только для теста) cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly; // Создание подписи для сообщения CMS/PKCS#7 signedCms.ComputeSignature(cmsSigner); // Создание сообщения CMS/PKCS#7 byte[] signature = signedCms.Encode();

var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signature);

var jwtToken = $"{message}.{encodedSignature}";

/ в итоге получаем токен из трех составляющих token.EncodedHeader + "." + token.EncodedPayload + "." + encodedSignature /

И при обращении, при любом раскладе получаем ошибку: "GW-059: Неверное значение закодированной формы токена". Что конкретно не так в токене, выяснить не удается.

В описании имеется фраза: "и убрать из нее данные сертификата;" - пробовали делать signedCms.Certificates.Clear(); перед вызовом signedCms.Encode();, не помогает.

Не можем понять что может быть. Может у нас что-то не так недонастроено, или какой-то строки кода не хватает. Или всё же пытаться выяснять что не так в формате.

Подскажите пожалуйста, что может быть, по какому пути идти ?

AlexMAS commented 8 months ago

Здравствуйте!

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

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

AlekseyDev commented 8 months ago

Спасибо огромное за ответ!

Проверка проходит.

Попробовал сделать message следующим образом, передать в него вместо string сертификата, List сертификатов, т.е.

Вместо: JwtHeader header = new JwtHeader() { { "alg", alg }, { "x5c", Convert.ToBase64String(cert.GetRawCertData()) }, };

Теперь передаю: var arr = new List<string>(); arr.Add(Convert.ToBase64String(cert.GetRawCertData()));' JwtHeader header = new JwtHeader() { { "alg", alg }, { "x5c", arr }, };

Токен генерится, обращаюсь к внешней системе, получаю:

400 Request Header Or Cookie Too Large

В документации (от внешней системы) имеется

Подпись токена (signature) должна быть сформирована в формате pkcs#7. При этом для подписи необходимо оставить только блок SignerInfo и убрать из нее данные сертификата;

Т.е. остается вопрос, как убрать из нее данные сертификата;.

Воспользовался другой библиотекой (но сразу оговорюсь, она на Net6, и на данный момент нам не подходит), но попробовал на ней. Библиотека: LibCore (https://github.com/CryptoPro/libcore)

using LibCore.Security.Cryptography.X509Certificates; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using System.Text;

LibCore.Initializer.Initialize(); var certificate = GetCertificate(serialNumber);

var arr = new List<string>(); arr.Add(Convert.ToBase64String(certificate.GetRawCertData())); JwtHeader header = new JwtHeader() { { "alg", "ECGOST3410-2012" }, { "x5c", arr } };

JwtPayload payload = new JwtPayload() { { "sub", "конкретный_гуид" }, { "aud", "http адрес" }, { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, { "exp", DateTimeOffset.UtcNow.Add(new TimeSpan(0, 10, 0)).ToUnixTimeSeconds() } };

JwtSecurityToken token = new JwtSecurityToken(header, payload);

string message = token.EncodedHeader + "." + token.EncodedPayload;

var contentInfo = new ContentInfo(Encoding.UTF8.GetBytes(message)); var signedCms = new SignedCms(contentInfo, true); CmsSigner cmsSigner = new CmsSigner(certificate) { IncludeOption = X509IncludeOption.EndCertOnly, };

signedCms.ComputeSignature(cmsSigner); signedCms.RemoveCertificate(certificate); var signature = signedCms.Encode();

var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signature); var jwtToken = $"{message}.{encodedSignature}";

При данном подходе, токен отработал и Успешно осуществился вызов API внешнего сервиса! Но, повторюсь, данная библиотека нам не подходит.

И как раз тут имеется: signedCms.RemoveCertificate(certificate);

Просьба, вопрос, получится ли добавить к библиотеке GostCryptography К классу: GostCryptography.Pkcs.GostSignedCms метод RemoveCertificate(certificate);

т.е. чтоб можно было бы вызвать аналогично: signedCms.RemoveCertificate(certificate);

Огромная просьба, пожалуйста, сообщите, реально ли, получится ли это сделать?

AlexMAS commented 8 months ago

Метод удаления сертификата SignedCms.RemoveCertificate() имеется только в .NET/.NET Core, но отсутствует в .NET Framework. В последних версиях реализация многих классов переработана так, что многие операции осущетсвляются самим .NET в managed-коде, в то время, как .NET Framework в большинстве своем использует Windows Crypto API. Насколько я понимаю, в этом причина, по которой аналогичная функциональность отсутствует в .NET Framework. В Windows Crypto API сертификат можно удалить только по его индексу в структуре сообщения. Чтобы в будущем не ломать обратную совместимость я добавил метод GostSignedCms.RemoveCertificates(). Эту операцию можно реализовать как в старом .NET Framework, так и в новом. Данный метод удалят из сообщения все сертификаты.

Я пока не вливал ветку, т.к. у меня сейчас нет возможности протестировать данную функциональность. Если у вас есть возможность, переключитесь на ветку remove-certs-from-signedcms и попробуйте повторить эксперимент. Дадите обратную связь, вольем изменения/выпустим новую версию или попробуем доработать метод. ;)

AlekseyDev commented 8 months ago

Скачал исходники. Собрал, подключил напрямую библиотеку, но не запустился.

Следующая ошибка:

Attempt by method 'GostCryptography.Reflection.SignedCmsHelper.RemoveCertificates(System.Security.Cryptography.Pkcs.SignedCms)' to access method 'GostCryptography.Reflection.SignedCmsHelper.GetMessageHandle(System.Security.Cryptography.Pkcs.SignedCms)' failed.

AlexMAS commented 8 months ago

Спасибо. Попробуйте обновиться на последний коммит в ветке remove-certs-from-signedcms и повторить попытку.

AlekseyDev commented 8 months ago

Спасибо огромное! Всё заработало!

AlexMAS commented 8 months ago

Отлично!

Выпустил версию 2.0.10.

В рамках версии добавлены два метода: