Выписываем цифровой сертификат и проверяем подпись с помощью BouncyCastle

Флагманским продуктом нашей компании является Рутокен ЭЦП — устройство с российской криптографией «на борту». Для интеграции устройства с браузерами был выпущен Рутокен Плагин. Демонстрацию возможностей плагина можно посмотреть на тестовых площадках. Некоторые из них предполагают работу с цифровыми сертификатами, хранящимися на устройствах. Например, Демо-банк при регистрации пользователей выписывает пользовательский сертификат, а при логине в систему запрашивает его и проверяет. Для реализации данных задач на сервере используется библиотека BouncyCastle.6416146868a64957aa64067d96963fdb.jpgВ данной статье будут рассмотрены примеры ее использования для выписки сертификатов по запросу PKCS#10, а также для проверки подписи CMS, выработанной по российским криптоалгоритмам.В основе нашего «центра сертификации» лежит библиотека BouncyCastle. Нужно заметить, что на сайте bouncycastle.org/csharp/ находится устаревшая версия библиотеки, не заработавшая в решении без фиксов. Рабочую версию можно взять на гитхабе — https://github.com/bcgit/bc-csharp.

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

Нам из всего этого нужно не много: — Работа с запросами PKCS#10— Выписка сертификатов по данным запроса

Если есть необходимость организации входа на сайт с помощью сертификата, реализуем еще один алгоритм, о нем ниже.

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

КлиентВ нашей системе во внешний мир смотрит сервер IIS с ASP.NET web-api с методом, выдающим сертификат по запросу PKCS#10. На клиенте, то-есть на самих демо-площадках, крутится приложение на AngularJs, работающее с плагином. Конечно же можно на чем угодно клиента писать, но суть работы на клиентской стороне сводится к следующему: — передаем функции плагина createPkcs10 данные полей для формирования запроса PKCS#10, получаем текст запроса.— текст запроса PKCS#10 передаем post-запросом на метод апи, получаем сертификат или ошибку в случае невозможности выписать сертификат.— передаем функции плагина importCertificate полученный сертификат, импортируем его на устройство.

Рабочая версия сайта с возможностью управления сертификатами на устройствах Рутокен ЭЦП сейчас крутится здесь — http://ra.rutoken.ru. Можно создать ключ и сделать запрос с необходимыми полями. Далее выписать тестовый сертификат, который будет импортирован на токен.! Для работы нужно установить плагин и подключить Рутокен ЭЦП!

Сервер Но вернемся на серверную часть. Итак, у нас есть корневой сертификат в формате PEM и закрытый ключ к нему. Будем выдавать пользовательский сертификат по запросу PKCS#10. Сам запрос также приходит от клиента в текстовом виде, в формате PEM. /* тестовый корневой сертификат */ const string cCACert = @»-----BEGIN CERTIFICATE----- *** сам сертификат *** -----END CERTIFICATE-----»;

/* ключ корневого сертификата*/ const string cCAKey = @»-----BEGIN PRIVATE KEY----- *** ключ *** -----END PRIVATE KEY-----»;

// выписываем тестовый сертификат public string generateTestCert (string pkcs10text) { // читаем приватный ключ PemReader pRd = new PemReader (new StringReader (cCAKey)); AsymmetricKeyParameter _cCAKey = (AsymmetricKeyParameter)pRd.ReadObject (); pRd.Reader.Close ();

// читаем корневой сертификат pRd = new PemReader (new StringReader (cCACert)); var _cCACert = (X509Certificate)pRd.ReadObject (); pRd.Reader.Close ();

// как вариант: //X509CertificateParser certParser = new X509CertificateParser (); //var _caCert = certParser.ReadCertificate (Base64.Decode (cCACert.Replace (»-----BEGIN CERTIFICATE-----», string.Empty).Replace (»-----END CERTIFICATE-----», string.Empty)));

Pkcs10CertificationRequest _pkcs10; // читаем pkcs10 using (StringReader _sr = new StringReader (pkcs10text)) { pRd = new PemReader (_sr); _pkcs10 = (Pkcs10CertificationRequest)pRd.ReadObject (); pRd.Reader.Close (); }

// выпускаем сертификат X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator ();

var requestInfo = _pkcs10.GetCertificationRequestInfo (); var subPub = _pkcs10.GetPublicKey (); var issPub = _cCACert.GetPublicKey ();

