HotPDF, Delphi PDF komponenta, sada odbija signature wrapping: od v2.759.0 i VerifyLoadedSignatureEx i batch validator traže da procijep između dvaju segmenata /ByteRange bude točno /Contents hex string, ograničitelje uključene, a v2.761.0 dodaje AddLoadedSignedSignatureField da se drugi potpis može nadodati već potpisanom PDF-u kao čista inkrementalna revizija. Dvije izmjene pripadaju zajedno, jer je ispravni drugi potpis upravo raspored koji stroži verifier očekuje
Situacija koja je obznanila problem svakidašnja je. Ugovor potpiše dobavljač, pa se usmjeri odobravatelju koji mora dodati protupotpis a da ne uznemiri prvi potpis. Druga se revizija nadodaje iza prve, njezin vlastiti /ByteRange obuhvaća cijelu naraslu datoteku, i oba bi se potpisa trebala verificirati. Doći tamo ručno značilo je sam napisati inkrementalnu sekciju, a testni fixture koji je točno to radio ispostavio se udžbeničkom signature-wrapping strukturom koju je stari verifier rado prihvatio. Ako ste verifikacijski API prije gledali, vodič kroz provjeru PDF digitalnih potpisa s HotPDF-om pokriva osnove na kojima ovaj članak gradi
Što točno pripada u ByteRange procijep?
Procijep mora sadržavati potpunu vrijednost /Contents i ništa drugo: ISO 32000-1 §12.8.3.3 kaže da heksadecimalni string, sa svojim ograničiteljima < i >, stane točno u prostor između dvaju byte raspona, i ISO 32000-2 §12.8.1 nosi isto pravilo dalje. Tablica 252 i PAdES dokumenti kažu samo da digest izostavlja vrijednost Contentsa, što je lako pročitati kao izostavljanje samih heksadecimalnih znamenki. Ranija HotPDF izdanja čitala su tako: PreparePDFForSigning i streaming CMS priprema hashirali su i uglaste zagrade, s komentarom u izvornom kodu koji je inzistirao da zagrade moraju biti pokrivene. Validatori koji uspoređuju procijep s vrijednošću potpisa označavaju taj raspored kao nevaljan byte raspon, pa v2.759.0 izvlači oba ograničitelja izvan potpisanih raspona. Brza neovisna provjera na bilo kojoj potpisanoj datoteci gleda dva bajta: bajt na offsetu ByteRange[1] mora biti < a bajt na offsetu ByteRange[2] - 1 mora biti >
Zašto provjera nepraznog procijepa promaši signature wrapping?
Provjera nepraznog procijepa dokazuje samo da je nešto izostavljeno iz digesta, ne što je izostavljeno, a to je cijela površina napada. Placeholder /Contentsa rezervira se s tisućama nula znamenki, dok pravi CMS spremnik rijetko kad ispuni. Napadač može zatvoriti hex string rano unutar tog nula paddinga s >, zapisati nove objekte ili krivotvorenu reviziju u ostatak rezerviranog prostora, i ostaviti byte raspone nedirnutima. CMS potpis se i dalje verificira jer je svaki potpisani bajt nepromijenjen, rasponi i dalje kreću od 0 i završavaju na veličini datoteke, i stari je HotPDF verifier javljao svValid s CoversWholeDocument postavljenim na True. PDF preglednik, u međuvremenu, parsira što god sjedi u toj nepotpisanoj rupi
HotPDF sada tretira procijep kao podatke koje treba validirati bajt po bajt. Verifier pročita procijep, odere ograničitelje, prihvaća samo hex znamenke plus PDF bijeli prostor (tab, line feed, form feed, carriage return, razmak), dekodira znamenke i traži da rezultat točno jednak rječniku potpisa /Contents. Sve ostalo spusti rezultat na svInvalidByteRange. Provjera radi i u putu jednog potpisa i u ValidateLoadedSignatureBatch, koji je čuvao vlastitu logiku pokrivenosti i trebao isti ispravak. Datoteke koje je HotPDF proizveo prije v2.759.0, čiji je procijep držao samo znamenke s zagradama upravo unutar raspona, i dalje se verificiraju, pa arhivirani dokumenti ne pocrvene odjednom
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('SignedTwice.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Status := Pdf.VerifyLoadedSignatureEx(I, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln(Info.FieldName, ': valid, covers the whole file')
else
Writeln(Info.FieldName, ': valid, ',
Info.UnsignedTrailingBytes, ' bytes appended later');
svInvalidByteRange:
Writeln(Info.FieldName, ': ByteRange gap rejected (wrapping?)');
else
Writeln(Info.FieldName, ': failed, status ', Ord(Status));
end;
end;
finally
Pdf.Free;
end;
end;
Kako dodati drugi potpis već potpisanom PDF-u?
Otvorite potpisanu datoteku s BeginIncrementalUpdateom, zovite AddLoadedSignedSignatureField, spremite s SaveIncrementalUpdateom, pa potpišite pripremljenu datoteku class funkcijom THotPDF.SignPDFWithPFX. Prije v2.761.0 dokumentirani recept zvanja THPDFPage.AddSignedSignatureField nakon BeginIncrementalUpdatea nije mogao raditi, jer je CurrentPage nil u inkrementalnom modu i ništa nije moglo prikačiti /V placeholder na polje na učitanom dokumentu. Nova metoda stvara widget na učitanoj stranici i objesi isti placeholder rječnik koji koristi put novog dokumenta pod /V, pa obje rute potpisivanja dijele jednu serializaciju. Za prvi potpis sam, članak o stvaranju PAdES digitalnih potpisa u Delphiju provlači PFX pipeline
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Stranica 0, pravokutnik widgeta u točkama, 8192 bajta rezervirano za CMS
FieldIndex := Pdf.AddLoadedSignedSignatureField(0, 320, 60, 520, 120,
'ApproverSignature', 8192);
if FieldIndex < 0 then
raise Exception.Create('Page index out of range');
Pdf.SaveIncrementalUpdate('Prepared.pdf');
finally
Pdf.Free;
end;
if not THotPDF.SignPDFWithPFX('Prepared.pdf', 'SignedTwice.pdf',
'approver.pfx', 'pfx-password') then
raise Exception.Create('Second signature failed');
end;
AddLoadedSignedSignatureField namjerno je tiši od svoje braće. Ostali tvorci polja AddLoaded* postavljaju /NeedAppearances true na AcroFormu, što priča pregledniku da regenerira appearance polja; na potpisanom dokumentu ta regeneracija može prepraviti potpisani sadržaj, pa nova metoda opet skida flag osim ako ga je izvor već nosio. /SigFlags čuva izvornu vrijednost OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Tablica 219). Ne trebate ni zvati MarkDirty na stranici: dodavanje u /Annots i /Fields širi dirty flag na posjedujući indirektni objekt, a izričito označavanje stranice samo bi odvuklo nepromijenjen rječnik stranice u novu reviziju, koju bi analiza revizija zatim prijavila kao izmjenu stranice. Napokon, placeholder zapisuje /ByteRange prije /Contents, jer patcher locira sentinel /ByteRange prvi i traži naprijed pripadajući hex string
Što se mijenja kad CMS proizvede vanjski potpisnik ili HSM?
U tijeku rada se ništa ne mijenja, ali offseti sada znače ono što kaže specifikacija. PreparePDFForSigning vraća dva 0-based raspona čiji je procijep cijeli string /Contentsa, a ContentsHexStart jest 1-based indeks prve hex znamenke u AnsiStringu. Kraći CMS podloži se s 0 na kraju, ispred zatvarajućeg >. Pošto PreparePDFForSigning zakrpi prvi nezakrpljeni sentinel koji nađe, pripremite točno jedan placeholder po reviziji, i dajte prednost InsertSignatureHexAtu s vraćenim offsetima pred pretraživanjem baziranim InsertSignatureHexom kad raniji potpisi već postoje u datoteci
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // vaš helper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Procijep je cijeli hex string: '<' zatvara raspon 1, '>' prethodi rasponu 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // vaš CMS potpisnik, hex DER
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // vaš helper
end;
Gdje su granice novih provjera?
Provjera procijepa zatvara jednu konkretnu rupu i ne bi je trebalo prepjevati. svValid i dalje znači integritet bajtova plus ključ koji se poklapa s ugrađenim certifikatom; povjerenje u taj certifikat zasebna je odluka. Procijep se validira samo kad verifier ima izvorne bajtove, koje VerifyLoadedSignatureEx čita iz učitane datoteke, a overloadi TStreama preuzimaju od vas. Za prvi potpis u protupotpisanoj datoteci CoversWholeDocument ispravno je False, i to je li nadodana revizija samo dodala potpis ili je mijenjala i stranice pitanje za DocMDP, FieldMDP i analizu revizija u HotPDF-u. Primijetite i da provjera priloženog PDF MAC-a uspoređuje offsete s pozicijama < i >, pa prima i stari i novi raspored; svaki vlastiti alat koji hardkodira offsete prije v2.759.0 past će prvi čim sretne svježe potpisanu datoteku
Ako vaša Delphi ili C++Builder aplikacija potpisuje, protupotpisuje ili auditira PDF-ove, najsigurniji je put dati jednoj biblioteci da proizvede i verificira isti raspored. HotPDF, nativna Delphi PDF komponenta isporučuje strožu validaciju procijepa, inkrementalne druge potpise i kuke za vanjske potpisnike prikazane gore u jednoj komponenti