Подписание PDF на JS и вставка подписи на C#, используя Крипто ПРО / Хабр

Подписание PDF на JS и вставка подписи на C#, используя Крипто ПРО / Хабр Электронная цифровая подпись

Itweek

Некоторое время назад в группе «ECM Forum Rus» на фейсбуке произошла

дискуссия

по вопросам применения электронной подписи в информационных системах органов власти (обсуждение  было не по теме топика, можно посмотреть в нижних комментариях). Суть спора — из-за OID’ов (object identifier – идентификатор объекта) информационных систем, которые необходимо прописывать в квалифицированных сертификатах электронной подписи (ЭП) должностных лиц, эти самые ЭП приходится менять даже чаще чем раз в год (что диктуется требованиями безопасности), а это, в свою очередь, ведет к дополнительным сложностям и издержкам, так как большинство органов работают с коммерческими УЦ, не имея собственных. Проблема усугубляется отсутствием  общего понимания, что именно эти OID дают и насколько они необходимы и/или обязательны.

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

Если начинать с базовых понятий, электронное подписание основывается на асимметричных алгоритмах шифрования. Основная особенность этих алгоритмов в том, что для шифрования и расшифровки сообщения используются два разных ключа. Широкой общественности более знакомы  симметричные алгоритмы, когда одним ключом (или паролем) мы и шифруем и расшифровываем сообщение, например архивируем файл с паролем или защищаем документ MS Word.

На асимметричных алгоритмах шифрования основаны многие вещи, хотя сам по себе тот факт, что для шифрования и расшифровки используются разные ключи, ещё не позволил бы найти сколько-нибудь полезного применения этим алгоритмам. Для этого они должны обладать ещё некоторыми дополнительными свойствами. Во-первых, ключи не должны быть вычисляемыми, то есть зная один ключ вы не можете вычислить второй. Также очень важно, чтобы разным ключам шифрования соответствовали разные ключи расшифровки и наоборот — одному ключу расшифровки соответствовал только один ключ шифрования.

При чем тут собственно подпись? Ведь нам надо подписать документ, а не зашифровать его. Для начала надо разобраться, что такое собственно подпись и для чего она нужна. Когда вы ставите свою собственноручную подпись на бумажный документ, вы тем самым заверяете, что именно вы (а не кто-то другой) видели (и согласны) именно этот документ (а не какой-то другой). Важнейшее свойство подписи  — неотрекаемость (non-repudiation). Это означает, что подписав документ, вы не можете позже отказаться от этого факта. В случае бумажной подписи вас уличит графологическая экспертиза, в случае электронной — математические методы, основанные на асимметричных алгоритмах шифрования.

Как все это работает, в двух словах. Берем асимметричный алгоритм шифрования, генерируем пару ключей (для шифрования и расшифровки). Ключ шифрования даем человеку, который будет подписывать документы. Он его должен всегда держать при себе и никому не давать. Поэтому его называют «закрытый» ключ. Другой ключ (расшифровки) даем всем желающим, поэтому он «открытый». Подписывая документ, человек должен зашифровать его своим закрытым ключом. На самом деле шифруется не сам документ, так как он может быть достаточно большим, а нам вообще-то и не нужно его шифровать. Поэтому по документу получают хэш — это некая числовая последовательность с большой долей вероятности разная для разных документов, как бы «отпечаток» документа. Его и шифруют закрытым ключом подписанта. Этот зашифрованный хэш и есть электронная подпись документа. Теперь имея документ, подпись и открытый ключ, любой может легко проверить, что именно этот документ был подписан именно этим закрытым ключом. Для этого снова получаем хэш документа, расшифровываем открытым ключом подпись и сравниваем. Должны получить две идентичные числовые последовательности.

Все это прекрасно, но пока мы получили неотрекаемость подписи для закрытого ключа, то есть доказали, что документ подписан конкретным ключом. Нам же необходимо доказать, что он был подписан конкретным человеком. Для этого существуют удостоверяющие центры и цифровые сертификаты. Самое важное, что делает удостоверяющий центр — он удостоверяет, что закрытый ключ принадлежит конкретному лицу. Чтобы это гарантировать, именно удостоверяющий центр генерирует пары ключей и выдает их лично в руки владельцам (есть варианты по доверенности, удаленно, но это уже детали). Вместе с ключами генерируется цифровой сертификат — это электронный документ (бывает и бумажное его представление), в котором содержится информация о владельце ключа, самом ключе, удостоверяющем центре и некоторая другая информация. Владелец, как правило, получает все это добро на защищенном носителе (смарт-карте, ру-токене и так далее), на котором содержится закрытый ключ и сертификат с внедренным открытым ключом. Сам носитель необходимо всегда держать при себе, а скопированный с него сертификат с открытым ключом можно давать всем желающим, чтобы они могли проверить вашу электронную подпись.

Итак, подписание производится закрытым ключом, а проверка подписи открытым. Поэтому фраза «документ подписывается набором OID» (прозвучавшая в упомянутом споре) лишена всякого смысла. В процедуре подписания и проверки участвуют только два ключа, в 63-ФЗ они и названы соответственно — ключ подписи и ключ проверки подписи.

А что такое эти пресловутые OID? Формат цифрового сертификата X.509 позволяет сохранять в нем расширения (extensions). Это некие необязательные атрибуты, при помощи которых можно хранить дополнительную информацию. Каждый такой атрибут является объектом, который задается идентификатором из иерархического справочника. Отсюда OID — Object Identifier. Углубляться в природу самих OID здесь смысла нет. По сути это некоторая дополнительная информация, которая может присутствовать в сертификате.

