Validator odbija PAdES potpis iz jednog od tri razloga u skoro svakom slučaju koji smo otklanjali (debugged) pomoću PDFium komponente u Delphi-ju: niz /ByteRange i dalje sadrži nulte privremene vrednosti (placeholders), vreme potpisivanja /M nije ispravno oblikovan PDF string datuma, ili je inkrementalno ažuriranje (incremental update) izbrisalo unos /Encrypt iz šifrovanog dokumenta. Sva tri slučaja proizvode datoteke koje se otvaraju i renderuju bez problema, ali padaju onog trenutka kada usaglašeni validator pročita rečnik potpisa
Scenario koji je inspirisao ovaj članak je izuzetno specifičan. Potpišete ugovor prateći radni tok iz članka o PAdES B-B potpisivanju, vaš sopstveni kod za inspekciju prijavljuje da je potpis prisutan i strukturno ispravan, svaki lokalni test je zelen — a zatim druga strana učita datoteku na svoju platformu za validaciju i dobije crveni krstić. Ništa od ovih grešaka nije vidljivo u pregledaču, jer nijedan od ova tri defekta ne utiče na sadržaj stranice. Oni žive isključivo u rečniku potpisa i trejleru datoteke (file trailer), što je upravo mesto gde validatori gledaju, a pregledači uglavnom ne
Zašto je moj PDF potpis ByteRange nevažeći
Odbijanje ByteRange-a skoro uvek znači da četiri polja nikada nisu popunjena, a ne da su opsezi suptilno pogrešni. Niz /ByteRange [A B C D] deklariše dva opsega, bajtove od A do A+B i bajtove od C do C+D, a standard EN 319 142-1 §6.3 (zahtev k) nalaže da oni zajedno pokrivaju čitavu datoteku osim heksadecimalno kodiranog stringa /Contents. Izraženo brojevima: A je 0, C >= A+B, C+D odgovara dužini datoteke, a praznina između B i C sadrži tačno <...> heksadecimalni string — dva bajta zagrada plus dva heksadecimalna karaktera 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-a
Mehaniku kako se ovo dešava vredi razumeti jer isti obrazac postoji u svakom sistemu za potpisivanje. Potpisnik ne može znati konačne ofsete dok se datoteka ne rasporedi, pa pisač emituje niz sa nultim rezervacijama fiksne širine i popunjava ih nakon raspoređivanja. U izdanjima PDFium komponente pre verzije 2.14.1, to popunjavanje je tražilo obrazac rezervisanog mesta počevši od pozicije /Contents — ali /ByteRange se nalazi pre /Contents u rečniku, pa pretraga nije pronašla ništa i sve tri zamene su tiho propale. CMS sažetak je izračunat nad ispravnim opsezima, tako da je kriptografija bila ispravna. Deklaracija tih opsega ostala je na nuli, pa je svaki usaglašeni validator odbio datoteku. Dokumenti sa PAdES B-LTA vremenskim žigovima (Document Time-stamps), koji koriste isti raspored rečnika, padali su na isti način — što je relevantno ako se oslanjate na dugotrajne B-LT i B-LTA potpise. Verzija 2.14.1 vrši popunjavanje unazad počevši od objekta potpisa, a ispravka je pokrivena regresionim testom koji parsira dobijenu datoteku i potvrđuje aritmetiku u nastavku
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 linija običnog RTL koda, bez poziva biblioteke, i to hvata čitavu klasu grešaka na samom mestu nastanka. Ako sačuvate samo jednu tvrdnju (assertion) iz ovog članka, neka to bude ova: parsirajte sopstveni potpisani izlaz i proverite tri jednakosti pre nego što datoteka napusti vaš proces
Zašto validatori označavaju vreme potpisivanja /M kao neispravno
Strogi validatori odbijaju vreme potpisivanja koje nije kompletan PDF string datuma, a dva dela koja najčešće nedostaju su prefiks D: i UTC marker ofseta. Standard ISO 32000-1 §7.9.4 definiše format datuma kao D:YYYYMMDDHHmmSS praćen ofsetom — Z za UTC ili potpisanim odnosom +HH'mm' prema njemu. Čist string 20260709143000 popustljivi čitač tumači kao datum, ali validator koji doslovno primenjuje gramatiku vidi neispravan string u polju obaveznog formata i označava potpis nevažećim. Izdanja PDFium komponente pre verzije 2.14.4 upisivala su unos /M za metode TPdf.SignPades i SignPadesBytes upravo u tom golom obliku. Počevši od verzije 2.14.4, unos nosi prefiks D: i marker Z, pa deklarisano vreme potpisivanja glasi D:20260709143000Z
Dve praktične napomene prate ovo polje. Prvo, upišite UTC vreme i to jasno navedite: vremenska oznaka bez markera ofseta primorava validator da nagađa odnos vremenskih zona, a §7.9.4 tretira ofset kao deo formata, a ne kao opcioni ukras. Drugo, zapamtite šta je /M — to je vreme koje je deklarisao sam potpisnik, tvrdnja a ne dokaz. Validator ovde proverava njegov format, a ne njegovu istinitost. Dokazivo vreme dolazi iz RFC 3161 vremenskog žiga na nivoima PAdES B-T i novijim. Ispravno formatiranje polja i ukazivanje poverenja istom su odvojene odluke, a validatori sprovode samo prvu
Zašto SignPades podiže izuzetak EPadesCrypto na šifrovanom PDF-u
PDFium komponenta odbija da potpiše šifrovani dokument jer je alternativa proizvodnja datoteke koju će usaglašeni čitači uništiti prilikom otvaranja. PAdES potpis se dodaje kao inkrementalno ažuriranje, a standard ISO 32000-1 §7.5.6 zahteva da novi trejler sekcije ažuriranja nosi svaki unos prethodnog trejlera osim /Prev — što u šifrovanom dokumentu uključuje i /Encrypt. Ako ga izostavite, najnoviji trejler deklariše datoteku kao nešifrovanu, pa usaglašeni čitač parsira šifrovano telo kao čisti tekst i dobija neupotrebljive podatke. Što je još gore, tokovi i stringovi koje potpisnik dodaje morali bi sami biti šifrovani ključem dokumenta da bi bili legalni, što injektor potpisa u čistom tekstu ne može da uradi. Ne postoji način da se doda važeći potpis u čistom tekstu na šifrovanu datoteku, pa od verzije 2.14.2 metode TPdf.SignPades, SignPadesBytes i InjectPadesDssMarkers podižu izuzetak EPadesCrypto umesto da emituju oštećen ili neproverljiv 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;
Izuzetak je ispravan ishod, pa projektujte radni tok oko njega umesto da ga hvatate (try-catch) i pokušavate ponovo. Prvo dešifrujte dokument sa vlasničkim (owner) ovlašćenjem, potpišite kopiju u čistom tekstu, a ako kanal distribucije zahteva šifrovanje, prihvatite da "šifrovanje pa potpisivanje" i "potpisivanje pa šifrovanje" proizvode različite artefakte sa različitim pričama o validaciji. Potpis izračunat nad bajtovima čistog teksta ne može preživeti ponovno šifrovanje tih bajtova, pa su jedine poštene opcije potpisan fajl u čistom tekstu ili odluka o bezbednosnoj politici dokumentovana pored koda
Kako dve greške mogu međusobno potvrditi ispravnost
Defekt sa ByteRange-om se skrivao toliko dugo jer je naš sopstveni validator grešio na komplementaran način, i to je najvažnija lekcija koju ovaj članak prenosi. Provera pokrivenosti u metodi ValidatePadesCompliance zahtevala je da drugi opseg počne tačno na A+B — bez praznine — što pogrešno klasifikuje standardni raspored gde praznina sadrži heksadecimalni string /Contents. Tako je naš interni validator odbijao upravo usaglašeni raspored, a interni generator ga nikada nije proizveo, pa su kompletne linije potpisivanja i validacije od kraja do kraja slučajno ostajale zelene. Svaka greška je uskratila testnom okruženju kontraprimer koji bi razotkrio onu drugu. Verzija 2.14.1 je popravila obe strane u istom izdanju: generator popunjava sva četiri polja, a validator prihvata C >= A+B sa C+D jednakim dužini datoteke
Metodološka ispravka je unakrsna validacija (cross-validation) u odnosu na implementaciju koju niste sami napisali. Generator i validator koji dele istu bazu koda, autora ili samo mentalni model formata mogu se neograničeno slagati u zajedničkom nerazumevanju. Nezavisni validator razbija tu simetriju. Pokrenite svoj potpisani izlaz kroz bar jedan eksterni validator usklađenosti pre isporuke, i zadržite strukturnu samoproveru pri izgradnji kao brzu prvu liniju odbrane — metoda TPdf.ValidatePades, opisana u članku o inspekciji potpisa, prijavljuje neuspeh 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;
Kontrolna lista odbijanja za potpisani izlaz
Kada platforma odbije vaš PAdES potpis, proverite jednostavne strukturne uzroke pre nego što posumnjate u sertifikate ili lance poverenja. Pročitajte niz /ByteRange i verifikujte tri jednakosti: prvo polje je nula, drugi opseg počinje na kraju prvog ili nakon njega, i drugi opseg se završava tačno na kraju datoteke. Pročitajte unos /M i verifikujte da li je to kompletan string datuma prema §7.9.4 sa prefiksom D: i markerom ofseta. Potvrdite da izvorni dokument nije bio šifrovan kada je potpis dodat — a ako ga je vaš sistem ipak potpisao bez prigovora, tretirajte to ćutanje kao grešku u sistemu. Sve tri provere se izvršavaju nad sirovim bajtovima u milisekundama, i u našem iskustvu one objašnjavaju odbijanje daleko češće nego bilo koji kriptografski uzrok
Pozivi za potpisivanje, inspekciju i validaciju koji se ovde koriste — SignPades, ValidatePades i PAdES provere usklađenosti sa njihovom korigovanom ByteRange aritmetikom, formatiranjem datuma i enkripcijskim proverama — dolaze sa PDFium komponentom za Delphi, C++Builder i Lazarus