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

    Флагманским продуктом нашей компании является Рутокен ЭЦП — устройство с российской криптографией «на борту». Для интеграции устройства с браузерами был выпущен Рутокен Плагин. Демонстрацию возможностей плагина можно посмотреть на тестовых площадках. Некоторые из них предполагают работу с цифровыми сертификатами, хранящимися на устройствах. Например, Демо-банк при регистрации пользователей выписывает пользовательский сертификат, а при логине в систему запрашивает его и проверяет. Для реализации данных задач на сервере используется библиотека BouncyCastle.


    В данной статье будут рассмотрены примеры ее использования для выписки сертификатов по запросу 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";
    		}
    


    Еще раз повторюсь, пример подходит для тестирования или демонстрации решений, работающих с российскими сертификатами.
    «Актив» 102,84
    Компания
    Поделиться публикацией
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое