Odborný článok

ML-DSA v Delphi: postkvantová FIPS 204 v PDFlibPas

PDFlibPas implementuje ML-DSA, algoritmus digitálneho podpisu založený na modulových mriežkach štandardizovaný vo FIPS 204, úplne v Object Pascale. Všetky tri sady parametrov sa dodávajú ako bežné funkcie: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, plus zodpovedajúce KeyGen a Verify vstupy. Žiadny OpenSSL, žiadna platformová DLL, žiadne C lepidlo. Jediná jednotka PDFlibMLDSA nespolieha na nič okrem SHAKE špongie knižnice a jej výstup sa zhoduje s oficiálnymi známymi testovacími vektormi FIPS 204 bajt po bajte

Ta posledná veta je jediná časť, ktorá si vyžiadala skutočnú prácu. Napísať mriežkovú aritmetiku v Pascale je mechanické; dosiahnuť zhodu s NIST nie. Nasleduje inžiniersky rozkaz portácie: ako tri sady parametrov skončili pri jednom zdieľanom engine a konkrétne defekty, ktoré delili skompiluje sa a beží od zhoduje sa s KAT. Ak hodnotíte postkvantové možnosti pre dokumentový pipeline v Delphi alebo C++Builder, defekty sú tá užitočná časť, pretože každý z nich produkuje presvedčivo vyzerajúci výstup, ktorý mlčky zlyháva v interoperabilite

Prečo písať postkvantový podpisovač v čistom Object Pascale?

Pretože alternatíva je jedna natívna závislosť na cieľ, a Delphi PDF knižnica ich už má dosť. PDFlibPas sa zostavuje naprieč Delphi, C++Builder a FPC/Lazarus na Win32, Win64 a Unix cieľoch; napojenie C postkvantovej knižnice by znamenalo sledovať jej zostavenie pre každý z týchto slotov, plus povrch konvencií volania a vlastníctva pamäte medzi nimi. Čistá Pascal jednotka sa kompiluje všade, kde sa kompiluje zvyšok knižnice, a to je celý argument

ML-DSA to robí neobyčajne lacným, pretože jeho jediná primitívna závislosť je SHAKE. Nie je tu vrstva veľkých celých čísel, žiadna eliptická krivka, žiadny samostatný hash balík. PDFlibPas získal streaming XOF v vydaní tesne pred portáciou: TPLShakeXOF v PDFlibDigest, kde PLShakeXOFInit vyberá SHAKE128 (rate 168) alebo SHAKE256 (rate 136), nasleduje PLShakeXOFAbsorb, PLShakeXOFFinalize a slučka PLShakeXOFSqueeze, ktorá pokračuje v permutácii pre ľubovoľnú dĺžku výstupu. Každá rutina rejection-sampling v ML-DSA jednotke je napísaná priamo proti tomuto štvorvolanovému API

Jeden engine, tri sady parametrov: TMLDSAParams

PDFlibPas popisuje celú sadu parametrov ML-DSA jedným záznamom a vyberá ju číslom sady, takže ML-DSA-44, 65 a 87 bežia cez rovnaké cesty kódu. Prvá fungujúca implementácia bola fixná 4x4 zostava natvrdo zapojená pre ML-DSA-44; zovšeobecnenie znamenalo zdvihnúť k a l, eta, tau, beta, gamma1 a gamma2, omega a dĺžku challenge do TMLDSAParams a odvodiť všetko ostatné. Verejné vstupné body sa stali trojriadkovými obalmi

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;

// Odvodené polia sa počítajú, nikdy neprepisujú z tabuľky
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;

Päť odvodených polí sa počíta zámerne, nie prepisuje z tabuliek FIPS 204. Ručne prepísané bitové šírky sú presne tou triedou konšt, ktorá vyzerá správne v revízii a je vedľa o jednu v produkcii, a dva zo skutočných defektov tejto portácie boli práve takéhoto tvaru. Deklarované veľkosti zostávajú ako pomenované konštanty pre validáciu: 1312 / 2560 / 2420 bajtov verejného kľúča, súkromného kľúča a podpisu pre ML-DSA-44, 1952 / 4032 / 3309 pre ML-DSA-65, 2592 / 4896 / 4627 pre ML-DSA-87