// серийный номер var randomGenerator = new CryptoApiRandomGenerator (); var random = new SecureRandom (randomGenerator); var serialNumber = BigIntegers.CreateRandomInRange ( BigInteger.One, BigInteger.ValueOf (Int64.MaxValue), random);

v3CertGen.Reset (); v3CertGen.SetSerialNumber (serialNumber); v3CertGen.SetIssuerDN (_cCACert.IssuerDN); v3CertGen.SetNotBefore (DateTime.UtcNow); // сертификат на год v3CertGen.SetNotAfter (DateTime.UtcNow.AddYears (1)); v3CertGen.SetSubjectDN (requestInfo.Subject); v3CertGen.SetPublicKey (subPub);

if (issPub is ECPublicKeyParameters) {

// в тестовых примерах можно посмотреть на генерацию с различными алгоритмами, на нужен только GOST3411withECGOST3410 ECPublicKeyParameters ecPub = (ECPublicKeyParameters)issPub; if (ecPub.AlgorithmName == «ECGOST3410») { v3CertGen.SetSignatureAlgorithm («GOST3411withECGOST3410»); } else { throw new Exception («нужен алгоритм подписи GOST3411withECGOST3410»); } } else { throw new Exception («нужен GOST3411withECGOST3410»); }

// extensions v3CertGen.AddExtension ( X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifier (SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo (subPub)));

v3CertGen.AddExtension ( X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifier (SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo (issPub)));

v3CertGen.AddExtension ( X509Extensions.BasicConstraints, false, new BasicConstraints (false));

X509Certificate _cert = v3CertGen.Generate (_cCAKey);

_cert.CheckValidity (); _cert.Verify (issPub);

var s = new StringWriter (); PemWriter pw = new PemWriter (s);

pw.WriteObject (_cert); pw.Writer.Close ();

return s.ToString (); }

Проверка подписанного CMS на сервере Будем генерировать CMS на клиенте и отправлять его на сервер, где проверим подпись и цепочку сертификатов.Из BouncyCastle задействуем: — Проверку подписи signed CMS— Построение цепочки сертификатов

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

Проверку подписанного CMS делаем так:

// сторим цепочку сертификатов public void verifyCert (X509Certificate cert) { try { // читаем корневой сертификат var pRd = new PemReader (new StringReader (cCACert)); var _cCACert = (X509Certificate)pRd.ReadObject (); pRd.Reader.Close ();

// список сертификатов для цепочки IList certList = new ArrayList (); certList.Add (_cCACert); certList.Add (cert);

IX509Store x509CertStore = X509StoreFactory.Create («Certificate/Collection», new X509CollectionStoreParameters (certList));

//делаем список корневых сертификатов, в данном случае один ISet trust = new HashSet (); trust.Add (new TrustAnchor (_cCACert, null));

PkixCertPathBuilder cpb = new PkixCertPathBuilder (); X509CertStoreSelector targetConstraints = new X509CertStoreSelector (); targetConstraints.Subject = cert.SubjectDN; PkixBuilderParameters parameters = new PkixBuilderParameters (trust, targetConstraints);

parameters.AddStore (x509CertStore); // отключаем проверку crl parameters.IsRevocationEnabled = false;

// строим цепочку, если построилась — ок PkixCertPathBuilderResult result = cpb.Build (parameters);

} catch (PkixCertPathBuilderException certPathEx) { throw new PkixCertPathBuilderException (string.Format («Ошибка проверки цепочки, {0}», certPathEx.Message)); } catch (Exception ex) { throw new Exception (string.Format («Ошибка проверки сертификата: {0}», cert.SubjectDN), ex); } }

// проверка signed CMS public string verifyCms (string cmsText) { CmsSignedData cms = new CmsSignedData (Base64.Decode (cmsText)); SignerInformationStore sif = cms.GetSignerInfos (); var signers = sif.GetSigners (); var ucrts = cms.GetCertificates («collection»); //var crl = cms.GetCrls («collection»); // нужно проверять все, но у нас один signer и один сертификат foreach (SignerInformation signer in signers) {

ICollection certCollection = ucrts.GetMatches (signer.SignerID); IEnumerator certEnum = certCollection.GetEnumerator ();

certEnum.MoveNext (); X509Certificate cert = (X509Certificate)certEnum.Current;

if (! signer.Verify (cert)) { throw new CertificateException («проверка подписи не прошла»); }

verifyCert (cert); }

return «ok»; } Еще раз повторюсь, пример подходит для тестирования или демонстрации решений, работающих с российскими сертификатами.

© Habrahabr.ru