Teknisk artikel

PDF-signatur-wrapping: ByteRange-gap och andra signaturer

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 >

Anatomin hos ett korrekt ifyllt PDF-signatur-ByteRange i HotPDF: det första intervallet täcker filen från byte noll, gapet håller den fullständiga /Contents-hex-strängen inklusive mindre-än- och större-än-avgränsarna, det andra intervallet täcker trailern till slutet, och två enbyte-kontroller vid ByteRange[1] och ByteRange[2] - 1 bekräftar layouten på vilken signerad fil som helst
Sedan v2.759.0 sitter avgränsarna utanför de signerade intervallen, så digesten täcker bara siffrorna och gapet kan valideras byte för byte

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

Hur signatur-wrapping utnyttjar en löst kontrollerad PDF-ByteRange i Delphi: angriparen stänger hex-strängen tidigt inuti tusentals reserverade nollsiffror, skriver en förfalskad revision in i det osignerade gapet utan att röra någon täckt byte, och den gamla HotPDF-kontrollen rapporterade svValid med CoversWholeDocument true tills v2.759.0 började validera gapet byte för byte
Ett icke-tomt gap bevisar bara att något lämnades utanför digesten, inte vad — det utfyllda hålet är hela angreppsytan
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

HotPDF Delphi-arbetsflödet för att motregistrera en redan signerad PDF: BeginIncrementalUpdate öppnar filen, AddLoadedSignedSignatureField skapar widgeten och reserverar /Contents-platshållaren, SaveIncrementalUpdate bifogar en andra revision, och SignPDFWithPFX fyller den, med den första signaturen kvar som giltig med UnsignedTrailingBytes medan den nya ByteRange:en spänner över hela den växta filen
En platshållare per revision, förberedd och patchad av samma serialisering på båda signeringsvägarna — den rena layout som den strängare verifieraren väntar sig

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