Использование сертификатов: Подпись данных на стороне клиента (исходники) – Программные продукты – Статьи

Использование сертификатов: Подпись данных на стороне клиента (исходники) - Программные продукты - Статьи Сертификаты

Disclaimer

О юридических тонкостях подписывания документов через Bouncy Castle и прочих СКЗИ я ничего не знаю и общаться не готов. Перед использованием кода в продакшене проконсультируйтесь с юристом.

А зачем это вообще надо? Хорошо написанно оригинальной статье. Повторяться не буду.

Powershell – создайте самоподписанный сертификат с root ca signer –

Сценарий. Я использую PowerShell в Windows Server 2021r2 для создания корневого сертификата и хочу использовать его для подписи вновь созданного промежуточного и веб-сертификата в динамически создаваемых (и уничтожаемых) средах разработки и тестирования. Сценарии развертываются удаленно, и цель состоит в том, чтобы по возможности сохранить их в чистом PowerShell. В Windows 10/2021 это относительно просто после генерации корневого сертификата:

$Cert = New-SelfSignedCertificate -Signer $Root -Subject "CN=$Subject"

Я сгенерировал Root-сертификат, используя COM X509Enrollment.CX509CertificateRequestCertificate и Security.Cryptography.X509Certificates.X509Certificate2 в убогом PS, который у меня был в течение некоторого времени, главным образом потому, что мне нужно было убедиться, что Предмет и Использование были установлены очень точно. Я не совсем уверен, как использовать это для подписания стандартного сертификата без вышеуказанного (который я использовал ранее).

Есть несколько примеров использования Bouncy Castle (см. Ниже) в C #, которые я мог бы привязать к PowerShell, но тогда мне нужно было бы дополнительно развернуть это в динамических средах разработки / тестирования, и я хочу иметь возможность сделать это в Powershell (через COM при необходимости) с наименьшим количеством зависимостей.

Основным решением в моем случае, избегая makecert и openssl, было использование Powershell и BouncyCastle. Я подписал репозиторий PSBouncyCastle от PSBouncyCastle от RLipscombe и выдвинул 1.8.1 Bouncy Castle. Моя раздвоенная версия – один, который я использовал для скрипта, находится на Forked: PSBouncyCastle.New.

Затем я использовал StackOverflow: C # Создание сертификатов на лету в качестве вдохновения для напишите следующий PowerShell ниже: Я добавлю это в свой GitHub и буду комментировать, и я исправлю это, как только сделаю :

Import-Module -Name PSBouncyCastle.New

