Что имеем на входе?
Лицензирование ПО и версииКриптоПро CSP версии 5.0 — у меня установлена версия 5.0.11944 КС1, лицензия встроена в ЭП. КриптоПро TSP Client 2.0 и КриптоПро OCSP Client 2.0 — лицензии покупается отдельно, а для гайда мне хватило демонстрационного срока. КриптоПро . NET Client версии 1.0.7132.2 — в рамках этого гайда я использовал демонстрационную версию клиентской части и все действия выполнялись локально. Лицензию на сервер нужно покупать отдельно. Контур. Крипто бесплатен, но требует регистрации. В нем также можно подписать документы КЭП, УКЭП и проверить созданную подпись загрузив ее файлы.
UPD1: Поменял в коде переменную, куда записываются байты файла подписи.
Также я забыл написать немного про подпись штампа времени — он подписывается сертификатом владельца TSP-сервиса. По гайду это ООО «КРИПТО-ПРО»:

UPD2: Про библиотеки CAdESCOM и CAPICOM
Если создать пустой проект на . NET Core 3.1, подключив непропатченные библиотеки, то при обращении к закрытому ключу выпадет исключение «System. NotSupportedException» c сообщением «The certificate key algorithm is not supported.»:

. NET Core 3.1 — Исключение без пропатченных библиотек
Но при использовании пропатченных библиотек это исключение не выпадает и с приватным ключем можно взаимодействовать:

Также код из гайда работает с . NET Framework 4.8 без использования пропатченных библиотек, но вместо обращения к пространству имен «System. Security. Cryptography», которое подменяется пропатченными библиотеками для . NET Core, CSP Gost3410_2012_256CryptoServiceProvider будет использован из пространства имен «CryptoPro. Sharpei»:

