Artikel Teknis

Mengapa Validator Menolak Tanda Tangan PAdES: PDFium di Delphi

Validator menolak tanda tangan PAdES karena salah satu dari tiga alasan dalam hampir setiap kasus yang kami debug dengan PDFium Component di Delphi: array /ByteRange masih menahan placeholder nol-nya, waktu penandatanganan /M bukan string tanggal PDF yang terbentuk dengan baik, atau pembaruan inkremental menghilangkan entri /Encrypt dari dokumen terenkripsi. Ketiganya menghasilkan berkas yang dapat dibuka dengan baik, dirender dengan baik, dan gagal saat validator yang patuh membaca kamus tanda tangan

Skenario yang mendasari artikel ini sangat spesifik. Anda menandatangani kontrak dengan alur kerja dari artikel penandatanganan PAdES B-B, kode pemeriksaan Anda sendiri melaporkan tanda tangan ada dan sehat secara struktural, setiap pengujian lokal bernilai hijau — lalu pihak lawan mengunggah berkas tersebut ke platform validasi mereka dan mendapatkan tanda silang merah. Tidak ada kegagalan yang terlihat di viewer, karena tidak satu pun dari ketiga cacat ini menyentuh konten halaman. Mereka hidup sepenuhnya di dalam kamus tanda tangan dan trailer berkas, yang merupakan tempat yang tepat di mana validator mencari dan viewer sebagian besar mengabaikannya

Mengapa ByteRange tanda tangan PDF saya tidak valid?

Penolakan ByteRange hampir selalu berarti keempat bidang tersebut tidak pernah diisi, bukan karena rentangnya sedikit salah. Array /ByteRange [A B C D] menyatakan dua rentang, byte A ke A+B dan byte C ke C+D, dan EN 319 142-1 §6.3 (persyaratan k) menuntut bahwa bersama-sama mereka mencakup seluruh berkas kecuali string /Contents yang dikodekan heksadesimal. Dalam angka: A adalah 0, C >= A+B, C+D sama dengan panjang berkas, dan celah antara B dan C menampung tepat string heksadesimal <...> — dua byte tanda kurung siku ditambah dua karakter heksadesimal per byte data CMS. Validator yang membaca [0 0 0 0] menyimpulkan tanda tangan tidak mencakup apa pun dan menolaknya, terlepas dari seberapa benar struktur CMS di dalam /Contents

Mekanisme bagaimana hal ini terjadi patut dipahami karena pola yang sama ada di setiap tumpukan penandatanganan. Penandatangan tidak dapat mengetahui offset akhir sampai berkas tersebut ditata letaknya, sehingga penulis memancarkan array dengan placeholder nol dengan lebar tetap dan mengisinya kembali (back-fills) setelah tata letak. Dalam rilis PDFium Component sebelum versi 2.14.1, pengisian kembali tersebut mencari pola placeholder mulai dari posisi /Contents — tetapi /ByteRange berada sebelum /Contents dalam kamus, sehingga pencarian tidak menemukan apa pun dan ketiga penggantian gagal secara diam-diam. Digest CMS dihitung berdasarkan rentang yang benar, sehingga kriptografinya sehat; deklarasi rentang tersebut tetap nol, sehingga setiap validator yang patuh menolak berkas tersebut. PAdES B-LTA Document Time-stamps, yang menggunakan tata letak kamus yang sama, gagal dengan cara yang sama — relevan jika Anda membangun di atas tanda tangan B-LT dan B-LTA jangka panjang. Versi 2.14.1 mengisi kembali dari awal objek tanda tangan, dan perbaikan tersebut dicakup oleh uji regresi yang mengurai berkas yang dihasilkan dan menegaskan aritmatika di bawah ini

function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
  Raw: TBytes;
  Text: AnsiString;
  P, N: Integer;
  F: array[0..3] of Int64;
begin
  Raw := TFile.ReadAllBytes(FileName);
  SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
  P := Pos('/ByteRange', Text);          // tanda tangan pertama saja
  Result := P > 0;
  if not Result then Exit;
  Inc(P, Length('/ByteRange'));
  for N := 0 to 3 do
  begin
    while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
    F[N] := 0;
    while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
    begin
      F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
      Inc(P);
    end;
  end;
  // EN 319 142-1 §6.3 req k: rentang mencakup semuanya kecuali /Contents
  Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
            (F[2] + F[3] = Int64(Length(Raw)));