function New-SelfSignedCertificate {
  [CmdletBinding()]
  param (
    [string]$SubjectName,
    [string]$FriendlyName = "New Certificate",
    [object]$Issuer,
    [bool]$IsCA = $false,
    [int]$KeyStrength = 2048,
    [int]$ValidYears = 2,
    [hashtable]$EKU = @{}
  )

  # Needed generators
  $random = New-SecureRandom
  $certificateGenerator = New-CertificateGenerator

  if($Issuer -ne $null -and $Issuer.HasPrivateKey -eq $true)
  {
    $IssuerName = $Issuer.IssuerName.Name
    $IssuerPrivateKey = $Issuer.PrivateKey
  }
  # Create and set a random certificate serial number
  $serial = New-SerialNumber -Random $random
  $certificateGenerator.SetSerialNumber($serial)

  # The signature algorithm
  $certificateGenerator.SetSignatureAlgorithm('SHA256WithRSA')

  # Basic Constraints - certificate is allowed to be used as intermediate.
  # Powershell requires either a $null or reassignment or it will return this from the function
  $certificateGenerator = Add-BasicConstraints -isCertificateAuthority $IsCA -certificateGenerator $certificateGenerator

  # Key Usage
  if($EKU.Count -gt 0) 
  {
    $certificateGenerator = $certificateGenerator | Add-ExtendedKeyUsage @EKU
  }
  # Create and set the Issuer and Subject name
  $subjectDN = New-X509Name -Name ($SubjectName)
  if($Issuer -ne $null) {
    $IssuerDN = New-X509Name -Name ($IssuerName)
  }
  else 
  {
    $IssuerDN = New-X509Name -Name ($SubjectName)
  }  
  $certificateGenerator.SetSubjectDN($subjectDN)
  $certificateGenerator.SetIssuerDN($IssuerDN)

  # Authority Key and Subject Identifier
  if($Issuer -ne $null)
  {
    $IssuerKeyPair = ConvertTo-BouncyCastleKeyPair -PrivateKey $IssuerPrivateKey
    $IssuerSerial = [Org.BouncyCastle.Math.BigInteger]$Issuer.GetSerialNumber()
    $authorityKeyIdentifier = New-AuthorityKeyIdentifier -name $Issuer.IssuerName.Name -publicKey $IssuerKeyPair.Public -serialNumber $IssuerSerial
    $certificateGenerator = Add-AuthorityKeyIdentifier -certificateGenerator $certificateGenerator -authorityKeyIdentifier $authorityKeyIdentifier
  }

  # Validity range of the certificate
  [DateTime]$notBefore = (Get-Date).AddDays(-1)
  if($ValidYears -gt 0) {
    [DateTime]$notAfter = $notBefore.AddYears($ValidYears)
  }
  $certificateGenerator.SetNotBefore($notBefore)
  $certificateGenerator.SetNotAfter($notAfter)


  # Subject public key ~and private
  $subjectKeyPair = New-KeyPair -Strength $keyStrength -Random $random
  if($IssuerPrivateKey -ne $null)
  {
    $IssuerKeyPair = [Org.BouncyCastle.Security.DotNetUtilities]::GetKeyPair($IssuerPrivateKey)
  }
  else 
  {
    $IssuerKeyPair = $subjectKeyPair
  }
  $certificateGenerator.SetPublicKey($subjectKeyPair.Public)

  # Create the Certificate
  $IssuerKeyPair = $subjectKeyPair
  $certificate = $certificateGenerator.Generate($IssuerKeyPair.Private, $random)
  # At this point you have the certificate and need to convert it and export, I return the private key for signing the next cert
  $pfxCertificate = ConvertFrom-BouncyCastleCertificate -certificate $certificate -subjectKeyPair $subjectKeyPair -friendlyName $FriendlyName
  return $pfxCertificate
}

Вот несколько примеров использования этого PowerShell:

Создайте корневой центр сертификации .

$TestRootCA = New-SelfSignedCertificate -subjectName "CN=TestRootCA" -IsCA $true
Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx

Создать стандартную собственную подпись

$TestSS = New-SelfSignedCertificate -subjectName "CN=TestLocal"
Export-Certificate -Certificate $TestSS -OutputFile "TestLocal.pfx" -X509ContentType Pfx

Создать сертификат, подписав его корневым сертификатом

$TestRootCA = New-SelfSignedCertificate -subjectName "CN=TestRootCA" -IsCA $true
$TestSigned = New-SelfSignedCertificate -subjectName "CN=TestSignedByRoot" -issuer $TestRootCA

Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx
Export-Certificate -Certificate $test -OutputFile "TestRootCA.pfx" -X509ContentType Pfx

Создайте самоподписанный файл с определенным использованием

$TestServerCert = New-SelfSignedCertificate -subjectName "CN=TestServerCert" -EKU @{ "ServerAuthentication" = $true }

Обратите внимание, что параметр -EKU принимает через splatting, он делает это, чтобы убедиться, что все, что добавлено в Add-ExtendedKeyUsage, правильно передается. Он принимает следующие виды сертификатов:

  • Цифровая подпись
  • неотказуемость
  • KeyEncipherment
  • DataEncipherment
  • KeyAgreement
  • KeyCertSign
  • CrlSign
  • EncipherOnly
  • DecipherOnly

Это соответствует моим потребностям и, похоже, работает на всех платформах Windows, которые мы используем для динамических сред.

Валидация электронных подписей на c# с использованием крипто про

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

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

Использование сертификатов: подпись данных на стороне клиента

Одна из задач, которая у вас может возникнуть – это подписать некоторые данные на стороне клиента в браузере пользователя. Такая задача может возникнуть тогда, когда пользователь подает некоторые важные данные, и для того, чтобы он в будущем не отказывался со словами “Это делал не я!” от этих данных. Для этого могут служить сертификаты. Подробнее о сертификатах и о SSL можно узнать где угодно, достаточно в поисковике набрать “SSL сертификаты” и информации об их назначении и о необходимости будет предостаточно.

