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

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 завършва стека с времеви печат на документа, който защитава натрупаните доказателства при отслабване на криптографските алгоритми

PDFlibPas свързва тези понятия със своя API за процеса на подписване. Маркерът на профила се задава чрез SetSignProcessCustomSubFilter. Ако политиката ви изисква указване на вида ангажимент, например доказателство за произход, доказателство за одобрение или друг ETSI идентификатор от 1 до 6, използвайте SetSignProcessCommitmentType. Явна политика за подпис се прикачва чрез SetSignProcessSignaturePolicy, който приема OID на политиката и нейния дайджест. Една стойност по подразбиране заслужава внимание: когато алгоритъмът за дайджест е оставен на автоматичен избор, библиотеката използва SHA-256 за ETSI и adbe.pkcs7.detached подписи и преминава към SHA-1 само за наследения път adbe.pkcs7.sha1. Въпреки това го задавайте изрично. Одиторите питат кой хеш е използван, а явната стойност в кода се защитава по-лесно от подразбиране, което трябва да обяснявате чрез ръководството

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

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

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