Artikel Teknis

ML-DSA di Delphi: Pasca-Kuantum FIPS 204 di PDFlibPas

PDFlibPas mengimplementasikan ML-DSA, Module-Lattice-Based Digital Signature Algorithm yang distandarisasi dalam FIPS 204, sepenuhnya dalam Object Pascal. Ketiga set parameter tersedia sebagai fungsi biasa: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, ditambah pasangan KeyGen dan Verify. Tanpa OpenSSL, tanpa DLL platform, tanpa perekat C. Unit tunggal PDFlibMLDSA hanya bergantung pada sponge SHAKE milik pustaka, dan keluarannya cocok dengan vektor uji known-answer resmi FIPS 204 byte demi byte

Kalimat terakhir itulah satu-satunya bagian yang menuntut kerja sungguhan. Menulis aritmetika lattice dalam Pascal itu mekanis; membuatnya cocok dengan NIST tidak. Yang berikut adalah catatan rekayasa dari porting ini: bagaimana ketiga set parameter akhirnya berbagi satu engine, dan cacat spesifik yang memisahkan kompilasi dan jalan dari cocok dengan KAT. Jika Anda sedang mengevaluasi opsi pasca-kuantum untuk pipeline dokumen Delphi atau C++Builder, cacat itulah bagian yang berguna, karena masing-masing menghasilkan output yang tampak masuk akal namun diam-diam gagal interoperabilitas

Mengapa menulis penandatangan pasca-kuantum dalam Object Pascal murni?

Karena alternatifnya adalah satu dependensi native per target, dan sebuah pustaka PDF Delphi sudah memiliki cukup banyak dependensi seperti itu. PDFlibPas membangun di seluruh Delphi, C++Builder dan FPC/Lazarus pada target Win32, Win64 dan Unix; mengikat pustaka pasca-kuantum berbasis C berarti mengikuti build pustaka itu untuk setiap slot tersebut, ditambah permukaan konvensi-pemanggilan dan kepemilikan memori di antara keduanya. Unit Pascal murni terkompilasi di mana saja sisa pustaka terkompilasi, dan itulah seluruh argumennya

ML-DSA membuat hal ini luar biasa murah, karena satu-satunya dependensi primitifnya adalah SHAKE. Tidak ada lapisan big-integer, tidak ada kurva eliptik, tidak ada suite hash terpisah. PDFlibPas mendapatkan XOF streaming pada rilis tepat sebelum porting ini: TPLShakeXOF di PDFlibDigest, dengan PLShakeXOFInit yang memilih SHAKE128 (rate 168) atau SHAKE256 (rate 136), diikuti PLShakeXOFAbsorb, PLShakeXOFFinalize, dan loop PLShakeXOFSqueeze yang terus memutasi untuk panjang output arbitrer. Setiap rutinitas rejection sampling di unit ML-DSA ditulis langsung di atas API empat-panggilan itu

Satu engine, tiga set parameter: TMLDSAParams

PDFlibPas menggambarkan satu set parameter ML-DSA yang utuh dengan satu record dan memilihnya berdasarkan nomor set, sehingga ML-DSA-44, 65 dan 87 melewati jalur kode yang sama. Implementasi kerja pertama adalah build 4x4 tetap yang hard-wired untuk ML-DSA-44; menggeneralisasinya berarti mengangkat k dan l, eta, tau, beta, gamma1 dan gamma2, omega, serta panjang tantangan ke dalam TMLDSAParams, lalu menurunkan sisanya. Titik masuk publik menjadi wrapper tiga baris

Type
  TMLDSAParams= Record
    K, L, D, Eta, Tau, Beta, Gamma1, Gamma2, Omega: Integer;
    Alpha, MW1: Cardinal;
    W1BW, EtaBW, Gamma1BW, T1BW: Integer;
    T0Rng: Cardinal;
    CTildaBytes: Integer;
    PublicKeyBytes, SecretKeyBytes, SignatureBytes: Integer;
  End;

// Field turunan dihitung, tidak pernah ditranskrip dari tabel
Params.Alpha:= 2* Cardinal(Params.Gamma2);
Params.MW1:= (Q- 1)div Params.Alpha;
Params.W1BW:= BitWidth(Params.MW1- 1);
Params.EtaBW:= BitWidth(2* Cardinal(Params.Eta));
Params.Gamma1BW:= BitWidth(Cardinal(Params.Gamma1));

Function MLDSA65Sign(Const SecretKey, Message, Context, Rnd: AnsiString;
  Out Signature: AnsiString): Boolean;
