HotPDF, Delphi PDF-komponenten, avviser nå signature wrapping: siden v2.759.0 krever både VerifyLoadedSignatureEx og bunkevalidatoren at gapet mellom de to /ByteRange-segmentene er nøyaktig /Contents-heksstrengen, skilletegn inkludert, og v2.761.0 legger til AddLoadedSignedSignatureField slik at en andre signatur kan føyes til en allerede signert PDF som en ren inkrementell revisjon. De to endringene hører sammen, for en korrekt andre signatur er nøyaktig oppsettet den strengere verifikatoren forventer
Situasjonen som eksponerte problemet, er helt vanlig. En kontrakt signeres av leverandøren, og sendes så til en godkjenner som må motunderskrive uten å forstyrre den første signaturen. Den andre revisjonen føyes til etter den første, dens egen /ByteRange spenner over hele den vokste filen, og begge signaturer bør verifisere. Å komme dit for hånd betydde å skrive en inkrementell seksjon selv, og testfixtureen som gjorde nøyaktig det, viste seg å være en lærebok-signature-wrapping-struktur som den gamle verifikatoren gladelig aksepterte. Hvis du ikke har sett verifiserings-API-et før, dekker guiden om å verifisere digitale PDF-signaturer med HotPDF grunnene denne artikkelen bygger på
Hva hører egentlig hjemme i ByteRange-gapet?
Gapet må inneholde den komplette /Contents-verdien og ingenting annet: ISO 32000-1 §12.8.3.3 sier at heksstrengen, med sine <- og >-skilletegn, passer nøyaktig i rommet mellom de to byte-områdene, og ISO 32000-2 §12.8.1 bærer samme regel videre. Tabell 252 og PAdES-dokumentene sier bare at digesten ekskluderer Contents-verdien, noe som er lett å lese som å ekskludere bare hekssifrene. Tidligere HotPDF-utgivelser leste den slik: PreparePDFForSigning og den strømmende CMS-forberedelsen hashet også vinkelklammene, med en kildekommentar som insisterte på at klammene måtte dekkes. Validatorer som sammenligner gapet mot signaturverdien flagger det oppsettet som en ugyldig byte range, så v2.759.0 flytter begge skilletegnene ut av de signerte områdene. En rask uavhengig sjekk på enhver signert fil er å se på to byte: byten ved offset ByteRange[1] må være < og byten ved offset ByteRange[2] - 1 må være >
Hvorfor bommer en ikke-tom gap-sjekk på signature wrapping?
En ikke-tom gap-sjekk beviser bare at noe ble latt utenfor digesten, ikke hva som ble latt utenfor, og det er hele angrepsflaten. /Contents-plassholderen reserveres med tusenvis av nullsiffer, mens en ekte CMS-container sjelden fyller den. En angriper kan lukke heksstrengen tidlig inne i den null-utfyllingen med en >, skrive nye objekter eller en forfalsket revisjon inn i resten av det reserverte rommet og la byte-områdene være urørt. CMS-signaturen verifiserer fortsatt fordi hver signert byte er uendret, områdene starter fortsatt på 0 og slutter ved filstørrelsen, og den gamle HotPDF-verifikatoren rapporterte svValid med CoversWholeDocument satt til True. En PDF-leser parser i mellomtiden hva som enn ligger i det usignerte hullet
HotPDF behandler nå gapet som data som skal valideres byte for byte. Verifikatoren leser gapet, stripper skilletegnene, godtar bare hekssiffer pluss PDF-whitespace (tab, linjemating, skjemamating, vognretur, mellomrom), dekoder sifrene og krever at resultatet er lik signaturordbokens /Contents nøyaktig. Alt annet nedgraderer resultatet til svInvalidByteRange. Sjekken kjører i både enkelt-signatur-stien og ValidateLoadedSignatureBatch, som beholdt sin egen dekningslogikk og trengte samme fiks. Filer produsert av HotPDF før v2.759.0, hvis gap holdt bare siffer med klammene like innenfor områdene, verifiserer fortsatt, så arkiverte dokumenter blir ikke plutselig 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 legger du til en andre signatur i en allerede signert PDF?
Åpne den signerte filen med BeginIncrementalUpdate, kall AddLoadedSignedSignatureField, lagre med SaveIncrementalUpdate, og signer så den forberedte filen med klassefunksjonen THotPDF.SignPDFWithPFX. Før v2.761.0 kunne den dokumenterte oppskriften med å kalle THPDFPage.AddSignedSignatureField etter BeginIncrementalUpdate ikke fungere, for CurrentPage er nil i inkrementell modus og ingenting kunne henge en /V-plassholder på et felt i et lastet dokument. Den nye metoden oppretter widgeten på den lastede siden og henger samme plassholderordbok som ny-dokument-stien bruker, under /V, så begge signeringsrutene deler én serialisering. For selve den første signaturen går artikkelen om å lage PAdES digitale signaturer i Delphi gjennom PFX-pipelinen
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Side 0, widget-rektangel i punkter, 8192 byte reservert for 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 er med vilje stillere enn sine søsken. De andre AddLoaded*-feltoppretterne setter /NeedAppearances true på AcroForm-en, som ber en viser om å regenerere feltutseender; på et signert dokument kan den regenereringen omskrive signert innhold, så den nye metoden fjerner flagget igjen med mindre kilden allerede bar det. /SigFlags beholder sin opprinnelige verdi OR 3 (SignaturesExist pluss AppendOnly, ISO 32000-1 tabell 219). Du trenger heller ikke å kalle MarkDirty på siden: å føye til i /Annots og /Fields sprer dirty-flagget til det eiende indirekte objektet, og en eksplisitt sidemarkering ville bare dratt en uendret sideordbok inn i den nye revisjonen, noe revisjonsanalysen da rapporterer som en sidemodifikasjon. Til slutt skriver plassholderen /ByteRange før /Contents, for patcheren finner sentinel-en /ByteRange først og søker fremover etter den matchende heksstrengen
Hva endrer seg når en ekstern signerer eller HSM produserer CMS-en?
Ingenting endrer seg i arbeidsflyten, men offsettene betyr nå det spesifikasjonen sier. PreparePDFForSigning returnerer to 0-baserte områder hvis gap er hele /Contents-strengen, og ContentsHexStart er den 1-baserte indeksen til det første hekssifferet i AnsiString-en. En kortere CMS fylles med 0 på slutten, før den avsluttende >. Fordi PreparePDFForSigning patcher den første upatchede sentinel-en den finner, forbered nøyaktig én plassholder per revisjon, og foretrekk InsertSignatureHexAt med de returnerte offsettene fremfor den søkebaserte InsertSignatureHex når tidligere signaturer allerede finnes i filen
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // din hjelper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// Gapet er hele heksstrengen: '<' avslutter område 1, '>' går foran område 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-signerer, 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 hjelper
end;
Hvor ligger grensene for de nye sjekkene?
Gap-sjekken lukker ett bestemt hull og bør ikke overselges. svValid betyr fortsatt byte-integritet pluss en nøkkel som matcher det innbakte sertifikatet; tillit til det sertifikatet er en egen avgjørelse. Gapet valideres bare når verifikatoren har kildebyte, noe VerifyLoadedSignatureEx leser fra den lastede filen og TStream-overloadene tar fra deg. For den første signaturen i en motunderskrevet fil er CoversWholeDocument korrekt False, og om den tilføyde revisjonen bare la til en signatur eller også endret sider, er et spørsmål for DocMDP, FieldMDP og revisjonsanalyse i HotPDF. Merk også at den tilknyttede PDF MAC-sjekken sammenligner offsetter mot <- og >-posisjonene, så den godtar både det gamle og det nye oppsettet; ethvert eget verktøy som hardkoder offsettene fra før v2.759.0, feiler først når det treffer en fersk signert fil
Hvis Delphi- eller C++Builder-applikasjonen din signerer, motunderskriver eller reviderer PDF-er, er den tryggeste stien å la ett bibliotek produsere og verifisere samme oppsett. HotPDF, den opprinnelige Delphi PDF-komponenten sender den strengere gap-valideringen, inkrementelle andre signaturer og ekstern-signerer-krokene vist over i én enkelt komponent