Technical Article

Изграждане на работна среда за съвместимост и подписване в Delphi с PDFlibPas

Работната среда, която свързва валидирането на съвместимост с цифровото подписване, трябва да координира четири стъпки в определен ред и да ги поддържа обвързани с един и същ набор от байтове през целия процес. Тя изпълнява предварителна проверка за PDF/A или PDF/UA. Прилага необходимите корекции според констатациите и записва коригирана версия. Подписва точно тази версия. Накрая прочита обратно подписания файл и потвърждава, че подписът наистина го покрива. Този ред не е козметичен. Пропуснете обратното четене и се доверявате на собствения си път за запис; оставете проверката да се изпълни срещу грешна версия и вашият отчет за съвместимост ще описва файл, който никога не сте изпращали.

Частта, която повечето собствени процеси объркват, е връзката между валидирането и подписването. Стартирайте ги като два отделни инструмента с междинен проход за коригиране и ще се появят поне три отделни версии на файла, всяка със собствени байтове. Отчетът за проверка, който предоставяте на одитор, описва една от тях. Подписът замразява друга. Нищо във файла не казва, че те са една и съща версия, и често това не е така. PDFlibPas, библиотеката за разработка на PDF от losLab за Delphi и C++Builder, поставя предварителната проверка и PAdES подписването зад един общ клас фасада, така че цялата последователност може да се изпълнява в един процес, който никога не губи следа за кои байтове става дума. Всяко извикване по-долу съществува в библиотеката днес, както и всеки описан капан.

Три версии на един документ и как се отваря разликата

Пребройте записванията. Оригиналът пристига от входящия поток. Проходът за корекция го зарежда, включва режим за съвместимост и записва коригирана версия. Проходът за подписване добавя подпис като инкрементална актуализация, което е трети запис. Три записвания, три оформления на байтовете и един отчет за проверка не означават нищо, освен ако не се посочи коя от трите версии покриват. SHA-256 хеш на файла, записан до всяка проверка и всеки подпис, е лесен начин да докажете, че валидираната версия е точно тази, която сте подписали.

Едно от поведенията на библиотеката засилва тази дисциплина още повече. Корекциите за съвместимост, поискани чрез SetPDFAMode или SetPDFUAMode, не влизат в сила в момента на извикването им. Те се прилагат по време на записването. Автоматичните корекции, като принудително задаване на флагове за печат на анотации или присвояване на ред за табулация в PDF/UA, се записват в изходния файл и никъде другаде, така че проверка на документа, който току-що сте "коригирали" в паметта, не ви казва нищо за байтовете, изпратени към модула за подписване. Запишете първо, след което направете проверка на записания файл. Състоянието в паметта е само чернова; само файлът на диска е реален.

Проверка от диск и нулата, която означава две неща

Основната точка за проверка на съвместимост е CheckFileCompliance(FileName, Password, ComplianceTest, Options). Тест 1 избира PDF/A (ISO 19005), тест 2 избира PDF/UA (ISO 14289). Функцията отваря файла чрез стрийминг четеца на библиотеката, така че няма нужда първо да използвате LoadFromFile, и връща дескриптор на списък с низове, съдържащ по една констатация за всеки запис:

var

  PDF: TPDFlib;

  ListID, I: Integer;

begin

  PDF := TPDFlib.Create;

  try

    ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0);  // 1 = PDF/A

    if ListID = 0 then

    begin

      if PDF.LastErrorCode <> 0 then

        raise Exception.Create('Preflight could not read the file')

      else

        Writeln('No PDF/A findings');

    end

    else

    begin

      for I := 0 to PDF.GetStringListCount(ListID) - 1 do

        Writeln(PDF.GetStringListItem(ListID, I));

      PDF.ReleaseStringList(ListID);

    end;

  finally

    PDF.Free;

  end;

end;

Капанът е във върнатата стойност и е от тип, който преминава успешно през всеки стандартен тест. Нулата означава "няма констатации". Нулата също така означава "файлът не може да бъде отворен", тъй като имплементацията връща 0 винаги, когато списъкът с резултати е празен, включително при грешка в четенето. Работна среда, която тълкува 0 като зелена светлина, безпроблемно ще одобри файл, заключен от друг процес. Комбинирането на извикването с LastErrorCode, както е показано по-горе, разграничава двата случая. Проверяващият модул също така отваря файла в режим на споделяне без право на запис, така че ако вашата стъпка по коригиране все още държи отворен поток за запис, проверката ще се провали по причина, свързана с незатворен поток, а не със съвместимостта.