Var
  Params: TMLDSAParams;
Begin
  BuildMLDSAParams(65, Params);
  Result:= MLDSASignInternal(Params, SecretKey, Message, Context, Rnd,
    Signature);
End;

Lima field turunan sengaja dihitung alih-alih disalin dari tabel FIPS 204. Lebar bit yang ditranskrip dengan tangan memang termasuk kelas konstanta yang tampak benar saat review dan meleset satu di produksi, dan dua dari cacat nyata dalam porting ini berbentuk seperti itu. Ukuran yang dideklarasikan tetap menjadi konstanta bernama untuk validasi: 1312 / 2560 / 2420 byte untuk kunci publik, kunci rahasia, dan tanda tangan ML-DSA-44, 1952 / 4032 / 3309 untuk ML-DSA-65, 2592 / 4896 / 4627 untuk ML-DSA-87

PDFlibPas melewatkan MLDSA44Sign, MLDSA65Sign dan MLDSA87Sign melalui BuildMLDSAParams ke satu record TMLDSAParams yang field turunannya dihitung alih-alih ditranskrip, sehingga satu engine bersama MLDSASignInternal melayani ketiga set parameter FIPS 204
Ketiga set parameter berbagi satu engine karena nomor set hanya memilih sebuah record, dan lebar bit turunan dihitung alih-alih ditranskrip dari tabel FIPS 204

Di mana porting ML-DSA dari nol pertama kali keliru?

Di expand_a, Algoritma 32 FIPS 204, dan mode kegagalannya menyesatkan dengan indah. Matriks A disampling dengan menanam SHAKE128 menggunakan rho diikuti dua byte indeks, sehingga buffer seed berukuran 34 byte: rho(32), lalu j, lalu i. Ditulis dalam Pascal dengan pengindeksan AnsiString berbasis 1, kedua byte itu adalah Msg[33] dan Msg[34]. Draf pertama porting ini menulisnya ke Msg[34] dan Msg[35], bergeser tepat satu byte, dan hasilnya sepasang kunci yang rho-nya cocok sempurna dengan vektor uji sementara setiap koefisien t salah. Hanya matriks yang tercemar, dan matriks adalah satu-satunya hal yang kunci publik tidak bawa secara verbatim

Dua cacat lain juga tinggal di rutinitas yang sama. Panjang absorb harus 34, bukan 35; satu byte sampah ekstra mengubah seluruh aliran yang disqueeze. Dan loop penolakan bagian dalam harus mengonsumsi setiap grup tiga byte yang bisa diberikan blok, termasuk yang dimulai pada offset 165 dari blok SHAKE128 168-byte, yaitu 56 grup per blok. Skrip cross-check yang berhenti di offset 162 membuang ekor setiap blok dan menggeser awalan t1 yang disampling mulai sekitar byte ketiga belas dan seterusnya

SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J);          // indeks kolom dulu
Msg[34]:= AnsiChar(I);          // lalu indeks baris
PLShakeXOFInit(Ctx, True);      // SHAKE128, rate 168
PLShakeXOFAbsorb(Ctx, @Msg[1], 34);
PLShakeXOFFinalize(Ctx);
Cnt:= 0;
While Cnt< N Do
Begin
  PLShakeXOFSqueeze(Ctx, @Buf[0], 168);
  BOff:= 0;
  // BOff+2 <= 167 menjaga grup di offset 165: 56 tripel per blok
  While (BOff+ 2<= High(Buf))And (Cnt< N) Do
  Begin
    T3:= ((Buf[BOff+ 2]and $7F)shl 16)xor (Buf[BOff+ 1]shl 8)xor Buf[BOff];
    If T3< Q Then
    Begin
      Poly^[Cnt]:= T3;
      Inc(Cnt);
    End;
    Inc(BOff, 3);
  End;
End;

Setelah ketiganya dikoreksi, digest SHA-256 dari kunci publik dan rahasia ML-DSA-44 yang lengkap cocok dengan vektor known-answer FIPS 204. Satu pelajaran debugging juga layak disebut, karena pelajaran itu memakan satu sesi: saat Anda membangun cross-check Python untuk loop penolakan yang digerakkan XOF, hashlib.shake_128().digest(n) mengembalikan prefiks yang sama pada setiap pemanggilan alih-alih melanjutkan aliran. Ambil seluruh panjangnya sekali, lalu potong menjadi blok sebesar rate, atau referensi Anda akan dengan senang hati mengonsumsi ulang nilai-nilai yang ditolak dengan benar oleh Pascal Anda