end;

Dua puluh baris kode RTL biasa, tanpa panggilan pustaka, dan ia menangkap seluruh kelas kegagalan pada titik produksi. Jika Anda menyimpan satu penegasan (assertion) dari artikel ini, simpan yang ini: urai keluaran bertanda tangan Anda sendiri dan periksa ketiga kesamaan tersebut sebelum berkas meninggalkan proses Anda

Mengapa validator menandai waktu penandatanganan /M sebagai cacat?

Validator yang ketat menolak waktu penandatanganan yang bukan string tanggal PDF lengkap, dan dua bagian yang paling sering hilang adalah awalan D: dan penanda offset UTC. ISO 32000-1 §7.9.4 mendefinisikan format tanggal sebagai D:YYYYMMDDHHmmSS diikuti oleh offset — Z untuk UTC, atau hubungan +HH'mm' bertanda dengannya. Angka kosong 20260709143000 diurai sebagai tanggal bagi pembaca yang toleran, tetapi validator yang menerapkan tata bahasa secara harfiah melihat string yang cacat dalam bidang format wajib dan menandai tanda tangan tersebut. Rilis PDFium Component sebelum versi 2.14.4 menulis entri /M dari TPdf.SignPades dan SignPadesBytes persis dalam bentuk kosong tersebut; sejak versi 2.14.4 entri tersebut membawa awalan D: dan penanda Z, sehingga waktu penandatanganan yang dideklarasikan tertulis D:20260709143000Z

Dua catatan praktis menyertai bidang ini. Pertama, tulis UTC dan nyatakan demikian: stempel waktu tanpa penanda offset memaksa validator untuk menebak hubungan zona waktu, dan §7.9.4 memperlakukan offset sebagai bagian dari format alih-alih sebagai pelengkap opsional. Kedua, ingat apa itu /M — waktu yang dideklarasikan oleh penandatangan, sebuah klaim alih-alih bukti. Validator memeriksa formatnya di sini, bukan kebenarannya; waktu yang dapat dibuktikan berasal dari stempel waktu RFC 3161 pada PAdES B-T ke atas. Memformat bidang dengan benar dan mempercayainya adalah keputusan yang terpisah, dan validator hanya menegakkan yang pertama

Mengapa SignPades memunculkan EPadesCrypto pada PDF terenkripsi?

PDFium Component menolak menandatangani dokumen terenkripsi karena alternatifnya adalah menghasilkan berkas yang akan dirusak oleh pembaca yang patuh saat dibuka. Tanda tangan PAdES ditambahkan sebagai pembaruan inkremental, dan ISO 32000-1 §7.5.6 mengharuskan trailer baru dari bagian pembaruan untuk membawa setiap entri dari trailer sebelumnya kecuali /Prev — dalam dokumen terenkripsi itu mencakup /Encrypt. Jika dihilangkan, trailer terbaru menyatakan berkas tidak terenkripsi, sehingga pembaca yang patuh mengurai badan terenkripsi sebagai plaintext dan mendapatkan sampah. Lebih buruk lagi, aliran dan string yang ditambahkan oleh penandatangan harus dienkripsi dengan kunci dokumen agar sah, yang tidak dapat dilakukan oleh penyuntik tanda tangan plaintext. Tidak ada cara untuk menambahkan tanda tangan plaintext yang valid ke berkas terenkripsi, sehingga sejak versi 2.14.2 TPdf.SignPades, SignPadesBytes, dan InjectPadesDssMarkers memunculkan EPadesCrypto alih-alih memancarkan hasil yang rusak atau tidak dapat diverifikasi

try
  if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
    Writeln('Signing reported failure');
except
  on E: EPadesCrypto do
  begin
    // e.g. 'SignPadesBytes: the source document is encrypted;
    //       hapus enkripsi sebelum menandatangani'
    Writeln('Cannot sign: ', E.Message);
  end;
end;