Когато човек, а не автоматизиран процес, трябва да прочете констатациите, CreatePreflightReport ги представя като четим отчет. ComparePreflightReports сравнява две проверки, което е удобен начин да се покаже, че корекциите са изчистили първоначалните проблеми, без да въвеждат нови.

Подписване на проверената версия със SignProcess

След като записаната версия премине проверката и нейният хеш е регистриран, подпишете точно този файл и никой друг. API за SignProcess прилича на строител (builder). Отворете дескриптор на процес, конфигурирайте го ред по ред, потвърдете промените и прочетете кода на резултата.

ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');

if ProcessID = 0 then

  raise Exception.Create('Cannot open source for signing');

PDF.SetSignProcessField(ProcessID, 'ApprovalSig');

PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);

PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');

PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached');  // PAdES baseline

PDF.SetSignProcessDigestAlgorithm(ProcessID, 2);                      // SHA-256

PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192);              // room for a later timestamp

PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');

if PDF.GetSignProcessResult(ProcessID) <> 1 then

  Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));

PDF.ReleaseSignProcess(ProcessID);

Два реда в тази последователност имат по-голямо значение, отколкото изглежда. SetSignProcessCustomSubFilter с ETSI.CAdES.detached избира PAdES подпис, съгласно ETSI EN 319 142-1, вместо остарялата фамилия adbe.pkcs7.detached, което определя дали европейски валидатор ще приеме подписа или ще го маркира като невалиден. SetSignProcessReserveContentsBytes запълва контейнера /Contents с празно пространство, като избраният тук размер е решение за бъдещето: ако се наложи добавяне на времеви печат, разширеният CMS трябва да се побере в запазеното сега пространство, тъй като то не може да се увеличи по-късно без повторно подписване на целия документ. Запазете твърде много и губите няколко килобайта, но ако запазеното място е твърде малко, стъпката за времеви печат ще се провали след месеци с грешка за препълване, която трудно ще свържете с този конкретен ред.

GetSignProcessResult връща код на резултат, а не булева стойност, и тези кодове си струва да се записват. 1 означава успех, 4 - грешна PDF парола, 7 - грешна парола за сертификат, 9 - PFX файл без частен ключ, 11 - грешка при прилагането на подписа. Ако ги обедините в обикновено вярно/грешно, ще загубите единствената информация, която разграничава проблема с грешна парола от този с ключ без частна част. Записвайте целочислената стойност.

Обратно четене: одит на току-що създадения файл

Никоя работна среда не трябва да се доверява сляпо на пътя, по който е записан файлът, който предстои да сертифицира. Одитният клас TPDFlibSignDoc отваря отново подписания резултат и чете записите в речника на подписа директно от диска:

var

  Doc: TPDFlibSignDoc;

  Names: TStringList;

  FS: TFileStream;

  I: Integer;

  SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;

begin

  // Capture the size before Open: the audit object holds a share lock on the file

  FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);

  SourceSize := FS.Size;

  FS.Free;

  Doc := TPDFlibSignDoc.Create;

  Names := TStringList.Create;

  try

    if not Doc.Open('invoice-signed.pdf', '', False) then Exit;

    Doc.GetSignatureFieldNames(Names);

    for I := 0 to Names.Count - 1 do

      if Doc.GetSignatureValueObjNum(Names[I]) > 0 then  // > 0 means the field is signed

      begin

        RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));

        GapStart   := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));

        TailStart  := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));

        TailLen    := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));

        if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then

          Writeln(Names[I], ': signature covers the file to EOF')

        else

          Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');

      end;

    Doc.Close;

  finally

    Names.Free;

    Doc.Free;

  end;

end;

Аргументите на ValueKey съответстват на записи в речника. Ключ 0 връща суровия CMS от /Contents, ключове 2 и 3 - имената на /Filter и /SubFilter, а от 11 до 14 - четирите числа на ByteRange. Текстовите стойности се извличат чрез GetSignatureTextValueByName: ключ 0 е декларираното време на подписване, а ключ 5 разграничава обикновен Sig от DocTimeStamp, което е важно, когато документът съдържа и двете.

