Выписываем цифровой сертификат и проверяем подпись с помощью 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"; 		} 

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

ссылка на оригинал статьи http://habrahabr.ru/post/257407/


Комментарии

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

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