Technical Article

Zašto validatori odbijaju PAdES potpise: PDFium u Delphiju

Validator odbija PAdES potpis iz jednog od tri razloga u gotovo svakom slučaju koji smo otklanjali (debuggirali) s PDFium komponentom u Delphiju: polje /ByteRange i dalje drži svoje nulte rezervirane položaje (placeholders), vrijeme potpisivanja /M nije ispravno oblikovan PDF niz datuma ili je inkrementalno ažuriranje ispustilo unos /Encrypt iz šifriranog dokumenta. Sva tri razloga proizvode datoteke koje se ispravno otvaraju i iscrtavaju, ali ne uspijevaju u trenutku kada sukladni validator pročita rječnik potpisa (signature dictionary)

Scenarij koji motivira ovaj članak bolno je specifičan. Potpišete ugovor s tijekom rada iz članka o potpisivanju PAdES B-B, vaš vlastiti kôd za inspekciju javlja da je potpis prisutan i strukturno ispravan, svaki lokalni test je zelen — a zatim druga ugovorna strana učita datoteku na svoju platformu za validaciju i dobije crveni križ. Ništa od neuspjeha nije vidljivo u pregledniku, jer nijedan od ova tri kvara ne dira sadržaj stranice. Oni žive u potpunosti u rječniku potpisa i na kraju datoteke (file trailer), što je točno mjesto gdje validatori gledaju, a preglednici uglavnom ne

Zašto je moj PDF potpis ByteRange nevaljan?

Odbijanje ByteRange-a gotovo uvijek znači da četiri polja nikada nisu popunjena, a ne da su rasponi suptilno pogrešni. Niz /ByteRange [A B C D] deklarira dva raspona, bajtove od A do A+B i bajtove od C do C+D, a EN 319 142-1 §6.3 (zahtjev k) zahtijeva da zajedno pokrivaju cijelu datoteku osim heksadecimalno kodiranog niza /Contents. U brojevima: A je 0, C >= A+B, C+D je jednak duljini datoteke, a praznina između B i C sadrži točno heksadecimalni niz <...> — dva bajta zagrada plus dva heksadecimalna znaka po bajtu CMS podataka. Validator koji pročita [0 0 0 0] zaključuje da potpis ne pokriva ništa i odbija ga, bez obzira na to koliko je ispravna CMS struktura unutar /Contents

Mehaniku kako se to događa vrijedi razumjeti jer isti obrazac postoji u svakom stogu za potpisivanje (signing stack). Potpisnik ne može znati konačne pomake dok se datoteka ne rasporedi, pa pisac emitira niz s nultim rezerviranim položajima fiksne širine i popunjava ih nakon rasporeda. U izdanjima PDFium komponente prije verzije 2.14.1, to popunjavanje je tražilo obrazac rezerviranog položaja počevši od pozicije /Contents — ali /ByteRange se nalazi ispred /Contents u rječniku, pa pretraga nije pronašla ništa i sva tri popunjavanja su tiho zakazala. CMS sažetak (digest) bio je izračunat nad ispravnim rasponima, pa je kriptografija bila čvrsta; deklaracija tih raspona ostala je na nuli, tako da je svaki sukladni validator odbio datoteku. Vremenske oznake dokumenata PAdES B-LTA, koje koriste isti izgled rječnika, zakazale su na isti način — što je važno ako gradite na dugotrajnim potpisima B-LT i B-LTA. Verzija 2.14.1 popunjava od početka objekta potpisa, a ispravak je pokriven regresijskim testom koji raščlanjuje proizvedenu datoteku i potvrđuje dolje navedenu aritmetiku

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;

Dvadeset redaka običnog RTL koda, bez poziva knjižnica, i hvata cijelu klasu pogrešaka na samom mjestu proizvodnje. Ako zadržite jednu tvrdnju (assertion) iz ovog članka, zadržite ovu: raščlanite vlastiti potpisani izlaz i provjerite tri jednakosti prije nego što datoteka napusti vaš proces

Zašto validatori označavaju vrijeme potpisivanja /M kao loše oblikovano?

Strogi validatori odbijaju vrijeme potpisivanja koje nije potpun PDF niz datuma, a dva dijela koja najčešće nedostaju su prefiks D: i oznaka UTC pomaka. ISO 32000-1 §7.9.4 definira format datuma kao D:YYYYMMDDHHmmSS praćen pomakom — Z za UTC ili potpisanim odnosom +HH'mm' prema njemu. Goli niz 20260709143000 popustljiviji čitač analizira kao datum, ali validator koji doslovno primjenjuje gramatiku vidi loše oblikovan niz u polju obveznog formata i označava potpis. Izdanja PDFium komponente prije 2.14.4 pisala su unos /M funkcija TPdf.SignPades i SignPadesBytes u točno tom golom obliku; od verzije 2.14.4 unos nosi prefiks D: i oznaku Z, pa deklarirano vrijeme potpisivanja glasi D:20260709143000Z

