Валидация электронных подписей на C# с использованием КРИПТО ПРО

—

от автора

Продолжая разговор на тему электронных подписей (далее ЭП), надо сказать о проверке. В предыдущей стать я разбирал более сложную часть задачи — создание подписи. В этой статье всё несколько проще. Большая часть кода это адаптация примеров из КРИПТО ПРО .NET SDK. Проверять будем в первую очередь подписи по ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-2012, для этого нам и нужен КРИПТО ПРО.

Задача для нас разбивается на 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-ов из сертификата и любой аргументированной критики.


ссылка на оригинал статьи https://habr.com/post/426645/


Комментарии

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

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