Preverjalnik (validator) zavrne podpis PAdES iz enega od treh razlogov v skoraj vsakem primeru, ki smo ga odpravljali s komponento PDFium Component v Delphiju: polje /ByteRange še vedno vsebuje svoje ničelne ogrdke (placeholders), čas podpisa /M ni pravilno oblikovan niz datuma PDF ali pa je postopna posodobitev (incremental update) odstranila vnos /Encrypt iz šifriranega dokumenta. Vsi trije razlogi ustvarijo datoteke, ki se brez težav odprejo, pravilno upodabljajo, a odpovejo v trenutku, ko skladen preverjalnik prebere slovar podpisa
Scenarij, ki motivira ta članek, je boleče specifičen. Pogodbo podpišete s potekom dela iz članka o podpisovanju PAdES B-B, vaša lastna koda za pregled poroča, da je podpis prisoten in strukturno brezhiben, vsi lokalni testi so zeleni — nato pa nasprotna stranka naloži datoteko na svojo validacijsko platformo in prejme rdeč križec. Nič o tej napaki ni vidno v pregledovalniku, saj nobena od teh treh napak ne vpliva na vsebino strani. Živijo izključno v slovarju podpisa in napovedniku (trailer) datoteke, kot je natanko tam, kjer preverjalniki iščejo, pregledovalniki pa večinoma ne
Zakaj je ByteRange mojega podpisa PDF neveljaven?
Zavrnitev polja ByteRange skoraj vedno pomeni, da štiri polja nikoli niso bila izpolnjena, ne pa, da so razponi subtilno napačni. Polje /ByteRange [A B C D] deklarira dva razpona, bajte od A do A+B in bajte od C do C+D, standard EN 319 142-1 §6.3 (zahteva k) pa zahteva, da skupaj pokrivata celotno datoteko razen s šestnajstiškim sistemom (hex) kodiranega niza /Contents. V številkah: A je 0, C >= A+B, C+D je enak dolžini datoteke, vrzel med B in C pa drži natanko hex niz <...> — dva bajta oklepajev plus dva hex znaka na bajt podatkov CMS. Preverjalnik, ki prebere [0 0 0 0], sklepa, da podpis ne pokriva ničesar, in ga zavrne, ne glede na to, kako pravilna je struktura CMS znotraj /Contents
Mehaniko, kako se to zgodi, je vredno razumeti, saj enak vzorec obstaja v vsakem podpisnem skladu. Podpisnik ne more poznati končnih odmikov (offsets), dokler datoteka ni postavljena, zato pisec odda polje s fiksno širino ničelnih ogrdkov in jih po postavitvi zapolni za nazaj (back-fills). V različicah komponente PDFium Component pred 2.14.1 je to zapolnjevanje iskalo vzorec ogrdka, začenši s položaja /Contents — toda /ByteRange se v slovarju nahaja pred /Contents, zato iskanje ni našlo ničesar in vse tri zamenjave so tiho spodletele. Povzetek CMS (digest) je bil izračunan čez pravilne razpone, zato je bila kriptografija pravilna; deklaracija teh razponov pa je ostala pri ničli, zaradi česar je vsak skladen preverjalnik zavrnil datoteko. Časovni žigi dokumenta PAdES B-LTA, ki uporabljajo enako postavitev slovarja, so odpovedali na enak način — kar je pomembno, če gradite na dolgoročnih podpisih B-LT in B-LTA. Različica 2.14.1 zapolnjuje od začetka objekta podpisa, popravek pa pokriva regresijski test, ki razčleni ustvarjeno datoteko in potrdi spodnjo aritmetiko
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); // samo prvi podpis
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 zahteva k: razponi pokrivajo vse razen /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Dvajset vrstic čiste VCL/RTL kode, brez klicev knjižnic, ujame celoten razred napak neposredno ob nastanku. Če obdržite eno trditev iz tega članka, naj bo to ta: razčlenite svoj podpisani izhod in preverite tri enakosti, preden datoteka zapusti vaš proces
Zakaj preverjalniki označijo čas podpisa /M kot napačno oblikovan?
Strogi preverjalniki zavrnejo čas podpisa, ki ni popoln niz datuma PDF, dela, ki najpogosteje manjkata, pa sta predpona D: in oznaka odklona UTC. Standard ISO 32000-1 §7.9.4 opredeljuje zapis datuma kot D:YYYYMMDDHHmmSS, ki mu sledi odklon (offset) — Z za UTC ali podpisano razmerje +HH'mm' do njega. Gol niz 20260709143000 se prizanesljivemu bralniku razčleni kot datum, vendar preverjalnik, ki slovnico uporablja dobesedno, vidi napačno oblikovan niz v polju z obvezno obliko in označi podpis kot sumljiv. Izdaje komponente PDFium Component pred različico 2.14.4 so vnos /M pri metodah TPdf.SignPades in SignPadesBytes zapisovale natanko v tej goli obliki; od različice 2.14.4 dalje vnos vsebuje predpono D: in oznako Z, tako da se deklarirani čas podpisa bere D:20260709143000Z
Temu polju se pripenjata dve praktični opombi. Prvič, zapišite UTC in to jasno povejte: časovni žig brez oznake odklona prisili preverjalnik v ugibanje časovnega pasu, razdelek §7.9.4 pa obravnava odklon kot del zapisa in ne kot neobvezno podrobnost. Drugič, ne pozabite, kaj /M sploh je — čas, ki ga deklarira podpisnik, kar je trditev in ne dokaz. Preverjalnik tukaj preveri obliko niza, ne pa njegove resničnosti; dokazljiv čas prihaja iz časovnega žiga RFC 3161 na ravni PAdES B-T in višje. Pravilno oblikovanje polja in zaupanje vanj sta ločeni odločitvi, preverjalniki pa uveljavljajo le prvo
Zakaj SignPades sproži izjeme EPadesCrypto na šifriranem PDF-ju?
Komponenta PDFium Component zavrne podpisovanje šifriranega dokumenta, ker je alternativa ustvarjanje datoteke, ki jo bodo skladni bralniki ob odpiranju uničili. Podpis PAdES se pripne kot postopna posodobitev (incremental update), standard ISO 32000-1 §7.5.6 zahteva, da nov napovednik (trailer) posodobljenega razdelka prenaša vsak vnos prejšnjega napovednika razen /Prev — v šifriranem dokumentu to vključuje tudi /Encrypt. Če ga izpustite, zadnji napovednik razglasi datoteko za nešifrirano, zato skladen bralnik razčleni šifrirano telo kot navadno besedilo in prejme smeti. Še huje, tokovi in nizi, ki jih podpisnik pripne, bi morali biti sami šifrirani s ključem dokumenta, česar pa injektor podpisov v navadnem besedilu ne more storiti. Šifrirani datoteki ni mogoče pripeti veljavnega podpisa v navadnem besedilu, zato od različice 2.14.2 dalje metode TPdf.SignPades, SignPadesBytes in InjectPadesDssMarkers sprožijo izjemo EPadesCrypto namesto oddajanja poškodovanega ali nepreverljivega rezultata
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// npr. 'SignPadesBytes: izvorni dokument je šifriran;
// odstranite šifriranje pred podpisovanjem'
Writeln('Cannot sign: ', E.Message);
end;
end;
Izjema je pravilen izid, zato oblikujte potek dela okoli nje, namesto da bi jo lovili in poskušali znova. Najprej dešifrirajte dokument z lastniškim pooblastilom, podpišite kopijo v navadnem besedilu in, če distribucijski kanal zahteva šifriranje, sprejmite dejstvo, da šifriranje-nato-podpisovanje ter podpisovanje-nato-šifriranje ustvarita različne artefakte z različnimi zgodbami preverjanja. Podpis, izračunan čez bajte navadnega besedila, ne more preživeti ponovnega šifriranja teh bajtov, zato so poštene možnosti podpisana datoteka v navadnem besedilu ali pa odločitev o politiki, dokumentirana poleg kode
Kako lahko dva hrošča potrdita drug drugega
Napaka ByteRange se je skrivala tako dolgo, ker je bil naš lasten preverjalnik napačen na dopolnjujoč se način, in to je najbolj prenosljiva lekcija v tem članku. Preverjanje pokritosti v metodi ValidatePadesCompliance je zahtevalo, da se drugi razpon začne natanko pri A+B — brez vrzeli — kar napačno razvršča standardno postavitev, kjer vrzel drži hex niz /Contents. Tako je lastni preverjalnik zavračal ravno skladno postavitev, lastni generator pa je nikoli ni ustvaril, cevovodi podpisovanja in preverjanja od konca do konca pa so kljub temu ostali zeleni. Vsak hrošč je testni zbirki preprečil nasprotni primer, ki bi razkril drugega. Različica 2.14.1 je odpravila obe strani v isti izdaji: generator zdaj zapolnjuje vsa štiri polja, preverjalnik pa sprejema C >= A+B s C+D, enakim dolžini datoteke
Metodološki popravek je navzkrižno preverjanje (cross-validation) z implementacijo, ki je niste napisali sami. Generator in preverjalnik, ki si delita kodo, avtorja ali celo samo mentalni model formata, se lahko v nedogled strinjata o skupnem nerazumevanju; neodvisen preverjalnik pa to simetrijo prebije. Pred izdajo pošljite svoj podpisani izhod skozi vsaj en zunanji preverjalnik skladnosti in ohranite strukturno samopreverjanje v gradnji kot hitro prvo linijo — metoda TPdf.ValidatePades, opisana v članeku o pregledu podpisov, poroča o napaki pokritosti kot o imenovani težavi
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange ne pokriva datoteke');
if not R.IsCompliant then
Writeln('Prisotne so strukturne težave: ne pošiljajte te datoteke');
end;
Kontrolni seznam za zavrnjeni podpisani izhod
Ko platforma zavrne vaš podpis PAdES, preverite preproste strukturne vzroke, preden posumite na potrdila (certificates) ali verige zaupanja (trust chains). Preberite polje /ByteRange in preverite tri enakosti: prvo polje nič, drugi razpon se začne na ali po koncu prvega, drugi razpon se konča natanko na koncu datoteke. Preberite vnos /M in potrdite, da gre za celoten niz datuma po §7.9.4 s predpono D: in oznako odklona. Prepričajte se, da izvorni dokument ni bil šifriran, ko se je podpis pripenjal — in če ga je vaš sklad kljub temu podpisal brez pritožb, obravnavajte to tišino kot hrošč v skladu. Vsa tri preverjanja se na surovih bajtih izvedejo v nekaj milisekundah in po naših izkušnjah pojasnijo zavrnitev veliko pogosteje kot kateri koli kriptografski vzrok
Klici za podpisovanje, pregled in potrjevanje, uporabljeni tukaj — SignPades, ValidatePades in preverjanja skladnosti PAdES z njihovo popravljeno aritmetiko ByteRange, oblikovanjem datumov in preverjanjem šifriranja — so del paketa PDFium Component za Delphi, C++Builder in Lazarus