Техническая статья

Улики LTV PAdES и seed-значения в HotPDF

PDF, который вы только что подписали, — это подпись B-B и не более того. Он доказывает, кто подписал и что байты не двигались, но не несёт доказательства, что сертификат подписанта был действителен в момент подписания, поэтому проверяющий спустя годы вынужден искать данные отзыва, которых может уже не быть. Закрыть этот разрыв — значит записать ответы OCSP и CRL в хранилище Document Security Store уровня документа, и в HotPDF это один вызов: PopulatePAdESLTVEvidence обходит каждую загруженную подпись, выводит запросы отзыва из набора сертификатов, исполняет их через транспорт, который вы предоставили, и записывает полученный материал вместе с цепочкой CMS в DSS. Он возвращает число подписей, чьи улики легли в документ, или минус единицу, когда поля подписи нет вовсе

Решение дизайна, которое стоит понять до использования: библиотека никогда не открывает сокет. Каждый байт, приходящий из сети, приходит через написанный вами обратный вызов. Это не осторожность ради осторожности; это единственный способ, которым данная возможность работает внутри сред, действительно требующих долгосрочной проверки

Почему библиотека отказывается делать собственный HTTP?

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

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

Поток PopulatePAdESLTVEvidence в HotPDF: предоставляемый вызывающим транспорт FetchEvidence, поля записи запроса и статусы по каждой подписи
Каждый сетевой байт проходит через ваш обратный вызов FetchEvidence, и у каждой подписи свой статус, поэтому один таймаут никогда не прерывает проход
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind говорит, что это: POST для OCSP или GET для CRL;
    // Request.ContentType и Request.Body уже подготовлены,
    // а Request.MaxResponseBytes — предел, который вы обязаны соблюсти
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry позволяет политике повторов сделать паузу; для 404
      // или неверного URL используйте setsPermanentFailure
      Result := setsRetry;
    end;
  end;
end;

// Одновызовное обновление B-B до B-LT для каждой подписи загруженного файла
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // Сохранение только дописыванием: байты, покрываемые существующими
      // подписями, сохраняются дословно
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

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

Цепочка, которую CMS забыла включить

Проверка отзыва нуждается в сертификате издателя, а удивительно много стеков подписания опускают промежуточные сертификаты в контейнере CMS. Путь восстановления — расширение Authority Information Access, метод доступа 1.3.6.1.5.5.7.48.2, который анонсирует URL, где сертификат издателя можно скачать. HPDFFetchAIAIntermediates обходит эти URL тем же транспортом, вырезает DER из каждого ответа и возвращает только те сертификаты, которых CMS ещё не несла, с ключом по хешу DER, чтобы дубликаты и циклы не могли закрутиться

Две детали решают, сработает ли это против настоящих удостоверяющих центров. Первая — кодирование: конечные точки CA отдают сертификат голым DER примерно так же часто, как и в PEM-оболочке, и надёжного типа содержимого для их различения нет. Прочный зонд — сначала текстовый, затем структурный. Ищите маркер -----BEGIN CERTIFICATE-----, снимайте оболочку и декодируйте base64, если она есть, а на обоих путях подтверждайте, что первый байт результата — $30, DER-тег SEQUENCE. Вторая — глубина: полученный промежуточный сертификат сам может анонсировать AIA URL для собственного издателя, поэтому обход дописывает новых кандидатов в очередь и достраивает цепочки, которым не хватает двух-трёх переходов. Это надо ограничивать, для чего и служит параметр MaxFetch

Схема достройки цепочки AIA в HotPDF: получение по URL caIssuers, зонд PEM против DER, дедупликация по хешу DER и предел глубины MaxFetch
HPDFFetchAIAIntermediates обходит URL caIssuers тем же транспортом, прощупывая PEM-оболочку и ограничивая очередь через MaxFetch

Что такое seed value поля подписи и почему это ломается молча?

