HotPDF, Delphi PDF-komponenten, afviser nu signature wrapping: siden v2.759.0 kræver både VerifyLoadedSignatureEx og batch-validatoren, at gabet mellem de to /ByteRange-intervaller er præcis /Contents-hex-strengen, skilletegn inkluderet, og v2.761.0 tilføjer AddLoadedSignedSignatureField, så en anden signatur kan tilføjes til en allerede signeret PDF som en ren inkrementel revision. De to ændringer hører sammen, for en korrekt anden signatur er præcis det layout, den strengere verifier forventer
Situationen, der udstillede problemet, er almindelig. En kontrakt signeres af leverandøren og sendes derefter til en godkender, der skal medsignere uden at forstyrre den første signatur. Den anden revision tilføjes efter den første, dens egen /ByteRange spænder over hele den voksede fil, og begge signaturer burde verificere. At nå dertil i hånd betød at skrive en inkrementel sektion selv, og test-fixturen, der gjorde netop det, viste sig at være en lærebogs-signature-wrapping-struktur, som den gamle verifier gladelig accepterede. Hvis du ikke har set verifikations-API'et før, dækker guiden om at verificere PDF digitale signaturer med HotPDF grundlaget, denne artikel bygger på
Hvad hører præcis hjemme i ByteRange-gabet?
Gabet skal indeholde den komplette /Contents-værdi og intet andet: ISO 32000-1 §12.8.3.3 siger, at hex-strengen med sine <- og >-skilletegn passer præcis i rummet mellem de to byte-intervaller, og ISO 32000-2 §12.8.1 fører samme regel videre. Tabel 252 og PAdES-dokumenterne siger kun, at digestet udelader Contents-værdien, hvilket er let at læse som kun hex-cifrene. Tidligere HotPDF-udgivelser læste den sådan: PreparePDFForSigning og den strømmende CMS-forberedelse hashed også vinkelbeslagene, med en kildekommentar, der insisterede på, at beslagene skulle være dækket. Validatorer, der sammenligner gabet med signaturværdien, markerer det layout som et ugyldigt byte-interval, så v2.759.0 flytter begge skilletegn ud af de signerede intervaller. Et hurtigt uafhængigt tjek på enhver signeret fil er at se på to bytes: byten ved offset ByteRange[1] skal være <, og byten ved offset ByteRange[2] - 1 skal være >
Hvorfor opdager et ikke-tomt gab-tjek ikke signature wrapping?
Et ikke-tomt gab-tjek beviser kun, at noget blev holdt uden for digestet, ikke hvad, og det er hele angrebsfladen. /Contents-pladsholderen reserveres med tusindvis af nul-cifre, mens en rigtig CMS-container sjældent fylder den. En angriber kan lukke hex-strengen tidligt inde i den nul-padding med et >, skrive nye objekter eller en forfalsket revision ind i resten af den reserverede plads og lade byte-intervallerne urørt. CMS-signaturen verificerer stadig, fordi hver signeret byte er uændret, intervallerne starter stadig ved 0 og slutter ved filstørrelsen, og den gamle HotPDF-verifier rapporterede svValid med CoversWholeDocument sat til True. En PDF-reader parser imens, hvad der nu ligger i det usignerede hul
HotPDF behandler nu gabet som data, der skal valideres byte for byte. Verifieren læser gabet, fjerner skiltegnene, accepterer kun hex-cifre plus PDF-whitespace (tab, line feed, form feed, carriage return, mellemrum), afkoder cifrene og kræver, at resultatet er præcis lig signatur-dictionaryens /Contents. Alt andet nedgraderer resultatet til svInvalidByteRange. Tjekket kører både i enkelt-signatur-stien og i ValidateLoadedSignatureBatch, som beholdt sin egen coverage-logik og trængte til samme fix. Filer produceret af HotPDF før v2.759.0, hvis gab kun holdt cifre med beslagene lige inden for intervallerne, verificerer stadig, så arkiverede dokumenter bliver ikke pludselig rø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;
Hvordan tilføjer du en anden signatur til en allerede signeret PDF?
Åbn den signerede fil med BeginIncrementalUpdate, kald AddLoadedSignedSignatureField, gem med SaveIncrementalUpdate og signér derefter den forberedte fil med klassefunktionen THotPDF.SignPDFWithPFX. Før v2.761.0 kunne den dokumenterede opskrift med at kalde THPDFPage.AddSignedSignatureField efter BeginIncrementalUpdate ikke virke, fordi CurrentPage er nil i inkrementel tilstand, og intet kunne hænge en /V-pladsholder på et felt i et indlæst dokument. Den nye metode opretter widgetten på den indlæste side og hænger samme pladsholder-dictionary, som stien til nye dokumenter bruger, under /V, så begge signeringsruter deler én serialisering. For selve den første signatur gennemgår artiklen om at oprette PAdES digitale signaturer i Delphi PFX-pipelinen
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Side 0, widget-rektangel i points, 8192 bytes reserveret til CMS'et
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 er bevidst mere stille end sine søskende. De andre AddLoaded*-feltoprettere sætter /NeedAppearances true på AcroForm'en, hvilket beder en viewer om at regenerere felt-appearances; på et signeret dokument kan den regenerering omskrive signeret content, så den nye metode fjerner flagget igen, medmindre kilden allerede bar det. /SigFlags beholder sin oprindelige værdi OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 tabel 219). Du behøver heller ikke at kalde MarkDirty på siden: tilføjelser til /Annots og /Fields udbreder dirty-flagget til det ejende indirekte objekt, og en eksplicit side-markering ville kun slæbe en uændret side-dictionary ind i den nye revision, som revisionsanalysen så rapporterer som en sidemodifikation. Til sidst skriver pladsholderen /ByteRange før /Contents, fordi patcheren finder sentinel-/ByteRange'en først og søger fremad efter den matchende hex-streng
Hvad ændrer sig, når en ekstern signer eller HSM producerer CMS'et?
Intet ændrer sig i arbejdsgangen, men offsettene betyder nu, hvad specifikationen siger. PreparePDFForSigning returnerer to 0-baserede intervaller, hvis gab er hele /Contents-strengen, og ContentsHexStart er det én-baserede indeks for det første hex-ciffer i AnsiString'en. En kortere CMS polstres med 0 til sidst, før det afsluttende >. Fordi PreparePDFForSigning patcher den første upatchede sentinel, den finder, så forbered præcis én pladsholder pr. revision, og foretræk InsertSignatureHexAt med de returnerede offsets frem for den søgningsbaserede InsertSignatureHex, når der allerede findes tidligere signaturer i filen
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // din helper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Gaben er hele hex-strengen: '<' afslutter interval 1, '>' går forud for interval 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-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'); // din helper
end;
Hvor ligger grænserne for de nye tjek?
Gab-tjekket lukker ét bestemt hul og bør ikke oversælges. svValid betyder stadig byte-integritet plus en nøgle, der matcher det indlejrede certifikat; tillid til det certifikat er en separat beslutning. Gabet valideres kun, når verifieren har kildebytes, som VerifyLoadedSignatureEx læser fra den indlæste fil, og som TStream-overloadene får fra dig. For den første signatur i en medsigneret fil er CoversWholeDocument korrekt False, og om den tilføjede revision kun tilføjede en signatur eller også ændrede sider er et spørgsmål for DocMDP, FieldMDP og revisionsanalyse i HotPDF. Bemærk også, at det tilknyttede PDF MAC-tjek sammenligner offsets med <- og >-positionerne, så det accepterer både det gamle og det nye layout; ethvert eget værktøj, der hardkoder offsetsene fra før v2.759.0, fejler først, når det møder en nysigneret fil
Hvis din Delphi- eller C++Builder-applikation signerer, medsignerer eller auditerer PDF'er, er den sikreste vej at lade ét bibliotek producere og verificere samme layout. HotPDF, den native Delphi PDF-komponent leverer den strengere gab-validering, inkrementelle anden-signaturer og external-signer-krogene vist ovenfor i én komponent