Sebuah signatureValue ECDSA di dalam container CMS adalah DER SEQUENCE { INTEGER r, INTEGER s }. Fungsi CNG Windows BCryptVerifySignature tidak menerima keduanya: yang ia inginkan adalah IEEE P1363 r || s lebar-tetap, tanpa tag dan tanpa panjang. HotPDF, komponen PDF VCL native untuk Delphi dan C++Builder, mengonversi antara keduanya di bawah aturan DER yang ketat sebelum mengimpor sebuah key
Kegagalan yang dicegah ini spesifik dan cukup membuat frustrasi. Acrobat membuka dokumen dan menampilkan centang hijau. Verifier Anda sendiri, yang menelusuri byte yang sama, mengembalikan invalid, atau CNG mengembalikan STATUS_INVALID_SIGNATURE tanpa penjelasan lebih lanjut. Tidak ada yang salah dengan tanda tangannya. Yang salah adalah kira-kira tujuh puluh byte ASN.1 diteruskan ke API yang mengharapkan enam puluh empat byte integer mentah, dan ketidakcocokan ini tidak terlihat kecuali Anda tahu harus mencarinya
Kenapa BCryptVerifySignature menolak tanda tangan ECDSA yang valid?
Karena kedua sisi pemanggilan ini memakai encoding tanda tangan yang berbeda, dan tidak satu pun yang mengumumkannya. ISO 32000-1 §12.8 menyatakan bahwa signature dictionary membawa sebuah CMS blob dalam /Contents; RFC 5652 §5.3 menyatakan bahwa signatureValue pada setiap SignerInfo adalah sebuah OCTET STRING yang isinya bergantung pada apa yang didefinisikan algoritma tanda tangannya. Untuk ECDSA, isinya adalah struktur DER SEC 1: sebuah SEQUENCE yang berisi dua INTEGER. Ini sengaja dibuat berpanjang variabel, karena r dan s adalah integer dan DER membuang octet nol di depan dari sebuah integer
IEEE P1363 mengambil pandangan yang sebaliknya. Ia mendefinisikan tanda tangan sebagai penggabungan dua koordinat, masing-masing di-padding nol di kiri hingga tepat sebesar lebar byte dari curve field. Tanda tangan P-256 selalu 64 byte. Encoding DER dari tanda tangan yang sama biasanya 70 atau 71 byte dan bisa berkisar dari sekitar 8 hingga 72. Berikan bentuk DER ke BCryptVerifySignature dan pemeriksaan panjang saja sudah menggagalkan pemanggilan itu, dan itulah sebabnya HotPDF menormalisasi sebelum melakukan verifikasi, bukan sesudahnya
uses
HPDFECDSA;
// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
out ARaw: TBytes): Boolean;
begin
Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;
Aturan DER yang tidak boleh dilonggarkan oleh parser tanda tangan
Setiap penolakan yang terdaftar di sini adalah penolakan yang sengaja dilakukan HotPDF, dan masing-masing menutup jalur yang akan dibiarkan terbuka oleh parser yang longgar. Godaan ketika menulis sebuah converter adalah menemukan dua node INTEGER, menyalin isinya, lalu lanjut. Itu bekerja pada input yang berbentuk baik dan diam-diam menerima satu keluarga re-encoding yang malleable pada input yang jahat. Karena itu HPDFECDSANormalizeSignature menolak integer negatif, yaitu r atau s mana pun yang octet konten pertamanya memiliki high bit aktif, karena scalar ECDSA yang valid selalu positif. Ia menolak nilai yang seluruhnya nol, karena r = 0 atau s = 0 tidak pernah menjadi tanda tangan yang sah. Ia menolak octet nol berlebih di depan: X.690 §8.3 mengizinkan tepat satu, dan hanya ketika octet berikutnya kalau tidak akan terbaca negatif, sehingga 00 yang diikuti octet di bawah 0x80 adalah re-encoding, bukan tanda tangan. Ia menolak header panjang yang tidak minimal, karena X.690 §10.1 mensyaratkan bentuk definite yang dienkode dengan octet sesedikit mungkin, dan panjang long-form yang sebenarnya bisa berupa short-form adalah string byte berbeda yang membawa makna yang sama. Ia menolak integer yang lebih lebar dari ukuran koordinat curve, karena nilai itu tidak mungkin menjadi field element. Dan ia menolak node berlebih apa pun setelah s, bersama SEQUENCE luar yang panjang totalnya tidak sama dengan panjang seluruh blob
Dua yang terakhir itu lebih penting daripada kelihatannya. Byte berlebih setelah SEQUENCE adalah trik signature-malleability yang klasik: tambahkan sampah, dan verifier yang longgar tetap menyatakan valid padahal byte string yang divalidasinya bukan byte string yang ditandatangani. Insting yang sama mendorong pengerasan panjang ASN.1 yang dijelaskan di catatan tentang parsing PKCS#12, dan itu adalah insting yang sama di sini. Dalam jalur verifikasi, struktur yang diterima padahal tidak pernah diterbitkan oleh signer yang patuh adalah sebuah cacat, bukan kebaikan hati
Lebar koordinat milik curve, bukan milik tanda tangan
HotPDF menurunkan lebar output dari OID curve yang disebutkan, tidak pernah dari panjang DER yang baru saja diurainya. Ini adalah separuh kedua dari konversi, dan separuh yang mudah salah secara halus. RFC 5480 §2.1.1 mengidentifikasi curve dalam parameter SubjectPublicKeyInfo sertifikat, dan HPDFECDSACurveFromOID memetakan tiga OID yang didukung HotPDF: 1.2.840.10045.3.1.7 untuk P-256, 1.3.132.0.34 untuk P-384, dan 1.3.132.0.35 untuk P-521. HPDFECDSACoordinateSize kemudian mengembalikan 32, 48, atau 66 byte, dan buffer P1363-nya dua kali itu: 64, 96, atau 132. Setiap integer yang didekode diratakan-kanan ke dalam paruhnya, sehingga r yang pendek di-padding nol di kiri, bukan digeser. P-521 adalah yang paling sering menjebak orang, karena 521 bit adalah 65,125 byte dan dibulatkan ke atas menjadi 66, menghasilkan tanda tangan 132 byte yang tidak akan diduga oleh intuisi pangkat-dua mana pun. Public key ikut berjalan bersamanya sebagai EC point tak terkompresi menurut RFC 5480 §2.2, yaitu 0x04 diikuti X dan Y, sehingga HotPDF memeriksa bahwa panjangnya tepat 1 + 2 * CoordinateSize byte dan dimulai dengan 0x04 sebelum menyentuh CNG
var
Digest, SigDER, PublicPoint: TBytes;
Curve: THPDFECDSACurve;
Res: THPDFECDSAVerifyResult;
begin
// secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');
// PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);
case Res of
evrValid:
Memo1.Lines.Add('signature verifies');
evrInvalid:
Memo1.Lines.Add('signature does not match the digest');
evrMalformed:
Memo1.Lines.Add('DER encoding or public point rejected');
evrUnsupported:
Memo1.Lines.Add('curve or algorithm not supported here');
evrProviderUnavailable:
Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
evrProviderError:
Memo1.Lines.Add('CNG returned an unexpected status');
end;
end;
Perhatikan parameter terakhir. HPDFECDSAVerifyDigest juga menerima eseP1363 untuk caller yang sudah memegang tanda tangan lebar-tetap, dari hardware token atau layanan penandatanganan jarak jauh yang mengembalikan r || s mentah. Jalur itu tetap menegakkan pemeriksaan panjang dan non-zero pada kedua paruhnya, sehingga sebuah buffer berukuran benar yang penuh dengan nol tetap ditolak, bukan diteruskan begitu saja ke provider
Kenapa nama algoritma ECDSA generik gagal di Windows lama?
Karena nama generik ini lebih baru daripada basis deployment yang Anda tuju. CNG mengekspos identifier algoritma ECDSA yang menyimpulkan curve dari key yang diimpor, dan itu cara yang bersih untuk menulis kode ini, tetapi BCryptOpenAlgorithmProvider hanya dijamin bisa meresolusinya pada versi Windows yang lebih baru. Pada mesin yang lebih lama, pemanggilan open gagal, handle provider tetap nil, dan setiap verifikasi ECDSA dalam aplikasi Anda melaporkan unsupported pada tanda tangan yang sebenarnya sempurna. HotPDF menghindari jurang ini dengan membuka identifier per-curve sebagai gantinya. Ia meresolusi ECDSA_P256, ECDSA_P384, dan ECDSA_P521 satu kali, menyimpan cache satu handle provider per curve, dan menutupnya di unit finalization. Setiap verifikasi kemudian hanya melakukan pekerjaan yang murah: mengimpor sebuah public key sementara dari ECCPUBLICBLOB, memanggil BCryptVerifySignature, menghancurkan key tersebut. Tidak ada LoadLibrary berulang, tidak ada GetProcAddress berulang, tidak ada open dan close provider per tanda tangan. Verifikasi batch atas ratusan dokumen terasa bedanya, begitu pula proses layanan yang jika tidak begitu akan menghasilkan churn handle provider di bawah beban
Kode hasilnya tetap jujur soal perbedaan ini. evrProviderUnavailable berarti mesin tidak bisa memberikan provider ke HotPDF; evrInvalid berarti CNG menjawab STATUS_INVALID_SIGNATURE. Menyatukan keduanya menjadi satu kegagalan adalah cara masalah deployment salah dilaporkan sebagai dokumen yang dipalsukan. Pemisahan yang sama antara kegagalan lingkungan dan kegagalan kriptografis berjalan lewat penanganan CNG dan CAPI di sisi penandatanganan, dibahas di artikel tentang penandatanganan certificate store dan urutan byte
Sertifikat mana yang menandatangani ini? SignerIdentifier adalah dua hal berbeda
RFC 5652 §5.3 menjadikan SignerIdentifier sebuah CHOICE, dan verifier yang hanya menangani satu cabang akan diam-diam memverifikasi terhadap key yang salah. Cabang pertama adalah issuerAndSerialNumber, sebuah SEQUENCE yang menyimpan Name issuer dalam DER mentah dan INTEGER serial, dan mencocokkannya adalah perbandingan byte terhadap setiap sertifikat dalam himpunan certificates CMS. Cabang kedua adalah [0] subjectKeyIdentifier, sebuah OCTET STRING yang ditandai secara implisit, dan mencocokkannya membutuhkan penggalian ke dalam sertifikat, bukan sekadar membandingkan field header-nya
Penggalian ini punya satu lapisan yang mengejutkan banyak orang. Key identifier berada di dalam sebuah extension X.509v3, sehingga HotPDF menelusuri field extensions [3] pada tbsCertificate, menemukan extension yang OID-nya 2.5.29.14, melewati BOOLEAN critical yang opsional, dan mengambil OCTET STRING extnValue. Octet string itu bukanlah identifier-nya. Menurut RFC 5280 §4.2.1.2, isinya sendiri adalah DER, dan tipe KeyIdentifier adalah OCTET STRING lain, sehingga Anda mengurai untuk kedua kalinya untuk mencapai byte yang sesungguhnya. Berhenti satu lapisan terlalu dini dan Anda membandingkan wrapper 22-byte terhadap identifier 20-byte, tidak ada sertifikat yang pernah cocok, dan verifier jatuh ke heuristik apa pun yang Anda tulis berikutnya, dan itulah bahaya sesungguhnya. Mengambil sertifikat pertama dalam himpunan adalah jalan pintas yang menggoda dan itu salah setiap kali CMS membawa sebuah chain, yang terjadi hampir sepanjang waktu, karena leaf tidak diwajibkan berada di urutan pertama. HotPDF hanya menerima sertifikat yang tidak cocok ketika container hanya berisi tepat satu; dengan beberapa sertifikat hadir, kecocokan SignerIdentifier yang persis adalah wajib. Memverifikasi sebuah digest terhadap public key CA perantara tidak menghasilkan error yang ramah, melainkan menghasilkan invalid yang meyakinkan pada dokumen yang sebenarnya baik-baik saja
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('signed.pdf') > 0 then
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
[String(Info.FieldName), String(Info.PublicKeyAlgorithm),
String(Info.CurveName), String(Info.HashAlgorithm),
String(Info.SignerName)]))
else
Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
finally
Pdf.Free;
end;
end;
THPDFSignatureInfo.CurveName melaporkan P-256, P-384, atau P-521 sehingga sebuah audit log mencatat curve mana yang sebenarnya dipakai, bukan sekadar kata ECDSA. Mekanisme level-dokumen di sekitar pemanggilan ini, khususnya bagaimana segmen /ByteRange di-hash dan kenapa digest harus dihitung atas berkas, bukan atas parsed object tree, adalah topik artikel pendamping tentang memverifikasi tanda tangan PDF
Apa yang tidak diberikan oleh ini
Hasil hijau dari HPDFECDSAVerifyDigest hanya menjawab satu pertanyaan: byte-byte ini ditandatangani oleh private key yang cocok dengan public key ini. Ia tidak mengatakan apa pun soal apakah key itu benar-benar milik seseorang yang layak Anda percayai. Pembangunan chain ke trust anchor, revocation lewat CRL atau OCSP, dan pemeriksaan kebijakan adalah pekerjaan terpisah, dan produk apa pun yang melaporkan tanda tangan valid tanpa itu semua sedang melaporkan lebih sedikit daripada yang diasumsikan pengguna. Tanggal validitas sertifikat ditampilkan terpisah di THPDFSignatureInfo justru karena alasan itu: sebuah tanda tangan bisa terverifikasi secara kriptografis padahal sertifikat yang membuatnya sudah kedaluwarsa dua tahun lalu. Dukungan curve-nya juga sengaja dibuat sempit. Tiga NIST prime curve ditangani, dan sebuah tanda tangan atas curve lain mana pun mengembalikan unsupported, bukan sebuah tebakan. Jalur CNG hanya untuk Windows, yang merupakan trade-off yang tepat untuk sebuah komponen VCL tetapi layak dinyatakan sebelum Anda merencanakan layanan lintas platform di sekitarnya. Dan ketatnya aturan ini tidak bisa dikonfigurasi: tidak ada mode longgar yang menerima panjang DER non-minimal hanya karena ada signer lama yang menerbitkannya begitu. Jika Anda menemukan berkas semacam itu di produksi, respons yang jujur adalah mencatatnya dan mengejar produsennya, bukan melebarkan parser sampai berkas itu lolos
Jalur verifikasi ECDSA yang dijelaskan di sini hadir sebagai bagian dari HotPDF Component standar untuk Delphi dan C++Builder, berdampingan dengan jalur RSA PKCS#1 v1.5 dan RSA-PSS serta catatan informasi tanda tangan lengkap; halaman produk memuat referensi tanda tangan digital yang lengkap