Данные дополнительные атрибуты могут использоваться для разных целей. Они могут либо предоставлять дополнительную информацию о владельце, ключах, УЦ, либо нести какую-то дополнительную информацию для приложений и сервисов, которые этот сертификат используют. Самое распространенное применение — это управление доступами на основе ролей. Например, в сертификате можно прописать, что владелец ключа является руководителем организации, и это даст ему возможность сразу во всех ИС получить доступ к нужным функциям и сведениям, без необходимости связываться с администраторами каждой ИС и менять настройки доступа. Все это конечно при условии, что все эти ИС используют сертификат пользователя для его авторизации и анализируют один и тот-же атрибут одинаковым образом (для того-то атрибуты и выбираются из справочника, а не задаются произвольно).

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

В мире эти два вида сертификатов называются соответственно Public Key Certificate (PKC) и Attribute Certificate (или Authorization Certificate – AC). Сертификат второго рода может выпускаться гораздо чаще первого, другой организацией и должен быть доступнее и проще в получении, чем персональный сертификат «открытого ключа». Во всяком случае, так рекомендует RFC 3281, посвященный этому виду сертификатов. Сертификат второго рода должен содержать лишь ссылку на сертификат открытого ключа, чтобы система, использующая его для авторизации пользователя, могла сначала идентифицировать персону при помощи PKC.

Теперь перенесемся ближе к нашим реалиям. На законодательном уровне вопросы, связанные с применением электронной подписи в Российской Федерации, регулируются двумя основными документами — законом РФ от 06.04.2020 №63-ФЗ «Об электронной подписи» и приказом ФСБ РФ от 27.12.2020 №795 «Об  утверждении требований к форме квалифицированного сертификата ключа проверки электронной подписи». Состав квалифицированного сертификата описан в 795-м приказе (ч. II «Требования к совокупности полей квалифицированного сертификата») и в нем нет требований к атрибутам, управляющим авторизацией в каких-либо информационных системах. В качестве дополнительных обязательных атрибутов указаны лишь сведения, позволяющие идентифицировать физическое или юридическое лицо в РФ (ИНН, СНИЛС и т. д.). Хотя ни закон ни приказ ФСБ не запрещают включать в квалифицированный сертификат другие сведения.

Читайте также:  Мобильная электронная подпись — сферы применения и настройка

Как видим, никакие законодательные нормы не диктуют обязательного наличия в квалифицированном сертификате атрибутов, связанных с авторизацией в каких-либо информационных системах. Откуда тогда эти требования берутся? А исходят они от разработчиков (или «владельцев») конкретных систем. Возьмём например «Методические рекомендации по использованию электронной подписи при межведомственном электронном взаимодействии (версия 4.3)», размещенные на технологическом портале СМЭВ. Действительно, в пункте 6 данного документа читаем: «При подготовке сведений для формирования сертификата ЭП-СП необходимо определить необходимость запроса сведений из Росреестра (выписки из ЕГРП). При необходимости такого запроса в поле “Улучшенный ключ” (OID=2.5.29.37) в сертификате ЭП-СП должен быть указан OID по требованиям Росреестра.». То есть информационная система Росреестра использует этот атрибут для определения сведений, которые можно выдавать владельцу сертификата. Однако этот же документ содержит важное примечание, а именно — данное требование действует до полного запуска ЕСИА (единый сервис авторизации в гос. системах) и подключения к ней системы Росреестра. Это важное замечание, запомним его.

Не буду разбираться с другими ИС, применяемыми в гос. органах. Подозреваю, что там ситуация похожая. Портал госзакупок, электронные торговые площадки, различные бухгалтерские и финансовые приложения также могут требовать наличия тех или иных дополнительных OID в сертификате пользователя. При этом утверждение, что прописывая OID информационной системы в сертификате, я каким-либо образом делегирую ответственность удостоверяющему центру, мягко говоря, неверно. УЦ вносит эти данные в сертификат согласно моей заявки. Если у меня изменилась должность, а я забыл подать заявку на отзыв старого и выпуск нового сертификата, УЦ никак не может отвечать за мою забывчивость. К тому-же закон 63-ФЗ прямо закрепляет ответственность за неправильное использование сертификата за его владельцем. В пункте 6 статьи 17 читаем:
Владелец квалифицированного сертификата обязан:
1) не использовать ключ электронной подписи и немедленно обратиться в аккредитованный удостоверяющий центр, выдавший квалифицированный сертификат, для прекращения действия этого сертификата при наличии оснований полагать, что конфиденциальность ключа электронной подписи нарушена;
2) использовать квалифицированную электронную подпись в соответствии с ограничениями, содержащимися в квалифицированном сертификате (если такие ограничения установлены).

Необходимость хранить в сертификате сведения о ролях и доступах пользователя в конкретных информационных системах приводит к проблеме, из-за которой разгорелся спор в фейсбуке, а именно — сертификат приходится перевыпускать гораздо чаще, чем это диктуют требования безопасности к персональной электронной подписи. Поменялась должность — перевыпускаем сертификат. Появилась новая ИС — перевыпускаем сертификат. Появилась необходимость запроса сведений из ИС новой организации (Росреестр) — перевыпускаем сертификат.

Налицо стопроцентное попадание в концепцию, именуемую в мире  Attribute Certificate (или Authorization Certificate), о которой говорилось выше и при которой рекомендуется выпускать эти сертификаты другим удостоверяющим центром (Attribute Autority, в отличие от Certificate Authority — обычного УЦ, выпускающего квалифицированные сертификаты ЭП) и по упрощенной схеме. Этот сертификат сам по себе не должен содержать ключа электронной подписи и информации о владельце. Вместо этого он содержит ссылку на сертификат открытого ключа владельца, из которого можно получить остальную необходимую информацию о персоне.

Необходимо заметить, что и эта схема имеет весьма ограниченное применение и не решает всех проблем. Что если очередная информационная система решит использовать то-же поле сертификата “Улучшенный ключ” (OID=2.5.29.37), которое уже занято значением Росреестра, для своих нужд? Вписать два разных значения в одно поле не получится. Следовательно придется выпускать ещё один AC! Другая проблема связана с коротким временем жизни PKC (один год). Если имеем несколько AC (в которых содержится ссылка на персональный сертификат), их все придется перевыпустить по истечении срока PKC. Для эффективного применения AC необходим некий единый центр авторизации пользователей во всех информационных системах, а все приложения должны согласованно и единообразно использовать атрибуты сертификатов.

