Technical Article

PAdES цифрови подписи в Delphi: Подписване и валидиране с PDFlibPas

Валидирането на един PAdES подпис изисква проверка на три независими неща, а зелената отметка в програмата за преглед ви информира само за третото. Първо: масивът /ByteRange трябва да покрива правилните байтове, като диапазонът от данни трябва да съответства точно на входящите данни за изчисляване на CMS дайджеста, без подписани байтове извън него. Второ: сертификатът в CMS трябва да води до доверен коренен сертификат и да съдържа подписания атрибут за сертификат на подписващия, изискван от PAdES. Трето: ако профилът изисква времеви печат, токен по RFC 3161 трябва да обвърже стойността на подписа с момент във времето преди изтичането на сертификата. Acrobat обединява тези три стъпки в една икона; програма за проверка на съвместимост ги анализира поотделно, както би трябвало да прави и кодът, който генерира тези файлове. losLab PDF Library (PDFlibPas) предоставя възможности за подписване, вграждане на времеви печат и одит на ByteRange, преди да се доверите на файла.

Една разлика затруднява почти всяка първа имплементация на PAdES, затова е добре да я изясним преди разглеждането на кода. Подпис, направен с /SubFilter /adbe.pkcs7.detached, е напълно коректен подпис по ISO 32000-1 §12.8, който Acrobat ще отчете като валиден. Той обаче не е PAdES подпис, тъй като стандартът ETSI EN 319 142-1 изисква използването на ETSI.CAdES.detached за всяко базово ниво. Валидатор за съвместимост с eIDAS ще отхвърли първия вариант и ще приеме втория, въпреки че криптографията е идентична. Профилът е декларация, която документът прави за себе си, а задаването на тази декларация се извършва с едно извикване в PDFlibPas.

Какво превръща подписа в PDF в PAdES подпис

ETSI EN 319 142-1 дефинира четири базови нива, надграждащи CMS формата. PAdES-B-B е отправната точка: CAdES подпис в PDF поле за подпис със SubFilter ETSI.CAdES.detached и подписан атрибут за сертификат на подписващия. PAdES-B-T добавя времеви печат по RFC 3161 над стойността на подписа, доказвайки съществуването му в момент от времето, който никой не може да промени с задна дата. PAdES-B-LT вгражда сертификати, CRL списъци и OCSP отговори за валидиране в речник Document Security Store (DSS), така че файлът остава проверим и след като издаващият орган (CA) прекрати своята инфраструктура. PAdES-B-LTA завършва стека с времеви печат на документа, който защитава натрупаните доказателства при отслабване на криптографските алгоритми.

Генериране на базов подпис

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

var

  Pdf: TPDFlib;

  SignId: Integer;

begin

  Pdf := TPDFlib.Create;

  try

    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');

    if SignId = 0 then

      raise Exception.Create('cannot open source PDF');

    Pdf.SetSignProcessField(SignId, 'Sig1');

    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);

    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');

    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');

    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256

    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later

    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');

    if Pdf.GetSignProcessResult(SignId) <> 1 then

      raise Exception.CreateFmt('signing failed, code %d',

        [Pdf.GetSignProcessResult(SignId)]);

    Pdf.ReleaseSignProcess(SignId);

  finally

    Pdf.Free;

  end;

end;

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

Добавяне на времеви печат по RFC 3161, който библиотеката няма да извлече вместо вас

PDFlibPas не съдържа вграден TSA клиент, което е съзнателно решение за ограничаване на обхвата, а не пропуск. Библиотеката изчислява хеша, който сървърът за времеви печат (TSA) трябва да подпише, и вгражда обратно разширения CMS; HTTP комуникацията и обработката на CMS са задача на извикващия код. Има сериозна техническа причина за това разделение: функцията на Windows CryptoAPI за добавяне на неподписани атрибути (CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR) се проваля с грешка CRYPT_E_INVALID_INDEX при структурата SignedData, използвана от PAdES. Поради тази причина разширеният CMS трябва да бъде генериран от кодер за CMS под ваш контрол. Никой инструмент не може безшумно да внедри токена с едно извикване и всеки, който твърди обратното, извършва тази операция скрито.

var

  Pdf: TPDFlib;

  StsId: Integer;

  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;

begin

  Pdf := TPDFlib.Create;

  try

    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');

    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');

    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);

    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);

    // both calls below are application code: an HTTP POST to your TSA,

    // and a CMS re-encode that attaches the token as an unsigned attribute

    TstDer := RequestTimeStampToken(HashHex);

    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);

    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);

    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);

    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');

    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then

      raise Exception.Create('timestamp embedding failed');

    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);

  finally

    Pdf.Free;

  end;

end;

Следете кодовете за грешка тук: 12 означава, че посоченото поле за подпис не съществува, 11 - че съществуващият CMS не може да бъде анализиран, а 13 - че разширеният CMS вече не се побира в запазеното място в /Contents. Грешка 13 е най-сериозната, тъй като единственото решение е повторно подписване: стандартният токен за времеви печат със своята верига от сертификати заема между 4 и 6 KB, а запазването на 8192 байта по време на фазата B-B съществува именно за да осигури достатъчно място за тази операция.