PDFlibPas smeruje MLDSA44Sign, MLDSA65Sign a MLDSA87Sign cez BuildMLDSAParams do jediného záznamu TMLDSAParams, ktorého odvodené polia sa počítajú namiesto prepisovania, takže jeden zdieľaný engine MLDSASignInternal obsluhuje všetky tri sady parametrov FIPS 204
Tri sady parametrov zdieľajú jeden engine, pretože číslo sady len vyberá záznam a odvodené bitové šírky sa počítajú namiesto prepisovania z tabuliek FIPS 204

Kde sa portácia ML-DSA od nuly pokazí najskôr?

V expand_a, FIPS 204 algoritmus 32, a režim zlyhania je nádherně zavádzajúci. Matica A sa vzorkuje zasadením SHAKE128 s rho nasledovaným dvoma indexovými bajtmi, takže buffer semena má 34 bajtov: rho(32), potom j, potom i. Napísané v Pascale s indexovaním AnsiString od 1 sú tie dva bajty Msg[33] a Msg[34]. Prvý návrh tejto portácie ich napísal do Msg[34] a Msg[35], posunutý presne o jeden bajt, a výsledok bol pár kľúčov, kde rho zodpovedal testovaciemu vektoru perfektne, zatiaľ čo každý koeficient t bol zlý. Znečistená bola len matica a matica je tá jediná vec, ktorú verejný kľúč neprenáša doslova

Dva ďalšie defekty žili v tej istej rutine. Absorb dĺžka musí byť 34, nie 35; jeden navyše smetný bajt zmení celý stlačený stream. A vnútorná odmietacia slučka musí spotrebovať každú trojbajtovú skupinu, ktorú blok dodá, vrátane tej začínajúcej na ofsete 165 168-bajtového bloku SHAKE128, čo je 56 skupín na blok. Skript krížovej kontroly, ktorý sa zastavil na ofsete 162, zahodil chvost každého bloku a posunul vzorkovaný prefix t1 približne od trinásteho bajta ďalej

SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J);          // najprv index stĺpca
Msg[34]:= AnsiChar(I);          // potom index riadku
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 zachováva skupinu na ofsete 165: 56 trojíc na 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;

S týmito troma opravami sa SHA-256 digesty kompletného verejného a súkromného kľúča ML-DSA-44 zhodovali so známymi vektormi FIPS 204. Jedna ladička lekcia stojí za pomenovanie, pretože stála sedenie: keď staviate Python krížovú kontrolu pre odmietaciu slučku riadenú XOF, hashlib.shake_128().digest(n) vracia rovnaký prefix pri každom volaní namiesto pokračovania streamu. Zoberte celú dĺžku raz a potom ju rozrežte na bloky veľkosti rate, inak vaša referencia rada znova skonzumuje presne tie hodnoty, ktoré váš Pascal správne odmietol

Rutina PDFlibPas ML-DSA expand_a zasiaha SHAKE128 34-bajtovým bufferom držiacim rho, index stĺpca a index riadku, vedľa prvého návrhu, ktorý posunul oba indexové bajty, a krížovej kontroly, ktorá zahodila poslednú trojbajtovú skupinu každého bloku
Dva defekty vedľa o jednu v tej istej rutine: indexové bajty zapísané jednu pozíciu neskôr a odmietacia slučka zastavujúca sa pred trojbajtovou skupinou na ofsete 165

Vzorkovanie eta: prečo ML-DSA-65 potrebuje vlastnú vetvu

