На нашем сайте мы начали публикацию цикла статей по основам криптографии. В первой части вы могли прочитать о основных шифрах применявшихся в современной истории, дальше мы рассказали вам о системах распределения ключей в криптосистемах. Сегодня мы решили коснуться практических вопросов реализации алгоритмов систем открытых ключей.
В этой статье я покажу, как реализовать основные операции по работе с PKI. Речь идет о подписи, проверке подписи, шифровании и расшифровании в контексте PKI. Теоретически данный код может использоваться с любым CSP (Cryptography Service Provider), поддерживающим интерфейс MS Crypto API. Я пользуюсь бесплатным отечественным CSP. Почему отечественным? Дело в том, что в .NET нет поддержки алгоритмов ГОСТ. А если мы работаем с реальными проектами и, следовательно, вынуждены соответствовать требованиям регуляторов в России, то без гостовских алгоритмов нам никак. Но если установить отечественный криптопровайдер, поддерживающий MS Crypto API, то все будет тип-топ, потому что .NET при работе с CMS дергает именно MS Crypto API.
Картина мира
Перед погружением в код давай разберем немного терминологии. PKI — инфраструктура открытых ключей. Как несложно догадаться, PKI основана на асимметричном шифровании. В симметричных шифрах для шифрования и расшифрования используется один ключ. В aсимметричных для шифрования используется один ключ, а для расшифрования — другой. Вместе они образуют ключевую пару.
Информация, необходимая для работы PKI, содержится в сертификате X.509. В PKI участвуют как минимум три стороны: Алиса, Боб и удостоверяющий центр (УЦ). У Алисы и Боба есть сертификаты с закрытым ключом, подписанные так называемым корневым сертификатом УЦ. У Алисы есть сертификат Боба с открытым ключом, а у Боба — сертификат Алисы с открытым ключом. Алиса и Боб доверяют УЦ и благодаря этому могут доверять друг другу.
Сертификаты X.509
Так повелось, что основным «активом» в PKI является сертификат X.509. Сертификат — это что-то вроде паспорта, он содержит информацию, позволяющую идентифицировать субъект, которому выдан сертификат (поле Subject), указывает, кем он был выпущен (поле Issuer), серийный номер сертификата и многое другое. В Windows управлять сертификатами можно с помощью оснастки «Сертификаты» (run->certmgr.msc
).
Сертификаты хранятся в хранилищах («Личнoе», «Доверенные центры сертификации», «Доверенные лица»…).
При получении сертификата важно установить его в правильное хранилище. Так, сертификаты, которые ты хочешь использовать для электронной подписи, должны быть установлены в хранилище «Личное», а сертификаты получателей, которым нужно будет отправлять зашифрованные сообщения, — в хранилище «Доверенные лица». Сертификаты удостоверяющих центров (УЦ) должны быть установлены в хранилище «Доверенные корневыe центры сертификации». При установке сертификата система предлагает два варианта: выбрать хранилище автоматически либо указать вручную. Рекомендую использовать второй вариант, так как автоматика иногда устанавливает сертификат не в то хранилище. Сертификат, которым мы хотим подписывать сообщения, должен иметь закрытый ключ. О наличии закрытого ключа можно узнать, посмотрев на свойства сертификата, где русским по белому будет написано: «есть закрытый ключ для этого сертификата».
Самое интересное о сертификате мы можем узнать на вкладке «Состав».
Обрати внимание на поля «Алгоритм подписи», «Алгоритм хеширования подписи» и «Открытый ключ». Если хочешь использовать сертификат для осуществления транзакций в России, во всех этих полях ты должен видеть слово «ГОСТ». Также следует обратить внимание на значение поля «Использование ключа» и поля «Действителен с» и «Действителен по»: первое позволит понять, возможно ли использование сертификата для выполнения нужной нам операции (шифрование, подпись), а второе и третье — возможно ли использовать данный сертификат в указанный момент времени. В дополнение к этому cледует убедиться, что сертификат действителен. В этом нам поможет вкладка «Путь сертификации». Если с сертификатом все хорошо, мы увидим надпись: «Этот сертификат действителен».
Цифровая подпись
Представь, дорогой читатель, что ты занимаешься некой очень ответственной работой. И результаты своей работы отправляешь в виде отчетов, от которых в конечном итоге зависят чьи-то конкретные судьбы и жизни. Получатели твоих отчетов принимают на их основе очень важные решения, и, если ты напортачишь, вполне можешь получить срок. Так вот, в таких ответственных организациях без электронной подписи никуда. Она позволяет тебе подписать тот самый суперважный секретный отчет своим сертификатом с закрытым ключом. Закрытый ключ, в идеале, может храниться на токене — специальном съемном устройстве, похожем на флешку, которое ты в редкие моменты достаешь из сейфа. Подпись гарантирует, что твой отчет отправлен именно тобой, а не уборщицей или сторожем. С другой стороны, ты не сможешь отказаться от авторства (это называется «неотрекаемость») и, если накосячишь в своем суперважном документе, на сторожа свалить вину не получится.
Электронная подпись применяется не только в спецслужбах и органах, но и в бизнесе. Например, для перевода пенсионных накоплений в НПФ: мы генерируем запрос на сертификат, отправляем его в удостоверяющий центр (УЦ). УЦ выпускает сертификат, мы подписываем сертификатом заявление на перевод пенсионных накоплений, отправляем — и вуаля. Подпись также позволяет осуществлять контроль целостности подписываемых данных. Если данные будут изменены, подпись не пройдет проверку.
Для программирования подписи необходимо ознакомиться с несколькими классами .NET Framework:
- X509Certificate2 — представляет собой сертификат X.509. Имя класса, оканчивающееся на 2, говорит о том, что класс является усовершенствованным аналогом класса X509Certificate.
- X509Chain.aspx) — позволяет строить и проверять цепочку сертификатов. Необходима для того, чтобы убедиться в действительности сертификата.
- SignedCms.aspx) — позволяет подписывать и проверять сообщения PKCS#7.
Перед тем как заюзать наш сертификат, необходимо его проверить. Процедура включает в себя проверку цепочки сертификации, проверку срока действия и проверку, не отозван ли сертификат. Если мы подпишем файл недействительным сертификатом, подпись будет недействительной.
X509Chain certificateChain = new X509Chain { ChainPolicy = { RevocationMode = X509RevocationMode.Online, VerificationFlags = X509VerificationFlags.IgnoreNotTimeValid, RevocationFlag = X509RevocationFlag.ExcludeRoot } }; bool chainOk = certificateChain.Build(certificate); bool certNotExpired = (certificate.NotAfter >= DateTime.Now) && (certificate.NotBefore <= DateTime.Now);
Мы проверили сертификат и убедились, что он в порядке. Переходим непосредственно к подписыванию данных. Подпись бывает двух видов: прикрепленная и открепленная.
Результатом прикрепленной подписи будет CMS (Cryptography Message Syntax) — сообщение, содержащее как подписываемые данные, так и саму подпись. Открепленная подпись содержит только саму подпись. Рекомендую использовать именно открепленную подпись, потому что с ней намного меньше мороки. В нее проще поставить метку времени, она меньше весит, так как не содержит подписываемые данные. Подписываемые данные легко открыть, посмотреть. В случае прикрепленной подписи для того, чтобы просмотреть подписанные данные, CMS-сообщение необходимо сначала декодировать. В общем, прикрепленной подписи я рекомендую избегать всеми силами. Если потребуется передавать подпись и контент вместе, рассмотри вариант архивирования (вместо использования прикрепленной подписи используй открепленную, просто заархивируй подписываемый файл и открепленную подпись). Посмотрим на код подписи (С#):
public byte[] SignAttached(X509Certificate2 certificate, byte[] dataToSign) { ContentInfo contentInfo = new ContentInfo(dataToSign); SignedCms cms = new SignedCms(contentInfo, false); CmsSigner signer = new CmsSigner(certificate); cms.ComputeSignature(signer, false); return cms.Encode(); } public byte[] SignDetached(X509Certificate2 certificate, byte[] dataToSign) { ContentInfo contentInfo = new ContentInfo(dataToSign); SignedCms cms = new SignedCms(contentInfo, true); CmsSigner signer = new CmsSigner(certificate); cms.ComputeSignature(signer, false); return cms.Encode(); }
Глядя на примеры кода, можно подумать, что работа с подписью в .NET реализована достаточно хорошо. Но рассмотрим, например, случай, в котором необходимо осуществить подпись большого файла, размером 600 MiB. Внимательные читатели обратили внимание на сигнатуру метода подписи — он принимает на вход массив байтов. При попытке загрузить в массив байтов 600 MiB мы получим OutOfMemoryException.
[ad name=»Responbl»]
Что же делать, спросишь ты? Обращаться к основам — отвечу я! Очевидно, раз нельзя загрузить в память 600 MiB, то необходимо файл грузить и обрабатывать по кусочкам. .NET-обертки над CMS так не умеют. На помощь нам приходит MS Crypto API. MS Crypto API содержит два набора функций для работы с CMS: Simplified Message Fuctions и Low Level Message Functions. Для работы с большими файлами нам нужны Low Level. Полную реализацию на C# можно посмотреть тут. Я же предпочитаю работать с криптографией на языке C++. Кода в результате писать приходится меньше, а работает он быстрее. Рассмотрим порядок действий для реализации подписи в поточном режиме:
- Получаем
PCCERT\_CONTEXT
; - Заполняем структуры
CMSG\_STREAM\_INFO
,CRYPT\_ALGORITHM\_IDENTIFIER
,CMSG\_SIGNER\_ENCODE\_INFO
,CMSG\_SIGNED\_ENCODE\_INFO
; - Получаем хендл сообщения с помощью функции
CryptMsgOpenToEncode
. Для открепленной подписи необходимо передать соответствующий флагCMSG\_DETACHED\_FLAG
; - В цикле вызываем функцию
CryptMsgUpdate
и «скармливаем» ей по кусочкам файл, который необходимо подписать.
На C++ будет что-то вроде:
ISigner signer = null; // Заполняем структуры ... // Открываем сообщение для кодирования HCRYPTMSG hMsg = CryptMsgOpenToEncode ( (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING), // Message encoding type dwFlags, // Flags CMSG_SIGNED, // Message type &SignedMsgEncodeInfo, // Pointer to structure NULL, // Inner content object ID &stStreamInfo // Stream information (not used) ); ... // Обрабатываем файл для подписи по кусочкам while ( ( bytesRead = inputStream->Read(buf, blockSize, 0, blockSize ) ) > 0 ) { processedDataLen += bytesRead; BOOL lastcall = (processedDataLen == streamLength); BOOL successful = CryptMsgUpdate(hMsg, (const BYTE*)buf, bytesRead, lastcall); } // Закрываем хендл CryptMsgClose(hMsg); return S_OK;
Вызов кода на C++ из C# будет выглядеть примерно так:
ISigner signer = null; PkiFactory.CreateSigner(out signer); X509Store store = new X509Store("My"); store.Open(OpenFlags.ReadOnly); var cert = GetCertificates(store); string file = @"d:\tmp\masyanya.bin"; using (var inputStream = File.OpenRead(file)) using (var outputStream = File.Create(file + ".sig")) { var reader = new StreamReader(inputStream); var writer = new StreamWriter(outputStream); int result = signer.Sign(reader, writer, cert, 1); Debug.Assert(result == 0, "Подпись не прошла."); }
Внимательный читатель удивится — что это за IStreamReader* inputStream
, IStreamWriter* outputStream
, ICertificate* signCertificate
? Ответ следует из названия переменных, но есть одна тонкость, которая для многих окажется сюрпризом. IStreamReader, IStreamWriter, ICertificate — это интерфейсы, реализованные на C#, и это не COM-объекты. При этом мы спокойно можем вызывать их методы в нативном C++. Как сделать такую красоту — тема отдельной статьи. В результате успешного выполнения операции мы получим криптографическое сообщение. Для кодирования сертификатов X.509 и криптографических сообщений используется Abstract Syntax Notation One, или, по-простому, ASN 1. Для просмотра файлов, закодированных в ASN 1, можно воспользоваться бесплатным ASN.1 Editor.
Проверка подписи и декодирование
А теперь, дорогой читатель, представь, что ты большой начальник и должен принять важное стратегическое решение на основе отчета, который тебе прислал сотрудник по электронной почте. Для твоего удобства отчет был подписан открепленной подписью. Открыв почту и скачав отчет, ты, как опытный, знающий жизнь человек, не спешишь принимать на веру содержимое отчета и проверяешь подпись. После проверки выясняешь, что подпись неверна — не сошлась контрольная сумма. В результате оповещаешь службу безопасности, которая проводит расследование и выясняет, что хитрые конкуренты взломали почтовый сервер и отправили тебе фальшивый документ. Тебя наградили за бдительность, конкурентов посадили, а компания наконец-то получила оригинальный отчет с проверенной электронной подписью.
[ad name=»Responbl»]
Если пользователь прислал тебе отчет в виде прикрепленной подписи, тебе для чтения придется его декодировать:
public bool VerifyAttached(byte[] dataToVerify) { try { var cms = new SignedCms(); cms.Decode(dataToVerify); foreach (var signer in cms.SignerInfos) { signer.CheckSignature(true); } return true; } catch (CryptographicException) { return false; } } public byte[] Decode(byte[] signedCms) { var cms = new SignedCms(); cms.Decode(signedCms); return cms.ContentInfo.Content; }
Нетрудно догадаться, что и тут разработчики .NET Framework подложили нам свинью. Не можем мы проверить подпись большого файла! По той же самой причине — OutOfMemoryException. Но и эту проблему несложно решить, обратившись к магии MS Crypto API. Так как код поточной проверки подписи достаточно длинный, остановлюсь на основных моментах:
// Заполняем структуры ... // Открываем сообщение для декодирования HCRYPTMSG msg = CryptMsgOpenToDecode(...); // Декодируем сообщение по кусочкам while ((bytesRead = inputStream->Read(&buf.at(0), blockSize, 0, blockSize)) > 0) { totalBytesRead += bytesRead; CryptMsgUpdate(msg, &buf.at(0), bytesRead, totalBytesRead == fileSize); } // Получаем информацию о подписанте PCCERT_CONTEXT pSignerCertContext = CertGetSubjectCertificateFromStore( hStore, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, (PCERT_INFO)((void*)(&signerCertInfo.at(0)))); // Проверяем подпись BOOL ok = CryptMsgControl(msg, 0, CMSG_CTRL_VERIFY_SIGNATURE, pSignerCertContext->pCertInfo));
А так будет выглядеть код проверки подписи при вызове из C#:
ISignatureVerifier verifier = null; PkiFactory.CreateSignatureVerifier(out verifier); using (var inputStream = File.OpenRead(file + ".sig")) using (var contentStream = File.OpenRead(file)) { var reader = new StreamReader(inputStream); var contentReader = new StreamReader(contentStream); var error = verifier.VerifyDetachedSign(contentReader, reader); Debug.Assert(error == 0, "Проверка подписи не прошла"); }
Шифрование
Зачем нужно шифрование, все уже знают. PKI нам дает полезную плюшку: мы можем зашифровать один документ так, что расшифровать его смогут несколько получателей. Это очень удобно. Для этого нам нужно иметь сертификаты получателей.
public byte[] Encrypt(byte[] dataToEncrypt, params X509Certificate2[] recepients) { var contentInfo = new ContentInfo(dataToEncrypt); var recipientsCertificates = new X509Certificate2Collection(recepients); var recipients = new CmsRecipientCollection(SubjectIdentifierType.IssuerAndSerialNumber, recipientsCertificates); var cms = new EnvelopedCms(contentInfo); cms.Encrypt(recipients); return cms.Encode(); }
Расшифрование
При расшифровании необходимо, чтобы сертификат, указанный при шифровании в коллекции получателей, был установлен в хранилище сертификатов. Так как сообщение может быть зашифровано и адресовано нескольким получателям, для расшифрования нам необходимо найти того получателя, сертификат которого установлен в нашем хранилище сертификатов.
public byte[] Decrypt(byte[] encryptedData) {
var envelopedCms = new EnvelopedCms();
envelopedCms.Decode(encryptedData);
X509Store store = new X509Store("My");
store.Open(OpenFlags.ReadOnly);
RecipientInfo recipientInfo = envelopedCms.RecipientInfos.Cast<RecipientInfo>()
.FirstOrDefault(x => FindCertificate((X509IssuerSerial)x.RecipientIdentifier.Value) != null);
envelopedCms.Decrypt(recipientInfo);
return envelopedCms.ContentInfo.Content;
}
private X509Certificate2 FindCertificate(X509IssuerSerial issuerSerial) {
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
return store.Certificates
.Find(X509FindType.FindByIssuerDistinguishedName, issuerSerial.IssuerName, false)
.Find(X509FindType.FindBySerialNumber, issuerSerial.SerialNumber, false).Cast<X509Certificate2>()
.FirstOrDefault();
}
Заключение
В статье не удалось охватить все аспекты PKI, так как их очень много. Тем не менее закодить основные операции ты теперь сможешь без проблем. Разработчикам, которые умеют писать на C#, рекомендую освоить C++ хотя бы на базовом уровне. Это очень пригодится, когда придется работать с нативными функциями. А до многих возможностей ОС по-другому и не добраться, так как .NET реализует весьма ограниченный набор возможностей. Например, .NET не имеет полной поддержки MS Crypto API и CNG (cryptography next geberation), поэтому тебе придется писать тонны P/invoke-кода на С# либо значительно меньше на C++.
[ad name=»Responbl»]
Как видишь, работа с PKI достаточно сложная и требует серьезной подготовки, выдержки и терпения. Перед тем как бросаться реализовывать классные фичи, крайне важно понимать основные концепции PKI.
За кадром остались поточное шифрование и расшифрование, подпись несколькими сертификатами, генерация запросов на сертификат и многое другое, но основу я тебе показал. Код примеров с тестами можно скачать на GitHub.
2 comments On Основы криптографии. Как работает система открытых ключей.
Очень помогла Ваша статья особенно в области открепленной цифровой подписи
Pingback: Защита электронной почты от А до Я - Cryptoworld ()