Такой единый центр авторизации для гос. органов уже есть — это ЕСИА. Вспомним про примечание, касающееся OID’ов Росреестра. В будущем они будут заменены информацией из ЕСИА. Так же должны поступать и прочие информационные системы, в которых работают гос. служащие. Вместо использования AC для авторизации, необходимо интегрироваться с ЕСИА и получать необходимую информацию оттуда. ЕСИА должна иметь возможность привязки квалифицированного сертификата ЭП к учетной записи, таким образом информационные системы смогут проводить аутентификацию пользователя по персональному ключу, а его авторизацию (предоставление доступа к приложению) через ЕСИА. Такая система представляется универсальней и надежней, чем применение полей сертификатов, а в перспективе позволит автоматизировать управление доступами. Если будет создана единая система кадрового учета гос. служащих, ЕСИА сможет брать информацию о полномочиях того или иного лица непосредственно оттуда. Перевелся человек на другую должность — автоматически потерял доступ к одним системам и получил к другим. При этом он продолжает пользоваться своим ключом ЭП для подписания документов, ничего перевыпускать не нужно.

Вывод — OID’ы информационных систем в сертификате, не являясь абсолютным злом сами по себе, требуют взвешенного применения. У данного подхода есть альтернативы, которые стоит рассмотреть, например единые сервисы авторизации.

Ответы на вопросы по электронным услугам росреестра

Электронные услуги Росреестра пользуются все большей популярностью. Сегодня почти все ведомства предоставляют государственные услуги в электронном виде. Электронные услуги Росреестра доступны всем заявителям на официальном сайте Росреестра rosreestr.ru и портале государственных услуг Российской Федерации. 

Одна из главных и приоритетных задач Управления – это максимально упростить предоставление услуг для граждан, юридических лиц, органов государственной власти и органов местного самоуправления. 

В связи с этим, специалисты подготовили ответы на самые распространенные вопросы заявителей.

Что нужно, для того, чтобы иметь возможность пользоваться услугами Росреестра в электронном виде?

Ответ: Чтобы воспользоваться сервисом, необходимо авторизоваться, то есть иметь логин и пароль на сайте www.gosuslugi.ru (используется Единая система идентификации и аутентификации для получения доступа к государственным услугам в электронном виде).

В настоящее время в личном кабинете можно подать в электронном виде заявление на получение всех наиболее востребованных услуг Росреестра: это регистрация прав, кадастровый учет, единая процедура (одновременное проведение этих процедур), получение сведений из ЕГРН. Для осуществления юридически значимых действий потребуется электронная подпись.

Какое дополнительное программное обеспечение необходимо установить на компьютере для получения государственных услуг, оказываемых Росреестром в электронном виде на официальном сайте Росреестра с применением усиленной квалифицированной электронной подписи?