Для реальных задач вам, скорее всего, понадобятся сертификаты удовлетворяющие алгоритмам ГОСТа, одна из программ реализующая их это КриптоПро, хотя даже вроде как и единственная. Но для того чтобы разрабатывать или тестировать сама программа вам не понадобится. Идея сертификатов так же в том, что пользователь и разработчик не должны вникать в то, какие алгоритмы они реализуют, какие типы ключей в них содержаться, и программа, работающая с сертификатами одного типа так же работала бы на сертификатах другого типа (это в теории, в практике может быть будут проблемы с некоторыми настройками контейнеров и вообще администрированием). Главное – для нашей задачи – это чтобы сертификат сервера и клиента соответствовали одному алгоритму.

Про сертификаты:  О тестировании -

Для тестирования мы можем воспользоваться Windows Server 2003 и его службой сертификации. Для этого необходимо при помощи мастера компонентов Windows установить службу сертификации, во время установки будут заданы некоторые вопросы, относительно имени сервера сертификации, срока его службы и остального. После установки мы будем иметь по адресу http://localhost/certsrv/ веб сайт, предоставляющий возможность запросить сертификат. А в администрировании в центре сертификации мы сможем подтверждать запросы на выдачу сертификатов. Если разрабатывать вы будете на Vista, либо Windows 7, тогда вам необходимо будет установить на Win2003 патч KB922706.

Другой способ создавать тестовые сертификаты при помощи программки makecert. Так же можно использовать и какие либо другие тестовые службы сертификации. У крипто про так же есть внешний тестовый сервер сертификации.

Первое что необходимо сделать – это установить сертификат ЦС на машину, на которой вы будете разрабатывать ваше приложение, чтобы она могла доверять этому центру выдачи и могла использовать сертификаты от этого центра.

Второе – это сгенерировать сертификат проверки сервера на эту машину. Имя сертификата должно совпадать с dns именем веб-приложения, но так как мы разрабатываем приложение локально, то для тестов мы возьмем имя localhost (хочу заметить, что если вы выдали сертификат на имя www.mydns.ru, то обратившись к mydns.ru вы увидите ошибку о том, что сертификат выдан не на это имя). Важно еще знать, что есть хранилище сертификатов пользователя, и отдельно хранилище сертификатов машины. По умолчанию, стандартно, сертификаты устанавливаются в хранилище пользователя, а для того чтобы сертификат проверки сервера использовать на сайте необходимо его положить в Local Computer/Personal. Посмотреть в каких местах какие сертификаты находятся можно следующим способом: запускаем Microsoft Management Console (вызывом mmc.exe) там нажимаем ctrl m и выбираем snap-in Certificates (сначала пользователя, затем Local Computer) и там вы видим полное дерево сертификатов.

Следующим действием нам нужно установить сертификат localhost на сайт. Открываем IIS и в зависимости от его версии настройки могут быть разные. В IIS 5.1 или 6.0 в окне свойств сайта нужно перейти на вкладку Security и там при помощи кнопки Security установить сертификат. В IIS 7 необходимо открыть свойства “Bindings…” и там добавить binding с типом https, в этом же окне необходимо будет выбрать сертификат (если сертификата в выпадающем списке нету, значит он лежит не в верной директории либо сертификат не с типом проверки подлинности сервера).

Теперь можем проверить, что наш веб-сайт откликается на https и может соединиться посредством SSL шифрования для этого набираем в браузере https://localhost/… (если не открывается – проверьте что установлен сертификат ЦС в Trusted Root Certification Authorities).

Установка сертификата на сайт дает нам доступ к сайту с шифрованием. Можно использовать при помощи настроек так же и двухстороннее шифрование, тогда каждый пользователь, заходящий на сайт по https пути должен будет указать и свой сертификат (настройка может быть “не указывать”, “желателен”, “необходим”).Для того чтобы производить подписывание на клиенте данных необходимости в установке серверного сертификата нету, но вряд ли перед вами будет стоят задача подписи без установки зашифрованного соединения.

Теперь приступим к подписыванию данных. Создаем ASP.NET веб-сайт, на страничку Default.aspx кладем следующие контролы:

