При разработке веб-решений на платформе 1С:Предприятие 8.3 часто возникает задача проверки электронной цифровой подписи (ЭЦП) документов, которые подписываются пользователями на их локальных компьютерах (например, в веб-клиенте или тонком клиенте). На стороне сервера 1С нам необходимо убедиться в математической корректности подписи и проверить статус доверия к сертификату подписанта.
Часто разработчики сталкиваются с вопросом: можно ли выполнить проверку подписи, имея только отпечаток (thumbprint) сертификата, и нужно ли хранить все сертификаты клиентов на сервере 1С?
В этой статье мы подробно разберем, как устроен процесс проверки подписей с помощью программного интерфейса (REST API) службы КриптоПро SVS (Signature Verification Service), как правильно формировать запросы из 1С и как интерпретировать ответы сервера.
---
Давайте проанализируем базовые принципы асимметричного шифрования, на которых строится технология электронной подписи. Отпечаток сертификата (хэш-сумма, обычно SHA-1 или SHA-256) — это уникальный идентификатор самого файла сертификата. Он позволяет быстро найти нужный сертификат в хранилище, но не содержит в себе криптографических ключей.
Для того чтобы математически проверить достоверность подписи, криптографическому провайдеру (СКЗИ) жизненно необходим открытый ключ подписанта. Этот открытый ключ содержится исключительно внутри самого сертификата подписи. Таким образом, без доступа к телу сертификата (и, соответственно, к открытому ключу) проверить подпись технически невозможно.
Однако это не означает, что нам нужно хранить файлы сертификатов всех пользователей на сервере 1С или передавать их вручную при каждом запросе к API. Давайте разберем, как КриптоПро SVS решает эту задачу в зависимости от формата подписи.
---
Необходимость передачи сертификата напрямую зависит от используемого формата подписи (параметр SignatureType в запросе API):
Certificate структуры SignedDocument является обязательной, иначе сервису попросту негде будет взять открытый ключ.---
При обращении к конечной точке SVS API /signatures мы передаем структуру SignedDocument. Одним из ключевых полей этой структуры является обязательное поле Content, принимающее массив байтов в кодировке Base64. Начинающие разработчики часто путают его с отпечатком подписи. Выясним, что именно должно находиться в этом поле в разных сценариях:
Content передается весь подписанный файл целиком (например, PDF-документ со встроенной подписью или CMS/CAdES-контейнер, содержащий и подпись, и исходные данные). Дополнительное поле Source при этом оставляют пустым.Content передается исключительно файл самой отсоединенной подписи (обычно файл с расширением .sig, .sgn или .p7s). При этом оригинальный документ, который был подписан, передается в параметре Source (также в кодировке Base64).Content мы помещаем бинарное значение подписи (64/128 байт в Base64), а сам исходный документ или его хэш-значение — в поле Source.---
При успешной отправке запроса на проверку подписи сервис возвращает массив объектов VerificationResultRest. В структуре ответа присутствуют поля SignerCertificate (сертификат в Base64) и детальная информация SignerCertificateInfo.
Рассмотрим подробнее происхождение и логику валидации этого сертификата:
Content.Результат комплексной проверки подписи и сертификата возвращается в булевом поле Result (значение true означает, что подпись и сертификат успешно прошли проверку), а расшифровка статуса проверки содержится в поле Message.
---
Рассмотрим практический пример отправки запроса на проверку отсоединенной (detached) подписи формата CAdES-BES из кода 1С. Мы сформируем JSON-запрос к локальному или облачному серверу КриптоПро SVS.
Разберем процесс по шагам в обучающем стиле:
// Функция выполняет проверку отсоединенной подписи через КриптоПро SVS
// Параметры:
// АдресСервисаSVS - Строка, например "https://svs.cryptopro.ru/api"
// ПутьКФайлуДокумента - Строка, путь к исходному документу на диске
// ПутьКФайлуПодписи - Строка, путь к файлу отсоединенной подписи (.sig)
//
Функция ПроверитьПодписьЧерезSVS(АдресСервисаSVS, ПутьКФайлуДокумента, ПутьКФайлуПодписи) Экспорт
// 1. Читаем файлы и переводим их в двоичные данные
Попытка
ДвоичныеДанныеДокумента = Новый ДвоичныеДанные(ПутьКФайлуДокумента);
ДвоичныеДанныеПодписи = Новый ДвоичныеДанные(ПутьКФайлуПодписи);
Исключение
ЗаписатьЛогТранзакции("Ошибка чтения файлов: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
// 2. Кодируем бинарные данные в формат Base64
СтрокаBase64Документ = Base64Строка(ДвоичныеДанныеДокумента);
СтрокаBase64Подпись = Base64Строка(ДвоичныеДанныеПодписи);
// Очищаем строки от символов переноса строк, если они присутствуют
СтрокаBase64Документ = СтрЗаменить(СтрокаBase64Документ, Символы.ПС, "");
СтрокаBase64Документ = СтрЗаменить(СтрокаBase64Документ, Символы.ВК, "");
СтрокаBase64Подпись = СтрЗаменить(СтрокаBase64Подпись, Символы.ПС, "");
СтрокаBase64Подпись = СтрЗаменить(СтрокаBase64Подпись, Символы.ВК, "");
// 3. Формируем тело запроса в соответствии с API SVS
СтруктураДокумента = Новый Структура;
СтруктураДокумента.Вставить("Content", СтрокаBase64Подпись); // Сама подпись
СтруктураДокумента.Вставить("Source", СтрокаBase64Документ); // Исходный документ
СтруктураДокумента.Вставить("SignatureType", "CAdES"); // Тип подписи
ТелоЗапросаСтруктура = Новый Структура;
ТелоЗапросаСтруктура.Вставить("Document", СтруктураДокумента);
// Записываем структуру в JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ТелоЗапросаСтруктура);
СтрокаЗапросаJSON = ЗаписьJSON.Закрыть();
// 4. Отправляем HTTP-запрос к сервису КриптоПро SVS
// Парсим адрес сервиса для разделения на хост и ресурс
СтруктураURL = ПолучитьСтруктуруURL(АдресСервисаSVS);
Если СтруктураURL.Схема = "https" Тогда
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL();
Иначе
ЗащищенноеСоединение = Неопределено;
КонецЕсли;
HTTPСоединение = Новый HTTPСоединение(
СтруктураURL.Хост,
СтруктураURL.Порт,
,,,
ТаймаутСоединения(),
ЗащищенноеСоединение
);
HTTPЗапрос = Новый HTTPRequest(СтруктураURL.ПутьРесурса + "/signatures");
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаЗапросаJSON, КодировкаТекста.UTF8);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Исключение
ЗаписатьЛогТранзакции("Не удалось связаться с сервисом SVS: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
// 5. Анализируем ответ сервера
Если HTTPОтвет.КодСостояния = 200 Тогда
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(HTTPОтвет.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
РезультатыПроверки = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
// Результат возвращается в виде массива объектов проверки
Если ТипЗнч(РезультатыПроверки) = Тип("Массив") И РезультатыПроверки.Количество() > 0 Тогда
Возврат РезультатыПроверки[0]; // Возвращаем результат первой подписи
КонецЕсли;
Иначе
ЗаписатьЛогТранзакции("Сервер SVS вернул ошибку: " + HTTPОтвет.КодСостояния);
КонецЕсли;
Возврат Неопределено;
КонецФункции
// Вспомогательная функция для парсинга URL
Функция ПолучитьСтруктуруURL(Знач Адрес)
Результат = Новый Структура("Схема, Хост, Порт, ПутьРесурса", "http", "", 80, "");
// Упрощенный парсинг для примера
Если Найти(Адрес, "https://") = 1 Тогда
Результат.Схема = "https";
Результат.Порт = 443;
Адрес = Сред(Адрес, 9);
ИначеЕсли Найти(Адрес, "http://") = 1 Тогда
Адрес = Сред(Адрес, 8);
КонецЕсли;
ПозицияСлэша = Найти(Адрес, "/");
Если ПозицияСлэша > 0 Тогда
Результат.Хост = Лев(Адрес, ПозицияСлэша - 1);
Результат.ПутьРесурса = Сред(Адрес, ПозицияСлэша);
Иначе
Результат.Хост = Адрес;
КонецЕсли;
Возврат Результат;
КонецФункции
Функция ТаймаутСоединения()
Возврат 30;
КонецФункции
Процедура ЗаписатьЛогТранзакции(Текст)
ЗаписьЖурналаРегистрации("КриптоПро.SVS", УровеньЖурналаРегистрации.Предупреждение,,, Текст);
КонецПроцедуры
---
Когда вы вызовете функцию проверки подписи, возвращенная структура будет содержать подробные сведения. Давайте посмотрим на пример обработки результата проверки на стороне 1С:
РезультатПроверки = ПроверитьПодписьЧерезSVS("https://svs.test.ru/api", "C:\docs\dogovor.pdf", "C:\docs\dogovor.pdf.sig");
Если РезультатПроверки <> Неопределено Тогда
ПодписьДействительна = РезультатПроверки.Result; // Булево (true/false)
СообщениеСервиса = РезультатПроверки.Message; // Текстовое пояснение результата
Если ПодписьДействительна Тогда
// Получаем информацию о сертификате подписанта
ИнформацияОСертификате = РезультатПроверки.SignerCertificateInfo;
ИмяПодписанта = ИнформацияОСертификате.Subject; // ФИО и реквизиты организации
ДействителенДо = ИнформацияОСертификате.NotAfter;
Сообщить(СтрШаблон("Подпись верна! Подписал: %1. Сертификат действителен до: %2", ИмяПодписанта, ДействителенДо));
Иначе
ВызватьИсключение "Документ не прошел проверку ЭЦП! Причина: " + СообщениеСервиса;
КонецЕсли;
КонецЕсли;
Таким образом, использование КриптоПро SVS API позволяет полностью отказаться от ведения локальной базы клиентских сертификатов на стороне сервера 1С. Серверная часть 1С просто перенаправляет подписанный документ и его подпись в службу верификации, которая автоматически извлекает нужные криптографические данные, проверяет их актуальность по спискам отзывов и возвращает гарантированный результат.