Кратко об электронной подписи
Принцип электронной подписи состоит в том что верифицируемая(приватная) сторона создает ключевую пару состоящую из открытого(публичного) и закрытого(приватного) ключа. Приватный ключ хранится только у нее и к нему не должно быть доступа со стороны. Публичный же ключ является общедоступным и предоставляется верифицирующей(публичная) стороне.
Далее, приватная сторона может подписывать сообщения своим приватным ключом и публичная сторона может верифицировать эту подпись публичным ключом. Таким образом публичная сторона может быть уверена в том что сообщения были получены именно от владельца приватного ключа из этой ключевой пары а не от посторонних лиц.
В свою очередь публичная сторона может шифровать своим публичным ключом сообщение и передать приватной стороне. Данные зашифрованные публичными ключами могут быть расшифрованы только приватным ключом, т.к. алгоритм создания ключевой пары является асинхронным и шифровка и расшифровка является односторонней. Приватный ключ не может шифровать данные т.к. публичный ключ является общедоступным, приватный ключ способен только «подписывать», и публичный ключ может эту подпись только верифицировать. Таким образом публичная сторона может передавать зашифрованные сообщения приватной стороне т.к. только она может их расшифровать.
Для безопасного общения между двумя сторонами необходимо чтобы обе стороны сгенерировали личную ключевую пару и обменялись публичными ключами. В этом случая одна сторона шифрует сообщение при помощи публичного ключа другой стороны и подписывает этот шифр своим приватным ключом. Таким образом вторая сторона может верифицировать подпись этого шифра предоставленным ей первой стороной публичным ключом и далее своим приватным ключом расшифровать сообщение которое было зашифровано публичным ключом из ее собственной ключевой пары.
Работа с средствами Bouncy Castle
Теперь давайте приступим к коду.
Первым делом необходимо создать ключевую пару (RSA или ECDSA):
//RSA key pair
var keyGenerationParameters = new KeyGenerationParameters(new SecureRandom(), keySize);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator. Init(keyGenerationParameters);
var rsaKeyPair = keyPairGenerator. GenerateKeyPair();
//ECDSA key pair
var curve = ECNamedCurveTable. GetByName(«secp256k1»);
var domainParameters = new ECDomainParameters(curve. Curve, curve. G, curve. N, curve. H, curve. GetSeed());
var keyGenerationParameters = new ECKeyGenerationParameters(domainParameters, new SecureRandom());
var generator = new ECKeyPairGenerator();
generator. Init(keyGenerationParameters);
var ecdsaKeyPair = generator. GenerateKeyPair();
Для публичного и приватного ключа в файлы формата .pem используем следующий код
Теперь когда у нас есть ключевая пара давайте создадим сертификаты для удобства.
Сначала можем создать запрос на создание сертификата (CSR). Код ниже взят из тестового консольного проекта в репозитории библиотеки.
var keyPair = keyPairPath. GetKeyPairFromPem();
var csr = Cryptography. GenerateCSR(«Test_KG», «Test_Chuy», «Test_Bishkek», «Test_osh», «Test_ITZone», «Test_Name», CryptographyAlgorithm. SHA256withRSA, keyPair);
csr. ToPemFile(csrPath);
Вот реализация метода GenerateCSR:
Далее создадим самоподписанный сертификат
var selfSignedCert = Cryptography. GenerateSelfSignedCert(csrPath. GetCSRPemFile(),
keyPairPath. GetPrivateKeyFromPem(),
DateTime. UtcNow,
DateTime. UtcNow. AddYears
);
selfSignedCert. ToPemFile(selfSignedCertPath);
Теперь создадим приватный PFX сертификат который будет содержать ключевую пару
Мы теперь можем создавать публичные сертификаты подписанные нашим приватным сертификатом
var signedCert = Cryptography. GenerateSignedCertificate(csrPath. GetCSRPemFile(),
privateCertPath. GetPrivateCert(privateCertPass),
privateCertPath,
DateTime. UtcNow,
DateTime. UtcNow. AddYears
);
signedCert. ToPemFile(signedCertPath);
Реализация метода GenerateSignedCertificate
Теперь уже мы можем при помощи публичного сертификата шифровать данные и дешифровать приватным PFX сертификатом
var message = «Hello world»;
var encoded = Cryptography. EncryptDataByPublicCert(message, signedCertPath. GetPublicCert());
var data = Cryptography. DecryptDataWithPrivateCert(encoded, privateCertPath. GetPrivateCert(privateCertPass), privateCertPass);
Console. WriteLine(Encoding. UTF8. GetString(data));
Метод для шифрования публичным ключом
Метод для дешифрования приватным ключом
В тестовом консольном проекте также предоставлены примеры шифровки и дешифровки при помощи самих ключей.
Извлечение данных из подписи(сигнатуры) приватного ключа
var message = «Hello world»;
var signature = Cryptography. SignDataByPrivateCert(message, privateCertPath. GetPrivateCert(privateCertPass));
var data = Cryptography. ExtractSignedData(signature);
Console. WriteLine(Encoding. UTF8. GetString(data));
Можно подписать сообщение самим приватным ключом а не сертификатом и проверить подпись публичным ключом
var message = «Hello world»;
var signature = Cryptography. SignDataByPrivateKey(message, privateKeyPath. GetPrivateKeyFromPem());
if(Cryptography. VerifySignedByPublicKey(message, signature, publicKeyPath. GetPublicKeyFromPem()))
Console. WriteLine(«True»);
else
Console. WriteLine(«False»);
Верифицировать подпись что она была сделана именно тем приватным сертификатом которым был подписан наш публичный подписанный сертификат.
Верифицировать подпись что она была сделана приватным сертификатом которыму мы доверяем.
Верифицировать подпись что она была сделана именно тем же самым сертификатом которым мы проверяем подпись.
Вот полный стек вызовов рассмотренных методов.
//Данный код представляет собой последовательность вызовов функций для различных тестов, связанных с цифровыми сертификатами, криптографией и безопасностью.
//Давайте разберем каждую из этих функций и их потенциальное назначение.
//Эта тестовая функция может генерировать пару открытого и закрытого ключей с использованием алгоритма шифрования RSA.
//Это широко используемый алгоритм для генерации безопасных ключей, который часто используется для безопасной связи.
Тесты. ТестGenerateRSAKeyPair();
//Эта тестовая функция может записывать пару ключей в файл или создавать закрытый и открытый ключи.
//Закрытые ключи используются при асимметричном шифровании, где один ключ используется для шифрования, а другой — для дешифрования.
Тесты. ТестWriteKeyPairInPemFile();
//Эта тестовая функция может генерировать запрос на подпись сертификата (CSR), который представляет собой сообщение, отправляемое в центр сертификации для запроса цифрового сертификата.
Тесты. ТестСгенерироватьCSR();
//Эта тестовая функция может генерировать самозаверяющий сертификат.
//Самоподписанные сертификаты — это цифровые сертификаты, подписанные тем же лицом, которое выдает сертификат.
Тесты. TestGenerateSelfSignedCert();
//Эта тестовая функция может создавать файл PFX — формат файла, используемый для безопасного хранения закрытых ключей и сертификатов.
Тесты. СоздатьПфкс();
//Эта тестовая функция может генерировать подписанный сертификат.
//Подписанный сертификат — это цифровой сертификат, подписанный доверенной третьей стороной, известной как центр сертификации.
Тесты. ТестGenerateSignedCert();
//Эта тестовая функция может расшифровывать данные с помощью сертификата.
//При асимметричном шифровании открытый ключ используется для шифрования, а закрытый ключ — для дешифрования.
Тесты. ТестDecryptWithCert();
//Эта тестовая функция может расшифровывать данные с помощью закрытого ключа.
Тесты. ТестDecryptWithKey();
//Эта тестовая функция может извлекать подписанный сертификат из частного сертификата.
Тесты. ТестExtractSignedByPrivateCert();
//Эта тестовая функция может проверять, подписан ли сертификат закрытым ключом.
Тесты. ТестVerifySignedByPrivateKey();
//Эта тестовая функция может проверять совпадение открытого и закрытого ключей.
Тесты. TestVerifyMatchBetweenPublicAndPrivateKeys();
//Эта тестовая функция может проверять, подписан ли сертификат частным сертификатом.
//ВНИМАНИЕ!!! При вызове этого метода результат будет «ложным», если какой-либо из тестируемых сертификатов отсутствует в хранилище доверенных сертификатов;
Тесты. ТестVerifySignedByPrivateCert();
//Эта тестовая функция может проверять, можно ли доверять подписанным данным с помощью корневого сертификата.
//ВНИМАНИЕ!!! При вызове этого метода результат будет «ложным», если какой-либо из тестируемых сертификатов отсутствует в хранилище доверенных сертификатов;
Тесты. TestVerifySignedDataRootCertAndTrustCommunication();
//Эта тестовая функция может проверять, можно ли доверять подписанным данным с использованием того же сертификата.
//ВНИМАНИЕ!!! При вызове этого метода результат будет «ложным», если какой-либо из тестируемых сертификатов отсутствует в хранилище доверенных сертификатов;
Тесты. ТестVerifySignedDataBySameCert();
В данной версии реализована работа с алгоритмами RSA и ECDSA. В будущем я добавляю функционал и по другим алгоритмам.
В репозитории предоставлен полный исходный код библиотеки с подробной документацией и тестовым консольным проектом, который можно запустить сразу из «коробки».
Благодарю за внимание. Буду рад и признателен, если дадите фидбек по найденным недоработкам, ошибкам или советам по продолжению кода и функционала.
Это вообще закономерно?
С удовольствием узнаю ваше мнение в комментариях.
