Sebuah workbench yang merangkai validasi compliance dengan penandatanganan digital harus mengoordinasikan empat langkah, dalam urutan ini, dan menjaga agar semuanya tetap terikat pada satu set byte yang sama dari awal sampai akhir. Ia menjalankan preflight PDF/A atau PDF/UA. Ia menerapkan perbaikan apa pun yang dituntut oleh temuan tersebut dan menyimpan revisi yang sudah dikoreksi. Ia menandatangani revisi yang persis itu. Kemudian ia membaca kembali file yang sudah ditandatangani dan memastikan signature-nya benar-benar mencakup file tersebut. Urutan ini bukan sekadar formalitas. Lewati langkah baca-kembali dan Anda hanya mempercayai jalur penulisan Anda sendiri; biarkan preflight berjalan terhadap revisi yang salah dan laporan compliance Anda mendeskripsikan file yang tidak pernah Anda kirimkan
Bagian yang paling sering salah ditangani oleh pipeline buatan sendiri adalah celah antara validasi dan penandatanganan. Jalankan keduanya sebagai dua tool terpisah dengan sebuah remediation pass di antaranya, dan setidaknya tiga revisi file yang berbeda akan muncul, masing-masing dengan byte-nya sendiri. Laporan preflight yang Anda serahkan ke auditor mendeskripsikan salah satu di antaranya. Signature-nya membekukan yang lain. Tidak ada apa pun di dalam file yang menyatakan bahwa keduanya adalah revisi yang sama, dan seringkali memang bukan. PDF Library for Delphi, losLab PDF Developer Library untuk Delphi dan C++Builder, menempatkan preflight dan penandatanganan PAdES di balik satu facade class, sehingga seluruh urutan ini bisa berjalan dalam satu proses yang tidak pernah kehilangan jejak byte mana yang sedang dibicarakannya. Setiap pemanggilan di bawah ini ada di dalam pustaka hari ini, begitu juga setiap jebakan yang dicatat di sampingnya
Tiga revisi dari satu dokumen, dan bagaimana celah itu terbuka
Hitung jumlah save-nya. File asli datang dari upstream. Remediation pass memuatnya, mengaktifkan sebuah compliance mode, dan menulis revisi yang sudah dikoreksi. Signing pass menambahkan signature sebagai incremental update, yang merupakan penulisan ketiga. Tiga save, tiga tata letak byte, dan sebuah laporan preflight tidak berarti apa-apa kecuali ia menyebutkan revisi mana dari ketiganya yang dicakupnya. SHA-256 dari file tersebut, dicatat di samping setiap proses preflight dan setiap signature, adalah anchor murah yang memungkinkan Anda membuktikan bahwa revisi yang Anda validasi adalah revisi yang Anda tandatangani
Satu perilaku pustaka ini semakin memperketat disiplin tersebut. Perbaikan compliance yang diminta lewat SetPDFAMode atau SetPDFUAMode tidak langsung berlaku pada saat Anda memanggilnya. Perbaikan itu diterapkan pada saat proses save. Auto-repair seperti memaksa annotation print flag atau menetapkan urutan tab PDF/UA baru masuk ke dalam file output dan tidak ke tempat lain, sehingga sebuah pemeriksaan yang dijalankan terhadap dokumen yang baru saja Anda "perbaiki" di memori tidak memberi tahu Anda apa pun tentang byte yang akan dikirim ke signer. Simpan dulu, baru preflight file yang sudah disimpan itu. State di dalam memori adalah draft; hanya file di disk yang nyata
Preflight dari disk, dan angka nol yang berarti dua hal
Entry point preflight versi flat adalah CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 memilih PDF/A (ISO 19005), test 2 memilih PDF/UA (ISO 14289). Fungsi ini membuka file lewat streaming reader milik pustaka, sehingga Anda tidak perlu memanggil LoadFromFile lebih dulu, dan ia mengembalikan sebuah string-list handle yang membawa satu temuan per entry:
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
Jebakannya ada di nilai return-nya, dan jenis jebakan ini lolos dari setiap happy-path test. Nol berarti "tidak ada temuan." Nol juga berarti "file tidak bisa dibuka," karena implementasinya mengembalikan 0 setiap kali result list-nya kosong, termasuk saat terjadi kegagalan baca. Sebuah workbench yang membaca 0 sebagai lampu hijau akan dengan senang hati menyetujui file yang sedang dikunci oleh proses lain. Memasangkan pemanggilan itu dengan LastErrorCode, seperti di atas, adalah yang membedakan kedua kasus tersebut. Checker ini juga membuka file dengan mode share deny-write, sehingga jika langkah remediation Anda masih memegang writer handle, preflight akan gagal karena alasan yang sama sekali tidak berhubungan dengan compliance dan sepenuhnya berhubungan dengan sebuah stream yang lupa Anda bebaskan
Ketika seorang manusia, bukan sebuah pipeline, yang perlu membaca temuan-temuan itu, CreatePreflightReport merender temuan-temuan tersebut menjadi laporan yang mudah dibaca. ComparePreflightReports membandingkan dua proses run, cara yang rapi untuk menunjukkan bahwa remediation sudah membersihkan temuan aslinya tanpa diam-diam memasukkan temuan baru
Menandatangani revisi yang sudah diperiksa dengan SignProcess
Begitu revisi yang tersimpan lolos preflight dan hash-nya sudah tercatat, tandatangani file yang persis itu dan tidak ada yang lain. API SignProcess terasa seperti sebuah builder. Buka sebuah process handle, konfigurasikan baris demi baris, commit, lalu baca kembali kode hasilnya
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // ruang untuk timestamp berikutnya
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Dua baris dalam urutan itu membawa bobot lebih besar dari yang terlihat. SetSignProcessCustomSubFilter dengan ETSI.CAdES.detached memilih signature PAdES sebagaimana diprofilkan dalam ETSI EN 319 142-1, bukan keluarga lama adbe.pkcs7.detached, dan itulah yang membedakan antara signature yang diterima oleh validator Eropa dan yang ditandai bermasalah. SetSignProcessReserveContentsBytes mengganjal placeholder /Contents, dan ukuran yang Anda pilih di sini adalah sebuah keputusan tentang masa depan: jika sebuah timestamp signature suatu saat akan menyusul, CMS yang membesar itu harus muat di ruang yang Anda reservasi sekarang, karena placeholder tidak bisa membesar belakangan tanpa menandatangani ulang seluruhnya. Reservasi secara longgar dan Anda hanya membuang beberapa kilobyte. Reservasi terlalu ketat dan langkah timestamp akan gagal beberapa bulan kemudian dengan sebuah overflow yang akan sulit Anda kaitkan kembali ke baris ini
GetSignProcessResult menjawab dengan sebuah kode, bukan boolean, dan kode-kode itu layak disimpan. 1 berarti sukses. 4 berarti password PDF yang salah, 7 password sertifikat yang salah, 9 sebuah PFX yang tidak membawa private key, 11 kegagalan saat signature sedang diterapkan. Kumpulkan semua itu menjadi sekadar true/false dan Anda membuang satu informasi yang membedakan kasus support password-salah dari kasus key-tanpa-private-part. Catat integer-nya
Read-back: mengaudit file yang baru saja Anda hasilkan
Tidak ada workbench yang seharusnya mempercayai jalur yang menulis file yang akan disertifikasinya sendiri. Audit class TPDFlibSignDoc membuka kembali output yang sudah ditandatangani dan membaca entry signature dictionary langsung dari disk:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Tangkap ukurannya sebelum Open: objek audit memegang share lock pada file
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 berarti field ini sudah ditandatangani
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Argumen ValueKey memetakan ke entry dictionary. Key 0 mengembalikan CMS mentah dari /Contents, key 2 dan 3 nama /Filter dan /SubFilter, dan 11 sampai 14 empat angka ByteRange. Text value justru kembali lewat GetSignatureTextValueByName: key 0 adalah waktu penandatanganan yang diklaim, dan key 5 membedakan Sig biasa dari DocTimeStamp, yang menjadi penting begitu sebuah dokumen membawa keduanya
Penangkapan ukuran file di awal contoh itu bersifat load-bearing, bukan sekadar housekeeping. TPDFlibSignDoc.Open memegang file di bawah share lock yang ketat sepanjang masa hidupnya, sehingga apa pun yang membutuhkan raw byte-nya (menghitung hash dari range yang ditandatangani, menghitung ulang digest CMS) harus membaca file itu sebelum Open dipanggil. Demo SigningWorkbench milik pustaka ini sendiri membaca seluruh file ke memori lebih dulu justru karena alasan ini, dan sebuah workbench yang mengabaikan urutan ini akan gagal secara sporadis, pada mesin mana pun yang kebetulan kalah dalam race tersebut
Aritmatika ByteRange yang membuktikan cakupan
File dengan single-signature yang sehat memiliki ByteRange berbentuk [0 a b c]: cakupan dimulai dari offset 0, melewati placeholder hex /Contents di antara a dan b, lalu dilanjutkan lagi melalui byte b+c. Ketika b+c sama dengan ukuran file, signature-nya mencakup semuanya hingga akhir file, yang merupakan hasil yang Anda inginkan. Ketika hasilnya kurang dari itu, berarti seseorang menambahkan sebuah incremental update setelah signature ditulis. Itu sepenuhnya sah menurut ISO 32000-1§12.8, karena pengisian form belakangan, signature kedua, dan sebuah DSS dictionary semuanya datang dengan cara yang persis sama. Ini juga persis fakta yang seharusnya dicatat oleh sebuah audit trail pada saat penandatanganan, bukan direkonstruksi di bawah tekanan saat terjadi sengketa
Perhatikan lebar integer-nya saat Anda melakukan aritmatika ini. GetSignProcessByteRange milik API flat mengembalikan sebuah Integer 32-bit, padahal nilai yang mendasarinya adalah Int64, sehingga pada file di atas 2 GB accessor flat itu diam-diam melakukan truncation. Gunakan TPDFlibSigner.GetByteRange di class-layer, yang mengembalikan Int64, atau ambil nilai-nilainya dari GetSignatureValueByName seperti yang dilakukan kode audit di atas
Apa yang diserahkan pustaka ini kepada Anda
Ada dua batasan yang lebih baik dipelajari pada saat design daripada di sprint terakhir. API TPDFlib versi flat sama sekali tidak membawa wrapper untuk verifikasi signature. Verifikasi kriptografis berada satu lapis di bawahnya, di TPDFlibSignatureVerifier, yang metode VerifySignature-nya menjawab valid, invalid, atau unknown. Pustaka ini juga tidak memiliki HTTP client bawaan untuk timestamp authority RFC 3161. Pustaka ini menghitung hash yang akan dikirim dan meng-embed ulang CMS yang sudah diperluas begitu token-nya kembali, tetapi round trip jaringan ke TSA harus Anda tulis sendiri. Keduanya mudah untuk dibungkus, dan sungguh tidak menyenangkan bila baru ditemukan hilang seminggu sebelum rilis, jadi rancanglah keduanya sejak sketsa pertama
Satu pertanyaan soal compliance layak dijawab dengan jelas, karena itu menentukan di mana gate terakhir diletakkan: apakah menambahkan signature merusak PDF/A? Tidak, dengan sendirinya tidak. Signature datang sebagai incremental update, dan ISO 19005-2 ke atas secara eksplisit mengizinkan dokumen yang ditandatangani. Yang perlu diwaspadai adalah tampilan signature-nya, yang tunduk pada aturan yang sama seperti konten halaman lainnya, termasuk embedded font dan larangan warna yang device-dependent. Jadi gate terakhir dalam workbench ini adalah satu proses preflight lagi, kali ini terhadap output yang sudah ditandatangani. Perlakukan CheckFileCompliance sebagai pemeriksaan cepat in-pipeline dan tetap verifikasi release candidate dengan tool independen seperti veraPDF, karena validator yang berbeda menerapkan rule set yang saling tumpang tindih namun tidak identik; ketika keduanya berbeda pendapat, teks temuannya biasanya menyebutkan klausul mana yang perlu dibaca
Satu hal soal urutan muncul dari semua ini. Penandatanganan dan pemberian timestamp bukan satu pass tunggal: signature baseline ditulis lebih dulu, kemudian sebuah proses timestamp terpisah memperluas CMS di dalam ruang /Contents yang sudah direservasi, dan itulah persisnya kenapa baris reserve-bytes tadi begitu penting. Untuk lapisan timestamp dan validasi jangka panjang yang dibangun di atas workbench ini, panduan penandatanganan dan validasi PAdES membawa signature dari baseline sampai B-LT, dan bagian preflight-nya dibahas lebih dalam di panduan preflight PDF/A dan PDF/UA. Dokumentasi API lengkap dan unduhan trial ada di halaman produk PDF Library for Delphi