Technický článek

Digitální podpisy PAdES v Delphi: Podepisování a ověřování s PDF Library for Delphi

Ověřit jeden podpis PAdES znamená zkontrolovat tři nezávislé věci a zelená fajfka v prohlížeči vám říká jen o té třetí. Zaprvé, pole /ByteRange musí pokrývat správné bajty: rozsahy, které pojmenovává, musí zrekonstruovat přesně ten vstup, nad nímž byl otisk CMS pořízen, a mimo ně nesmí zůstat žádný podepsaný bajt. Zadruhé, certifikát uvnitř CMS musí vést řetězem ke kořeni, jemuž důvěřujete, a nést podepsaný atribut signing-certificate, který PAdES vyžaduje. Zatřetí, hlásí-li profil časové razítko, musí token podle RFC 3161 vázat hodnotu podpisu k okamžiku před vypršením certifikátu. Acrobat všechny tři věci slévá do jediné ikony; kontrolor shody je drží odděleně a stejně by je měl držet i kód, který tyto soubory vyrábí. losLab PDF Library (PDF Library for Delphi) vám dává podepisovací stranu, opětovné vložení časového razítka a auditní volání k prozkoumání pole ByteRange dřív, než mu uvěříte

Jeden rozdíl podráží nohy téměř každé první implementaci PAdES, takže stojí za zmínku ještě před jakýmkoli kódem. Podpis zapsaný s /SubFilter /adbe.pkcs7.detached je zcela v pořádku podle ISO 32000-1 §12.8 a Acrobat jej ohlásí jako platný. Zároveň to není podpis PAdES, protože ETSI EN 319 142-1 vyžaduje ETSI.CAdES.detached na každé baseline úrovni. Kontrolor shody podle eIDAS ten první odmítne a druhý přijme, přestože je kryptografie totožná. Profil je tvrzení, které dokument činí sám o sobě, a nastavit toto tvrzení správně je v PDF Library for Delphi jedno volání

Co dělá z podpisu v PDF podpis PAdES

ETSI EN 319 142-1 definuje čtyři baseline úrovně naskládané na formát CMS. PAdES-B-B je vstupní branou: podpis CAdES v podpisovém poli PDF se SubFilterem ETSI.CAdES.detached a podepsaným atributem signing-certificate. PAdES-B-T přidává časové razítko podle RFC 3161 nad hodnotou podpisu, čímž dokládá, že podpis existoval před okamžikem, který nikdo nemůže antedatovat. PAdES-B-LT vkládá certifikáty, seznamy CRL a odpovědi OCSP potřebné k ověření do úložiště Document Security Store, takže soubor zůstává ověřitelný i poté, co vydávající CA odstaví svou infrastrukturu. PAdES-B-LTA celou stavbu korunuje časovým razítkem dokumentu, jež nashromážděné důkazy znovu chrání ve chvíli, kdy algoritmy slábnou

PDF Library for Delphi tyto pojmy mapuje na své API podepisovacího procesu. Značkou profilu je SetSignProcessCustomSubFilter. Vyžaduje-li vaše politika údaj o typu závazku (doklad o původu, doklad o schválení nebo některý z dalších identifikátorů ETSI číslovaných 1 až 6), ten jde přes SetSignProcessCommitmentType. Explicitní politika podpisu se připojuje přes SetSignProcessSignaturePolicy, které bere OID politiky a její otisk. Jedno výchozí nastavení si zaslouží pozornost: je-li algoritmus otisku ponechán na automatice, volí knihovna pro podpisy ETSI a adbe.pkcs7.detached algoritmus SHA-256 a k SHA-1 sahá jen u starší cesty adbe.pkcs7.sha1. Nastavte jej stejně výslovně. Auditoři se ptají, jaký hash jste použili, a explicitní hodnota v kódu se obhajuje snáz než výchozí volba, kterou musíte jít vysvětlovat do manuálu

Žebříček základních úrovní PAdES B-B, B-T, B-LT a B-LTA postavený s PDF Library for Delphi ukazující, jak každá úroveň přidává časová razítka, DSS důkazy nebo obnovitelné časové razítko dokumentu na jádro ETSI.CAdES.detached
Každá úroveň ETSI baseline staví na stejném jádru CAdES jednu další záruku, od podepsaných atributů po obnovitelné časové razítko dokumentu

Výroba základního podpisu