Ответ: Для получения государственных услуг, оказываемых Росреестром в электронном виде, на официальном сайте Росреестра (rosreestr.ru) с возможностью подписания форм запросов усиленной квалифицированной электронной подписью (далее – ЭП, на компьютере необходимо:

– установить криптопровайдер, при помощи которого в удостоверяющем центре был изготовлен квалифицированный сертификат ключа проверки электронной подписи (например «Крипто-Про CSP»);

– установить квалифицированный сертификат ключа проверки ЭП удостоверяющего центра, выдавшего ЭП пользователю;

– установить квалифицированный сертификат ключа проверки ЭП пользователя;

– установить свободно распространяемый компонент Microsoft CAPICOM;

– зарегистрировать библиотеку CAPICOM.dll;

– в свойствах интернета добавить в надежные узлы адрес: https://*.rosreestr.ru.

Следует иметь ввиду, что программный продукт Microsoft CAPICOM имеет ряд требований к системе, с которыми можно ознакомиться на сайте производителя.

Если после выполнения вышеуказанных действий будет появляться ошибка, связанная с CAPICOM, рекомендуем обратиться в службу технической поддержки производителя программного продукта.

Квалифицированный сертификат ключа проверки электронной подписи с объектным идентификатором 1.2.643.5.1.24.2.30 выпускается на юридическое или физическое лицо?

Объектному идентификатору 1.2.643.5.1.24.2.30 (по распоряжению Росреестра от 20.05.2020 № Р/0083) соответствует категория заявителя «Юридическое лицо», исходя из чего изготавливается квалифицированный сертификат ключа проверки электронной подписи юридического лица. В качестве владельца такого сертификата, наряду с указанием наименования юридического лица, указывается физическое лицо, действующее от имени юридического лица на основании учредительных документов юридического лица или доверенности.

Запросы Администрации муниципального района обрабатываются с ошибкой. Что не так?

Ответ:  Квалифицированный сертификат ключа проверки электронной подписи, содержащийся в электронной подписи (далее – сертификат), которой подписаны запросы, содержит объектный идентификатор (далее – ОИД) 1.2.643.5.1.24.2.1.3. Данному ОИДу соответствует субъект/получатель – Правообладатель – физическое лицо. 

Читайте также:  Регистрация в личном кабинете налогоплательщика на сайте налоговой

Судя по тому, что сертификат выдан на Администрацию муниципального района (представитель Иванов Иван Иванович), прописанный в сертификате ОИД не соответствует полномочиям владельца сертификата. По этой причине и возникли проблемы при создании запросов.

В случае, если Администрации муниципального района необходимо формировать запросы о предоставлении сведений из ЕГРН в качестве лица, имеющего право на безвозмездное получение сведений, то ОИД необходимо выбрать соответствующий правовому статусу Администрации муниципального района  (устав, положение).

Исходя из вышесказанного предлагаем Администрации муниципального района обратиться в удостоверяющий центр, выдавший усиленную квалифицированную электронную подпись, для перевыпуска сертификата с соответствующим ОИД. 

Каким образом подать заявление на регистрацию ограничения права и обременения через личный кабинет  Росреестра, если у здания имеются 2 собственника. Подавать 2 заявления от двух собственников с разных личных кабинетов или одно заявление с одного личного кабинета?

Ответ: Посредством Личного кабинета официального сайта Росреестра можно формировать несколько заявлений. В вашем случае необходимо сформировать два заявления на регистрацию ограничения права и обременения под учетной записью одного из собственников.

Главный специалист-эксперт отдела эксплуатации информационных систем, 

технических средств и каналов связи С.Б. Ханхаева

Подписание pdf на js и вставка подписи на c#, используя крипто про

Итак. Пришла задача. Используя браузер предложить пользователю подписать PDF электронной подписью (далее ЭП). У пользователя должен быть токен, содержащий сертификат, открытый и закрытый ключ. Далее на сервере надо вставить подпись в PDF документ. После этого надо проверить подпись на валидность. В качестве back-end используем ASP.NET и соответственно C#.

Вся соль в том, что надо использовать подпись в формате CAdES-X Long Type 1, и российские ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2021 и т.п. Кроме того подписей может быть более одной, то есть пользователи могут по очереди подписывать файл. При этом предыдущие подписи должны оставаться валидными.

В процессе решения задачу решили усложнить для нас, и уменьшить объем передаваемых данных на клиент. Передавать только hash документа, но не сам документ.

В исходниках буду опускать малозначимые для темы моменты, оставлю только то что касается криптографии. Код на JS приведу только для нормальных браузеров, JS-движки которых поддерживают Promise и function generator. Думаю кому нужно для IE напишут сами (мне пришлось «через не хочу»).

Что нужно:

  1. Пользователь должен получить пару ключей и сертификат.
  2. Пользователь должен установить plug-in от Крипто ПРО. Без этого средствами JS мы не сможем работать с криптопровайдером.

Замечания:

  1. Для тестов у меня был сертификат выданный тестовым ЦС Крипто ПРО и нормальный токен, полученный одним из наших сотрудников (на момент написания статьи ~1500р с годовой лицензией на Крипто ПРО и двумя сертификатами: но «новому» и «старому» ГОСТ)
  2. Говорят, plug-in умеет работать и с ViPNet, но я не проверял.

Теперь будем считать что у нас на сервере есть готовый для подписывания PDF.

Добавляем на страницу скрипт от Крипто ПРО:

<script src="/Scripts/cadesplugin_api.js" type="text/javascript"></script>

Дальше нам надо дождаться пока будет сформирован объект cadesplugin

window.cadespluginLoaded = false;
cadesplugin.then(function () {
    window.cadespluginLoaded = true;
});

Запрашиваем у сервера hash. Предварительно для этого нам ещё надо знать каким сертификатом, а значит и алгоритмом пользователь будет подписывать. Маленькая ремарка: все функции и «переменные» для работы с криптографией на стороне клиента я объединил в объект CryptographyObject.

Метод заполнения поля certificates объекта CryptographyObject:

    fillCertificates: function (failCallback) {

            cadesplugin.async_spawn(function*() {
                try {
                    let oStore = yield cadesplugin.CreateObjectAsync("CAPICOM.Store");

                    oStore.Open(cadesplugin.CAPICOM_CURRENT_USER_STORE,
                        cadesplugin.CAPICOM_MY_STORE,
                        cadesplugin.CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED);

                    let certs = yield oStore.Certificates;
                    certs = yield certs.Find(cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_VALID);
                    let certsCount = yield certs.Count;
                    for (let i = 1; i <= certsCount; i  ) {
                        let cert = yield certs.Item(i);
                        CryptographyObject.certificates.push(cert);
                    }
                    oStore.Close();
                } catch (exc) {
                     failCallback(exc);
                }
            });
    }

Комментарий: пробуем открыть хранилище сертификатов. В этот момент система пользователя выдаст предупреждение, что сайт пытается что-то сделать с сертификатами, криптографией и прочей магической непонятной ерундой. Пользователю тут надо будет нажать кнопку «Да»

Далее получаем сертификаты, валидные по времени (не просроченные) и складываем их в массив certificates. Это надо сделать из-за асинхронной природы cadesplugin (для IE всё иначе 😉 ).

Метод получения hash:

getHash: function (certIndex, successCallback, failCallback, какие-то ещё параметры) {
        try {
            cadesplugin.async_spawn(function*() {
                let cert = CryptographyObject.certificates[certIndex];
                let certPublicKey = yield cert.PublicKey();
                let certAlgorithm = yield certPublicKey.Algorithm;
                let algorithmValue = yield certAlgorithm.Value;
                let hashAlgorithm;
                //определяем алгоритм подписания по данным из сертификата и получаем алгоритм хеширования
                    if (algorithmValue === "1.2.643.7.1.1.1.1") {
                        hashAlgorithm = "2021256";
                    } else if (algorithmValue === "1.2.643.7.1.1.1.2") {
                        hashAlgorithm = "2021512";
                    } else if (algorithmValue === "1.2.643.2.2.19") {
                        hashAlgorithm = "3411";
                    } else {
                        failCallback("Реализуемый алгоритм не подходит для подписания документа.");
                        return;
                    }

                $.ajax({
                    url: "/Services/SignService.asmx/GetHash",
                    method: "POST",
                    contentType: "application/json; charset=utf-8 ",
                    dataType: "json",
                    data: JSON.stringify({
                        //какие-то данные для определения документа
                        //не забудем проверить на сервере имеет ли пользователь нужные права
                        hashAlgorithm: hashAlgorithm,
                    }),
                    complete: function (response) {
                        //получаем ответ от сервера, подписываем и отправляем подпись на сервер
                        if (response.status === 200) {
                            CryptographyObject.signHash(response.responseJSON,
                                function(data) {
                                    $.ajax({
                                        url: CryptographyObject.signServiceUrl,
                                        method: "POST",
                                        contentType: "application/json; charset=utf-8",
                                        dataType: "json",
                                        data: JSON.stringify({
                                            Signature: data.Signature,
                                            //какие-то данные для определения файла
                                            //не забудем про серверную валидацию и авторизацию
                                        }),
                                        complete: function(response) {
                                            if (response.status === 200)
                                                successCallback();
                                            else
                                                failCallback();
                                        }
                                    });
                                },
                                certIndex);
                        } else {
                            failCallback();
                        }
                    }
                });
            });
        } catch (exc) {
            failCallback(exc);
        }
    }

Комментарий: обратите внимание на cadesplugin.async_spawn, в нее передаётся функция-генератор, на которой последовательно вызывается next(), что приводит к переходу к yield.

Таким образом получается некий аналог async-await из C#. Всё выглядит синхронно, но работает асинхронно.

Теперь что происходит на сервере, когда у него запросили hash.

Во-первых необходимо установить nuget-пакет iTextSharp (на момент написания стать актуальная версия 5.5.13)

Во-вторых нужен CryptoPro.Sharpei, он идёт в нагрузку к Крипто ПРО .NET SDK

Теперь можно получать hash

                //определим hash-алгоритм
                HashAlgorithm hashAlgorithm;

                switch (hashAlgorithmName)
                {
                    case "3411":
                        hashAlgorithm = new Gost3411CryptoServiceProvider();
                        break;
                    case "2021256":
                        hashAlgorithm = new Gost3411_2021_256CryptoServiceProvider();
                        break;
                    case "2021512":
                        hashAlgorithm = new Gost3411_2021_512CryptoServiceProvider();
                        break;
                    default:
                        GetLogger().AddError("Неизвестный алгоритм хеширования", $"hashAlgorithmName: {hashAlgorithmName}");
                        return HttpStatusCode.BadRequest;
                }
                //получим hash в строковом представлении, понятном cadesplugin
                string hash;
                using (hashAlgorithm)
                //downloadResponse.RawBytes - просто массив байт исходного PDF файла
                using (PdfReader reader = new PdfReader(downloadResponse.RawBytes))
                {
                    //ищем уже существующие подписи
                    int existingSignaturesNumber = reader.AcroFields.GetSignatureNames().Count;
                    using (MemoryStream stream = new MemoryStream())
                    {
                        //добавляем пустой контейнер для новой подписи
                        using (PdfStamper st = PdfStamper.CreateSignature(reader, stream, '', null, true))
                        {
                            PdfSignatureAppearance appearance = st.SignatureAppearance;
                            //координаты надо менять в зависимости от существующего количества подписей, чтоб они не наложились друг на друга
                            appearance.SetVisibleSignature(new Rectangle(36, 100, 164, 150), reader.NumberOfPages,
                                //задаём имя поля, оно потом понадобиться для вставки подписи
                                $"{SignatureFieldNamePrefix}{existingSignaturesNumber   1}");
                            //сообщаем, что подпись придёт извне
                            ExternalBlankSignatureContainer external =
                                new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                            //третий параметр - сколько места в байтах мы выделяем под подпись
                            //я выделяю много, т.к. CAdES-X Long Type 1 содержит все сертификаты по цепочке до самого корневого центра
                            MakeSignature.SignExternalContainer(appearance, external, 65536);
                            //получаем поток, который содержит последовательность, которую мы хотим подписывать
                            using (Stream contentStream = appearance.GetRangeStream())
                            {
                                //вычисляем hash и переводим его в строку, понятную cadesplugin
                                hash = string.Join(string.Empty,
                                    hashAlgorithm.ComputeHash(contentStream).Select(x => x.ToString("X2")));
                            }
                        }
                        //сохраняем stream куда хотим, он нам пригодиться, что бы вставить туда подпись
                    }
                }

На клиенте, получив hash от сервера подписываем его

    //certIndex - индекс в массиве сертификатов. На основании именно этого сертификата мы получали алгоритм и формировали hash на сервере
    signHash: function (data, callback, certIndex, failCallback) {
        try {
            cadesplugin.async_spawn(function*() {
                certIndex = certIndex | 0;

                let oSigner = yield cadesplugin.CreateObjectAsync("CAdESCOM.CPSigner");

                let cert = CryptographyObject.certificates[certIndex];

                oSigner.propset_Certificate(cert);
                oSigner.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN);
                //тут надо указать нормальный адрес TSP сервера. Это тестовый от Крипто ПРО
                oSigner.propset_TSAAddress("https://www.cryptopro.ru/tsp/");

                let hashObject = yield cadesplugin.CreateObjectAsync("CAdESCOM.HashedData");

                let certPublicKey = yield cert.PublicKey();
                let certAlgorithm = yield certPublicKey.Algorithm;
                let algorithmValue = yield certAlgorithm.Value;

                if (algorithmValue === "1.2.643.7.1.1.1.1")  {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_256);
                    oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2021);
                } else if (algorithmValue === "1.2.643.7.1.1.1.2") {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_512);
                    oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2021);
                } else if (algorithmValue === "1.2.643.2.2.19") {
                    yield hashObject.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411);
                    oSigner.propset_TSAAddress(CryptographyObject.tsaAddress2001);
                } else {
                    alert("Невозможно подписать документ этим сертификатом");
                    return;
                }
                //в объект описания hash вставляем уже готовый hash с сервера
                yield hashObject.SetHashValue(data.Hash);

                let oSignedData = yield cadesplugin.CreateObjectAsync("CAdESCOM.CadesSignedData");
                oSignedData.propset_ContentEncoding(cadesplugin.CADESCOM_BASE64_TO_BINARY);
                //результат подписания в base64
                let signatureHex =
                    yield oSignedData.SignHash(hashObject, oSigner, cadesplugin.CADESCOM_CADES_X_LONG_TYPE_1);

                data.Signature = signatureHex;
                callback(data);
            });
        } catch (exc) {
            failCallback(exc);
        }
    }

