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

PDFium VCL отдалечен PAdES подпис: HSM и облачни ключове

PDFiumPas разделя PAdES подписването на две извиквания, така че частният ключ никога не се налага да бъде в процеса ви. PreparePadesRemoteSignature записва incremental update с празен, фиксиран по ширина /Contents placeholder и връща заявка, носеща SHA-256 дайджеста на документа, точния ByteRange и отпечатък на подготвения файл. CompletePadesRemoteSignature взима detached CMS, върнат от вашата услуга за подпис, и го поставя в тази резервирана позиция

Между тези две извиквания могат да минат минути или часове, процесът може да се рестартира, а работата може да се премести на друга машина. Точно тази пролука е цялата причина API-то да е оформено по този начин

Защо отдалечен ключ не може да използва обикновеното извикване за подпис?

Защото SignPadesBytes предполага, че операцията по подписване се случва вътре в извикването. То изгражда incremental update-а, изчислява дайджеста върху ByteRange, подписва го и записва резултата, всичко преди да върне управлението. Това е точно правилно, когато ключът живее в Windows certificate store или в PKCS#12 файл, който сте заредили

Невъзможно е, когато ключът живее в мрежов HSM, quалифицирано устройство за създаване на подпис, управлявано от доставчик на доверителни услуги, или облачно API за подпис, изискващо потребителят да потвърди на телефона си. В тези случаи последователността не е извикване на функция, тя е разговор: изпращате дайджест, нещо друго authentication-ира човек, а CMS се връща по-късно. Синхронно API не може да изрази „по-късно“, без да блокира нишка на операция, която може да се нуждае от втори фактор

Протоколът в две фази

Фаза едно подготвя документа. PDFiumPas добавя полето и речника за стойността на подписа, резервира ContentsSize байта hex-кодирано пространство в /Contents, изчислява ByteRange около тази резервация и произвежда TPadesRemoteSigningRequest, съдържащ FormatVersion, PreparedFingerprint, DocumentDigest, четириелементния ByteRange, ContentsHexOffset и ContentsSize

Единствената стойност, от която услугата ви за подпис се нуждае, е DocumentDigest: SHA-256, който върнатият CAdES SignedData трябва да носи като своя message digest. Всичко останало в записа съществува, за да може фаза две да докаже, че файлът, който завършва, е файлът, от който е изчислен този дайджест

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // hex байтове, резервирани за CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Запазете сесията, за да може по-късно изпълнение - или друга машина - да я довърши
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Какво отказва Complete и защо съществува всяка проверка?

Завършването е мястото, където дизайн за отдалечено подписване обикновено се проваля, затова валидацията е съзнателно безкомпромисна. CompletePadesRemoteSignature отхвърля подготвен PDF, чийто отпечатък вече не съвпада със заявката, ByteRange, който не съвпада със записаните координати на placeholder-а, променени разделители на /Contents, placeholder, който вече не е празен, CMS, по-голям от резервацията, CMS, който не е точно една DER стойност, неподдържана форма на SignedData, липсващ атрибут signing-certificate-v2 и CMS, чийто message digest не се равнява на подготвения дайджест на документа

Всяко от тях съответства на реален провал. Проверките за отпечатък и ByteRange засичат случая, при който някой е регенерирал подготвения файл между фазите, което би произвело подпис, валидиращ се спрямо байтове, които никой не притежава. Проверката за празен placeholder засича двойно завършване, при което втори CMS се записва върху подпис, който вече съществува. Проверката на message digest засича най-опасния случай от всички: правилно оформен CMS, подписан върху различен документ, което е онова, което получавате, когато опашка обърка две едновременни сесии за подпис. Без нея бихте произвели файл, който изглежда подписан и се проваля при валидиране навсякъде, или по-лошо, който носи одобрението на някой друг

Изискването за signing-certificate-v2 е въпрос на PAdES съответствие, а не на цялостност. ETSI EN 319 142 изисква подписващият сертификат да бъде обвързан в подписаните атрибути, а CMS без този атрибут не е PAdES подпис, дори ако се проверява криптографски успешно. Отхвърлянето му при завършване означава, че научавате това тук, не в отчет от валидатор на клиент, тема, разгледана допълнително в защо валидаторите отхвърлят PAdES подписи

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // върнат от HSM или TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Всяко отхвърляне носи конкретна причина; логвайте я дословно
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Преминаване между процеси и машини

SavePadesRemoteSigningRequest и LoadPadesRemoteSigningRequest сериализират сесията чрез стабилен, версиониран двоичен формат, което прави дизайна практичен, а не само коректен. Уеб приложение може да подготви документ в една заявка, да съхрани подготвения PDF и session blob-а, да върне дайджест на браузъра за подпис със смарт карта и да завърши файла в напълно различен request handler

Полето FormatVersion е онова, което пази това безопасно през ъпгрейди. Сесия, записана от по-стара компилация и заредена от по-нова, се разпознава или отхвърля изрично, вместо да бъде погрешно разчетена като различно оформен запис. Ако опашката ви може да пази сесии с дни, третирайте версията на формата като оперативен факт, който си струва да се логва, не детайл от имплементацията

Определяне на размера на placeholder-а

ContentsSize е единственият параметър, за който трябва да мислите, защото е фиксиран, преди CMS изобщо да съществува. Брои hex-кодираната резервация, така че 6 KB DER CMS се нуждае от поне 12 KB пространство, а имплементацията ограничава резервацията до 64 MiB

Резервирайте твърде малко и завършването се проваля с грешка за прекалено голям CMS, след като услугата ви за подпис вече е свършила работата си, което при таксувана quалифицирана услуга за подпис означава изхабена операция. Резервирайте твърде много и всеки подписан документ носи допълването завинаги. Разумният подход е да измерите: подпишете един документ с реалната си верига от сертификати, погледнете дължината на DER, удвоете за hex, после добавете щедър марж за timestamp токена, ако възнамерявате да надградите до подпис на T-ниво. Вериги с няколко междинни звена и дълъг OCSP отговор растат по-бързо, отколкото хората очакват

Какво идва след подписа

Завършен отдалечен подпис е PAdES B-B. Дългосрочната валидация се нуждае от timestamp и материала за валидация, което е отделен incremental update, добавящ DSS и неговите per-signature VRI речници, описан в дългосрочните подписи с RFC 3161 timestamp-и и DSS. Тази стъпка е локална: тя добавя сертификати, OCSP отговори и CRL-и, нито един от които се нуждае от частния ключ

Преди да пуснете в продукция, проверете онова, което сте произвели, със същия път код, който би използвала доверяваща се страна, разгледан в инспекцията на цифрови подписи и PAdES нива. Подписването и проверката са различен код, а конвейер за отдалечено подписване е точно мястото, където двете могат да се разминат, без някой да забележи, докато външен валидатор не каже

PDFiumPas е Delphi и Lazarus компонент около движока PDFium с нативен Pascal PAdES стек, така че подписването, timestamp-ването и валидацията работят без външни инструменти от командния ред. Пълната API документация и пробна версия са на страницата на PDFium компонента за Delphi