Ploché API řídí podepisování jako jednorázový stavový automat: otevřete proces nad zdrojovým souborem, nakonfigurujete jej, dokončíte do výstupního souboru, přečtete výsledný kód. Sekvence níže vytvoří podpis PAdES-B-B se SHA-256. Řádek, na kterém záleží nejvíc, přitom se samotným podpisem nemá nic společného. Je to záměrně předimenzovaná rezervace /Contents, protože právě tu nelze později změnit, kdyby k tomuto podpisu mělo být někdy přidáno časové razítko

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);  // místo pro pozdější razítko
    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 vrací 0, když zdroj vůbec nelze otevřít. Poté GetSignProcessResult odlišuje způsoby selhání, které v produkci skutečně nastávají: 4 znamená špatné heslo k PDF, 7 špatné heslo k PFX, 9 soubor certifikátu bez soukromého klíče, 10 výstupní cestu, do níž nelze zapisovat, 11 selhání během vkládání bajtů podpisu. Zalogování číselného kódu vedle názvu vstupního souboru promění mlhavý požadavek podpory v minutovou diagnózu

Přidání časového razítka podle RFC 3161, které za vás knihovna nestáhne

PDF Library for Delphi nedodává žádného klienta TSA a jde o záměrnou hranici, ne o mezeru. Knihovna spočítá otisk, který musí autorita časových razítek spolupodepsat, a poté rozšířené CMS znovu vloží; výměna po HTTP a chirurgie nad CMS mezi tím patří volajícímu. Pro to rozdělení existuje tvrdý technický důvod. Řídicí prvek Windows CryptoAPI, který nominálně přidává nepodepsané atributy, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, selhává s CRYPT_E_INVALID_INDEX nad odpojeným rozvržením SignedData, jaké PAdES používá. Rozšířené CMS tedy musí vzejít z kodéru CMS pod vaší vlastní kontrolou. Žádná knihovna nedokáže token tiše přiložit jediným systémovým voláním a každá, která to tvrdí, provádí tu chirurgii někde, kam nevidíte

Pipeline pro přidání časového razítka RFC 3161 k podpisu PAdES v Delphi oddělující hašování a vkládání PDF Library for Delphi od TSA žádosti volajícího a překódování CMS uvnitř vyhrazeného prostoru /Contents
Knihovna hashuje a znovu vkládá, zatímco váš kód vyzvedne token a provede CMS operaci; výsledek musí dopadnout do rezervace /Contents o 8192 bajtech
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);
    // obě volání níže jsou aplikační kód: HTTP POST na vaši TSA
    // a překódování CMS, které token připojí jako nepodepsaný atribut
    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;

Hlídejte tady výsledné kódy: 12 znamená, že pojmenované podpisové pole neexistuje, 11 že stávající CMS nešlo rozebrat a 13 že se rozšířené CMS už nevejde do rezervovaného zástupného místa /Contents. Kód 13 je ten, co bolí, protože jedinou nápravou je podepsat znovu: typický token časového razítka s řetězcem certifikátů má 4 až 6 KB a rezervace 8192 bajtů udělaná v kroku B-B existuje právě proto, aby měl tento krok kam dosednout

Ověření začíná u ByteRange, ne u řetězce certifikátů

Zelená fajfka v prohlížeči je rozhodnutí o důvěře vůči úložišti certifikátů daného stroje, nikoli strukturální verdikt o souboru. Programové ověřování by mělo začínat níž, otázkou, kterou inkrementální aktualizace činí záludnou: které bajty vlastně každý podpis pokrývá? Každé vylepšení, o němž je tu řeč, ať jde o druhý podpis, slovník DSS nebo časové razítko dokumentu, přichází inkrementální aktualizací a každá taková aktualizace připojí bajty mimo /ByteRange dřívějšího podpisu. Tyto připojené bajty jsou legitimní. Validátor je přesto musí zařadit vůči politice úprav dokumentu a úroveň DocMDP pro jednotlivá pole, v níž tato politika žije, se čte přes GetSignatureDocMDPLevelByName

Audit rozložení bajtů podepsaného PDF v Delphi ukazující pokryté úseky ByteRange, vyloučené bajty /Contents, připojené inkrementální aktualizace mimo rozsah a verdikt pokrytí proti velikosti souboru
Dva pokryté úseky s vyloučenými vlastními bajty podpisu vyprávějí pravdivý příběh pokrytí a připojené updaty se klasifikují podle politiky DocMDP místo aby vyvolávaly obavy
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // před Open: SignDoc drží zámek sdílení
  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 znamená skutečně podepsáno
        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;