PDFlibPas drží dve samostatné cesty v expand_s, pretože FIPS 204 algoritmus 33 ich skutočne definuje dve. Pre eta = 2 sa každý nibble odmieta, keď dosiahne 15, a inak sa redukuje mod 5. Pre eta = 4 sa nibble odmieta pri 9 a vyššie a potom sa použije priamo bez akejkoľvek modulárnej redukcie. ML-DSA-65 je jediná dodávaná sada s eta = 4 a opätovné použitie cesty mod 5 pre ňu rozosunie s1 a s2 od prvého koeficientu, produkuje pár kľúčov, ktorý je interne konzistentný, verifikuje sa proti sebe samej a nezhoduje sa s ničím, čo produkujú ostatní

Procedure StoreNibble(Nibble: Byte);
Var
  M: Integer;
  Centered: Cardinal;
Begin
  If Cnt>= N Then
    Exit;
  If Eta= 4 Then
  Begin
    If Nibble>= 9 Then        // odmietnuť, potom vziať nibble tak, ako je
      Exit;
    M:= Nibble;
  End
  Else
  Begin
    If Nibble>= 15 Then       // eta = 2: odmietnuť 15, potom redukovať 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;

Veľkosti sú test: dĺžka c-tilde a bitová šírka gamma1

Dva parametre kódovania sa menia s bezpečnostnou úrovňou spôsobmi, ktoré sa ľahko prehliadnú, keď fungujúca zostava ML-DSA-44 stojí hneď vedľa. Challenge hash c-tilde má 2 x lambda / 8 bajtov, čo je 32 pre ML-DSA-44, 48 pre ML-DSA-65 a 64 pre ML-DSA-87. Nechať ho fixný na 32 dáva podpis ML-DSA-65 o 3293 bajtoch namiesto štandardných 3309 a KAT prefix diverguje okamžite. Pole záznamu CTildaBytes existuje presne preto, aby sa toto číslo nedalo zabudnúť

Druhý je pakovacia šírka maskového polynómu z. PDFlibPas ju počíta ako BitWidth(Gamma1), nie ako exponent: gamma1 = 2^19 pre ML-DSA-65 a 87 potrebuje 20 bitov na koeficient, nie 19, a ten jediný bit rozhoduje, či každý polynóm z zaberá 640 bajtov alebo niečo, čo žiadny verifikátor nerozparsuje. Verifikátor nosil zodpovedajúci defekt počas portácie, kde bol buffer deserializácie z veľkosti 192 bajtov namiesto 576. Dĺžka podpisu je najlacnejší regresný test, aký kedy napíšete: asertujte 2420, 3309 a 4627 proti Length(Signature) a väčšina chýb parametrovania sa ohlási skôr, než dosiahnete jediné kryptografické asertovanie

PDFlibPas viaže dva parametre kódovania ML-DSA na bezpečnostnú úroveň: challenge hash c-tilde rastie z 32 na 48 na 64 bajtov a maskový polynóm z sa pakuje na BitWidth bitoch gamma1, s dĺžkou podpisu ako regresným testom
Dva parametre sa menia s bezpečnostnou úrovňou a podpis, ktorý vyjde o 3293 bajtov namiesto 3309, ohlási chybu skôr, než beží akékoľvek kryptografické asertovanie

Podpisovanie bez neohraničenej slučky

Podpisovanie ML-DSA je založené na odmietaní, takže opakuje s inkrementovaným kappa, kým kandidátsky podpis prejde normou a kontrolou hint. PDFlibPas to obmedzuje explicitným externým rozpočtom 65535 pokusov; pri vyčerpaní MLDSASignInternal vracia False a nechá podpis prázdny namiesto krútenia vo vlákne produkujúcom dokumenty. V praxi oficiálny vektor ML-DSA-44 uspeje pri kappa = 4 s 55 hintmi proti stropu omega 80, takže rozpočet je bezpečnostné zábradlie, nie pracovný limit