Seed value — это ограничение, которое автор документа прикрепляет к полю подписи, чтобы сказать подписанту, какая подпись приемлема: какой SubFilter, какой алгоритм дайджеста, какие причины, какая минимальная версия PDF, обязательно ли встраивать информацию об отзыве. Оно живёт в словаре /SV на поле и определено в ISO 32000-1 §12.7.5.5. HotPDF записывает его методом AttachPAdESSeedValue и проверяет методом CheckLoadedSignatureSeedValue, который возвращает True, когда поле не ограничено или все присутствующие ограничения соблюдены, а при False называет первое нарушенное ограничение через выходной параметр, который можно вставить прямо в сообщение об ошибке

Механизм, из-за которого seed values легко понять неверно, — запись флагов /Ff, описанная в §12.7.5.5.3. Установленный бит помечает своё ограничение как обязательное: несовпадение — ошибка, и подписант обязан отказаться. Сброшенный бит помечает то же ограничение как предпочтение: значение фильтрует то, что интерфейс должен предлагать, и ничего больше. Отсюда две ловушки. Первая: /Ff живёт внутри словаря /SV, а не на аннотации виджета, поэтому код, читающий /Ff уровня поля, вечно получает пустой ответ и заключает, что ничто не обязательно. Вторая: назначения битов — не простой ряд один, два, четыре, восемь; в HotPDF писатель выдаёт 2 для SubFilter, 4 для MinVersion, 32 для AddRevInfo и 64 для DigestMethod. Читатель, предполагающий последовательные биты, декодирует каждое ограничение как необязательное и проходит все тесты, кроме того единственного, что важен

Таблица битов флагов seed value для подписания PAdES в HotPDF: биты Ff 2, 4, 32 и 64 и обработка обязательных против предпочтительных ограничений
Запись /Ff живёт внутри /SV, и каждая позиция бита решает, будет ли несовпадение жёстким отказом или предпочтением интерфейса
var
  Violation: AnsiString;
begin
  // Спросите поле, разрешён ли профиль, которым мы собираемся подписать
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // Ограничение соблюдено: продолжаем проход подписания
end;

Тест, вскрывший исходный баг декодирования, не был позитивным тестом. Это было утверждение, что вынужденное несовпадение обязано отвергаться, и это единственный сорт тестов, способный поймать этот класс дефектов: декодер, читающий не тот словарь или не те позиции битов, выдаёт «ни одно ограничение не нарушено» для каждого входа, что выглядит в точности как корректное поведение, пока вы не нарушите одно ограничение нарочно

Где это стоит на лестнице LTV

Четыре ступени, и каждой нужна нижняя. B-B — голая подпись. B-T добавляет доверенную метку времени, фиксирующую момент подписания, чтобы проверяющий знал, к какому моменту оценивать отзыв. B-LT добавляет улики отзыва в DSS — это и автоматизирует PopulatePAdESLTVEvidence. B-LTA добавляет метки времени документа, обновляемые прежде, чем ослабнет предыдущая, продлевая действительность неограниченно; в HotPDF это RenewPAdESLTATimestamp, дописывающий новую метку как инкрементальную ревизию и сохраняющий каждую прежнюю подпись, метку и запись DSS нетронутыми

Модель инкрементального обновления — единственный корректный способ добавить улики в подписанный документ, ведь перезапись файла сломала бы диапазоны байтов, которые покрывают существующие подписи. Если нужно рассуждать о том, что изменилось между ревизиями и относятся ли эти изменения к тому сорту, который подпись разрешает, этот анализ рассмотрен отдельно в анализе ревизий DocMDP и FieldMDP. Сам конвейер подписания, включая источники сертификатов и подводные камни порядка байтов, — в пошаговом руководстве по подписанию PAdES, а сторона проверки — в материале о проверке подписей на загруженных документах

Одно практическое предостережение о порядке. Собирайте улики как можно скорее после подписания, в идеале в том же задании. Ответчики, способные ответить за сертификат, в сети, пока сертификат действителен, и исчезают спустя годы, поэтому документ, покидающий ваш конвейер как B-B, может больше никогда не быть обновляемым. HotPDF работает как нативный VCL-компонент для Delphi и C++Builder, и весь проход улик — внутрипроцессный, не считая вашего собственного транспорта; поддерживаемые профили перечислены на странице продукта HotPDF Delphi PDF component