Teknisk artikkel

PDF signature wrapping: ByteRange-gaper og andre signaturer

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 >

Anatomien til en korrekt fylt PDF-signatur ByteRange i HotPDF: det første området dekker filen fra byte null, gapet holder den komplette /Contents-heksstrengen inkludert mindre-enn- og større-enn-skilletegnene, det andre området dekker traileren til slutten, og to enbyte-sjekker ved ByteRange[1] og ByteRange[2] - 1 bekrefter oppsettet på enhver signert fil
Siden v2.759.0 sitter skilletegnene utenfor de signerte områdene, så digesten dekker bare sifrene og gapet kan valideres byte for byte

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

Hvordan signature wrapping utnytter en løst sjekket PDF ByteRange i Delphi: angriperen lukker heksstrengen tidlig inne i tusenvis av reserverte nullsiffer, skriver en forfalsket revisjon inn i det usignerte gapet uten å røre noen dekket byte, og den gamle HotPDF-sjekken rapporterte svValid med CoversWholeDocument satt til true helt til v2.759.0 begynte å validere gapet byte for byte
Et ikke-tom gap beviser bare at noe ble latt utenfor digesten, ikke hva — det utfylte hullet er hele angrepsflaten
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

HotPDF Delphi-arbeidsflyten for å motunderskrive en allerede signert PDF: BeginIncrementalUpdate åpner filen, AddLoadedSignedSignatureField oppretter widgeten og reserverer /Contents-plassholderen, SaveIncrementalUpdate føyer til en andre revisjon, og SignPDFWithPFX fyller den, slik at den første signaturen forblir gyldig med UnsignedTrailingBytes mens den nye ByteRange-en spenner over hele den vokste filen
Én plassholder per revisjon, forberedt og patchet av samme serialisering på begge signeringsrutene — det rene oppsettet den strengere verifikatoren forventer

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