Комментарий: полученную подпись отправляем на сервер (см. выше)

Ну и наконец вставляем подпись в документ на стороне сервера


//всякие нужные проверки
                //downloadResponse.RawBytes - ранее созданный PDF с пустым контейнером для подписи
                using (PdfReader reader = new PdfReader(downloadResponse.RawBytes))
                {
                    using (MemoryStream stream = new MemoryStream())
                    {
                        //requestData.Signature - собственно подпись от клиента
                        IExternalSignatureContainer external = new SimpleExternalSignatureContainer(Convert.FromBase64String(requestData.Signature));
                    //lastSignatureName - имя контейнера, которое мы определили при формировании hash
                        MakeSignature.SignDeferred(reader, lastSignatureName, stream, external);
                        
                    //сохраняем подписанный файл
                    }
                }

Комментарий: SimpleExternalSignatureContainer — это простейший класс, реализующий интерфейс IExternalSignatureContainer

        /// <summary>
        /// Простая реализация контейнера внешней подписи
        /// </summary>
        private class SimpleExternalSignatureContainer : IExternalSignatureContainer
        {
            private readonly byte[] _signedBytes;

            public SimpleExternalSignatureContainer(byte[] signedBytes)
            {
                _signedBytes = signedBytes;
            }

            public byte[] Sign(Stream data)
            {
                return _signedBytes;
            }

            public void ModifySigningDictionary(PdfDictionary signDic)
            {

            }
        }

