Odborný článok

Prečo validátory odmietajú podpisy PAdES: PDFium v Delphi

Validátor odmietne podpis PAdES takmer v každom prípade, ktorý sme s PDFium Component v Delphi debugovali, z jedného z troch dôvodov: pole /ByteRange stále obsahuje svoje nulové zástupné znaky (placeholders), čas podpisu /M nie je správne naformátovaný reťazec dátumu PDF alebo prírastková aktualizácia vynechala položku /Encrypt zo zašifrovaného dokumentu. Všetky tri prípady produkujú súbory, ktoré sa bez problémov otvoria a vykreslia, no zlyhajú v momente, keď štandardný validátor prečíta slovník podpisu

Scenár, ktorý motivuje tento článok, je bolestne špecifický. Podpíšete zmluvu pomocou postupu z článku o podpisovaní PAdES B-B, váš vlastný kód na kontrolu hlási, že podpis je prítomný a štrukturálne v poriadku, každý lokálny test prejde na zeleno — a potom druhá strana nahrá súbor na svoju validačnú platformu a dostane červený krížik. Na zlyhanie vás neupozorní nič v prehliadači, pretože žiadna z týchto troch chýb sa nedotýka obsahu stránky. Nachádzajú sa výhradne v slovníku podpisu a na konci súboru, čo je presne to miesto, kam sa validátory pozerajú, no prehliadače ho väčšinou ignorujú

Prečo je môj PDF podpis ByteRange neplatný?

Odmietnutie ByteRange takmer vždy znamená, že štyri polia neboli nikdy vyplnené, a nie, že by rozsahy boli len mierne nesprávne. Pole /ByteRange [A B C D] deklaruje dva rozsahy, od bajtu A po A+B a od bajtu C po C+D, a norma EN 319 142-1 §6.3 (požiadavka k) vyžaduje, aby spoločne pokrývali celý súbor okrem hexadecimálne kódovaného reťazca /Contents. Vyjadrené v číslach: A je 0, C >= A+B, C+D sa rovná celkovej dĺžke súboru a medzera medzi B a C obsahuje presne hexadecimálny reťazec <...> — dva znaky zátvoriek plus dva hexadecimálne znaky na každý bajt dát CMS. Validátor, ktorý prečíta [0 0 0 0], dospeje k záveru, že podpis nepokrýva nič a odmietne ho bez ohľadu na to, ako správna je štruktúra CMS vo vnútri /Contents

Mechanizmus, ako k tomu dochádza, stojí za pochopenie, pretože rovnaký vzor existuje v každom podpisovom nástroji. Podpisujúci nemôže poznať finálne posuny (offsets), kým nie je súbor rozvrhnutý, takže zapisovač vygeneruje pole s nulovými zástupnými hodnotami s pevnou šírkou a po rozvrhnutí ich spätne doplní. Vo verziách PDFium Component pred verziou 2.14.1 toto spätné dopĺňanie hľadalo vzor zástupných hodnôt od pozície /Contents — lenže /ByteRange sa v slovníku nachádza pred /Contents, takže hľadanie nič nenašlo a všetky tri nahradenia potichu zlyhali. Kryptografický odtlačok CMS bol vypočítaný pre správne rozsahy, takže šifrovanie bolo v poriadku; deklarácia týchto rozsahov však zostala na nule, takže každý vyhovujúci validátor súbor odmietol. Časové pečiatky dokumentu PAdES B-LTA, ktoré používajú rovnaké rozvrhnutie slovníka, zlyhávali rovnakým spôsobom — čo je dôležité, ak staviate na dlhodobých podpisoch B-LT a B-LTA. Verzia 2.14.1 dopĺňa hodnoty od začiatku objektu podpisu a oprava je pokrytá regresným testom, ktorý analyzuje vytvorený súbor a overuje matematiku uvedenú nižšie

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // first signature only
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: spans cover everything except /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

Dvadsať riadkov čistého RTL kódu bez volania externých knižníc zachytí celú túto kategóriu zlyhaní priamo pri generovaní. Ak si z tohto článku máte odniesť jednu kontrolu, vyberte si túto: analyzujte svoj vlastný podpísaný výstup a pred odoslaním súboru skontrolujte tieto tri rovnosti

Prečo validátory označujú čas podpisu /M za neplatný?

Prísne validátory odmietnu čas podpisu, ktorý nie je kompletným dátumovým reťazcom PDF, pričom najčastejšie chýbajú dve časti: predpona D: a označenie posunu UTC. Norma ISO 32000-1 §7.9.4 definuje formát dátumu ako D:YYYYMMDDHHmmSS, za ktorým nasleduje posun — Z pre UTC, alebo znamienkom určený vzťah +HH'mm' k nemu. Samotný reťazec 20260709143000 tolerantná čítačka prečíta ako dátum, avšak validátor uplatňujúci pravidlá doslovne uvidí nesprávny formát v povinnom poli a podpis označí za chybný. Verzie komponentu PDFium pred verziou 2.14.4 zapisovali položku /M pre metódy TPdf.SignPades a SignPadesBytes presne v tejto holej podobe; od verzie 2.14.4 táto položka obsahuje predponu D: a označenie Z, takže deklarovaný čas podpisu má tvar D:20260709143000Z

K tomuto poľu sa viažu dve praktické poznámky. Po prvé, zapisujte čas v UTC a explicitne to uveďte: časová pečiatka bez označenia posunu núti validátor hádať časové pásmo, pričom sekcia §7.9.4 považuje posun za pevnú súčasť formátu a nie za voliteľný doplnok. Po druhé, pamätajte na to, čo je /M — inde o čas deklarovaný podpisujúcim, teda tvrdenie a nie dôkaz. Validátor tu overuje iba formát a nie pravdivosť údajov; overiteľný čas pochádza z časovej pečiatky RFC 3161 na úrovni PAdES B-T a vyššie. Správne formátovanie poľa a dôvera k nemu sú dve odlišné veci a validátory vyžadujú iba to prvé

