HotPDF, Delphi PDF komponent, teraz odmieta signature wrapping: od v2.759.0 vyžadujú aj VerifyLoadedSignatureEx, aj dávkový validator, aby medzera medzi dvoma segmentmi /ByteRange bola presne hex reťazec /Contents vrátane delimiterov, a v2.761.0 pridáva AddLoadedSignedSignatureField, takže druhý podpis sa dá pripojiť k už podpísanému PDF ako čistá inkrementálna revízia. Tieto dve zmeny patria k sebe, lebo korektný druhý podpis je presne to rozloženie, ktoré prísnejší verifikátor očakáva
Situácia, ktorá problém odhalila, je každodenná. Zmluvu podpísi dodávateľ, potom putuje k schvaľovateľovi, ktorý musí pripísať svoj podpis bez narušenia prvého. Druhá revízia sa pripojí za prvú, jej vlastný /ByteRange sa rozprestrie cez celý narastlý súbor a oba podpisy majú verifikovať. Dostať sa tam ručne znamenalo napísať si inkrementálnu sekciu sám, a testovacia fixtúra, ktorá presne to robila, sa ukázala byť učebnicová štruktúra signature wrappingu, ktorú starý verifikátor radiel prijímal. Ak ste sa k verifikačnému API ešte nepozreli, sprievodca verifikáciou digitálnych podpisov PDF s HotPDF pokrýva základy, na ktorých tento článok stavia
Čo presne patrí do medzery ByteRange?
Medzera musí obsahovať kompletnú hodnotu /Contents a nič iné: ISO 32000-1 §12.8.3.3 hovorí, že hexadecimálny reťazec so svojimi delimitrami < a > zapadá presne do priestoru medzi dvoma bajtovými rozsahmi a ISO 32000-2 §12.8.1 toto pravidlo nesie ďalej. Tabuľka 252 a dokumenty PAdES hovoria len to, že digest vylučuje hodnotu Contents, čo sa ľahko prečíta ako vylúčenie len hex číslic. Skoršie vydania HotPDF to čítali práve takto: PreparePDFForSigning aj streamová príprava CMS hashovali aj lomené zátvorky, so zdrojovým komentárom trvajúcim na tom, že zátvorky musia byť pokryté. Validátory, ktoré porovnávajú medzeru s hodnotou podpisu, označia toto rozloženie ako neplatný bajtový rozsah, takže v2.759.0 vyňal oba delimitre z podpísaných rozsahov. Rýchla nezávislá kontrola na akomkoľvek podpísanom súbore je pozrieť sa na dva bajty: bajt na offsete ByteRange[1] musí byť < a bajt na offsete ByteRange[2] - 1 musí byť >
Prečo kontrola neprázdnej medzery prehliadne signature wrapping?
Kontrola neprázdnej medzery dokazuje len to, že niečo ostalo mimo digestu, nie čo, a to je celá útočná plocha. Zástupný symbol /Contents sa rezervuje s tisíckami nulových číslic, kým reálny CMS kontajner ich zriedka naplní. Útočník môže uzavrieť hex reťazec skoro vnútri toho nulového paddingu >, zapísať nové objekty alebo sfalovanú revíziu do zvyšku rezervovaného priestoru a nechať bajtové rozsahy nedotknuté. CMS podpis stále verifikuje, lebo každý podpísaný bajt je nezmenený, rozsahy stále začínajú na 0 a končia na veľkosti súboru a starý verifikátor HotPDF hlásil svValid s CoversWholeDocument nastaveným na True. PDF čítač medzitým parsuje čokoľvek, čo sedí v tom nepodpísanom otvore
HotPDF teraz berie medzeru ako dáta na validáciu bajt po bajte. Verifikátor prečíta medzeru, odstráni delimitre, akceptuje len hex číslice plus PDF biely priestor (tab, line feed, form feed, carriage return, medzera), dekóduje číslice a vyžaduje, aby výsledok presne zodpovedal /Contents slovníka podpisu. Čokoľvek iné degraduje výsledok na svInvalidByteRange. Kontrola beží v ceste jedného podpisu aj v ValidateLoadedSignatureBatch, ktorá si držala vlastnú logiku pokrytia a potrebovala tú istú opravu. Súbory vyrobené HotPDF pred v2.759.0, ktorých medzera držala len číslice so zátvorkami tesne vo vnútri rozsahov, sa stále verifikujú, takže archivované dokumenty nezčervenejú zo dňa na deň
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;
Ako pridať druhý podpis k už podpísanému PDF?
Otvorte podpísaný súbor cez BeginIncrementalUpdate, zavolajte AddLoadedSignedSignatureField, uložte cez SaveIncrementalUpdate a potom podpíšte pripravený súbor triedovou funkciou THotPDF.SignPDFWithPFX. Pred v2.761.0 zdokumentovaný recept volania THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate nemohol fungovať, lebo CurrentPage je v inkrementálnom režime nil a nič nedokázalo zavesiť zástupný symbol /V na pole v načítanom dokumente. Nová metóda vytvorí widget na načítanej strane a zavesí pod /V ten istý zástupný slovník, ktorý používa cesta nového dokumentu, takže obe podpisové cesty zdieľajú jednu serializáciu. Prvý podpis samotný prechádza PFX pipeline v článku o tvorbe digitálnych podpisov PAdES v Delphi
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Strana 0, obdĺžnik widgetu v bodoch, 8192 bajtov rezervovaných pre 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 je zámerne tichšia než jej sestry. Ostatní stavitelia polí AddLoaded* nastavia na AcroForme /NeedAppearances true, čo prikáže prehliadaču regenerovať appearances polí; na podpísanom dokumente môže táto regenerácia prepísať podpísaný obsah, takže nová metóda flag zas odstráni, pokiaľ ho zdroj už neniesol. /SigFlags drží svoju pôvodnú hodnotu OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabuľka 219). Tiež nemusíte volať MarkDirty na strane: pridanie do /Annots a /Fields prenesie dirty flag na vlastniaci nepriamy objekt a explicitná značka strany by do novej revízie len potiahla nezmenený slovník strany, čo by analýza revízií potom hlásila ako zmenu strany. Napokon zástupný symbol zapisuje /ByteRange pred /Contents, lebo patcher nájde najprv strážny /ByteRange a dopredu hľadá príslušný hex reťazec
Čo sa mení, keď CMS vyrába externý podpisovateľ alebo HSM?
Vo workflow sa nemení nič, ale offsety teraz znamenajú to, čo hovorí špecifikácia. PreparePDFForSigning vráti dva rozsahy od nuly, ktorých medzerou je celý reťazec /Contents, a ContentsHexStart je index prvej hex číslice v AnsiString od jedničky. Kratší CMS sa doplní 0 na konci, pred uzatvárajúcou >. Keďže PreparePDFForSigning zapatchuje prvý nenapatchovaný strážny znak, ktorý nájde, pripravte presne jeden zástupný symbol na revíziu a uprednostnite InsertSignatureHexAt s vrátenými offsetmi pred vyhľadávacím InsertSignatureHex, keď v súbore už existujú skoršie podpisy
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // váš helper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Medzerou je celý hex reťazec: '<' ukončuje rozsah 1, '>' predchádza rozsahu 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // váš CMS podpisovateľ, 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'); // váš helper
end;
Kde sú hranice nových kontrol?
Kontrola medzery zatvára jednu konkrétnu dieru a netreba ju predávať nadsene. svValid stále znamená integritu bajtov plus kľúč zodpovedajúci vloženému certifikátu; dôvera v ten certifikát je samostatné rozhodnutie. Medzera sa validuje len vtedy, keď má verifikátor zdrojové bajty, ktoré VerifyLoadedSignatureEx číta z načítaného súboru a overloady TStream preberajú od vás. Pri prvom podpise v súbore s pripísaným podpisom je CoversWholeDocument korektne False a či pripojená revízia len pridala podpis, alebo zmenila aj strany, je otázka pre analýzu DocMDP, FieldMDP a revízií v HotPDF. Všimnite si tiež, že kontrola pripojeného PDF MAC porovnáva offsety s pozíciami < a >, takže prijíma staré aj nové rozloženie; akýkoľvek vlastný nástroj, ktorý natvrdo kóduje offsety z čias pred v2.759.0, zlyhá prvý, keď stretne čerstvo podpísaný súbor
Ak vaša aplikácia v Delphi či C++Builder podpisuje, pripisuje podpisy alebo audituje PDF, najbezpečnejšia cesta je nechať jednu knižnicu vyrábať aj verifikovať to isté rozloženie. HotPDF, natívny Delphi PDF komponent dodáva prísnejšiu validáciu medzery, inkrementálne druhé podpisy aj háky pre externých podpisovateľov zobrazené vyššie v jednom komponente