Chyba, ktorá toto zábradlie urobila nutným, nebola číselná vôbec. Podpisovanie sa javilo zaseknuté, podozrenie padlo na decompose a make_hint (FIPS 204 algoritmy 36 a 39) a skutočná príčina bola obrátený akumulačný cieľ: vektor kŕmiaci výpočet hintu musí akumulovať c*t0, zatiaľ čo pôvodné c*t0 musí prežiť nedotknuté pre normu. Namierte oba do toho istého bufferu a slučka odmieta navždy s dokonale správnou aritmetikou. Na oboch cestách — úspechu aj vyčerpania rozpočtu — jednotka nuluje odvodené semená, súkromné polynómy, masky, challenge a kódovacie buffery; dodané volajúcim semeno, súkromný kľúč a rnd ostávajú zodpovednosťou volajúceho, čo je správne rozdelenie pre knižnicu, ktorá nemôže vedieť, odkiaľ tie reťazce prišli

Kde sa ML-DSA dnes stretáva s PDF podpisovým stackom?

Buďte presní v tom, čo existuje. PDFlibPas dodáva ML-DSA ako verifikované podpisové primitívy plus napojenie na mechanizmus PKCS #11, nie ako drop-in náhradu vášho súčasného PAdES výstupu. Tokenová cesta je TPDFlibPKCS11Client.SignMLDSA a je zámerne samostatným vstupným bodom, pretože CKM_ML_DSA konzumuje surovú správu namiesto predpočítaného digestu, takže existujúce SignHash a external-digest callbacky sa nedajú znova použiť. Objavovanie bez certifikátu vyžaduje explicitne zapnuté CertificateOptional spolu s labelom alebo ID súkromného kľúča a klient validuje CKA_PARAMETER_SET proti whieliste CKP_ML_DSA_44 / 65 / 87 v čase pripojenia, takže predvolené párovanie certifikátov RSA a ECDSA sa nikdy neomylom neuvoľní

Integrácia na úrovni dokumentov je časť ešte riadená štandardizačnou prácou, nie kódom knižnice. ISO 32000-2 §12.8 definuje podpisový slovník a jeho CMS payload a ISO/TS 32002 je vozidlo na rozšírenie tejto podpory o novšie hash a podpisové algoritmy; pokiaľ vaši validátori a partneri nenalejavia, klasické podpisovanie ostáva produkčnou cestou. Praktická postura sú paralelné koľaje: pokračujte v dodávaní podpisov PAdES B-B až B-LTA s timestampovaním a dátami dlhodobej validácie pre všetko, čo musí tretia strana dnes validovať, a vedľa toho dokazujte správu ML-DSA kľúčov a tokenovú integráciu. Pre lokálne experimenty dáva rovnaký workflow samopodpísaného certifikátu postavený na CryptoAPI podpisovú identitu bez zapojenia verejnej CA

Testujte zmenu sady parametrov tak, ako by ste testovali akúkoľvek inú zmenu podpisovania. Najprv veľkosti, potom oficiálne vektory, potom negatívne prípady: poškriabaný bajt podpisu, nezhodujúci sa kontextový reťazec, skrátený kľúč. PDFlibPas pokrýva všetky tie prípady v svojej DUnitX sade a rovnaká disciplína patrí aj do vášho vlastného pipeline, ideálne vedľa compliance a podpisovej workbench, ktorá dávkuje validáciu naprieč korpusom dokumentov, aby regresia nikdy nedosiahla zákazníka nepozorovane

Postkvantová pripravenosť pre dokumentový softvér nepríde ako jediný prepínač. Príde ako primitívy, ktoré môžete testovať, tokenová cesta, ktorú môžete napojiť, a štandardizačná koľaj, ktorú sledujete bez stávky aktuálneho vydania na nej. Ako sedí ML-DSA jednotka vedľa zvyšku podpisovania, šifrovania a nástrojov PDF/A v natívnom Object Pascal kódexu, ukazuje produktová stránka PDFlibPas Delphi PDF library, ktorá vypisuje kompletnú sadu komponentov a maticu podporovaných kompilátorov