Rutinitas expand_a ML-DSA PDFlibPas menanam SHAKE128 dengan buffer 34 byte berisi rho, indeks kolom dan indeks baris, di samping draf pertama yang menggeser kedua byte indeks dan cross-check yang membuang grup tiga byte terakhir setiap blok
Dua cacat off-by-one di rutinitas yang sama: byte indeks ditulis satu posisi terlambat, dan loop penolakan yang berhenti sebelum grup tiga byte di offset 165

Sampling eta: mengapa ML-DSA-65 butuh cabangnya sendiri

PDFlibPas menyimpan dua jalur terpisah di expand_s karena Algoritma 33 FIPS 204 memang mendefinisikan dua. Untuk eta = 2 setiap nibble ditolak saat mencapai 15 dan jika tidak direduksi mod 5. Untuk eta = 4 nibble ditolak pada 9 ke atas lalu dipakai langsung, tanpa reduksi modular sama sekali. ML-DSA-65 adalah satu-satunya set yang dirilis dengan eta = 4, dan memakai ulang jalur mod 5 untuknya mendiskalakan s1 dan s2 sejak koefisien pertama, menghasilkan pasangan kunci yang konsisten secara internal, terverifikasi terhadap dirinya sendiri, dan tidak cocok dengan apa pun yang dihasilkan orang lain

Procedure StoreNibble(Nibble: Byte);
Var
  M: Integer;
  Centered: Cardinal;
Begin
  If Cnt>= N Then
    Exit;
  If Eta= 4 Then
  Begin
    If Nibble>= 9 Then        // tolak, lalu ambil nibble apa adanya
      Exit;
    M:= Nibble;
  End
  Else
  Begin
    If Nibble>= 15 Then       // eta = 2: tolak 15, lalu reduksi mod 5
      Exit;
    M:= Nibble mod 5;
  End;
  If Eta>= M Then
    Centered:= Eta- M
  Else
    Centered:= Q- (M- Eta);
  Vec[I][Cnt]:= Centered;
  Inc(Cnt);
End;

Ukuran adalah ujinya: panjang c-tilde dan lebar bit gamma1

Dua parameter encoding berubah mengikuti tingkat keamanan dengan cara yang mudah terlewat saat build ML-DSA-44 yang bekerja ada di depan mata. Hash tantangan c-tilde berukuran 2 x lambda / 8 byte, yaitu 32 untuk ML-DSA-44, 48 untuk ML-DSA-65 dan 64 untuk ML-DSA-87. Membiarkannya tetap 32 menghasilkan tanda tangan ML-DSA-65 sebesar 3293 byte alih-alih 3309 standar, dan prefiks KAT langsung menyimpang. Field record CTildaBytes ada persis agar angka itu tidak bisa dilupakan

Yang kedua adalah lebar pengemasan polinom mask z. PDFlibPas menghitungnya sebagai BitWidth(Gamma1), bukan sebagai eksponen: gamma1 = 2^19 untuk ML-DSA-65 dan 87 membutuhkan 20 bit per koefisien, bukan 19, dan satu bit itu menentukan apakah setiap polinom z menempati 640 byte atau sesuatu yang tidak akan pernah di-parse verifier mana pun. Verifier membawa cacat yang cocok selama porting berlangsung, saat buffer deserialisasi z diukur 192 byte alih-alih 576. Panjang tanda tangan adalah uji regresi termurah yang akan pernah Anda tulis: assert 2420, 3309 dan 4627 terhadap Length(Signature) dan sebagian besar kesalahan parameterisasi mengumumkan diri sebelum Anda sampai ke satu pun asersi kriptografi

PDFlibPas mengaitkan dua parameter encoding ML-DSA ke tingkat keamanan: hash tantangan c-tilde tumbuh dari 32 ke 48 ke 64 byte, dan polinom mask z dikemas pada BitWidth bit gamma1, dengan panjang tanda tangan sebagai uji regresi
Dua parameter berubah mengikuti tingkat keamanan, dan tanda tangan yang keluar 3293 byte alih-alih 3309 mengumumkan kesalahan itu sebelum ada asersi kriptografi yang berjalan

Menandatangani tanpa loop tanpa batas

Penandatanganan ML-DSA berbasis penolakan, jadi ia mencoba ulang dengan kappa yang ditambah hingga tanda tangan kandidat lolos pemeriksaan norm dan hint-nya. PDFlibPas membatasinya dengan anggaran eksternal eksplisit sebesar 65535 percobaan; saat habis, MLDSASignInternal mengembalikan False dan membiarkan tanda tangan kosong alih-alih berputar di dalam thread produksi dokumen. Dalam praktiknya vektor resmi ML-DSA-44 berhasil pada kappa = 4 dengan 55 hint terhadap plafon omega 80, sehingga anggaran itu adalah pagar pengaman, bukan batas kerja

