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

Списки доверия eIDAS и квалифицированные подписи PDF

Решить, что подпись PDF квалифицирована по eIDAS, — значит ответить на вопрос, не имеющий отношения к криптографии: выдан ли сертификат службой доверия, которую государство-член перечислило как квалифицированную, в момент, когда подпись была сделана. Ответ живёт в списке доверия — XML-документе, публикуемом по каждой территории, и вся ценность этого документа зависит от его подлинности. Поэтому компонент PDFium отказывается заглядывать внутрь, пока кто-то за него не поручился. TPdfEuropeanTrustedList.ParseAuthenticated передаёт полные сырые байты предоставленному вызывающим IPdfTrustedListAuthenticator прежде, чем разобрать хоть одну службу, и создаёт снимок, только если тот аутентификатор явно подтверждает

Поток аутентификации до разбора европейского списка доверия в Delphi: сырой XML из свежей выборки или кэшированного снимка проходит через IPdfTrustedListAuthenticator, прежде чем TPdfEuropeanTrustedList что-либо разберёт
Свежие и кэшированные списки проходят один и тот же аутентификатор, и снимок существует лишь после его подтверждения

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

Разобранное не значит доверенное

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

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

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // Ваша политика живёт здесь: проверьте enveloped-подпись XMLDSIG
  // по сертификату подписания списка, закреплённому по внешнему
  // каналу, и опишите проверенное для журнала аудита
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // Снимок существует только потому, что аутентификатор сказал да
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

Валидатор не владеет сетевой политикой

Валидатору PAdES не дело решать, как добраться до списка списков доверия, идти ли через прокси, как часто повторять попытку или что делать, когда территория недостижима. Это решения приложения и развёртывания, и в регулируемых средах их часто аудитируют. Поэтому обновления приходят через IPdfTrustedListSource, которому дают URI и предел байтов, а он возвращает байты

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

Инварианты обновления, обеспечиваемые компонентом списков доверия PDFium: неизменная территория, строго возрастающий порядковый номер и время выпуска, никогда не движущееся назад, что вместе блокирует атаки понижения
Три монотонные проверки отличают подлинное обновление от повторенного или подменённого списка

Пределы парсера и никакого DTD вовсе

TPdfTrustedListOptions ограничивает размер XML, число токенов, глубину вложенности, число служб, число сертификатов и размер отдельного сертификата, а класс-функция Default даёт пригодные значения. Списки доверия — публикуемые документы предсказуемого размера, поэтому границы дёшево выставить, и нет законного списка, которому нужно их превышать

Отдельно и безусловно парсер отвергает объявления DTD и сущностей. Это закрывает одним отказом и отказ в обслуживании через разрастание сущностей, и маршрут раскрытия через внешние сущности, и не стоит ничего, ведь списки доверия не используют сущности. Любой XML-парсер, достижимый из недоверенного ввода, стоит так настраивать; различие здесь в том, что отказ не конфигурируем, поэтому его не выключить доброжелательной правкой опции

Квалифицированный статус записывается рядом с доверием цепочки, а не сливается с ним

Сторона вычисления сознательно отдельна. TPadesTrustValidationOptions.QualifiedTrustEvaluator принимает IPdfQualifiedTrustEvaluator, который реализует снимок списка доверия. Во время проверки вычислитель получает листовой сертификат, цепочку и время проверки, сопоставляет сертификаты служб точным сравнением DER с подписантом и цепочкой, комбинирует статус службы, идентификатор типа службы и URI квалификаторов на тот момент времени и возвращает запись вычисления

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

Вычисление квалифицированного доверия PAdES в Delphi: IPdfQualifiedTrustEvaluator сопоставляет сертификаты служб точным сравнением DER и заполняет QualifiedTrustStatus и QualifiedTrust, пока CertificateTrustStatus не тронут
Доверие цепочке и квалифицированный статус eIDAS записываются рядом, чтобы оба вывода оставались видимыми
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // аутентифицированный снимок
  Options.QualifiedValidationTime := SigningTime; // не Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // Нет совпавшей службы, или снимок не может ответить за это время
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

Почему время проверки — не сейчас

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

Список несёт нужное для этого: каждая запись службы имеет время начала статуса и флаг, отличающий исторические записи от текущих, и вычислитель комбинирует их против времени, которое вы подаёте. На практике это время берётся из доверительной метки на подписи, а не из заявленного в CMS времени подписания, потому материал долговременной проверки важен даже для вопроса, выглядящего как поиск по политике; сторона меток времени и DSS разобрана в статье о долговременных подписях

Что вам всё ещё предстоит построить

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

От компонента вы получаете часть, которую легко тонко испортить: порядок аутентификации до разбора, ограниченный и свободный от сущностей разбор XML, монотонные инварианты обновления, сопоставление служб точным DER, вычисление исторического статуса и результат, остающийся отдельным от обычного доверия цепочке. Если ваша насущная проблема базовее — валидатор отвергает подпись, которую вы считаете годной, — обычные причины каталогизированы в материале почему валидаторы отвергают подписи PAdES, а поверхность осмотра подписей описана в осмотре подписей и уровней PAdES. Возможности компонента перечислены на странице продукта PDFium Delphi component