HotPDF, het Delphi PDF-component, wijst signature wrapping nu af: sinds v2.759.0 eisen zowel VerifyLoadedSignatureEx als de batch-validator dat de gap tussen de twee /ByteRange-segmenten exact de hex-string van /Contents is, delimiters inbegrepen, en v2.761.0 voegt AddLoadedSignedSignatureField toe, zodat een tweede handtekening als een schone incrementele revisie aan een al ondertekende PDF kan worden toegevoegd. De twee wijzigingen horen bij elkaar, want een correcte tweede handtekening is precies de layout die de strengere verifier verwacht
De situatie die het probleem blootlegde is alledaags. Een contract wordt door de leverancier ondertekend en gaat daarna naar een goedkeurder die moet medetekenen zonder de eerste handtekening te verstoren. De tweede revisie wordt achter de eerste geplakt, zijn eigen /ByteRange omspannt het hele gegroeide bestand, en beide handtekeningen zouden moeten verifiëren. Dat met de hand bereiken betekende zelf een incrementele sectie schrijven, en de testfixture die precies dat deed bleek een schoolboek-structuur van signature wrapping die de oude verifier graag accepteerde. Als u de verificatie-API nog niet eerder hebt bekeken, behandelt de gids over het verifiëren van PDF digitale handtekeningen met HotPDF de basis waarop dit artikel voortbouwt
Wat hoort er precies in de ByteRange-gap?
De gap moet de complete /Contents-waarde bevatten en niets anders: ISO 32000-1 §12.8.3.3 zegt dat de hexadecimale string, met zijn <- en >-delimiters, precies in de ruimte tussen de twee byte-bereiken past, en ISO 32000-2 §12.8.1 neemt dezelfde regel mee. Table 252 en de PAdES-documenten zeggen alleen dat de digest de Contents-waarde uitsluit, wat makkelijk te lezen is als alleen de hex-cijfers uitsluiten. Eerdere HotPDF-releases lazen het zo: PreparePDFForSigning en de streaming CMS-voorbereiding hashten ook de hoekhaken mee, met een source comment dat erop stond dat de haken gedekt moesten worden. Validators die de gap tegen de handtekeningwaarde vergelijken vlaggen die layout als een ongeldig byte-bereik, dus v2.759.0 zet beide delimiters buiten de ondertekende bereiken. Een snelle onafhankelijke controle op elk ondertekend bestand is naar twee bytes kijken: de byte op offset ByteRange[1] moet < zijn en de byte op offset ByteRange[2] - 1 moet > zijn
Waarom mist een niet-lege gap-controle signature wrapping?
Een niet-lege gap-controle bewijst alleen dat er iets buiten de digest is gelaten, niet wat, en dat is het hele aanvalsoppervlak. De placeholder van /Contents is gereserveerd met duizenden nulcijfers, terwijl een echte CMS-container hem zelden vult. Een aanvaller kan de hex-string vroegtijdig sluiten binnen die nulopvulling met een >, nieuwe objecten of een vervalste revisie schrijven in de rest van de gereserveerde ruimte, en de byte-bereiken onaangeroerd laten. De CMS-handtekening verifieert nog steeds omdat elke ondertekende byte onveranderd is, de bereiken nog steeds bij 0 beginnen en bij de bestandsgrootte eindigen, en de oude HotPDF-verifier meldde svValid met CoversWholeDocument op True. Een PDF-lezer parst ondertussen wat er ook in dat onondertekende gat zit
HotPDF behandelt de gap nu als data die byte voor byte gevalideerd wordt. De verifier leest de gap, haalt de delimiters eraf, accepteert alleen hex-cijfers plus PDF-witruimte (tab, line feed, form feed, carriage return, spatie), decodeert de cijfers en eist dat het resultaat exact gelijk is aan de /Contents van de signature dictionary. Al het andere degradeert het resultaat naar svInvalidByteRange. De controle draait in zowel het single-signature-pad als ValidateLoadedSignatureBatch, dat zijn eigen coveragelogica hield en dezelfde fix nodig had. Bestanden die HotPDF vóór v2.759.0 produceerde, waarvan de gap alleen cijfers bevatte met de haken net binnen de bereiken, verifiëren nog steeds, dus gearchiveerde documenten worden niet plotseling rood
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;
Hoe voegt u een tweede handtekening toe aan een al ondertekende PDF?
Open het ondertekende bestand met BeginIncrementalUpdate, roep AddLoadedSignedSignatureField aan, sla op met SaveIncrementalUpdate, en onderteken het voorbereide bestand daarna met de class function THotPDF.SignPDFWithPFX. Vóór v2.761.0 kon het gedocumenteerde recept van THPDFPage.AddSignedSignatureField na BeginIncrementalUpdate niet werken, omdat CurrentPage in incrementele modus nil is en niets een /V-placeholder aan een veld op een geladen document kon hangen. De nieuwe methode maakt de widget op de geladen pagina aan en hangt dezelfde placeholder-dictionary die het nieuwe-document-pad gebruikt onder /V, dus beide signing-routes delen één serialisatie. Voor de eerste handtekening zelf loopt het artikel over het maken van PAdES digitale handtekeningen in Delphi door de PFX-pijplijn
var
Pdf: THotPDF;
FieldIndex: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('Signed.pdf');
// Pagina 0, widget-rechthoek in punten, 8192 bytes gereserveerd voor de 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 is bewust stiller dan zijn broertjes. De andere AddLoaded*-veldmakers zetten /NeedAppearances true op de AcroForm, wat een viewer vraagt om veldappearances te regenereren; op een ondertekend document kan die regeneratie ondertekende content herschrijven, dus de nieuwe methode haalt de vlag weer weg tenzij de bron hem al droeg. /SigFlags houdt zijn oorspronkelijke waarde OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Table 219). U hoeft ook geen MarkDirty op de pagina aan te roepen: toevoegen aan /Annots en /Fields propageert de dirty-vlag naar de eigenaars van het indirecte object, en een expliciete paginamarkering zou alleen een onveranderde page dictionary in de nieuwe revisie slepen, die de revisieanalyse dan als een paginawijziging rapporteert. Tot slot schrijft de placeholder /ByteRange vóór /Contents, omdat de patcher eerst de sentinel /ByteRange opspoort en vooruit zoekt naar de bijbehorende hex-string
Wat verandert er als een externe signer of HSM de CMS produceert?
Aan de werkwijze verandert niets, maar de offsets betekenen nu wat de specificatie zegt. PreparePDFForSigning geeft twee 0-based bereiken terug waarvan de gap de hele /Contents-string is, en ContentsHexStart is de 1-based index van het eerste hex-cijfer in de AnsiString. Een kortere CMS wordt met 0 opgevuld aan het einde, vóór de sluitende >. Omdat PreparePDFForSigning de eerste ongepatchte sentinel patcht die hij vindt, bereid precies één placeholder per revisie voor, en geef bij al bestaande eerdere handtekeningen in het bestand de voorkeur aan InsertSignatureHexAt met de teruggegeven offsets boven de op zoeken gebaseerde InsertSignatureHex
var
Bytes, ToSign, CmsHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
Bytes := LoadFileAsAnsiString('Prepared.pdf'); // uw helper
if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
R2Start, R2Len, HexStart, HexLen) then
raise Exception.Create('No signature placeholder found');
// De gap is de hele hex-string: '<' sluit bereik 1 af, '>' gaat bereik 2 vooraf
Assert(Bytes[R1Start + R1Len + 1] = '<');
Assert(Bytes[R2Start] = '>');
ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
CmsHex := SignDetachedWithHsm(ToSign); // uw CMS-signerder, 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'); // uw helper
end;
Waar liggen de grenzen van de nieuwe controles?
De gap-controle dicht één specifiek gat en moet niet worden oversold. svValid betekent nog steeds byte-integriteit plus een key die matcht met het ingesloten certificaat; vertrouwen in dat certificaat is een aparte beslissing. De gap wordt alleen gevalideerd als de verifier bronbytes heeft, die VerifyLoadedSignatureEx uit het geladen bestand leest en die de TStream-overloads van u ontvangen. Voor de eerste handtekening in een medetekend bestand is CoversWholeDocument terecht False, en of de toegeplakte revisie alleen een handtekening toevoegde of ook pagina's veranderde, is een vraag voor DocMDP-, FieldMDP- en revisieanalyse in HotPDF. Merk ook op dat de bijgevoegde PDF MAC-controle offsets tegen de <- en >-posities vergelijkt, dus hij accepteert zowel de oude als de nieuwe layout; elke eigen tool die de pre-v2.759.0-offsets hardcodeert, faalt het eerst zodra hij een vers ondertekend bestand tegenkomt
Als uw Delphi- of C++Builder-applicatie PDF's ondertekent, medetekent of auditeert, is het veiligste pad één library hetzelfde layout laten produceren en verifiëren. HotPDF, het native Delphi PDF-component scheept de strengere gap-validatie, incrementele tweede handtekeningen en de external-signer-hooks die hierboven getoond worden in één component