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


Продолжая разговор на тему электронных подписей (далее ЭП), надо сказать о проверке. В предыдущей стать я разбирал более сложную часть задачи — создание подписи. В этой статье всё несколько проще. Большая часть кода это адаптация примеров из КРИПТО ПРО .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-ов из сертификата и любой аргументированной критики.




К сожалению, не доступен сервер mySQL