Собственно с подписанием PDF на этом всё. Проверка будет описана в продолжении статьи. Надеюсь, она будет…

Читайте также:  Фильм Крипто (2019) смотреть онлайн бесплатно в хорошем HD 1080 / 720 качестве

Внёс исправления из комментария о получении Oid алгоритма подписи. Спасибо

Список методов и свойств обьекта cadesplugin

Название метода или свойстваОписание
CreateObject(objname)Cоздает и возвращает обьект типа objname. Доступен в браузерах с поддержкой NPAPI и Internet Explorer.
CreateObjectAsync(objname)Cоздает обьект типа objname и возвращает Promise на созданный обьект. Доступен в браузерах Chrome(Chromium), Opera и Яндекс.Браузер.
async_spawn(function*(){})Функция принимающая на вход функцию генератор и позволяющая “синхронизировать” асинхронную работу с Promise при использовании совместно с ключевым словом yield.
set_log_level(log_level)Устанавливает уровень ведения логов в log_level. Аргумент может принимать значения cadesplugin.LOG_LEVEL_DEBUG (отладочные, информационные сообщения и ошибки), cadesplugin.LOG_LEVEL_INFO(информационные сообщения и ошибки), cadesplugin.LOG_LEVEL_ERROR(только ошибки).
getLastError(exception)Возвращает строку с описанием ошибки из исключения, порождённого плагином. Для Firefox данный метод является единственным способом получения кода ошибки и её текстового описания от плагина.
JSModuleVersionВозвращает версию JavaScript-модуля.
ReleasePluginObjects()Удаляет объекты, созданные плагином. В случае успеха возвращает true.

Для удобства в объект cadesplugin добавлены свойства, содержащие значения констант, используемых в различных вызовах плагина.