Prečo SignPades vyvoláva výnimku EPadesCrypto na zašifrovanom PDF?

PDFium Component odmieta podpísať zašifrovaný dokument, pretože druhou možnosťou je vytvorenie súboru, ktorý štandardné čítačky pri otvorení poškodia. Podpis PAdES sa pripája ako prírastková aktualizácia (incremental update) a norma ISO 32000-1 §7.5.6 vyžaduje, aby nová koncová sekcia (trailer) aktualizovanej časti obsahovala všetky položky predchádzajúceho traileru okrem /Prev — čo pri zašifrovanom dokumente zahŕňa aj /Encrypt. Ak ju vynecháte, najnovší trailer vyhlási súbor za nezašifrovaný, takže vyhovujúca čítačka bude analyzovať zašifrované telo ako čistý text a výsledkom budú poškodené dáta. Čo je ešte horšie, prúdy a reťazce, ktoré podpisujúci pripája, by museli byť zašifrované kľúčom dokumentu, aby boli platné, čo program na vkladanie čistého textu podpisu nedokáže. Neexistuje spôsob, ako pripojiť platný nezašifrovaný podpis k zašifrovanému súboru, preto od verzie 2.14.2 metódy TPdf.SignPades, SignPadesBytes and InjectPadesDssMarkers namiesto produkcie poškodeného alebo neoveriteľného súboru radšej vyvolajú výnimku EPadesCrypto

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // e.g. 'SignPadesBytes: the source document is encrypted;
    //       remove encryption before signing'
    Writeln('Cannot sign: ', E.Message);
  end;
end;

Vyvolanie výnimky je správnym výsledkom, preto navrhnite proces spracovania okolo nej a nesnažte sa ju len zachytiť a opakovať pokus. Najprv dešifrujte dokument s právami vlastníka, podpíšte nezašifrovanú kópiu a ak distribučný kanál vyžaduje šifrovanie, akceptujte, že postupy šifrovanie-potom-podpis a podpis-potom-šifrovanie vytvárajú rôzne artefakty s rôznymi príbehmi validácie. Podpis vypočítaný nad nezašifrovanými bajtmi nemôže prežiť následné šifrovanie týchto bajtov, takže reálnymi možnosťami sú podpísaný nezašifrovaný súbor alebo strategické rozhodnutie zdokumentované priamo pri kóde

Ako môžu dve chyby vzájomne potvrdiť svoju správnosť

Chyba ByteRange sa skrývala tak dlho preto, lebo náš vlastný validátor bol chybný komplementárnym spôsobom, a to je najdôležitejšie ponaučenie z tohto prípadu. Kontrola pokrytia v ValidatePadesCompliance vyžadovala, aby druhý rozsah začínal presne na A+B — teda s nulovou medzerou — čo nesprávne klasifikuje štandardné rozvrhnutie, kde medzera obsahuje hexadecimálny reťazec /Contents. Interný validátor preto odmietal presne to rozvrhnutie, ktoré bolo v súlade s normou, a interný generátor ho nikdy nevyprodukoval, takže procesy typu podpíš-a-over zostávali zelené. Každá chyba tak pripravila testovaciu sadu o protipríklad, ktorý by odhalil tú druhú. Verzia 2.14.1 opravila obe strany v rovnakom vydaní: generátor dopĺňa všetky štyri polia a validátor akceptuje C >= A+B, pričom C+D sa rovná dĺžke súboru

Metodickou opravou je krížová validácia voči implementácii, ktorú ste sami nenapísali. Generátor a validátor, ktoré zdieľajú rovnaký kód, autora alebo len rovnakú predstavu o formáte, sa môžu na vzájomnom nepochopení zhodovať donekonečna; nezávislý validátor túto symetriu narúša. Pred vydaním spustite svoj podpísaný výstup cez aspoň jeden externý validačný nástroj a ponechajte štrukturálnu vlastnú kontrolu v procese zostavenia ako rýchlu prvú líniu obrany — metóda TPdf.ValidatePades opísaná v článku o inšpekcii podpisov hlási zlyhanie pokrytia ako pomenovaný problém

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange does not cover the file');
  if not R.IsCompliant then
    Writeln('Structural issues present: do not ship this file');
end;

Kontrolný zoznam pri odmietnutí podpísaného výstupu

Keď platforma odmietne váš podpis PAdES, skontrolujte najprv jednoduché štrukturálne príčiny a až potom hľadajte vinu v certifikátoch či reťazcoch dôvery. Prečítajte pole /ByteRange a overte tri rovnosti: prvé pole nula, druhý rozsah začínajúci na konci prvého alebo za ním, druhý rozsah končiaci presne na konci súboru. Prečítajte položku /M a skontrolujte, či ide o kompletný reťazec dátumu podľa §7.9.4 s predponou D: a označením časového posunu. Overte, či zdrojový dokument nebol zašifrovaný pri pripájaní podpisu — a ak ho váš nástroj napriek tomu bez sťažností podpísal, považujte toto ticho za chybu nástroja. Všetky tri kontroly prebehnú nad čistými bajtmi v priebehu milisekúnd a podľa našich skúseností vysvetľujú odmietnutie oveľa častejšie než akákoľvek kryptografická príčina

Volania na podpisovanie, kontrolu a validáciu, ktoré sú tu použité — SignPades, ValidatePades a kontroly zhody PAdES s ich opravenou matematikou ByteRange, formátovaním dátumu a blokovaním šifrovania — sa dodávajú s produktom PDFium Component pre Delphi, C++Builder a Lazarus