Artikel Teknis

Signature Wrapping PDF: Celah ByteRange dan Signature Kedua

HotPDF, komponen PDF Delphi, kini menolak signature wrapping: sejak v2.759.0 baik VerifyLoadedSignatureEx maupun validator batch mensyaratkan celah antara dua segmen /ByteRange berupa hex string /Contents persis, delimiter termasuk, dan v2.761.0 menambahkan AddLoadedSignedSignatureField supaya signature kedua bisa ditambahkan ke PDF yang sudah ditandatangani sebagai revisi inkremental yang bersih. Dua perubahan ini sepasang, karena signature kedua yang benar persis merupakan tata letak yang diharapkan verifier yang lebih ketat

Situasi yang membuka masalah ini biasa saja. Kontrak ditandatangani vendor, lalu dialirkan ke seorang approver yang harus countersign tanpa mengganggu signature pertama. Revisi kedua ditambahkan setelah yang pertama, /ByteRange-nya sendiri mencakup seluruh file yang sudah membesar, dan kedua signature seharusnya verify. Menjangkau situ itu secara manual berarti menulis bagian inkremental sendiri, dan test fixture yang persis melakukan itu ternyata struktur signature-wrapping buku teks yang diterima verifier lama dengan senang hati. Kalau Anda belum pernah melihat API verifikasinya, panduan memverifikasi signature digital PDF dengan HotPDF membahas dasar-dasarnya yang jadi pijakan artikel ini

Apa persisnya yang berada di celah ByteRange?

Celah harus memuat value /Contents yang utuh dan tak lebih dari itu: ISO 32000-1 §12.8.3.3 menyatakan hex string, dengan delimiter < dan >-nya, pas persis di ruang antara dua rentang byte, dan ISO 32000-2 §12.8.1 melanjutkan aturan yang sama. Tabel 252 dan dokumen PAdES hanya menyebut digest mengecualikan value Contents, yang gampang dibaca sebagai mengecualikan hanya digit hex-nya. Rilis HotPDF terdahulu membacanya begitu: PreparePDFForSigning dan persiapan CMS streaming meng-hash kurung sudutnya juga, dengan komentar sumber yang bersikeras kurungnya harus tercakup. Validator yang membandingkan celah dengan value signature menandai tata letak itu sebagai byte range tak valid, jadi v2.759.0 memindahkan kedua delimiter keluar dari rentang yang ditandatangani. Pengecekan mandiri cepat pada file bertanda tangan mana pun adalah melihat dua byte: byte di offset ByteRange[1] harus < dan byte di offset ByteRange[2] - 1 harus >

Anatomi ByteRange signature PDF yang terisi benar di HotPDF: rentang pertama mencakup file dari byte nol, celah menampung hex string /Contents utuh termasuk delimiter kurang-dari dan lebih-dari, rentang kedua mencakup trailer sampai akhir, dan dua pengecekan satu byte di ByteRange[1] serta ByteRange[2] - 1 mengonfirmasi tata letak itu di file bertanda tangan mana pun
Sejak v2.759.0 delimiter berada di luar rentang yang ditandatangani, sehingga digest mencakup digit saja dan celah bisa divalidasi byte demi byte

Kenapa pengecekan celah tak kosong lolos dari signature wrapping?

Pengecekan celah tak kosong hanya membuktikan bahwa sesuatu dikeluarkan dari digest, bukan apa yang dikeluarkan, dan itulah seluruh permukaan serangannya. Placeholder /Contents dicadangkan dengan ribuan digit nol, sementara kontainer CMS sungguhan jarang mengisinya. Penyerang bisa menutup hex string lebih awal di dalam padding nol itu dengan sebuah >, menulis objek baru atau revisi palsu ke sisa ruang cadangan itu, dan membiarkan rentang byte tak tersentuh. Signature CMS tetap verify karena setiap byte yang ditandatangani tak berubah, rentangnya tetap mulai dari 0 dan berakhir di ukuran file, dan verifier HotPDF lama melaporkan svValid dengan CoversWholeDocument disetel True. PDF reader, sementara itu, mem-parse apa pun yang duduk di lubang tak bertanda tangan itu