V této auditní cestě číhají dvě pasti. TPDFlibSignDoc.Open drží soubor výhradním zámkem sdílení, takže validátor, který chce zároveň hašovat surové bajty souboru kvůli ověření CMS, musí soubor načíst do paměti dřív, než jej otevře k auditu. Otočte to pořadí a čtení selže na zámku, který jste si nasadili sami. Druhá past je tichá, ne hlasitá: protějšek z plochého API GetSignProcessByteRange vrací Integer, zatímco podkladové offsety jsou Int64, takže nad 2 GB ploché volání bez námitek ořízne, a proto tento příklad tahá offsety přes auditní třídu. Za pojmenování stojí i jedna nepřítomnost. Plochá vrstva nemá vůbec žádnou obálku VerifySignature. Kryptografické verdikty přicházejí z třídní úrovně, z TPDFlibSignatureVerifier, jenž vrací vsValid, vsInvalid, nebo vsUnknown, případně od externího validátoru, jemuž vaše politika shody už důvěřuje

Dlouhodobé ověřování: DSS, VRI a časové razítko dokumentu

PAdES-B-LT existuje proto, že infrastruktura pro odvolávání certifikátů je smrtelná. ETSI EN 319 142-1 §5.4.2.2 specifikuje úložiště Document Security Store: slovník na úrovni dokumentu nesoucí certifikáty, seznamy CRL a odpovědi OCSP, volitelně indexovaný po jednotlivých podpisech přes položky VRI klíčované otiskem /Contents každého podpisu. Postup v PDF Library for Delphi zrcadlí návrh pro časové razítko. NewPAdESDSSProcessFromFile proces otevře; AddPAdESDSSCertificate, AddPAdESDSSCRL a AddPAdESDSSOCSP přijímají bloby v DER; AddPAdESDSSVRI naváže vybraný materiál na jeden podpis; EndPAdESDSSProcessToFile zapíše všechno jako inkrementální aktualizaci. Ta těžká část zůstává na vaší straně. Obstarat materiál o odvolání a posoudit, zda je dost čerstvý, aby stál za vložení, je úkolem volajícího. Knihovna zaručuje, že slovníky jsou strukturálně konformní; nemůže zaručit, že váš OCSP responder mluvil pravdu

Archivní koncový bod, B-LTA, přidává časové razítko dokumentu: samostatné podpisové pole, jehož typ je DocTimeStamp, nikoli Sig, vyráběné přes SetSignProcessDocTimeStamp s rezervovanou délkou podpisu. Nenahrazuje časové razítko podpisu z kroku B-T. Časové razítko podpisu dokládá, kdy existoval jeden konkrétní podpis; časové razítko dokumentu chrání celý soubor včetně důkazů v DSS a je tím prvkem, který dlouhodobý archiv každých pár let obnovuje, jak algoritmy slábnou. Zralý archivní profil nese obojí. Kvůli čtečkám starším než tyto struktury zapíše TPDFlibSignDoc.EnsurePAdESExtensions do katalogu dokumentu vývojářské rozšíření ESIC, čímž oznámí, že soubor používá prvky definované ETSI

Jedné reakci na tohle všechno stojí za to předejít, protože vypadá jako chyba, a není jí. Prohlížeč u souboru se zcela správnou strukturou PAdES často hlásí „platnost neznámá“. Důvěra a struktura jsou nezávislé osy. Prohlížeč prostě nedokáže podepisujícího navázat řetězem na kořen, jemuž na daném stroji důvěřuje, což je u privátních CA a testovacích certifikátů běžné, i když audit ByteRange i ověření CMS projdou. Nápravou je řádně distribuovat kořenový certifikát, nebo, je-li skutečným cílem kvalifikovaný status podle eIDAS, vyhodnocovat vůči důvěryhodným seznamům EU, nikoli sahat do podepisovacího kódu

Pohled ze strany auditu, tedy vyjmenování podpisových polí napříč korpusem, výpis rozvržení ByteRange a hromadné čtení úrovní DocMDP, najdete v doprovodném textu o pracovním prostředí pro shodu a podepisování. Podepsané dokumenty, které musí splnit i archivní politiku, patří do postupu popsaného v článku Preflight PDF/A a PDF/UA v Delphi. Kompletní dokumentace API a evaluační verze ke stažení jsou na produktové stránce losLab PDF Library for Delphi