PDFium Component versi 3.114.20 memperbaiki pengodean RSASSA-PSS-params di ketiga backend penandatanganan PAdES: Windows CNG, macOS Keychain, dan PKCS#11. RFC 4055 §3.1 memberi setiap field RSASSA-PSS-params tag context-specific eksplisit, [0] sampai [3], dan backend-nya mengeluarkan saltLength sebagai INTEGER universal telanjang sambil ikut menulis trailerField yang nilainya sama dengan default-nya. Byte signature-nya benar dari awal sampai akhir. AlgorithmIdentifier yang mendeskripsikannya tidak, dan itu saja sudah cukup membuat verifier menolak tanda tangannya
Bagian yang menjengkelkan adalah di mana bug-nya bersembunyi. Tanda tangan CMS punya dua paruh: operasi kriptografinya dan ASN.1 yang memberi tahu verifier bagaimana operasi itu dilakukan. Benarkan yang pertama dan salahkan yang kedua, hasilnya dokumen yang tidak bisa dibedakan oleh tool mana pun yang mengikuti spesifikasi dari sebuah pemalsuan. Artikel ini hanya membahas paruh keduanya: bagaimana RSASSA-PSS-params harus diberi tag, bagaimana tiga backend melakukan kesalahan yang sama, dan seperti apa DER yang dibetulkan dalam istilah TDerWriter
Kenapa verifier menolak tanda tangan RSASSA-PSS yang byte-nya benar?
Karena RSASSA-PSS adalah satu-satunya skema RSA yang parameter-nya tidak bisa dipulihkan verifier dari tanda tangannya sendiri. Padding PKCS#1 v1.5 sepenuhnya ditentukan oleh OID sha256WithRSAEncryption, jadi parameternya berupa NULL telanjang dan tidak ada yang bisa salah. PSS diparameterkan oleh sebuah fungsi hash, sebuah mask generation function dengan hash-nya sendiri, dan panjang salt, dan RFC 8017 §A.2.3 membiarkan ketiganya terbuka. Penandatangannya yang memilih, AlgorithmIdentifier yang membawanya, dan verifier harus mereproduksi ketiganya persis sebelum EMSA-PSS-VERIFY bahkan bisa dimulai
Jadi ketika PDFium Component menandatangani dengan SHA-256, MGF1 atas SHA-256, dan salt 32 byte, ketiga fakta itu harus bertahan melewati pengodean DER di sini dan pendekodean DER di implementasi lain. Blok parameter yang tidak bisa di-parse verifier mengakhiri verifikasi sebelum eksponensiasi modular mana pun terjadi. Blok yang di-parse secara berbeda lebih buruk lagi, karena RFC 4055 §3.1 memberi saltLength default 20. Decoder yang melewati field yang tidak ia kenali akan mendarat di default itu, menjalankan EMSA-PSS-VERIFY dengan salt 20 byte terhadap tanda tangan yang dihitung dengan 32, lalu melaporkan tanda tangan yang buruk tanpa petunjuk bahwa masalahnya ada di metadata dan bukan di key-nya. Kedua hasil itu yang dihasilkan pengodean 3.114.19, tergantung seberapa ketat verifier-nya, dan tidak satu pun menunjuk ke AlgorithmIdentifier-nya
Apa yang sebenarnya diwajibkan RFC 4055 §3.1 atas RSASSA-PSS-params
RFC 4055 §3.1 mendefinisikan RSASSA-PSS-params sebagai SEQUENCE berisi empat field, masing-masing membawa tag context-specific eksplisit dan sebuah nilai DEFAULT:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
Explicit tagging di DER berarti setiap field dibungkus dalam TLV context-specific yang constructed, A0 untuk [0], A1 untuk [1], A2 untuk [2], dan A3 untuk [3], dengan pengodean universal nilainya bersarang di dalamnya. Setiap field diberi tag justru karena setiap field opsional lewat default-nya. Tanpa tag, decoder tidak bisa tahu apakah SEQUENCE yang membawa satu AlgorithmIdentifier sedang membawa hashAlgorithm atau maskGenAlgorithm, karena keduanya bertipe SEQUENCE; dengan tag, nomor tag-nya mengidentifikasi field-nya terlepas dari tetangga mana yang ada. Nilai yang dikeluarkan PDFium Component mengikuti profil di ETSI TS 119 312 §7, SHA-256, MGF1 dengan SHA-256, dan salt sepanjang digest-nya, dan nilainya mencerminkan persis apa yang diberitahukan ke setiap pemanggilan penandatanganan di platformnya: BCRYPT_PSS_PADDING_INFO dengan cbSalt 32 untuk NCryptSignHash, CK_RSA_PKCS_PSS_PARAMS dengan sLen 32 untuk mekanisme PKCS#11-nya, dan algoritma PSS penandatanganan digest SHA-256 di Security framework
Bagaimana tiga backend melakukan kesalahan yang sama
Pengodean 3.114.19 memberi tag pada dua field pertama dan membiarkan dua field terakhir telanjang, secara identik di TWinCmsSigner, TKeychainCmsSigner, dan TPkcs11CmsSigner. Kesimetrian itu bukan kebetulan: ketiganya mengimplementasikan antarmuka ICmsSigner dari FPdfCms.pas, dan isi GetSignatureAlgorithmParams masing-masing ditulis dari satu template. Template-nya berbunyi seperti ini:
// Sebelum 3.114.20: [0] dan [1] diberi tag, [2] dan [3] tidak
Result := W.Sequence(Concat4(
W.ContextSpecific(0, W.AlgId(OID_SHA256), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID(OID_MGF1), W.AlgId(OID_SHA256))), True),
W.IntegerOf(32), // INTEGER telanjang di tempat yang seharusnya [2] EXPLICIT
W.IntegerOf(1))); // trailerField, sama dengan DEFAULT, harus tidak ada
Decoder yang menelusuri SEQUENCE itu melihat A0, membaca algoritma hash-nya, melihat A1, membaca mask generation function-nya, lalu bertemu 02 01 20. Itu INTEGER universal, dan RSASSA-PSS-params tidak punya anggota INTEGER tanpa tag di mana pun. Decoder yang ketat berhenti di situ. Decoder yang longgar melewati elemen yang tidak dikenalnya, tidak pernah menemukan A2, memberi saltLength nilai default 20, lalu menemukan INTEGER nyasar kedua, 02 01 01, dan menghadapi masalah yang sama lagi. Tidak ada satu pun jalur yang sampai ke salt 32 byte. Template bersama itu efisien kalau benar dan sama efisiennya untuk salah tiga kali kalau tidak, itulah sebabnya perbaikannya mendarat di ketiga unit itu dalam satu commit dan kenapa isi ketiga method-nya tetap identik secara struktural setelahnya. Backend di masa depan sebaiknya menyalin bloknya dari salah satu ini alih-alih menurunkannya lagi, karena penurunannya justru tempat kesalahannya terjadi
Kenapa trailerField dihilangkan alih-alih diberi tag [3]?
Karena X.690 §11.5 menyatakan encoder DER tidak boleh mengodekan komponen yang nilainya sama dengan DEFAULT-nya, dan trailerField punya DEFAULT trailerFieldBC, yaitu integer 1. Perbaikan yang kelihatan jelas atas kode lama, mengganti W.IntegerOf(1) telanjang dengan W.ContextSpecific(3, W.IntegerOf(1), True), menghasilkan blok yang diterima decoder BER yang permisif dan berhak ditolak decoder DER yang ketat. Nilainya tidak salah. Kehadirannya yang salah. Aturan yang sama juga yang membuat tiga field lainnya ada: SHA-256 bukan default sha1, MGF1 dengan SHA-256 bukan default mgf1SHA1, dan 32 bukan default 20. Seandainya backend-nya menandatangani dengan SHA-1 dan salt 20 byte, RFC 4055 §3.1 akan meruntuhkan parameter-nya menjadi SEQUENCE kosong, 30 00, dan SEQUENCE kosong itulah, bukan NULL, yang diharapkan verifier. PDFium Component tidak pernah mengeluarkan bentuk itu karena tidak pernah menandatangani dengan nilai-nilai tersebut, tapi itulah kasus yang menjebak siapa pun yang mengasumsikan "tanpa parameter" selalu dieja 05 00
Inilah pembedaan antara DER dan BER yang khususnya penting untuk tanda tangan. BER membiarkan encoder menyertakan komponen bernilai default; DER melarangnya, karena DER ada agar satu nilai punya tepat satu pengodean, dan tanda tangan atas struktur dengan dua pengodean legal adalah tanda tangan yang bisa diperdebatkan. Semua yang ada di dalam signedAttrs CMS adalah DER karena alasan itu, dan blok parameter-nya berjalan di dalam signedAttrs lewat atribut cmsAlgorithmProtection maupun di signatureAlgorithm luarnya, jadi ia tidak mendapat pengecualian
Pengodean TDerWriter yang dibetulkan
PDFium Component kini membangun parameter-nya dengan tiga pemanggilan TDerWriter.ContextSpecific dari FPdfAsn1.pas, satu per field non-default, masing-masing dengan argumen Constructed diset True untuk menghasilkan wrapper tag eksplisitnya, dan tanpa satu baris pun untuk field trailer-nya. Ini isi TWinCmsSigner.GetSignatureAlgorithmParams dengan OID-nya ditulis lengkap; unit Keychain dan PKCS#11-nya mengeja nilai yang sama sebagai OID_SHA256, OID_MGF1, dan OID_RSASSA_PSS:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1 memberi tag keempat field. saltLength adalah [2];
// INTEGER telanjang di sini dibaca sebagai awal field lain. trailerField
// adalah [3] dengan DEFAULT 1, dan X.690 11.5 melarang mengodekan nilai
// yang sama dengan default-nya, jadi ia dihilangkan sepenuhnya
Result := W.Sequence(Concat3(
W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
W.ContextSpecific(1, W.Sequence(ConcatBytes(
W.OID('1.2.840.113549.1.1.8'), // id-mgf1
W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
W.ContextSpecific(2, W.IntegerOf(32), True)));
finally
W.Free;
end;
end
else
Result := nil; // PKCS#1 v1.5 dan ECDSA: AlgIdWithParams menulis NULL
end;
Dua detail dari mesin di sekelilingnya penting. TDerWriter.AlgId menghasilkan AlgorithmIdentifier dengan parameter NULL, yang memang diperintahkan RFC 4055 §2.1 untuk dihasilkan encoder bagi hashAlgorithm bersarang dan bagi hash dalam MGF1. Dan pembangun CMS di FPdfCms.pas memasangkan OID signature-nya dengan byte-byte ini lewat TDerWriter.AlgIdWithParams, yang menggantinya dengan NULL ketika parameter-nya nil; itulah sebabnya psRsaPkcs1v15 dan psEcdsa sekadar mengembalikan nil dan tidak pernah terpengaruh, dan kenapa 1.2.840.113549.1.1.10, id-RSASSA-PSS, adalah satu-satunya OID signature di antara ketiganya yang membawa blok parameter sungguhan. Byte yang dihasilkan untuk profil SHA-256 itu tetap dan cukup pendek untuk diperiksa dengan mata: SEQUENCE 30 34 di luar yang memuat A0 0F di sekitar AlgorithmIdentifier SHA-256 sepanjang 15 byte, A1 1C di sekitar AlgorithmIdentifier MGF1 sepanjang 28 byte yang parameter-nya adalah AlgorithmIdentifier SHA-256 yang sama, dan A2 03 02 01 20 untuk salt-nya. Kalau dump signatureAlgorithm Anda menunjukkan 02 01 20 di level teratas SEQUENCE parameter-nya alih-alih di dalam A2, Anda sedang melihat pengodean 3.114.19
Kenapa suite ujinya tidak menangkap AlgorithmIdentifier yang malformed?
Karena uji PAdES-nya menjalankan pembangun CMS lewat signer palsu yang melaporkan sha256WithRSAEncryption dan mengembalikan nil dari GetSignatureAlgorithmParams, jadi blok parameter PSS-nya tidak pernah dibangun sama sekali di dalam uji. Itu rancangan yang wajar untuk uji yang harus berjalan tanpa certificate store, Keychain, atau token, dan ia punya titik buta dengan bentuk yang presisi: apa pun yang hanya dihasilkan backend sungguhan hanya diuji oleh backend sungguhan. Lapisan keduanya lebih menarik. PDFium Component juga menaruh AlgorithmIdentifier signature-nya, termasuk parameter-nya, di dalam atribut bertanda tangan cmsAlgorithmProtection dari RFC 6211, dan verifier membandingkan salinan itu dengan signatureAlgorithm luarnya. Kedua salinan datang dari pemanggilan yang sama, jadi keduanya cocok sempurna dan setiap pemeriksaan konsistensi internalnya lolos. Pengodeannya konsisten dengan dirinya sendiri dan salah, kategori bug yang tidak bisa dibeberkan oleh pembandingan struktur apa pun dengan dirinya sendiri, dan pelajaran yang sama dengan struktur berbeda diceritakan di signedAttrs CMS dan pengurutan DER SET OF, tempat SET yang di-hash dalam satu urutan lalu dikeluarkan dalam urutan lain tampak baik-baik saja sampai verifier asing menghitung ulang hash-nya
Yang benar-benar menangkap kelas bug ini adalah decoder yang tidak ditulis penulis encoder-nya, dijalankan atas output sungguhan dari backend sungguhan. Jalur verifikasi Windows di PDFium Component melewati CryptoAPI alih-alih reader milik library-nya sendiri, dan tanda tangan PSS yang ditolak di situ yang menuntun kembali ke parameter-nya. ASN.1 apa pun yang dikeluarkan sebuah implementasi untuk dibaca implementasi lain layak mendapat setidaknya satu round trip lewat decoder yang tidak ia kendalikan, dan semakin banyak default dan tag yang dimiliki strukturnya, semakin berharga round trip itu
Di mana ini cocok dengan sisa cerita PSS
Perbaikan ini independen dari dua tempat lain yang bisa keliru pada PSS di tanda tangan PAdES, dan memisahkan keduanya mempersingkat debugging. Backend macOS bisa mendapati key tertentu atau sistem yang lebih lama menolak PSS lalu turun ke PKCS#1 v1.5, dan AlgorithmIdentifier-nya harus mengikuti penurunan itu; itu pertanyaan kapabilitas, dibahas di menandatangani PAdES dengan identitas macOS Keychain. Backend PKCS#11 bisa menyerahkan ke token sebuah CK_RSA_PKCS_PSS_PARAMS yang tata letaknya dibaca berbeda oleh tokennya karena ketidakcocokan lebar integer; itu pertanyaan ABI, dibahas di CK_ULONG dan jebakan packing PKCS#11. Artikel ini membahas kegagalan ketiga, ketika key-nya bersedia, tokennya menghitung byte yang benar, dan DER yang mendeskripsikan hasilnya tidak sesuai RFC 4055 §3.1
Penandatangan yang mendeklarasikan PSS memikul kewajiban yang tidak pernah dimiliki penandatangan v1.5: mendeskripsikan parameternya sendiri dalam bentuk yang didekode implementasi lain menjadi tiga nilai yang sama. RFC 4055 §3.1 menetapkan tag-nya, X.690 §11.5 menetapkan field mana yang boleh muncul, dan ETSI TS 119 312 §7 menetapkan nilai yang layak dipilih. Ketiga backend komponen PDFium Delphi dirilis sebagai source, jadi isi GetSignatureAlgorithmParams di atas adalah yang bisa Anda baca, dump, dan bandingkan dengan verifier Anda sendiri alih-alih diterima begitu saja