Pengecualian (exception) adalah hasil yang benar, jadi rancang alur kerja di sekitarnya alih-alih menangkap dan mencoba kembali. Hapus enkripsi terlebih dahulu dengan otoritas pemilik, tanda tangani salinan plaintext, dan jika saluran distribusi memerlukan enkripsi, terima bahwa enkripsi-lalu-tanda-tangan dan tanda-tangan-lalu-enkripsi menghasilkan artefak yang berbeda dengan cerita validasi yang berbeda. Tanda tangan yang dihitung atas byte plaintext tidak dapat bertahan terhadap enkripsi ulang byte tersebut, sehingga pilihan yang jujur adalah berkas plaintext yang ditandatangani atau keputusan kebijakan yang didokumentasikan di samping kode

Bagaimana dua bug dapat saling menyatakan benar

Cacat ByteRange tersembunyi sekian lama karena validator kami sendiri salah dengan cara yang saling melengkapi, dan itu adalah pelajaran yang paling dapat ditransfer dalam artikel ini. Pemeriksaan cakupan di ValidatePadesCompliance mengharuskan rentang kedua dimulai tepat di A+B — celah nol — yang salah mengklasifikasikan tata letak standar di mana celah tersebut menampung string heksadesimal /Contents. Jadi validator internal menolak tata letak yang patuh secara persis dan generator internal tidak pernah menghasilkan satu pun, dan pipa penandatanganan-lalu-validasi ujung-ke-ujung (end-to-end) kebetulan tetap hijau. Setiap bug menghalangi rangkaian pengujian dari contoh kontra yang akan mengungkap bug lainnya. Versi 2.14.1 memperbaiki kedua sisi dalam rilis yang sama: generator mengisi kembali keempat bidang, dan validator menerima C >= A+B dengan C+D sama dengan panjang berkas

Perbaikan metodologis adalah validasi silang terhadap implementasi yang tidak Anda tulis. Generator dan validator yang berbagi basis kode, penulis, atau bahkan sekadar model mental dari format tersebut dapat menyetujui kesalahpahaman bersama tanpa batas waktu; validator independen mematahkan simetri tersebut. Jalankan keluaran bertanda tangan Anda melalui setidaknya satu pemeriksaan kepatuhan eksternal sebelum rilis, dan pertahankan pemeriksaan mandiri struktural dalam build sebagai jalur cepat pertama — TPdf.ValidatePades, yang dijelaskan dalam artikel pemeriksaan tanda tangan, melaporkan kegagalan cakupan sebagai masalah bernama

var
  R: TPadesValidationResult;
begin
  Pdf.FileName := 'contract-signed.pdf';
  Pdf.Active := True;
  R := Pdf.ValidatePades;
  if ppeiByteRangeNotCoveringFile in R.Issues then
    Writeln('ByteRange does not cover the file');
  if not R.IsCompliant then
    Writeln('Structural issues present: do not ship this file');
end;

Daftar periksa penolakan untuk keluaran bertanda tangan

Ketika sebuah platform menolak tanda tangan PAdES Anda, periksa penyebab struktural yang mudah sebelum mencurigai sertifikat atau rantai kepercayaan. Baca array /ByteRange dan verifikasi ketiga kesamaan: bidang pertama nol, rentang kedua dimulai pada atau setelah akhir yang pertama, rentang kedua berakhir tepat di akhir berkas. Baca entri /M dan verifikasi bahwa itu adalah string tanggal §7.9.4 lengkap dengan awalan D: dan penanda offset. Pastikan dokumen sumber tidak dienkripsi ketika tanda tangan ditambahkan — dan jika tumpukan Anda tetap menandatanganinya tanpa keluhan, perlakukan kebisuan itu sebagai bug di dalam tumpukan. Ketiga pemeriksaan tersebut berjalan pada byte mentah dalam milidetik, dan dalam pengalaman kami, mereka menjelaskan penolakan jauh lebih sering daripada penyebab kriptografis apa pun

Panggilan penandatanganan, pemeriksaan, dan validasi yang digunakan di sini — SignPades, ValidatePades, dan pemeriksaan kepatuhan PAdES dengan aritmatika ByteRange, pemformatan tanggal, dan gerbang enkripsi yang diperbaiki — dikirimkan bersama PDFium Component untuk Delphi, C++Builder, dan Lazarus