HotPDF, Delphi-PDF-komponenten, avvisar nu signatur-wrapping: sedan v2.759.0 kräver både VerifyLoadedSignatureEx och batchvalideraren att gapet mellan de två /ByteRange-intervallen är exakt hex-strängen i /Contents, avgränsare inkluderade, och v2.761.0 lägger till AddLoadedSignedSignatureField så att en andra signatur kan bifogas en redan signerad PDF som en ren inkrementell revision. De två ändringarna hör ihop, för en korrekt andra signatur är precis den layout som den strängare verifieraren väntar sig
Situationen som exponerade problemet är helt vanlig. Ett kontrakt signeras av leverantören och skickas sedan vidare till en godkännare som måste motregistrera utan att rubba den första signaturen. Den andra revisionen bifogas efter den första, dess egen /ByteRange spänner över hela den växta filen, och båda signaturerna ska verifieras. Att ta sig dit för hand innebar att själv skriva en inkrementell sektion, och testfixturen som gjorde exakt det visade sig vara en läroboksexempel på en signatur-wrapping-struktur som den gamla verifieraren glatt accepterade. Om du inte har tittat på verifierings-API:et tidigare täcker guiden till att verifiera PDF-digitala signaturer med HotPDF grunderna som denna artikel bygger på
Vad hör exakt hemma i ByteRange-gapet?
Gapet måste innehålla det fullständiga /Contents-värdet och inget annat: ISO 32000-1 §12.8.3.3 säger att hex-strängen, med dess <- och >-avgränsare, passar exakt i utrymmet mellan de två byte-intervallen, och ISO 32000-2 §12.8.1 för samma regel vidare. Tabell 252 och PAdES-dokumenten säger bara att digesten undantar Contents-värdet, vilket är lätt att läsa som att bara hex-siffrorna undantas. Tidigare HotPDF-releaser läste det så: PreparePDFForSigning och den strömmande CMS-förberedelsen hashade vinkelhakarna också, med en källkommentar som insisterade på att hakarna måste täckas. Validerare som jämför gapet mot signaturvärdet flaggar den layouten som en ogiltig byte range, så v2.759.0 flyttar båda avgränsarna utanför de signerade intervallen. Ett snabbt oberoende test på vilken signerad fil som helst är att titta på två byte: byten på offset ByteRange[1] måste vara < och byten på offset ByteRange[2] - 1 måste vara >
Varför missar en kontroll av icke-tomt gap signatur-wrapping?
En kontroll av icke-tomt gap bevisar bara att något lämnades utanför digesten, inte vad som lämnades utanför, och det är hela angreppsytan. /Contents-platshållaren reserveras med tusentals nollsiffror, medan en verklig CMS-behållare sällan fyller den. En angripare kan stänga hex-strängen tidigt inuti den nollutfyllnaden med en >, skriva nya objekt eller en förfalskad revision in i resten av det reserverade utrymmet och lämna byte-intervallen orörda. CMS-signaturen verifierar fortfarande, eftersom varje signerad byte är oförändrad, intervallen börjar fortfarande på 0 och slutar på filstorleken, och den gamla HotPDF-verifieraren rapporterade svValid med CoversWholeDocument satt till True. En PDF-läsare, å andra sidan, parsar vadhelst som ligger i det osignerade hålet
HotPDF behandlar nu gapet som data som ska valideras byte för byte. Verifieraren läser gapet, klipper av avgränsarna, accepterar bara hex-siffror plus PDF-blanksteg (tabb, radmatning, form feed, vagnretur, blanksteg), avkodar siffrorna och kräver att resultatet exakt är lika med signaturordbokens /Contents. Allt annat nedgraderar resultatet till svInvalidByteRange. Kontrollen körs både i singelsignaturvägen och i ValidateLoadedSignatureBatch, som behöll sin egen täckningslogik och behövde samma fix. Filer producerade av HotPDF före v2.759.0, vars gap bara höll siffror med hakarna precis inuti intervallen, verifierar fortfarande, så arkiverade dokument blir inte plötsligt röda
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;
Hur lägger du till en andra signatur i en redan signerad PDF?
Öppna den signerade filen med BeginIncrementalUpdate, anropa AddLoadedSignedSignatureField, spara med SaveIncrementalUpdate och signera sedan den förberedda filen med klassfunktionen THotPDF.SignPDFWithPFX. Före v2.761.0 kunde det dokumenterade receptet att anropa THPDFPage.AddSignedSignatureField efter BeginIncrementalUpdate inte fungera, för CurrentPage är nil i inkrementellt läge och ingenting kunde fästa en /V-platshållare vid ett fält på ett inläst dokument. Den nya metoden skapar widgeten på den inlästa sidan och hänger samma platshållarordbok som den nya-dokument-vägen använder under /V, så båda signeringsvägarna delar en serialisering. För den första signaturen själv går artikeln om att skapa PAdES-digitala signaturer i Delphi igenom PFX-pipelinen
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Sida 0, widget-rektangel i punkter, 8192 byte reserverade för CMS:en
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 är medvetet tystare än sina syskon. De andra AddLoaded*-fältskaparna sätter /NeedAppearances true på AcroForm:en, vilket säger till en visare att generera om fältappearance:er; på ett signerat dokument kan den omgenereringen skriva om signerat innehåll, så den nya metoden tar bort flaggan igen om inte källan redan bar den. /SigFlags behåller sitt ursprungliga värde OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabell 219). Du behöver inte heller anropa MarkDirty på sidan: tillägg i /Annots och /Fields fortplantar dirty-flaggan till det ägande indirekta objektet, och en explicit sidmarkering skulle bara släpa med en oförändrad sidordbok in i den nya revisionen, vilken revisionsanalysen sedan rapporterar som en sidändring. Slutligen skriver platshållaren /ByteRange före /Contents, eftersom patcharen lokaliserar sentinelen /ByteRange först och söker framåt efter den matchande hex-strängen
Vad ändras när en extern signerare eller HSM producerar CMS:en?
Ingenting ändras i arbetsflödet, men offseterna betyder nu vad specifikationen säger. PreparePDFForSigning returnerar två nollbaserade intervall vars gap är hela /Contents-strängen, och ContentsHexStart är det enbaserade indexet för den första hex-siffran i AnsiString:en. En kortare CMS fylls ut med 0 på slutet, före avslutande >. Eftersom PreparePDFForSigning patchar den första opatchade sentinelen den hittar, förbered exakt en platshållare per revision, och föredra InsertSignatureHexAt med de returnerade offseterna framför den sökningsbaserade InsertSignatureHex när tidigare signaturer redan finns i filen
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // din hjälpare
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Gapet är hela hex-strängen: '<' avslutar intervall 1, '>' föregår intervall 2
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // din CMS-signerare, 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'); // din hjälpare
end;
Var finns gränserna för de nya kontrollerna?
Gapkontrollen stänger ett specifikt hål och bör inte översäljas. svValid betyder fortfarande byteintegritet plus en nyckel som matchar det inbäddade certifikatet; förtroendet för certifikatet är ett separat beslut. Gapet valideras bara när verifieraren har källbyte, vilka VerifyLoadedSignatureEx läser från den inlästa filen och TStream-överloaderna tar från dig. För den första signaturen i en motregistrerad fil är CoversWholeDocument korrekt False, och huruvida den bifogade revisionen bara lade till en signatur eller också ändrade sidor är en fråga för DocMDP-, FieldMDP- och revisionsanalys i HotPDF. Notera också att den bifogade PDF MAC-kontrollen jämför offseter mot <- och >-positionerna, så den accepterar både den gamla och den nya layouten; vilket eget verktyg som helst som hårdkodar offseterna före v2.759.0 kommer att fallera först när det möter en nysignerad fil
Om din Delphi- eller C++Builder-applikation signerar, motregistrerar eller granskar PDF:er är den säkraste vägen att låta ett bibliotek producera och verifiera samma layout. HotPDF, den inbyggda Delphi-PDF-komponenten levererar den strängare gap-valideringen, inkrementella andra signaturer och hookarna för externa signerare som visas ovan i en enda komponent