HotPDF, komponenta PDF pro Delphi, teď odmítá signature wrapping: od v2.759.0 vyžadují VerifyLoadedSignatureEx i batch validator, aby mezera mezi oběma segmenty /ByteRange byla přesně hex string /Contents, včetně delimiterů, a v2.761.0 přidává AddLoadedSignedSignatureField, takže druhý podpis lze k už podepsanému PDF připojit jako čistou inkrementální revizi. Ty dvě změny patří k sobě, protože korektní druhý podpis je přesně to rozvržení, které přísnější verifikátor očekává
Situace, která problém vystavila na světlo, je běžná. Smlouvu podepíše dodavatel, pak putuje ke schvalovateli, který musí podepsat doplňkově, aniž by poprvé vytvořený podpis narušil. Druhá revize se připojí za tou první, její vlastní /ByteRange se táhne přes celý vyrostlý soubor a oba podpisy by měly projít ověřením. Dostat se tam ručně znamenalo napsat inkrementální sekci vlastníma rukama a testovací fixture, která přesně to dělala, se ukázala jako učebnicová struktura signature wrappingu, kterou starý verifikátor rád přijal. Pokud jste verifikační API ještě neviděli, průvodce ověřováním digitálních podpisů PDF s HotPDF pokrývá základy, na nichž tenhle článek staví
Co přesně patří do mezery ByteRange?
Mezera musí obsahovat kompletní hodnotu /Contents a nic víc: ISO 32000-1 §12.8.3.3 říká, že hexadecimální řetězec, včetně delimiterů < a >, zapadne přesně do prostoru mezi oběma bajtovými rozsahy, a ISO 32000-2 §12.8.1 pravidlo nese dál. Tabulka 252 a dokumenty PAdES říkají jen, že digest hodnotu Contents vynechává, což se lehce přečte jako vynechání jen hexadecimálních číslic. Dřívější vydání HotPDF to tak četly: PreparePDFForSigning i streamová CMS příprava hashovaly i hranaté závorky, se zdrojovým komentářem, který naléhal, že závorky kryté být musí. Validátory, které srovnávají mezeru s hodnotou podpisu, označují takové rozvržení jako nevalidní bajtový rozsah, takže v2.759.0 přesune oba delimitery mimo podepsané rozsahy. Rychlá nezávislá kontrola jakéhokoli podepsaného souboru je podívat se na dva bajty: bajt na offsetu ByteRange[1] musí být < a bajt na offsetu ByteRange[2] - 1 musí být >
Proč kontrola neprázdné mezery signature wrapping minout?
Kontrola neprázdné mezery dokáže jen to, že z digestu bylo vypuštěno něco, ne co, a přesně tohle je celá útočná plocha. Placeholder /Contents si rezervuje tisíce nulových číslic, zatímco skutečný CMS kontejner je zaplní zřídka. Útočník může hexadecimální řetězec uzavřít brzo uvnitř té nulové výplně pomocí >, zapsat do zbytku rezervovaného prostoru nové objekty nebo zfalšovanou revizi a bajtové rozsahy nechat nedotčené. CMS podpis stále projde, protože každý podepsaný bajt je beze změny, rozsahy stále začínají na 0 a končí na velikosti souboru a starý verifikátor HotPDF hlásil svValid s CoversWholeDocument nastaveným na True. PDF čtenář mezitím parsuje, co se v tom nepodepsaném otvoru nachází
HotPDF teď bere mezeru jako data k validaci bajt po bajtu. Verifikátor přečte mezeru, ořeže delimitery, přijme jen hexadecimální číslice plus PDF bílé znaky (tabulátor, line feed, form feed, carriage return, mezera), dekóduje číslice a vyžaduje, aby výsledek odpovídal slovníku podpisu /Contents přesně. Cokoli jiného degraduje výsledek na svInvalidByteRange. Kontrola běží jak v cestě jednotlivého podpisu, tak v ValidateLoadedSignatureBatch, která si držela vlastní logiku pokrytí a potřebovala tutéž opravu. Soubory vyrobené HotPDF před v2.759.0, jejichž mezera držela jen číslice se závorkami sedícími hned uvnitř rozsahů, pořád projdou, takže archivované dokumenty nezčervenají ze dne na den
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;
Jak přidat druhý podpis k už podepsanému PDF?
Otevřete podepsaný soubor přes BeginIncrementalUpdate, zavolejte AddLoadedSignedSignatureField, uložte přes SaveIncrementalUpdate a pak připravený soubor podepište class funkcí THotPDF.SignPDFWithPFX. Před v2.761.0 nemohl zdokumentovaný recept, tedy volání THPDFPage.AddSignedSignatureField po BeginIncrementalUpdate, vůbec fungovat, protože CurrentPage je v inkrementálním režimu nil a nic nemohlo přivěsit placeholder /V k poli na načteném dokumentu. Nová metoda vytvoří widget na načtené stránce a pověsí pod /V tutéž placeholder slovník, kterouž cesta nového dokumentu používá, takže obě podepisovací trasy sdílejí jednu serializaci. U samotného prvního podpisu provede článek o vytváření digitálních podpisů PAdES v Delphi PFX pipeline
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Stránka 0, obdélník widgetu v bodech, 8192 bajtů rezervováno pro 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áměrně tichší než její sourozenci. Ostatní tvůrci polí AddLoaded* nastavují na AcroForm /NeedAppearances true, což prohlížeči říká, aby regeneroval appearances polí; na podepsaném dokumentu může tahle regenerace přepsat podepsaný obsah, takže nová metoda flag zase odstraní, pokud už ho zdroj nenese. /SigFlags si drží původní hodnotu OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabulka 219). Na stránce taky nemusíte volat MarkDirty: přidání do /Annots a /Fields propaguje dirty flag na vlastnící nepřímý objekt a explicitní označení stránky by jen vleklo nezměněný slovník stránky do nové revize, kterouž revizní analýza pak hlásí jako úpravu stránky. Nakonec placeholder zapisuje /ByteRange před /Contents, protože patcher nejdřív najde sentinel /ByteRange a hledá odpovídající hex string dopředu
Co se změní, když CMS vyrobí externí signer nebo HSM?
Ve workflow se nezmění nic, ale offsety nyní znamenají to, co říká specifikace. PreparePDFForSigning vrací dva 0-based rozsahy, jejichž mezera je celý řetězec /Contents, a ContentsHexStart je 1-based index první hexadecimální číslice v AnsiString. Kratší CMS se doplní 0 na konci, před uzavírajícím >. Protože PreparePDFForSigning zaplátuje první nezaplátovaný sentinel, který najde, připravujte přesně jeden placeholder na revizi a upřednostněte InsertSignatureHexAt s vrácenými offsety před vyhledáváním založeným InsertSignatureHex, když už v souboru dřívější podpisy existují
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');
// Mezera je celý hex string: '<' ukončuje rozsah 1, '>' předchází 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 signer, 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 jsou meze nových kontrol?
Kontrola mezery zavírá jeden konkrétní otvor a nesmí se to přeprodávat. svValid pořád znamená bajtovou integritu plus klíč, který odpovídá vloženému certifikátu; důvěra v tenhle certifikát je samostatné rozhodnutí. Mezera se validuje, jen když má verifikátor zdrojové bajty, které si VerifyLoadedSignatureEx přečte z načteného souboru a overloady přes TStream dostanou od vás. U prvního podpisu v doplňkově podepsaném souboru je CoversWholeDocument správně False a to, zda připojená revize jen přidala podpis, nebo měnila i stránky, je otázka pro DocMDP, FieldMDP a analýzu revizí v HotPDF. Všimněte si také, že připojená kontrola PDF MAC srovnává offsety s pozicemi < a >, takže přijme staré i nové rozvržení; jakýkoli váš vlastní nástroj, který má offsety z doby před v2.759.0 natvrdo, selže nejdřív, až potká čerstvě podepsaný soubor
Pokud vaše aplikace v Delphi nebo C++Builder podepisuje, podepisuje doplňkově nebo audituje PDF, nejbezpečnější cesta je nechat jednu knihovnu vyrábět i ověřovat totéž rozvržení. HotPDF, nativní komponenta PDF pro Delphi, dodává přísnější validaci mezery, inkrementální druhé podpisy i hooky pro externí signery ukázané výše v jedné komponentě