Bug yang membuat pagar itu terasa perlu sama sekali tidak bersifat numerik. Proses penandatanganan tampak menggantung, kecurigaan jatuh pada decompose dan make_hint (Algoritma 36 dan 39 FIPS 204), dan penyebab sebenarnya adalah target akumulasi yang terbalik: vektor yang memberi makan perhitungan hint harus mengakumulasi c*t0, sementara c*t0 asli harus tetap utuh untuk pemeriksaan norm. Tunjukkan keduanya ke buffer yang sama dan loop akan menolak selamanya dengan aritmetika yang benar sempurna. Pada jalur sukses maupun jalur anggaran-habis, unit ini mengosongkan seed turunan, polinom rahasia, mask, tantangan, dan buffer encoding; seed, kunci rahasia, dan rnd yang diberikan pemanggil tetap menjadi tanggung jawab pemanggil, pembagian yang tepat untuk pustaka yang tidak bisa tahu dari mana string-string itu berasal

Di mana ML-DSA bertemu dengan tumpukan tanda tangan PDF hari ini?

Bersikap presisi tentang apa yang memang ada. PDFlibPas mengirim ML-DSA sebagai primitif tanda tangan yang terverifikasi ditambah binding mekanisme PKCS #11, bukan sebagai pengganti drop-in untuk output PAdES Anda saat ini. Jalur token adalah TPDFlibPKCS11Client.SignMLDSA, dan sengaja dijadikan titik masuk terpisah karena CKM_ML_DSA mengonsumsi pesan mentah alih-alih digest yang sudah dihitung sebelumnya, sehingga callback SignHash dan digest-eksternal yang ada tidak dapat dipakai ulang. Penemuan tanpa sertifikat mengharuskan CertificateOptional diaktifkan secara eksplisit bersama label atau ID kunci privat, dan klien memvalidasi CKA_PARAMETER_SET terhadap whitelist CKP_ML_DSA_44 / 65 / 87 pada saat connect, sehingga pasangan sertifikat RSA dan ECDSA bawaan tidak pernah dilonggarkan secara tidak sengaja

Integrasi tingkat dokumen adalah bagian yang masih diatur oleh kerja standar alih-alih oleh kode pustaka. ISO 32000-2 §12.8 mendefinisikan kamus tanda tangan dan payload CMS-nya, dan ISO/TS 32002 adalah kendaraan untuk memperluas dukungan itu ke algoritma hash dan tanda tangan yang lebih baru; sampai validator dan mitra Anda mengikuti, penandatanganan klasik tetap menjadi jalur produksi. Sikap praktisnya adalah jalur paralel: terus kirimkan tanda tangan PAdES B-B hingga B-LTA dengan timestamping dan data validasi jangka panjang untuk apa pun yang harus divalidasi pihak ketiga hari ini, sambil membuktikan penanganan kunci ML-DSA dan integrasi token di sampingnya. Untuk eksperimen lokal, alur kerja sertifikat self-signed yang dibangun di atas CryptoAPI yang sama memberi Anda identitas penandatangan tanpa melibatkan CA publik

Uji perubahan set parameter seperti Anda menguji perubahan penandatanganan lainnya. Ukuran dulu, lalu vektor resmi, lalu kasus negatif: satu byte tanda tangan yang diubah, string konteks yang tidak cocok, kunci yang terpotong. PDFlibPas mencakup semuanya dalam suite DUnitX-nya, dan disiplin yang sama layak ada di pipeline Anda sendiri, idealnya bersama workbench kepatuhan dan penandatanganan yang menjalankan validasi secara batch di seluruh korpus dokumen agar regresi tidak pernah sampai ke pelanggan tanpa terdeteksi

Kesiapan pasca-kuantum untuk perangkat lunak dokumen tidak akan datang sebagai satu saklar. Ia datang sebagai primitif yang bisa Anda uji, jalur token yang bisa Anda sambungkan, dan jalur standar yang Anda ikuti tanpa mempertaruhkan rilis saat ini padanya. Untuk melihat bagaimana unit ML-DSA berdampingan dengan sisa tooling penandatanganan, enkripsi dan PDF/A dalam basis kode Object Pascal native, halaman produk pustaka PDF Delphi PDFlibPas menampilkan set komponen lengkap dan matriks kompiler yang didukung