HotPDF memvalidasi PDF MAC ISO/TS 32004 per revisi, bukan per file. THotPDF.ValidatePDFMACChain menyusuri setiap pembaruan inkremental dari anchor rantai ke depan dan memverifikasi setiap MAC terhadap stream baca-saja berbatas yang berakhir di startxref dan %%EOF milik revisi itu sendiri. Satu MAC valid pada revisi terbaru tidak membuktikan apa pun tentang revisi di bawahnya
Inilah skenario yang memotivasi semuanya. Anda mengirim PDF terenkripsi AES-256 dengan PDF MAC padanya. Seseorang membuka file di editor heksadesimal, membalik satu byte di dalam revisi pertama yang terlindungi MAC, lalu menambahkan revisi segar berisi MAC yang benar-benar valid miliknya sendiri. Setiap penampil membuka file tanpa keluhan, dan pemeriksa naif yang meng-hash rentang byte saat ini terhadap MAC di trailer aktif melaporkan sukses — karena MAC itu memang benar untuk byte yang dicakupnya. Kerusakan duduk dua revisi ke bawah, di wilayah yang tak pernah diperiksa ulang siapa pun
Mengapa MAC tingkat atas yang valid tidak membuktikan file utuh?
Karena PDF MAC menutupi prefix, bukan dokumen. Pembaruan inkremental adalah warga kelas satu format ini: setiap penyimpanan menambahkan body baru, bagian cross-reference baru, dan trailer baru, sementara byte lama tetap persis di tempatnya. ISO/TS 32004 menunggangi model itu, jadi setiap revisi membawa kamus /AuthCode miliknya sendiri yang mengautentikasi file sebagaimana ia berdiri saat itu, dan memverifikasi hanya yang terbaru menyisakan setiap revisi lebih awal tak tersentuh pemeriksaan. Karena itu HotPDF mengekspos dua pertanyaan itu sebagai dua panggilan, dan bedanya adalah seluruh inti artikel ini. ValidatePDFMAC menjawab "apakah revisi saat ini autentik", mengisi record THPDFPDFMACValidationInfo; ValidatePDFMACChain menjawab "apakah setiap revisi terlindungi MAC di file ini autentik", mengisi THPDFPDFMACChainValidationInfo dengan array per-revisi plus alasan kegagalan yang terbaca mesin. Pada file yang diubah lalu di-MAC ulang di atas, panggilan pertama mengembalikan True dan yang kedua mengembalikan False terhadap indeks revisi 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// Failure is one of pmcfRevisionBoundary, pmcfNoPDFMAC,
// pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
// pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
Setiap MAC diverifikasi pada stream prefix-nya sendiri, tidak pernah pada panjang file akhir
Bug paling mahal di area ini adalah memakai ukuran file akhir sebagai batas atas saat meng-hash ulang revisi lama, yang melipat byte ekor ke setiap digest kecuali yang terbaru dan melaporkan perusakan pada file yang sehat. HotPDF sebagai gantinya merekonstruksi, untuk setiap revisi, stream baca-saja berbatas yang berakhir di nilai startxref revisi itu diikuti %%EOF-nya, dan meng-hash hanya itu. Menemukan batasnya lebih cerewet daripada kelihatan: literal %%EOF bisa muncul di dalam content stream atau string, jadi kandidat diterima hanya ketika startxref tepat sebelumnya ter-parse menjadi angka yang sama dengan offset cross-reference dari bagian yang sedang divalidasi, tanpa apa pun kecuali whitespace di antara keduanya. Revisi kemudian menyerap tepat satu urutan akhir-baris setelah penanda — satu CR, satu LF, atau satu pasangan CRLF — dan tak lebih. Aturan terakhir itu menggigit dalam praktik, karena penulis yang menulis satu baris kosong ekstra di antara dua revisi telah menghasilkan byte milik revisi berikutnya, dan menelan semua whitespace ekor ke revisi sebelumnya diam-diam mengubah kedua digest. Enumerasi bagian mengikuti disiplin yang sama: HotPDF menyusuri bagian cross-reference dari yang tertua ke termuda tepat satu kali, memutar ulang entri free, direct, dan object-stream sehingga bagian belakang menimpa state lebih awal, kebalikan dari semantik first-seen-wins yang diterapkan parser xref aktif
Di mana rantai mendarat, dan apa yang memutuskannya?
Revisi pertama yang membawa /AuthCode valid adalah anchor, dan FirstMACRevisionIndex melaporkan di mana perlindungan dimulai; apa pun sebelum itu tak terlindungi secara konstruksi, dan itu normal. Semua setelahnya harus terlindungi MAC, jadi menambahkan satu pembaruan inkremental polos ke file terlindungi MAC gagal dengan pmcfRequiredRevisionMissing beserta indeks revisi pelanggar — menoleransi celah akan membiarkan penyerang mencabut perlindungan hanya dengan menyimpan sekali lagi. Tiga invarian lain berlaku melintasi rantai, masing-masing dengan kode kegagalannya sendiri
pmcfKDFSaltChanged—/KDFSaltharus tetap stabil dari anchor ke depan, karena salt yang berputar membiarkan pemalsu menurunkan ulang kunci di bawah parameter pilihannya sendiripmcfDigestDowngrade— kekuatan digest dibandingkan terhadap MAC terakhir yang terverifikasi alih-alih revisi tepat sebelumnya, sehingga rantai yang mulai di profil Modern pada SHA-384 tak bisa diam-diam berlanjut dengan SHA-256pmcfPermissionDowngrade— sebuah revisi tidak boleh menghapus persyaratan PDF MAC yang telah diautentikasi revisi lebih awal
Konsekuensi yang layak dicerna adalah MAC historis diverifikasi secara independen bahkan setelah mereka tak lagi menjadi trailer aktif. Itulah mengapa serangan sunting-revisi-lama-lalu-tambah-MAC-baru dari pembuka tidak selamat: MAC terbaru lolos dengan sendirinya, ValidatePDFMAC senang, dan rantai tetap mendarat di revisi 1 dengan pmcfRevisionInvalid
Urutan signature: kunci trailer dulu, signatureDigest terakhir
Ketika MAC menempel pada signature CMS alih-alih berdiri sendiri, urutan tulis berhenti menjadi soal gaya. HotPDF menuntut /AuthCode, /KDFSalt, ekstensi pengembang ISO 32004, dan /SigObjRef ditulis ke revisi yang sama sebelum /ByteRange signature dihitung; menambahkan salah satunya setelahnya menempatkan byte-byte itu di luar rentang yang dicakup signature, menghasilkan file yang signature-nya terverifikasi sementara ikatan MAC tak bertanda. Dua digest kemudian berjalan ke arah sebaliknya, yang tampak melingkar sekilas dan ternyata tidak. PDF MAC signatureDigest mengikat oktet konten mentah dari CMS SignerInfo.signature OCTET STRING — bukan seluruh CMS DER, dan bukan atribut bertanda — jadi ia dibangun setelah nilai signature mentah ada dan disuntikkan sebagai atribut tak bertanda id-attr-pdfMacData. Karena /Contents dikecualikan dari ByteRange signature dan atribut tak bertanda tak pernah masuk perhitungan signature, urutan buat-signature, bangun-MAC, bungkus-CMS menutup bersih tanpa loop kriptografis. Dua korolari mengikuti: sentinel /ByteRange dan placeholder /Contents harus tetap plaintext dan di luar object stream bahkan di file terenkripsi, atau patcher lebar-tetap tak bisa menemukannya; dan saat digest MAC juga SHA-256, digest penandatanganan dipakai langsung, jika tidak kedua konteks digest diperbarui dalam satu lintasan atas output stream
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // SHA-384 document digest
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location = pmlAttachedToSignature, and the two digests
// are reported separately
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
Validasi menelusuri ulang jalur yang sama dari ujung lainnya: baca /AuthCode langsung dari trailer cross-reference klasik yang sedang aktif, ikuti /SigObjRef indirect yang sadar-generasi, pastikan ia mengikat /V satu field signature itu, dan laporkan kegagalan digest dokumen secara terpisah dari kegagalan digest signature. Itu dua diagnosis berbeda, dan meruntuhkan keduanya menjadi satu boolean membuang satu-satunya informasi yang mengatakan apakah konten halaman atau nilai signature yang tersentuh. Jika Anda sudah mengerjakan CMS, ini berdampingan dengan artikel penandatanganan PAdES dan panduan memverifikasi signature di dokumen terbuka
Jangan pernah percaya /P: dekripsi dulu /Perms 16-byte
ISO/TS 32004 menandakan "dokumen ini menuntut PDF MAC" lewat bit izin 13, dan cara paling jelas untuk membacanya adalah yang salah, karena integer /P di kamus enkripsi adalah plaintext dan tak terautentikasi — siapa pun bisa membalik bit itu di editor teks dan menurunkan persyaratannya. ISO 32000-2 §7.6 menyediakan jawabannya di entri /Perms, dan HotPDF memakainya: dekripsi string /Perms 16-byte dengan kunci enkripsi file di bawah AES-256 CBC, IV nol, tanpa padding, lalu periksa setiap field plaintext sebelum percaya apa pun. Byte 1 sampai 4 menyimpan nilai izin dalam urutan little-endian dan harus sama persis dengan integer /P; byte 5 sampai 8 adalah 0xFF; byte 9 adalah flag enkripsi-metadata T atau F; byte 10 sampai 12 adalah penanda literal adb. Hanya ketika semuanya terpenuhi PermissionsAuthenticated menjadi True dan bit 13 dibaca — dan perhatikan polaritasnya, karena persyaratan MAC ditegaskan saat bit 0x1000 justru bersih. Ketidakcocokan antara /P dan izin terdekripsi bukan peringatan untuk dicatat lalu dilewati; itu set izin yang dipalsukan, dan respons yang tepat adalah gagal tertutup
Agilitas algoritma berhenti di digest
ISO/TS 32004 membiarkan Anda memilih digest dokumen, dan hanya digest dokumen. HotPDF menahan HMAC-SHA-256 untuk autentikasi, HKDF-SHA-256 per RFC 5869 untuk derivasi kunci, dan AES-256 key wrap per RFC 3394 tetap di bawah THPDFPDFMACDigestAlgorithm variabel yang membentang dari pmdaSHA256 sampai pmdaSHA3_512, karena kesalahan alaminya adalah memperlakukan "profil SHA3-512" sebagai izin menukar HMAC juga, yang menghasilkan file yang tak lagi PDF MAC dalam arti interoperabel apa pun. Satu detail implementasi layak disalin jika Anda menulis verifier sendiri: baca OID digest dari CMS AuthenticatedData sebelum meng-hash rentang byte, karena meng-hardcode SHA-256 dan mendamaikan setelahnya mengubah agilitas menjadi label dan membiarkan file bermusuhan membuat Anda mengalirkan seluruh dokumen sebelum Anda tahu algoritmanya tak pernah didukung. CMSAlgorithmProtection, algoritma digest AuthenticatedData, integrity-info messageDigest, dan digest rentang-byte harus semuanya menyebut satu algoritma, dan setiap ketidaksepakatan gagal tertutup
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, accepts all six
Options := THPDFPDFMACOptions.Modern; // SHA-384, rejects 256-bit
Options := THPDFPDFMACOptions.HighAssurance; // SHA3-512 only, AES-GCM
// A custom profile is legal, but the algorithm it generates with
// must also appear in the validation allowlist, or the configuration
// is rejected before a single byte is written
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Apa yang PDF MAC buktikan dan tidak buktikan
Rantai PDF MAC terverifikasi membuktikan bahwa setiap revisi terlindungi identik byte dengan yang ditulis oleh pemegang kunci enkripsi file, bahwa tak ada revisi terlindungi yang dibuang atau diurut ulang, dan bahwa tak ada revisi tak terlindungi yang ditambahkan setelah anchor — persis kelas serangan yang membiarkan enkripsi AES-256 polos terbuka, karena kerahasiaan tak mengatakan apa pun tentang integritas dan PDF terenkripsi dengan revisi yang disambung dekripsinya sama bahagianya dengan yang utuh. Yang tidak ia buktikan adalah kepengarangan. Kunci MAC diturunkan dari kunci enkripsi file, jadi siapa pun yang bisa membuka dokumen juga bisa menghasilkan MAC valid atas versi yang dimodifikasi, semua penerima sah termasuk; ia primitif simetris, dan primitif simetris tak bisa mengatribusikan. Jika Anda perlu tahu siapa yang mengubah sesuatu, Anda butuh signature digital dengan sertifikat di baliknya, dan PDF MAC lalu melengkapinya dengan melindungi struktur inkremental yang sendirian tak dicakup signature. Perlakukan keduanya sebagai lapisan dan biarkan dua putusan dilaporkan independen alih-alih diruntuhkan jadi satu ikon status
Titik masuk PDF MAC yang dijelaskan di sini — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC, dan ValidatePDFMACChain — dikirim dengan HotPDF Delphi Component standar untuk Delphi dan C++Builder, tempat halaman produk membawa referensi penuh untuk record opsi, enumerasi status, dan array validasi per-revisi