<div>
<!--сюда пишем данные,которые будем подписывать-->
<asp:TextBox runat="server" ID="tbDataText" Width="400px"/>
</div>
<div>
<!--кнопка, которая выполняет функцию подписи данных-->
<asp:Button runat="server" ID="btnSignData" Text="Подписать данные"
OnClientClick="if (SignData() == false) return false;"
onclick="btnSignData_Click" />
</div>
<div>
<!--сюда записываем данные-->
<asp:TextBox runat="server" ID="tbSignedData" TextMode="MultiLine" Width="600px" Rows="24" />
</div>
<div>
<!--сюда выводим сообщения после подписи данных-->
<asp:Label runat="server" ID="lblData" />
</div>

Кнопка btnSignData сначала вызывает javascript функцию SignData() со следующим кодом:

<script language="javascript" type="text/javascript" >
function SignData()
{
// Необходимые константы
var CAPICOM_STORE_OPEN_READ_ONLY = 0;
var CAPICOM_CURRENT_USER_STORE = 2;
// проверяем, что поддерживаются ActiveXObject (Internet Explorer)
if (window.ActiveXObject) {
try {
// Подписываемые данные
var tbDataText = document.getElementById('<%= tbDataText.ClientID %>');
//Создаем необходимые объекты ActiveX
var CertStore = new ActiveXObject("CAPICOM.Store");
var Signer = new ActiveXObject("CAPICOM.Signer");
var SignedAuth = new ActiveXObject("CAPICOM.SignedData");
//Открываем хранилище сертификатов пользователя только для чтения
CertStore.Open(CAPICOM_CURRENT_USER_STORE, "MY", CAPICOM_STORE_OPEN_READ_ONLY);
//Выводим пользователю окно выбора сертификата
try {
var certificate = CertStore.Certificates.Select(
"Выберите сертификат для подписи документа."
, "Выберите один из сертификатов", false);
}
catch (e) {
// Пользователь не выбрал сертификат
returnfalse;
}
//Подписываемые данные
SignedAuth.Content = "Дата подписи: " (new Date()) ", Данные: " tbDataText.value;
//Выбранный сертификат
Signer.Certificate = certificate.Item(1);
//Сюда запишем данные (можно писать в hidden поле, тут сделано для примера)
var lblData = document.getElementById('<%= tbSignedData.ClientID %>');
// Подписываем
lblData.value = SignedAuth.Sign(Signer, false);
} catch (e) {
alert('Невозможно подписать данные. Убедитесь что браузером разрешно использование ActiveX. Добавьте сайт в Trusted Sites.');
returnfalse;
}
returntrue;
}
else {
alert('Используйте Internet Explorer для просмотра данного сайта');
returnfalse;
}
}
</script>

Данный скрипт как раз и подписывает данные на стороне клиента. Он работает только в IE, во время работы скрипта он может ругаться, требовать прав на запуск ActiveX объектов, самое простое добавить данный сайт в зону Trusted Sites.

На стороне сервера можем выполнять следующий код при нажатии на кнопку:

protectedvoid btnSignData_Click(object sender, EventArgs e)
{
StringBuilder sb = new StringBuilder();
// Смотрим, что подписано
SignedCms cms = new SignedCms();
cms.Decode(Convert.FromBase64String(tbSignedData.Text));
// Проверяем подпись
cms.CheckSignature(false);
// Что подписано
sb.AppendLine(Encoding.Unicode.GetString(cms.ContentInfo.Content));
//Сравним сертификат, которым были подписаны данные, и клиентский сертификат
if (Request.IsSecureConnection && Request.ClientCertificate.IsPresent)
{
X509Certificate2 cert = new X509Certificate2(Request.ClientCertificate.Certificate);
if (cms.SignerInfos.Count > 0 &&
string.Compare(cms.SignerInfos[0].Certificate.SerialNumber, cert.SerialNumber) == 0
&& string.Compare(cms.SignerInfos[0].Certificate.Issuer, cert.Issuer) == 0)
{
sb.AppendLine("<br/>Данные подписаны клиентским сертификатом");
}
else
{
sb.AppendLine("<br/>Данные подписаны отличным от клиентского сертификатом");
}
}
lblData.Text = sb.ToString();
}

Для выполнения данного кода может потребоваться установить reference на библиотеку System.Security, включающую namespace System.Security.Cryptography.

Про сертификаты:  Lanmax, торговая компания по адресу г.Москва ул.Киевское шоссе 22 км, 6 ст1 к А4 - телефон, адрес, отзывы, контакты