Вземането на размера на файла в горната част на този пример е структурно важно, а не просто почистване на кода. TPDFlibSignDoc.Open държи файла под ограничаващо заключване за споделяне през целия си жизнен цикъл, така че всеки процес, който изисква суровите байтове (хеширане на подписания диапазон, преизчисляване на CMS дайджеста), трябва да прочете файла, преди да извика Open. Демонстрационният проект SigningWorkbench на самата библиотека първо чете целия файл в паметта точно поради тази причина. Работна среда, която игнорира този ред на операции, ще се проваля произволно на машината, която загуби конкуренцията за достъп.

Аритметика на ByteRange, която доказва покритието

Правилният файл с единичен подпис има ByteRange във формат [0 a b c]: покритието започва от отместване 0, пропуска шестнадесетичния контейнер /Contents между 'a' и 'b', и продължава до байт b+c. Когато сумата b+c е равна на размера на файла, подписът покрива всичко до края на файла, което е желаният резултат. Когато стойността е по-малка, някой е добавил инкрементална актуализация след полагането на подписа. Това е напълно допустимо съгласно ISO 32000-1 §12.8, тъй като попълването на формуляри, втори подпис и DSS речник се добавят точно по този начин. Това е и фактът, който одитната следа трябва да запише в момента на подписване, вместо да го възстановява при евентуален спор.

Следете ширината на целочислените типове, докато правите тези изчисления. Функцията GetSignProcessByteRange от основния API връща 32-битово цяло число, но базовите стойности са от тип Int64, така че при файл над 2 GB достъпът през основния API безшумно ще съкрати стойността. Използвайте класовия слой TPDFlibSigner.GetByteRange, който връща Int64, или анализирайте стойностите от GetSignatureValueByName, както е показано в одитния код по-горе.

Какво оставя библиотеката на вас

Две граници е по-добре да бъдат проучени по време на проектирането, а не в последния момент. Основният API на TPDFlib не съдържа обвивка за проверка на подписи. Криптографската проверка се намира едно ниво по-надолу, в TPDFlibSignatureVerifier, чийто метод VerifySignature връща резултат за валиден, невалиден или неизвестен подпис. Също така няма вграден HTTP клиент за сървъри за времеви печат по RFC 3161. Библиотеката изчислява хеша за изпращане и вгражда обратно допълнения CMS, след като получи токен, но мрежовата комуникация с TSA е ваша задача. И двете функционалности се обвиват лесно, но е изключително неприятно да установите липсата им седмица преди релийз, така че ги предвидете още в самото начало.

Въпросът за съвместимостта трябва да бъде изяснен предварително, тъй като той определя къде се поставя последната защита: нарушава ли добавянето на подпис съвместимостта с PDF/A? Не и само по себе си. Подписът се добавя като инкрементална актуализация, а стандартите от ISO 19005-2 нататък изрично позволяват подписани документи. Уловката е във визуалния изглед на подписа, който следва същите правила като всяко друго съдържание на страницата, включително изискванията за вградени шрифтове и липса на цветове, зависещи от устройството. Така че последното ниво на проверка в работната среда е още едно стартиране на валидация, този път върху подписания резултат. Използвайте CheckFileCompliance като бърза автоматизирана проверка в процеса, но все пак проверявайте финалните версии с независим инструмент като veraPDF, тъй като валидаторите имплементират припокриващи се, но не идентични набори от правила; когато двата инструмента се разминават, текстът на констатацията обикновено посочва конкретния параграф от стандарта.

От всичко това произтича важен извод за последователността. Подписването и поставянето на времеви печат не стават с едно преминаване: първо се записва основният подпис, а след това отделен процес за времеви печат разширява CMS в рамките на запазеното пространство в /Contents. Ето защо редът за запазване на байтове по-горе има толкова голямо значение. За времевия печат и слоевете за дългосрочно валидиране, които се изграждат върху тази работна среда, ръководството за PAdES подписване и валидиране проследява подписа от базов до ниво B-LT, а темата за проверката е разгледана по-подробно в ръководството за PDF/A и PDF/UA проверка. Пълната документация за API и пробни версии са налични на продуктовата страница на PDFlibPas.