HotPDF kini memperlakukan celah sebagai data yang divalidasi byte demi byte. Verifier membaca celah itu, mencabut delimiternya, hanya menerima digit hex plus whitespace PDF (tab, line feed, form feed, carriage return, spasi), men-decode digitnya, dan mensyaratkan hasilnya sama persis dengan /Contents dictionary signature. Apa pun selain itu menurunkan hasilnya ke svInvalidByteRange. Pengecekan berjalan di jalur signature tunggal maupun ValidateLoadedSignatureBatch, yang mempertahankan logika coverage-nya sendiri dan butuh perbaikan yang sama. File yang dihasilkan HotPDF sebelum v2.759.0, yang celahnya hanya berisi digit dengan kurung duduk tepat di dalam rentang, tetap verify, jadi dokumen arsip tak mendadak memerah

Cara signature wrapping mengeksploitasi ByteRange PDF yang diperiksa longgar di Delphi: penyerang menutup hex string lebih awal di dalam ribuan digit nol cadangan, menulis revisi palsu ke celah tak bertanda tangan tanpa menyentuh satu byte tercakup pun, dan pengecekan HotPDF lama melaporkan svValid dengan CoversWholeDocument true sampai v2.759.0 mulai memvalidasi celah byte demi byte
Celah tak kosong hanya membuktikan sesuatu dikeluarkan dari digest, bukan apa — lubang berpading itulah seluruh permukaan serangannya
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;

Bagaimana menambahkan signature kedua ke PDF yang sudah ditandatangani?

Buka file bertanda tangan dengan BeginIncrementalUpdate, panggil AddLoadedSignedSignatureField, simpan dengan SaveIncrementalUpdate, lalu tandatangani file yang sudah disiapkan dengan class function THotPDF.SignPDFWithPFX. Sebelum v2.761.0, resep terdokumentasi berupa memanggil THPDFPage.AddSignedSignatureField setelah BeginIncrementalUpdate tak mungkin jalan, karena CurrentPage adalah nil di mode inkremental dan tak ada yang bisa menempelkan placeholder /V ke sebuah field di dokumen termuat. Method baru membuat widget di halaman termuat dan menggantungkan dictionary placeholder yang sama yang dipakai jalur dokumen baru di bawah /V, jadi kedua jalur penandatanganan berbagi satu serialisasi. Untuk signature pertamanya sendiri, artikel membuat signature digital PAdES di Delphi menuntun pipeline PFX-nya

var
  Pdf: THotPDF;
  FieldIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('Signed.pdf');
    // Halaman 0, rectangle widget dalam poin, 8192 byte dicadangkan untuk 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 sengaja lebih pendiam daripada saudara-saudaranya. Pembuat field AddLoaded* lainnya menyetel /NeedAppearances true pada AcroForm, yang menyuruh viewer meregenerasi appearance field; di dokumen bertanda tangan, regenerasi itu bisa menulis ulang konten yang ditandatangani, jadi method baru mencabut flag itu lagi kecuali sumbernya memang sudah membawanya. /SigFlags mempertahankan nilai aslinya OR 3 (SignaturesExist plus AppendOnly, ISO 32000-1 Tabel 219). Anda juga tak perlu memanggil MarkDirty di halaman: penambahan ke /Annots dan /Fields merambatkan flag dirty ke objek tak langsung pemiliknya, dan penanda halaman eksplisit hanya akan menyeret dictionary halaman yang tak berubah ke revisi baru, yang lalu dilaporkan analisis revisi sebagai modifikasi halaman. Terakhir, placeholder menulis /ByteRange sebelum /Contents, karena patcher menemukan sentinel /ByteRange lebih dulu lalu mencari maju hex string yang cocok