Таким способом вы сможете не просто хранить какие-либо данные, может быть важные вам, а к тому же и быть уверенным в их достоверности. Надеюсь, эта статья вам поможет в начальном изучении сертификатов.

Получение ключа с токена

image

Все известные мне УЦ выдают ключи с сертификатами на подобных токенах. На токен записан контейнер КриптоПро с приватным ключом и сертификатом. При экспорте ключа через CryptoPro CSP он экспортируется в особый «КриптоПро pfx» не совместимый ни с чем.

Просьбы выдать ключ в стандартном pfx или любом другом типовом контейнере УЦ игнорируют.Если кто знает УЦ выдающие подписи в стандартных контейнерах поделитесь координатами в комментариях. Хороших людей не стыдно попиарить.

Для преобразования контейнера КриптоПро в стандартный pfx мы как и в оригинальной статье будем использовать P12FromGostCSP. Старые взломанные версии не работают с ключами по 2021 Госту. Идем на сайт авторов и покупаем новую.

Итак мы получили стандартный pfx с ключом и сертификатом.

Обновление Bouncy Castle

Обновляем Bouncy Castle до 1.60 Старые версии могут не поддерживать алгоритмы ГОСТ 2021.

Инициализация Bouncy Castle

    static {
        BouncyCastleProvider bcProvider = new BouncyCastleProvider();
        String name = bcProvider.getName();
        Security.removeProvider(name); // remove old instance
        Security.addProvider(bcProvider);
    }

Разбор pfx

               
KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
ByteArrayInputStream baos = new ByteArrayInputStream(pfxFileContent);
keyStore.load(baos, password.toCharArray());

Enumeration<String> aliases = keyStore.aliases();

while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();
    if (keyStore.isKeyEntry(alias )) {
        Key key = keyStore.getKey(alias , keyPassword.toCharArray());
        java.security.cert.Certificate certificate = keyStore.getCertificate(alias );
        addKeyAndCertificateToStore((PrivateKey)key, (X509Certificate)certificate);
    }
}

Алиасы обязательно изменяем. Утилита P12FromGostCSP задает всегда один и тот же алиас «csp_exported» и при обработке уже второго ключа будут проблемы.

Для удобства работы ключ из pfx необходимо загрузить в стандартный Java KeyStore и дальше работать только с ним.

Загрузка KeyStore

FileInputStream is = new FileInputStream(keystorePath);
keystore = KeyStore.getInstance(KeyStore.getDefaultType());
char[] passwd = keystorePassword.toCharArray();
keystore.load(is, passwd);

Сохранение ключа с сертификатом в KeyStore


public void addKeyAndCertificateToStore(PrivateKey key, X509Certificate certificate) {
    synchronized (this) {
        keystore.setKeyEntry(alias.toLowerCase(), key, keyPassword.toCharArray(), new X509Certificate[] {certificate});

        FileOutputStream out = new FileOutputStream(keystorePath);
        keystore.store(out, keystorePassword.toCharArray());
        out.close();
   }
}

Загрузка ключей и сертификатов из KeyStore


Enumeration<String> aliases = keystore.aliases();

while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();

    if (keystore.isKeyEntry(alias)) {
        Key key = keystore.getKey(alias, keyPassword.toCharArray());
        keys.put(alias.toLowerCase(), key); //any key,value collection

        Certificate certificate = keystore.getCertificate(alias);
        if (certificate instanceof X509Certificate)
            certificates.put(alias.toLowerCase(), (X509Certificate) certificate); //any key,value collection
    }
}

Подпись файла


CMSProcessableByteArray msg = new CMSProcessableByteArray(dataToSign);
 
List certList = new ArrayList();
certList.add(cert);

Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder("GOST3411WITHECGOST3410-2021-256").setProvider("BC").build(privateKey);

gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()).build(signer, certificate));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, false);
byte[] sign = sigData.getEncoded(); //result here

Есть вариант JcaContentSignerBuilder(«GOST3411WITHECGOST3410-2021-512») для 512 битовых ключей. Мои УЦ выдают 256 битные ничего не спрашивая и игнорируют уточняющие вопросы.

Проверка подписи


byte[] data = ...; //signed file data
byte[] signature = ...;//signature
boolean checkResult = false;