Валидирането започва от ByteRange, а не от веригата сертификати

Зелената отметка в четеца е решение за доверие спрямо базата данни със сертификати на съответната машина, а не структурна проверка на файла. Програмното валидиране трябва да започне от по-ниско ниво, с въпроса, който инкременталните актуализации правят сложен: кои байтове действително покрива всеки подпис? Всяко разширение, независимо дали е втори подпис, DSS речник или времеви печат на документа, се добавя като инкрементална актуализация, като всяка актуализация добавя байтове извън първоначалния /ByteRange на подписа. Тези добавени байтове са напълно легитимни. Валидаторът трябва да ги класифицира съгласно политиката за промяна на документа, като нивото DocMDP на тази политика за всяко поле може да бъде прочетено чрез GetSignatureDocMDPLevelByName.

var

  Doc: TPDFlibSignDoc;

  Names: TStringList;

  I: Integer;

  B0, B1, B2, B3, FileSize: Int64;

begin

  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock

  Doc := TPDFlibSignDoc.Create;

  try

    if not Doc.Open('invoice-bt.pdf', '', False) then

      raise Exception.Create('cannot open for audit');

    Names := TStringList.Create;

    try

      Doc.GetSignatureFieldNames(Names);

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

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

        begin

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

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

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

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

          if (B0 = 0) and (B2 + B3 = FileSize) then

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

          else

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

        end;

    finally

      Names.Free;

    end;

    Doc.Close;

  finally

    Doc.Free;

  end;

end;

Два капана съществуват в този одитен процес. Методът TPDFlibSignDoc.Open заключва файла с изключително право, така че валидаторът, който иска да изчисли хеша на файла за проверка на CMS, трябва да прочете файла в паметта преди отварянето му за одит. Ако обърнете този ред, четенето ще се провали поради заключване, което сами сте наложили. Вторият капан е тих: функцията GetSignProcessByteRange от основния API връща тип Integer, докато базовите отмествания са Int64, което означава, че при файл над 2 GB стойността ще бъде съкратена без предупреждение. Поради тази причина в този пример отместванията се извличат през одитния клас. Трябва да отбележим и една липса: основният API не съдържа обвивка VerifySignature. Криптографските резултати се извличат от класа TPDFlibSignatureVerifier, който връща vsValid, vsInvalid или vsUnknown, или от външен валидатор, на когото се доверявате.

Дългосрочно валидиране: DSS, VRI и времеви печат на документа

PAdES-B-LT съществува, тъй като инфраструктурата за проверка на отменени сертификати не е вечна. ETSI EN 319 142-1 §5.4.2.2 специфицира Document Security Store: речник на ниво документ, съдържащ сертификати, CRL списъци и OCSP отговори, опционално индексирани за всеки подпис чрез VRI записи, свързани с хеша на /Contents на подписа. Процесът в PDFlibPas съответства на този при времевия печат. NewPAdESDSSProcessFromFile отваря процеса; AddPAdESDSSCertificate, AddPAdESDSSCRL, и AddPAdESDSSOCSP приемат DER масиви; AddPAdESDSSVRI свързва данните с конкретен подпис; EndPAdESDSSProcessToFile записва всичко като инкрементална актуализация. Основната задача остава за вас: извличането на информация за отменени сертификати и оценката дали тя е достатъчно актуална за вграждане е отговорност на разработчика. Библиотеката гарантира съвместимостта на речниците, но не може да гарантира достоверността на OCSP отговорите.

Крайната архивна точка (B-LTA) добавя времеви печат на документа: отделно поле за подпис с тип DocTimeStamp вместо Sig, създадено чрез SetSignProcessDocTimeStamp със запазена дължина. То не заменя времевия печат на подписа от стъпка B-T. Времевият печат на подписа доказва съществуването на конкретен подпис, докато времевият печат на документа защитава целия файл (включително DSS данните) и е елементът, който архивните системи подновяват през няколко години при остаряване на алгоритмите. Пълният архивен профил съдържа и двата елемента. За четци, които не поддържат тези структури, методът TPDFlibSignDoc.EnsurePAdESExtensions записва разширението ESIC в каталога на документа, указвайки използването на ETSI спецификации.

Една реакция трябва да бъде предотвратена, тъй като изглежда като софтуерен проблем, но не е: четецът често показва статус "неизвестна валидност" на файл с правилна PAdES структура. Доверието и структурата са различни неща. Четецът просто не може да свърже сертификата с доверен корен на съответната машина, което е нормално при частни CA и тестови сертификати, въпреки че проверката на ByteRange и CMS съвпадат. Решението е правилно разпространение на коренния сертификат или проверка спрямо списъците с доверени доставчици на ЕС (EU trusted lists), ако целта е придобиване на квалифициран eIDAS статус.

За одитна гледна точка (изброяване на полета за подписи, ByteRange структури и DocMDP нива в големи обеми), вижте статията за работна среда за съвместимост и подписване. Подписаните документи, които трябва да изпълняват и политики за архивиране, следват процеса, описан в PDF/A и PDF/UA проверка в Delphi. Пълната документация за API и версии за оценка са налични на продуктовата страница на PDFlibPas за Delphi.