Продолжая разговор на тему электронных подписей (далее ЭП), надо сказать о проверке. В предыдущей стать я разбирал более сложную часть задачи — создание подписи. В этой статье всё несколько проще. Большая часть кода это адаптация примеров из КРИПТО ПРО .NET SDK. Проверять будем в первую очередь подписи по ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-2012, для этого нам и нужен КРИПТО ПРО.
Задача для нас разбивается на 3 части: отделённая подпись, подпись в PDF и подпись в MS Word.
Проверка отделённой подписи:
Комментарии все в коде, обращу только ваше внимание на получение сертификата, он нам понадобиться далее, т.к. сертификат мы будем проверять отдельно.
Ну и не забываем оборачивать всё в try-catch и прочие using. В примере я этого намеренно не делаю, что бы сократить объём
Валидация подписи в PDF. Тут нам понадобиться iTextSharp (актуальная версия на момент написания 5.5.13):
Комментировать опять же особенно нечего. Разве что надо сказать о Oid «1.2.840.113549.1.9.5» — это Oid даты подписания.
И последний в нашем списке это docx, пожалуй самый простой вариант:
Теперь будем разбирать сертификат и валидировать всю цепочку сертификатов. Поэтому сборка должна работать из под пользователя, у которого есть доступ в сеть.
И тут начинается ад, т.к. я не знаю как получить информацию о владельце сертификата через Oid, поэтому буду парсить строку. Смейтесь громче: цирк начинается.
А если серьёзно, то милости прошу в комменты тех, кто знает как сделать это через Oid-ы:
Код максимально сокращён, для лучшего понимания сути.
В общем это всё, жду комментариев по получению Oid-ов из сертификата и любой аргументированной критики.
Задача для нас разбивается на 3 части: отделённая подпись, подпись в PDF и подпись в MS Word.
Проверка отделённой подписи:
//dataFileRawBytes - массив байт подписанного файла ContentInfo contentInfo = new ContentInfo(dataFileRawBytes); SignedCms signedCms = new SignedCms(contentInfo, true); //signatureFileRawBytes - массив байт подписи signedCms.Decode(signatureFileRawBytes); if (signedCms.SignerInfos.Count == 0) { //обработка в случае отсутствия подписей } foreach (SignerInfo signerInfo in signedCms.SignerInfos) { //получаем дату подписания DateTime? signDate = (signerInfo.SignedAttributes .Cast<CryptographicAttributeObject>() .FirstOrDefault(x => x.Oid.Value == "1.2.840.113549.1.9.5") ?.Values[0] as Pkcs9SigningTime)?.SigningTime; bool valid; try { signerInfo.CheckSignature(true); valid = true; } catch (CryptographicException exc) { valid = false; } //получаем сертификат для проверки. Пригодится при проверке сертификата X509Certificate2 certificate = signerInfo.Certificate;
Комментарии все в коде, обращу только ваше внимание на получение сертификата, он нам понадобиться далее, т.к. сертификат мы будем проверять отдельно.
Ну и не забываем оборачивать всё в try-catch и прочие using. В примере я этого намеренно не делаю, что бы сократить объём
Валидация подписи в PDF. Тут нам понадобиться iTextSharp (актуальная версия на момент написания 5.5.13):
using (MemoryStream fileStream = new MemoryStream(dataFileRawBytes)) using (PdfReader pdfReader = new PdfReader(fileStream)) { AcroFields acroFields = pdfReader.AcroFields; //получаем названия контейнеров подписей List<string> signatureNames = acroFields.GetSignatureNames(); if (!signatureNames.Any()) { //обработка отсутствия ЭП } foreach (string signatureName in signatureNames) { //далее следует магия получения подписи из контейнера PdfDictionary singleSignature = acroFields.GetSignatureDictionary(signatureName); PdfString asString1 = singleSignature.GetAsString(PdfName.CONTENTS); byte[] signatureBytes = asString1.GetOriginalBytes(); RandomAccessFileOrArray safeFile = pdfReader.SafeFile; PdfArray asArray = singleSignature.GetAsArray(PdfName.BYTERANGE); using ( Stream stream = new RASInputStream( new RandomAccessSourceFactory().CreateRanged( safeFile.CreateSourceView(), asArray.AsLongArray()))) { using (MemoryStream ms = new MemoryStream((int)stream.Length)) { stream.CopyTo(ms); byte[] data = ms.GetBuffer(); ContentInfo contentInfo = new ContentInfo(data); SignedCms signedCms = new SignedCms(contentInfo, true); signedCms.Decode(signatureBytes); bool checkResult; //получили подпись и проверяем её, без проверки сертификата try { signedCms.CheckSignature(true); checkResult = true; } catch (Exception) { checkResult = false; } foreach (SignerInfo signerInfo in signedCms.SignerInfos) { //получаем дату подписания DateTime? signDate = (signerInfo.SignedAttributes .Cast<CryptographicAttributeObject>() .FirstOrDefault(x => x.Oid.Value == "1.2.840.113549.1.9.5") ?.Values[0] as Pkcs9SigningTime)?.SigningTime; //получаем сертификат X509Certificate2 certificate = signerInfo.Certificate; } } } } }
Комментировать опять же особенно нечего. Разве что надо сказать о Oid «1.2.840.113549.1.9.5» — это Oid даты подписания.
И последний в нашем списке это docx, пожалуй самый простой вариант:
using (MemoryStream fileStream = new MemoryStream(dataFileRawBytes)) using (Package filePackage = Package.Open(fileStream)) { PackageDigitalSignatureManager digitalSignatureManager = new PackageDigitalSignatureManager(filePackage); if (!digitalSignatureManager.IsSigned) { //обрабатываем ситуацию отсутствия подписей } foreach (PackageDigitalSignature signature in digitalSignatureManager.Signatures) { DateTime? signDate = signature.SigningTime; bool checkResult = signature.Verify() == VerifyResult.Success; //обратите внимание на способ получения сертификата X509Certificate2 certificate = new X509Certificate2(signature.Signer); } }
Теперь будем разбирать сертификат и валидировать всю цепочку сертификатов. Поэтому сборка должна работать из под пользователя, у которого есть доступ в сеть.
И тут начинается ад, т.к. я не знаю как получить информацию о владельце сертификата через Oid, поэтому буду парсить строку. Смейтесь громче: цирк начинается.
А если серьёзно, то милости прошу в комменты тех, кто знает как сделать это через Oid-ы:
private static void FillElectronicSignature(X509Certificate2 certificate) { foreach (KeyValuePair<string, string> item in ParseCertificatesSubject(certificate.Subject)) { switch (item.Key) { case "C": string certificatesCountryName = item.Value; break; case "S": string certificatesState = item.Value; break; case "L": string certificatesLocality = item.Value; break; case "O": string certificatesOrganizationName = item.Value; break; case "OU": string certificatesOrganizationalUnitName = item.Value; break; case "CN": string certificatesCommonName = item.Value; break; case "E": string certificatesEmail = item.Value; break; case "STREET": string certificatesStreet = item.Value; break; //тут интересный момент, если Window русскоязычный, то КРИПТО ПРО вернёт ИНН, а если англоязычный, то INN //именно тут начиналась не пойми что после deploy на тестовый стенд //локально работает, на тестовом - нет case "ИНН": case "INN": case "1.2.643.3.131.1.1": string certificatesInn = item.Value; break; //аналогично предыдущему case "ОГРН": case "OGRN": case "1.2.643.100.1": string certificatesOgrn = item.Value; break; //аналогично предыдущему case "СНИЛС": case "SNILS": case "1.2.643.100.3": string certificatesSnils = item.Value; break; case "SN": string certificatesOwnerLastName = item.Value; break; case "G": string certificatesOwnerFirstName = item.Value; break; //тут рекомендую добавить блок default и всё что не удалось определить ранее писать в лог } } DateTime certificateNotBefore = certificate.NotBefore; DateTime certificateNotAfter = certificate.NotAfter; string certificatesSerialNumber = certificate.SerialNumber; if (!certificate.Verify()) { //строим цепочку сертификатов using (X509Chain x509Chain = new X509Chain()) { x509Chain.Build(certificate); //получаем все ошибки цепочки X509ChainStatus[] statuses = x509Chain.ChainStatus; //собираем все флаги ошибок в один int, так проще хранить int certificatesErrorCode = statuses.Aggregate(X509ChainStatusFlags.NoError, (acc, chainStatus) => acc | chainStatus.Status, result => (int)result); } } } /// <summary> /// Разобрать строку с данными о владельце сертификата /// </summary> private static Dictionary<string, string> ParseCertificatesSubject(string subject) { Dictionary<string, string> result = new Dictionary<string, string>(); //количество двойных кавычек, для определения конца значения int quotationMarksCount = 0; //признак что сейчас обрабатывается "ключ или значение" bool isKey = true; //переменная для сбора ключа string key = string.Empty; //Переменная для сбора значения string value = string.Empty; for (int i = 0; i < subject.Length; i++) { char c = subject[i]; if (isKey && c == '=') { isKey = false; continue; } if (isKey) key += c; else { if (c == '"') quotationMarksCount++; bool isItemEnd = (c == ',' && subject.Length >= i + 1 && subject[i + 1] == ' '); bool isLastChar = subject.Length == i + 1; if ((isItemEnd && quotationMarksCount % 2 == 0) || isLastChar) { if (isItemEnd) i++; if (isLastChar) value += c; isKey = true; if (value.StartsWith("\"") && value.EndsWith("\"")) value = value.Substring(1, value.Length - 2); value = value.Replace("\"\"", "\""); result.Add(key, value); key = string.Empty; value = string.Empty; quotationMarksCount = 0; continue; } value += c; } } return result; }
Код максимально сокращён, для лучшего понимания сути.
В общем это всё, жду комментариев по получению Oid-ов из сертификата и любой аргументированной критики.