CMSProcessable signedContent = new CMSProcessableByteArray(data);
CMSSignedData signedData;
try {
    signedData = new CMSSignedData(signedContent, signature);
} catch (CMSException e) {
    return SIGNATURE_STATUS.ERROR;
}

SignerInformation signer;
try {
    Store<X509CertificateHolder> certStoreInSing = signedData.getCertificates();
    signer = signedData.getSignerInfos().getSigners().iterator().next();

    Collection certCollection = certStoreInSing.getMatches(signer.getSID());
    Iterator certIt = certCollection.iterator();

    X509CertificateHolder certHolder = (X509CertificateHolder) certIt.next();
    X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certHolder);

    checkResult = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(certificate));

} catch (Exception ex) {
    return SIGNATURE_STATUS.ERROR;
}


Проверка подписи полностью аналогична проверке по Госту 2001 года. Можно ничего не менять.

Резюме

В результате всех вышеописанных действий мы получили относительно легкий способ избавиться от тяжкой ноши Крипто Про в 2021 году.

Эцп по гост на gnu/linux с помощью openssl

Понадобилось как-то раз срочно подписать важный документ и отправить его контрагенту, который доверяет только бумаге с нотариально заверенной подписью или верифицированной ЭЦП. Попробуем же сэкономить время и деньги и по максимуму избежать платных проприетарных и аппаратных решений.

Удостоверяющий Центр выдал нам ключ и сертификат в одном PKCS#12 файле auth.p12
Я исхожу из того, что у нас в наличии есть linux и docker. Можно даже не локально – вполне можно закинуть наш документ куда-нибудь на хостинг и подключиться по SSH. Подробности установки docker и работы с докер-образами оставим за пределами этой заметки. Перейдём сразу к делу:

Прямо из папки, где лежит наш документ на подпись (document.pdf) и PKCS#12 файл (auth.p12) запускаем замечательный docker образ OpenSSL с поддержкой ГОСТ и заходим в контейнер:

sudo docker run --rm -i -t -v `pwd`:`pwd` -w `pwd` rnix/openssl-gost bash

Вытаскиваем из PKCS#12 файла приватный ключ и сохраняем его в pem-формате. Может потребоваться ввести ваш пароль от PKCS#12 файла.

openssl pkcs12 -in auth.p12 -out key.pem -engine gost -nodes -clcerts

Аналогично вытаскиваем из PKCS#12 файла и сертификат.

openssl pkcs12 -in auth.p12 -clcerts -nokeys -out pub.crt

Подписываем документ. Подпись будет отсоединенная, в формате PKCS#7 в отдельном файле (document.pdf.sig)

openssl smime -sign -signer pub.crt -inkey key.pem -engine gost -binary -noattr -outform DER -in document.pdf -out document.pdf.sig

Проверить подпись можно много где, как локально так и на сайте госуслуг, например.
Всё. Можно отправлять контрагенту документ и соответствующий sig файл.

Если вдруг у вас сертификат в формате DER (в Windows часто это файл с расширением .cer), то можно конвертировать такой сертификат в pem-формат:

openssl x509 -inform DER -in pub.cer -out pub.crt

Файлы в примерах команд выше:

auth.p12 – бинарный PFX-файл, содержащий сертификат и закрытый ключ
pub.crt – сертификат (содержащий открытый ключ) в текстовом формате PEM
pub.cer – сертификат (содержащий открытый ключ) в бинарном формате DER
key.pem – закрытый ключ
document.pdf – pdf-документ, который необходимо подписать
document.pdf.sig – файл куда будет сохранена отсоединённая подпись к документу

Источники вдохновения:

Работаем с реестром запрещенных ресурсов
Не ждем, а готовимся к переходу на новые стандарты криптографической защиты информации
Docker-образы с поддержкой ГОСТ-сертификатов в openssl, curl, php, nginx
OpenSSL: простое шифрование с открытым ключом
https://stackoverflow.com/questions/52980370/how-to-convert-p12-to-crt-file
https://www.emaro-ssl.ru/blog/convert-ssl-certificate-formats/
https://qna.my-sertif.ru/q/213942
http://rodji.net/blog/2021/12/27/openssl-по-гост-подписывание-шифрование-пр/
https://polikarpoff.ru/all/eksport-ecp-v-formate-pkcs-12/

Оцените статью
Мой сертификат
Добавить комментарий