Technisch artikel

Signature wrapping: ByteRange-gap en tweede handtekening

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

Anatomie van een correct gevulde PDF-signature ByteRange in HotPDF: het eerste bereik dekt het bestand vanaf byte nul, de gap bevat de complete /Contents-hex-string inclusief de kleiner-dan- en groter-dan-delimiters, het tweede bereik dekt de trailer tot het einde, en twee een-byte-controles op ByteRange[1] en ByteRange[2] - 1 bevestigen de layout op elk ondertekend bestand
Sinds v2.759.0 zitten de delimiters buiten de ondertekende bereiken, dus de digest dekt alleen de cijfers en de gap kan byte voor byte gevalideerd worden

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

Hoe signature wrapping een los gecontroleerde PDF ByteRange in Delphi misbruikt: de aanvaller sluit de hex-string vroegtijdig binnen duizenden gereserveerde nulcijfers, schrijft een vervalste revisie in de onondertekende gap zonder een gedekte byte aan te raken, en de oude HotPDF-controle meldde svValid met CoversWholeDocument true totdat v2.759.0 de gap byte voor byte ging valideren
Een niet-lege gap bewijst alleen dat er iets buiten de digest is gelaten, niet wat — de opgevulde hole is het hele aanvalsoppervlak
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

De HotPDF Delphi-werkwijze voor het medetekenen op een al ondertekende PDF: BeginIncrementalUpdate opent het bestand, AddLoadedSignedSignatureField maakt de widget aan en reserveert de /Contents-placeholder, SaveIncrementalUpdate plakt een tweede revisie erachter, en SignPDFWithPFX vult hem, waarbij de eerste handtekening geldig blijft met UnsignedTrailingBytes terwijl de nieuwe ByteRange het hele gegroeide bestand omspannt
Eén placeholder per revisie, voorbereid en gepatcht door dezelfde serialisatie op beide signing-routes — de schone layout die de strengere verifier verwacht

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