Название свойстваОписание
CADESCOM_STRING_TO_UCS2LE = 0x00Данные будут перекодированы в UCS-2 little endian.
CADESCOM_BASE64_TO_BINARY = 0x01Данные будут перекодированы из Base64 в бинарный массив.
CAPICOM_LOCAL_MACHINE_STORE = 1Локальное хранилище компьютера.
CAPICOM_CURRENT_USER_STORE = 2Хранилище текущего пользователя.
CADESCOM_LOCAL_MACHINE_STORE = 1Локальное хранилище компьютера.
CADESCOM_CURRENT_USER_STORE = 2Хранилище текущего пользователя.
CADESCOM_CONTAINER_STORE = 100Хранилище сертификатов в контейнерах закрытых ключей. В данный Store попадут все сертификаты
из контейнеров закрытых ключей которые доступны в системе в момент открытия.
CAPICOM_MY_STORE = “My”Хранилище персональных сертификатов пользователя.
CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2Открывает хранилище на чтение/запись, если пользователь имеет права на чтение/запись. Если прав на запись нет, то хранилище открывается за чтение.
CADESCOM_XML_SIGNATURE_TYPE_ENVELOPED = 0Вложенная подпись.
CADESCOM_XML_SIGNATURE_TYPE_ENVELOPING = 1Оборачивающая подпись.
CADESCOM_XML_SIGNATURE_TYPE_TEMPLATE = 2Подпись по шаблону.
XmlDsigGost3410UrlObsolete = “http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411”Алгоритм подписи для XmlDsig, ГОСТ 2001.
XmlDsigGost3411UrlObsolete = “http://www.w3.org/2001/04/xmldsig-more#gostr3411”Алгоритм хеширования для XmlDsig, ГОСТ 2001.
XmlDsigGost3410Url = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411”Алгоритм подписи для XmlDsig, ГОСТ 2001.
XmlDsigGost3411Url = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411”Алгоритм хеширования для XmlDsig, ГОСТ 2001.
XmlDsigGost3410Url2021256 = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102021-gostr34112021-256”Алгоритм подписи для XmlDsig, ГОСТ 2021 (256).
XmlDsigGost3411Url2021256 = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112021-256”Алгоритм хеширования для XmlDsig, ГОСТ 2021 (256).
XmlDsigGost3410Url2021512 = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102021-gostr34112021-512”Алгоритм подписи для XmlDsig, ГОСТ 2021 (512).
XmlDsigGost3411Url2021512 = “urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112021-512”Алгоритм хеширования для XmlDsig, ГОСТ 2021 (512).
CADESCOM_CADES_DEFAULT = 0Тип подписи по умолчанию (CAdES-X Long Type 1).
CADESCOM_CADES_BES = 1Тип подписи CAdES-BES.
CADESCOM_CADES_T = 0x5Тип подписи CAdES-T.
CADESCOM_CADES_X_LONG_TYPE_1 = 0x5dТип подписи CAdES-X Long Type 1.
CADESCOM_PKCS7_TYPE = 0xffffТип подписи PKCS7.
CADESCOM_ENCODE_BASE64 = 0Кодировка BASE64.
CADESCOM_ENCODE_BINARY = 1Бинарные данные.
CADESCOM_XADES_DEFAULT = 0x00000010Тип подписи по умолчанию (XAdES-X Long Type 1).
CADESCOM_XADES_BES = 0x00000020Тип подписи XAdES-BES.
CADESCOM_XADES_T = 0x00000050Тип подписи XAdES-T.
CADESCOM_XADES_X_LONG_TYPE_1 = 0x000005d0Тип подписи XAdES-X Long Type 1.
CADESCOM_XADES_A = 0x000007d0Тип подписи XAdES-A.
CADESCOM_XMLDSIG_TYPE = 0Тип подписи XMLDSIG.
CAPICOM_CERTIFICATE_INCLUDE_CHAIN_EXCEPT_ROOT = 0Сохраняет все сертификаты цепочки за исключением корневого.
CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN = 1Сохраняет полную цепочку.
CAPICOM_CERT_INFO_SUBJECT_SIMPLE_NAME = 0Возвращает имя наименования сертификата.
CAPICOM_CERT_INFO_ISSUER_SIMPLE_NAME = 1Возвращает имя издателя сертификата.
CAPICOM_CERTIFICATE_FIND_SHA1_HASH = 0Возвращает сертификаты соответствующие указанному хэшу SHA1.
CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME = 1Возвращает сертификаты, наименование которого точно или частично совпадает с указанным.
CAPICOM_CERTIFICATE_FIND_ISSUER_NAME = 2Возвращает сертификаты, наименование издателя которого точно или частично совпадает с указанным.
CAPICOM_CERTIFICATE_FIND_ROOT_NAME = 3Возвращает сертификаты, у которых наименование корневого точно или частично совпадает с указанным.
CAPICOM_CERTIFICATE_FIND_TEMPLATE_NAME = 4Возвращает сертификаты, у которых шаблонное имя точно или частично совпадает с указанным.
CAPICOM_CERTIFICATE_FIND_EXTENSION = 5Возвращает сертификаты, у которых имеется раcширение, совпадающее с указанным.
CAPICOM_CERTIFICATE_FIND_EXTENDED_PROPERTY = 6Возвращает сертификаты, у которых идентификатор раcширенного свойства совпадает с указанным.
CAPICOM_CERTIFICATE_FIND_CERTIFICATE_POLICY = 8Возвращает сертификаты, содержащие указанный OID политики.
CAPICOM_CERTIFICATE_FIND_TIME_VALID = 9Возвращает действующие на текущее время сертификаты.
CAPICOM_CERTIFICATE_FIND_TIME_NOT_YET_VALID = 10Возвращает сертификаты, время которых невалидно.
CAPICOM_CERTIFICATE_FIND_TIME_EXPIRED = 11Возвращает просроченные сертификаты.
CAPICOM_CERTIFICATE_FIND_KEY_USAGE = 12Возвращает сертификаты, содержащие ключи, которые могут быть использованны указанным способом.
CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE = 128Ключ может быть использован для создания цифровой подписи.
CAPICOM_PROPID_ENHKEY_USAGE = 9EKU.
CAPICOM_OID_OTHER = 0Объект не соответствует ни одному из предуставленных типов.
CAPICOM_OID_KEY_USAGE_EXTENSION = 10Расширение сертификата, содержащее информацию о назначении открытого ключа.
CAPICOM_EKU_OTHER = 0Сертификат может быть использован для чего-то, что не предустановлено.
CAPICOM_EKU_SERVER_AUTH = 1Сертификат может быть использован для аутентификации сервера.
CAPICOM_EKU_CLIENT_AUTH = 2Сертификат может быть использован для аутентификации клиента.
CAPICOM_EKU_CODE_SIGNING = 3Сертификат может быть использован для создания цифровой подписи.
CAPICOM_EKU_EMAIL_PROTECTION = 4Сертификат может быть использован для защиты электронной подписи.
CAPICOM_EKU_SMARTCARD_LOGON = 5Сертификат может быть использован для входа со смарт карты.
CAPICOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME = 0Время подписи. Совпадает с CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME
CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME = 1Название документа. Совпадает с CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME
CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION = 2Описание документа. Совпадает с CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION
CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME = 0Время подписи.
CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME = 1Название документа.
CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION = 2Описание документа.
CADESCOM_ATTRIBUTE_OTHER = -1Прочие атрибуты.
CADESCOM_DISPLAY_DATA_NONE = 0Данные не будут пересылаться в устройство.
CADESCOM_DISPLAY_DATA_CONTENT = 1Отображаемые данные лежат в теле сообщения.
CADESCOM_DISPLAY_DATA_ATTRIBUTE = 2Отображаемые данные лежат в подписанном атрибуте сообщения.
CADESCOM_ENCRYPTION_ALGORITHM_RC2 = 0Алгоритм RSA RC2.
CADESCOM_ENCRYPTION_ALGORITHM_RC4 = 1Алгоритм RSA RC4.
CADESCOM_ENCRYPTION_ALGORITHM_DES = 2Алгоритм DES.
CADESCOM_ENCRYPTION_ALGORITHM_3DES = 3Алгоритм 3DES.
CADESCOM_ENCRYPTION_ALGORITHM_AES = 4Алгоритм AES.
CADESCOM_ENCRYPTION_ALGORITHM_GOST_28147_89 = 25Алгоритм ГОСТ 28147-89.
CADESCOM_HASH_ALGORITHM_SHA1 = 0Алгоритм SHA1.
CADESCOM_HASH_ALGORITHM_MD2 = 1Алгоритм MD2.
CADESCOM_HASH_ALGORITHM_MD4 = 2Алгоритм MD4.
CADESCOM_HASH_ALGORITHM_MD5 = 3Алгоритм MD5.
CADESCOM_HASH_ALGORITHM_SHA_256 = 4Алгоритм SHA1 с длиной ключа 256 бит.
CADESCOM_HASH_ALGORITHM_SHA_384 = 5Алгоритм SHA1 с длиной ключа 384 бита.
CADESCOM_HASH_ALGORITHM_SHA_512 = 6Алгоритм SHA1 с длиной ключа 512 бит.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411 = 100Алгоритм ГОСТ Р 34.11-94.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_256 = 101Алгоритм ГОСТ Р 34.11-2021.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_512 = 102Алгоритм ГОСТ Р 34.11-2021.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411_HMAC = 110Алгоритм ГОСТ Р 34.11-94 HMAC.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_256_HMAC = 111Алгоритм ГОСТ Р 34.11-2021 HMAC.
CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_512_HMAC = 112Алгоритм ГОСТ Р 34.11-2021 HMAC.
LOG_LEVEL_DEBUG = 4Уровень ведения логов DEBUG.
LOG_LEVEL_INFO = 2Уровень ведения логов INFO.
LOG_LEVEL_ERROR = 1Уровень ведения логов ERROR.
CADESCOM_AllowNone = 0x00Флаг запрета установки недоверенных сертификатов или сертификатов, для которых нет соответствующего запроса.
CADESCOM_AllowNoOutstandingRequest = 0x01Флаг создания закрытого ключа из ответа на запрос.
CADESCOM_AllowUntrustedCertificate = 0x02Флаг установки недоверенных сертификатов конечного пользователя и центров сертификации.
CADESCOM_AllowUntrustedRoot = 0x04Флаг установки сертификата, даже если корневой центр сертификации для него не является доверенным.
CADESCOM_SkipInstallToStore = 0x10000000Флаг пропуска установки сертификата в хранилище сертификатов.
ENABLE_CARRIER_TYPE_CSP = 0x01Обычный криптоконтейнер (HDIMAGE, REGISTRY).
ENABLE_CARRIER_TYPE_FKC_NO_SM = 0x02ФКН без SM.
ENABLE_CARRIER_TYPE_FKC_SM = 0x04ФКН с SM.
ENABLE_ANY_CARRIER_TYPE = 0x07ENABLE_CARRIER_TYPE_CSP | ENABLE_CARRIER_TYPE_FKC_NO_SM | ENABLE_CARRIER_TYPE_FKC_SM.
DISABLE_EVERY_CARRIER_OPERATION = 0x00Запрещенные виды носителей полностью запрещены.
ENABLE_CARRIER_OPEN_ENUM = 0x01На запрещенных видах носителей можно открывать и перечислять контейнеры.
ENABLE_CARRIER_CREATE = 0x02На запрещенных видах носителей можно создавать контейнеры.
ENABLE_ANY_OPERATION = 0x03ENABLE_CARRIER_OPEN_ENUM | ENABLE_CARRIER_CREATE.
MEDIA_TYPE_REGISTRY = 0x00000001Реестр.
MEDIA_TYPE_HDIMAGE = 0x00000002Жесткий диск.
MEDIA_TYPE_CLOUD = 0x00000004Облачный носитель.
MEDIA_TYPE_SCARD = 0x00000008Смарт-карта или любое другое устройство с интерфейсом смарт-карты.
XCN_CRYPT_STRING_BASE64HEADER = 0Кодировка base64 с использованием открывающего и закрывающего заголовков сертификата.
AT_KEYEXCHANGE = 1Использование ключа для подписывания и шифрования.
AT_SIGNATURE = 2Использование ключа только для подписывания.