Alur kerja HotPDF Delphi untuk countersign PDF yang sudah ditandatangani: BeginIncrementalUpdate membuka file, AddLoadedSignedSignatureField membuat widget dan mencadangkan placeholder /Contents, SaveIncrementalUpdate menambahkan revisi kedua, dan SignPDFWithPFX mengisinya, membiarkan signature pertama valid dengan UnsignedTrailingBytes sementara ByteRange baru mencakup seluruh file yang membesar
Satu placeholder per revisi, disiapkan dan ditambal oleh serialisasi yang sama di kedua jalur penandatanganan — tata letak bersih yang diharapkan verifier yang lebih ketat

Apa yang berubah ketika signer eksternal atau HSM menghasilkan CMS?

Tak ada yang berubah di alur kerjanya, tapi offset-offsetnya kini bermakna sebagaimana kata spesifikasi. PreparePDFForSigning mengembalikan dua rentang berbasis 0 yang celahnya adalah string /Contents utuh, dan ContentsHexStart adalah indeks berbasis 1 dari digit hex pertama di AnsiString. CMS yang lebih pendek di-pad dengan 0 di ujung, sebelum > penutup. Karena PreparePDFForSigning menambal sentinel pertama yang belum ditambal yang ditemukannya, siapkan tepat satu placeholder per revisi, dan utamakan InsertSignatureHexAt dengan offset yang dikembalikan daripada InsertSignatureHex berbasis pencarian ketika signature lebih awal sudah ada di file

var
  Bytes, ToSign, CmsHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, HexStart, HexLen: Integer;
begin
  Bytes := LoadFileAsAnsiString('Prepared.pdf');   // helper Anda
  if not THotPDF.PreparePDFForSigning(Bytes, R1Start, R1Len,
    R2Start, R2Len, HexStart, HexLen) then
    raise Exception.Create('No signature placeholder found');

  // Gap-nya adalah hex string utuh: '<' mengakhiri rentang 1, '>' mendahului rentang 2
  Assert(Bytes[R1Start + R1Len + 1] = '<');
  Assert(Bytes[R2Start] = '>');

  ToSign := Copy(Bytes, R1Start + 1, R1Len) + Copy(Bytes, R2Start + 1, R2Len);
  CmsHex := SignDetachedWithHsm(ToSign);           // signer CMS Anda, 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');  // your helper
end;

Di mana batas-batas pengecekan baru ini?

Pengecekan celah menutup satu lubang spesifik dan tak boleh dilebih-lebihkan. svValid tetap berarti integritas byte plus kunci yang cocok dengan sertifikat tertanam; kepercayaan pada sertifikat itu adalah keputusan terpisah. Celah divalidasi hanya ketika verifier punya byte sumber, yang dibaca VerifyLoadedSignatureEx dari file termuat dan yang overload TStream ambil dari Anda. Untuk signature pertama di file yang di-countersign, CoversWholeDocument benar bernilai False, dan apakah revisi tambahannya hanya menambah signature atau juga mengubah halaman adalah pertanyaan untuk analisis DocMDP, FieldMDP dan revisi di HotPDF. Perhatikan juga bahwa pengecekan PDF MAC terlampir membandingkan offset dengan posisi < dan >, jadi ia menerima tata letak lama maupun baru; tool milik Anda sendiri yang hard-code offset pra-v2.759.0 akan gagal lebih dulu saat bertemu file yang baru ditandatangani

Kalau aplikasi Delphi atau C++Builder Anda menandatangani, countersign, atau mengaudit PDF, jalur paling aman adalah membiarkan satu library menghasilkan dan memverifikasi tata letak yang sama. HotPDF, komponen PDF Delphi native mengirim validasi celah yang lebih ketat, signature kedua inkremental, dan hook signer eksternal yang ditunjukkan di atas dalam satu komponen