Dvije praktične napomene odnose se na ovo polje. Prvo, napišite UTC i tako ga naznačite: vremenska oznaka bez oznake pomaka tjera validator da pogađa odnos vremenske zone, a §7.9.4 tretira pomak kao dio formata, a nula kao opcionalnu sitnicu. Drugo, zapamtite što je /M — vrijeme koje je deklarirao sam potpisnik, tvrdnja radije nego dokaz. Validator ovdje provjerava njegov format, a ne istinitost; dokazivo vrijeme dolazi iz vremenske oznake RFC 3161 na razini PAdES B-T i novijim. Ispravno oblikovanje polja i vjerovanje tom polju dvije su zasebne odluke, a validatori provode samo prvu

Zašto SignPades izaziva iznimku EPadesCrypto na šifriranom PDF-u?

PDFium komponenta odbija potpisati šifrirani dokument jer je alternativa proizvodnja datoteke koju će sukladni čitači uništiti pri otvaranju. PAdES potpis se dodaje kao inkrementalno ažuriranje, a ISO 32000-1 §7.5.6 zahtijeva da novi trailer odjeljka ažuriranja nosi svaki unos prethodnog trailera osim /Prev — što u šifriranom dokumentu uključuje /Encrypt. Ako se to ispusti, najnoviji trailer deklarira datoteku nešifriranom, pa sukladni čitač raščlanjuje šifrirano tijelo kao običan tekst i dobiva smeće. Što je još gore, struje i nizovi koje potpisnik dodaje morali bi sami biti šifrirani ključem dokumenta da bi bili legalni, što ubrizgavač (injector) potpisa u običnom tekstu ne može učiniti. Ne postoji način za dodavanje valjanog potpisa u običnom tekstu šifriranoj datoteci, pa od verzije 2.14.2 funkcije TPdf.SignPades, SignPadesBytes i InjectPadesDssMarkers izazivaju iznimku EPadesCrypto umjesto da emitiraju oštećen ili neprovjerljiv rezultat

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;

Iznimka (exception) je ispravan ishod, stoga dizajnirajte tijek rada oko nje radije nego da je hvatate i pokušavate ponovno. Najprije dešifrirajte dokument s autoritetom vlasnika, potpišite kopiju u običnom tekstu, a ako distribucijski kanal zahtijeva šifriranje, prihvatite da šifriranje-pa-potpisivanje i potpisivanje-pa-šifriranje proizvode različite artefakte s različitim pričama o validaciji. Potpis izračunat nad bajtovima običnog teksta ne može preživjeti ponovno šifriranje tih bajtova, tako da su jedine iskrene mogućnosti potpisana datoteka u običnom tekstu ili odluka o politici dokumentirana uz kôd

Kako dvije pogreške mogu potvrditi jedna drugu

Kvar ByteRange-a skrivao se toliko dugo jer je naš vlastiti validator bio pogrešan na komplementaran način, što je lekcija iz ovog članka koja se najlakše prenosi dalje. Provjera pokrivenosti u ValidatePadesCompliance zahtijevala je da drugi raspon počne točno na A+B — bez praznine — što pogrešno klasificira standardni raspored gdje praznina drži heksadecimalni niz /Contents. Tako je interni validator odbijao upravo sukladan raspored, a interni generator ga nikada nije proizvodio, pa su end-to-end cjevovodi potpiši-pa-validiraj ostajali zeleni. Svaka je pogreška uskraćivala testnom paketu protuprimjer koji bi izložio onu drugu. Verzija 2.14.1 ispravila je obje strane u istom izdanju: generator popunjava sva četiri polja, a validator prihvaća C >= A+B s C+D jednakim duljini datoteke

Metodološki ispravak je unakrsna validacija (cross-validation) u odnosu na implementaciju koju niste sami napisali. Generator i validator koji dijele bazu koda, autora ili čak samo mentalni model formata mogu se unedogled slagati oko zajedničkog nerazumijevanja; neovisni validator razbija tu simetriju. Provucite svoj potpisani izlaz kroz barem jedan vanjski provjerivač sukladnosti prije izdanja i zadržite strukturnu samoprovjeru u izgradnji kao brzu prvu liniju — TPdf.ValidatePades, opisan u članku o inspekciji potpisa, prijavljuje neuspjeh pokrivenosti kao imenovani problem

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;

Kontrolni popis za provjeru odbijenog potpisanog izlaza

Kada platforma odbije vaš PAdES potpis, provjerite jednostavne strukturne uzroke prije nego što posumnjate na certifikate ili lance povjerenja. Pročitajte niz /ByteRange i provjerite tri jednakosti: prvo polje nula, drugi raspon počinje na ili nakon kraja prvog, drugi raspon završava točno na kraju datoteke. Pročitajte unos /M i provjerite je li to potpuni niz datuma prema §7.9.4 s prefiksom D: i oznakom pomaka. Potvrdite da izvorni dokument nije bio šifriran kada je potpis dodan — a ako ga je vaš stog ipak potpisao bez pritužbi, tretirajte tu šutnju kao pogrešku u stogu. Sve tri provjere izvode se na sirovim bajtovima u milisekundama, i prema našem iskustvu, one objašnjavaju odbijanje daleko češće od bilo kojeg kriptografskog uzroka

Pozivi za potpisivanje, inspekciju i validaciju koji se ovdje koriste — SignPades, ValidatePades i provjere sukladnosti PAdES-a s njihovom ispravljenom ByteRange aritmetikom, oblikovanjem datuma i filtriranjem šifriranja — isporučuju se s PDFium komponentom za Delphi, C++Builder i Lazarus