Обьект cadesplugin при инициализации может использовать константы у обьекта window.

Название свойства обьекта windowНазначение
cadesplugin_load_timeoutТаймаут для ожидания загрузки плагина КриптоПро ЭЦП Browser plug-in в мс. По умолчанию 20000.

Перечень вызовов и методов, использование которых при разработке систем на основе ПО «КриптоПро ЭЦП Browser plug-in» версия 2.0, может привести к необходимости дополнительных тематических исследований:

  • использование объекта CPHashedData для вычисления HMAC с помощью вызова метода Hash при выборе пользователем идентификатора алгоритма ключевого хэширования (CADESCOM_HASH_ALGORITHM_CP_GOST_3411_HMAC, CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_256_HMAC, CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2021_512_HMAC)
  • установка значения хеша для объекта CPHashedData с помощью вызова метода SetHashValue
  • вызовы метода SignHash и VerifyHash обьекта RawSignature для создании ЭП
  • вызовы метода CoSignHash, SignHash и VerifyHash обьекта SignedData для создании ЭП
  • использование обьекта SymmetricAlgorithm

В системах, требующих контроля поддерживаемых криптопровайдеров, рекомендуется устанавливать параметр ProviderTypeStrictMode ветки реестра “HKLM\Software\Crypto Pro\CAdES” типа DWORD (“\config\CAdES\ProviderTypeStrictMode” типа long для Unix платформ) в значение 1, предназначенный для проверки соответствия используемого типа криптопровайдера одному из типов криптопровайдера, поддерживаемых СКЗИ «КриптоПро CSP» (PROV_GOST_2001_DH, PROV_GOST_2021_256, PROV_GOST_2021_512).

Оцените статью
ЭЦП Эксперт
Добавить комментарий