HotPDF, componenta Delphi PDF, respinge acum signature wrapping: din v2.759.0, atât VerifyLoadedSignatureEx cât și validatorul de lot cer ca golul dintre cele două segmente /ByteRange să fie exact șirul hex /Contents, delimitatori incluși, iar v2.761.0 adaugă AddLoadedSignedSignatureField, ca o a doua semnătură să poată fi adăugată unui PDF deja semnat ca o revizie incrementală curată. Cele două schimbări merg împreună, pentru că o a doua semnătură corectă e exact aranjamentul pe care îl așteaptă verificatorul mai strict
Situația care a scos la iveală problema e obișnuită. Un contract e semnat de furnizor, apoi ajunge la un aprobator care trebuie să contrasemeze fără să tulbure prima semnătură. A doua revizie e adăugată după prima, propriul ei /ByteRange acoperă tot fișierul crescut, și ambele semnături ar trebui să verifice. Să ajungi acolo de mână însemna să scrii singur o secțiune incrementală, iar fixture-ul de test care făcea exact asta s-a dovedit a fi o structură de manual de signature wrapping pe care vechiul verificator o accepta fericit. Dacă nu v-ați uitat până acum la API-ul de verificare, ghidul verificării semnăturilor digitale PDF cu HotPDF acoperă bazele pe care se construiește articolul de față
Ce aparține exact de golul ByteRange?
Golul trebuie să conțină valoarea completă /Contents și nimic altceva: ISO 32000-1 §12.8.3.3 spune că șirul hexazecimal, cu delimitatorii lui < și >, încape exact în spațiul dintre cele două intervale de octeți, iar ISO 32000-2 §12.8.1 duce regula aceea mai departe. Tabelul 252 și documentele PAdES spun doar că digestul exclude valoarea Contents, ceea ce e ușor de citit ca excluzând doar cifrele hex. Versiunile HotPDF anterioare o citeau așa: PreparePDFForSigning și pregătirea CMS în streaming hash-uiau și parantezele unghiulare, cu un comentariu din sursă care insista că parantezele trebuie acoperite. Verificatoarele care compară golul cu valoarea semnăturii marchează acel aranjament ca interval de octeți invalid, deci v2.759.0 scoate ambii delimitatori din intervalele semnate. O verificare independentă rapidă pe orice fișier semnat e să vă uitați la doi octeți: octetul de la offset-ul ByteRange[1] trebuie să fie <, iar octetul de la offset-ul ByteRange[2] - 1 trebuie să fie >
De ce o verificare care cere doar gol nevid ratează signature wrapping?
O verificare de gol non-gol dovedește doar că ceva a fost lăsat în afara digestului, nu ce a fost lăsat, iar asta e toată suprafața de atac. Placeholder-ul /Contents e rezervat cu mii de cifre zero, în timp ce un container CMS real îl umple rar. Un atacator poate închide șirul hex devreme în interiorul acelui padding de zerouri cu un >, poate scrie obiecte noi sau o revizie falsificată în restul spațiului rezervat și poate lăsa intervalele de octeți neatinse. Semnătura CMS se verifică în continuare pentru că fiecare octet semnat e neschimbat, intervalele pornesc tot la 0 și se termină la mărimea fișierului, iar vechiul verificator HotPDF raporta svValid cu CoversWholeDocument setat pe True. Un cititor de PDF, între timp, parsează orice stă în gaura aceea nesemnată
HotPDF tratează acum golul ca date care trebuie validate octet cu octet. Verificatorul citește golul, taie delimitatorii, acceptă doar cifre hex plus whitespace PDF (tab, line feed, form feed, carriage return, spațiu), decodează cifrele și cere rezultatului să egaleze exact /Contents-ul dicționarului de semnătură. Orice altceva degrandează rezultatul la svInvalidByteRange. Verificarea rulează atât pe calea cu o singură semnătură, cât și în ValidateLoadedSignatureBatch, care își ținea propria logică de acoperire și a avut nevoie de aceeași reparare. Fișierele produse de HotPDF înainte de v2.759.0, al căror gol ținea doar cifre cu parantezele așezate chiar în interiorul intervalelor, se verifică în continuare, deci documentele arhivate nu devin deodată roșii
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;
Cum adăugați o a doua semnătură unui PDF deja semnat?
Deschideți fișierul semnat cu BeginIncrementalUpdate, apelați AddLoadedSignedSignatureField, salvați cu SaveIncrementalUpdate, apoi semnați fișierul pregătit cu funcția de clasă THotPDF.SignPDFWithPFX. Înainte de v2.761.0, rețeta documentată de apelare a lui THPDFPage.AddSignedSignatureField după BeginIncrementalUpdate nu putea funcționa, pentru că CurrentPage e nil în modul incremental și nimic nu putea atârna un placeholder /V de un câmp pe un document încărcat. Metoda nouă creează widget-ul pe pagina încărcată și atârnă același dicționar placeholder pe care îl folosește calea document-nou sub /V, deci ambele rute de semnare partajează o singură serializare. Pentru prima semnătură în sine, articolul despre crearea de semnături digitale PAdES în Delphi trece pas cu pas prin pipeline-ul PFX
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Pagina 0, dreptunghi de widget în puncte, 8192 de octeți rezervați pentru 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 e în mod deliberat mai liniștită decât suratele ei. Celelalte creatoare de câmpuri AddLoaded* setează /NeedAppearances true pe AcroForm, ceea ce spune unui viewer să regenereze aparențele câmpurilor; pe un document semnat, regenerarea aceea poate rescrie conținut semnat, deci metoda nouă scoate flag-ul înapoi cu excepția cazului în care sursa îl avea deja. /SigFlags își păstrează valoarea originală OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Tabelul 219). Nu trebuie nici să apelați MarkDirty pe pagină: adăugarea la /Annots și /Fields propagă flag-ul de dirty la obiectul indirect care le deține, iar o marcare explicită a paginii ar trage doar un dicționar de pagină neschimbat în revizia nouă, pe care analiza de revizii ar raporta-o apoi ca modificare de pagină. În fine, placeholder-ul scrie /ByteRange înaintea lui /Contents, pentru că patcher-ul localizează întâi sentinela /ByteRange și caută înainte șirul hex pereche
Ce se schimbă când un semnatar extern sau un HSM produce CMS-ul?
Nimic nu se schimbă în flux, dar offset-urile înseamnă acum ce spune specificația. PreparePDFForSigning întoarce două intervale pe bază de zero al căror gol e tot șirul /Contents, iar ContentsHexStart e indexul pe bază de unu al primei cifre hex din AnsiString. Un CMS mai scurt e umplut cu 0 la capăt, înaintea lui > de închidere. Fiindcă PreparePDFForSigning patch-uiește prima sentinelă nepatch-uită pe care o găsește, pregătiți exact un placeholder per revizie și preferați InsertSignatureHexAt cu offset-urile întoarse în locul lui InsertSignatureHex bazat pe căutare când în fișier există deja semnături anterioare
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // helper-ul vostru
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Golul e tot șirul hex: '<' închide intervalul 1, '>' precede intervalul 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // semnatarul vostru CMS, DER hex
if not THotPDF.InsertSignatureHexAt(Bytes, HexStart, HexLen, CmsHex) then
raise Exception.Create('CMS does not fit the reserved space');
SaveAnsiStringToFile(Bytes, 'SignedTwice.pdf'); // helper-ul vostru
end;
Unde sunt limitele noilor verificări?
Verificarea golului închide o anumită gaură și nu trebuie vândută cu prea mult. svValid înseamnă în continuare integritate de octeți plus o cheie care se potrivește cu certificatul încorporat; încrederea în certificatul acela e o decizie separată. Golul e validat doar când verificatorul are octeții sursă, pe care VerifyLoadedSignatureEx îi citește din fișierul încărcat, iar overload-urile TStream le iau de la voi. Pentru prima semnătură dintr-un fișier contrasemnat, CoversWholeDocument e corect False, iar dacă revizia adăugată a doar semnat sau a și schimbat pagini e o întrebare pentru DocMDP, FieldMDP și analiza de revizii în HotPDF. Rețineți de asemenea că verificarea PDF MAC atașată compară offset-urile cu pozițiile lui < și >, deci acceptă ambele aranjamente, vechi și nou; orice unealtă proprie care hard-codează offset-urile de dinainte de v2.759.0 va eșua prima când întâlnește un fișier proaspăt semnat
Dacă aplicația voastră Delphi sau C++Builder semnează, contrasemnează sau auditează PDF-uri, calea cea mai sigură e să lasați o singură bibliotecă să producă și să verifice același aranjament. HotPDF, componenta Delphi PDF nativă livrează validarea mai strictă a golului, a doua semnătură incrementală și hook-urile de semnatar extern arătate mai sus într-o singură componentă