Tehnični članak

ML-DSA v Delphiju: FIPS 204 post-kvantno v PDFlibPas

PDFlibPas izvaja ML-DSA, algoritem digitalnih podpisov na modulnih mrežah, standardiziran v FIPS 204, v celoti v Object Pascalu. Vsi trije nabori parametrov pridejo kot običajne funkcije: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, poleg njih še ustrezni vstopi KeyGen in Verify. Brez OpenSSL, brez DLL platforme, brez lepila C. Enota PDFlibMLDSA ni odvisna od ničesar razen knjižnične gobe SHAKE, njen izhod pa se s uradnimi testnimi vektorji z znanimi odgovori FIPS 204 ujema bajt za bajt

Zadnji stavek je edini del, ki je zahteval pravo delo. Mrežna aritmetika v Pascalu je mehanska; spraviti jo v soglasje z NIST pa ni. Sledi inženirski zapis preselitve: kako so trije nabori parametrov končali z enim skupnim pogonom in katere konkretne napake so ločile se prevede in teče od se ujema s KAT. Če vrednotite možnosti po kvantni dobi za dokumentni cevovod v Delphi ali C++Builder, so napake koristen del, ker vsaka od njih da izhod, ki izgleda prepričljivo, tiho pa odpove pri interoperabilnosti

Zakaj pisati podpisovalnik po kvantni dobi v čistem Object Pascalu?

Ker je alternativa ena izvorna odvisnost na cilj, knjižnica PDF za Delphi pa jih ima že dovolj. PDFlibPas se gradi čez Delphi, C++Builder in FPC/Lazarus na ciljih Win32, Win64 in Unix; vezava C knjižnice za po kvantni dobi bi pomenila sledenje njeni izgradnji za vsak od teh mest, plus površino klicnih konvencij in lastništva pomnilnika med njimi. Čista Pascalova enota se prevede, kamor koli se prevede ostanek knjižnice, in to je celoten argument

ML-DSA to naredi nenavadno poceni, ker je njegova edina primitivna odvisnost SHAKE. Ni plasti velikih celih števil, ni eliptične krivulje, ni ločene zbirke zgoščevalnih funkcij. PDFlibPas je pridobil pretakni XOF v izdaji tik pred preselitvijo: TPLShakeXOF v PDFlibDigest, kjer PLShakeXOFInit izbere SHAKE128 (hitrost 168) ali SHAKE256 (hitrost 136), sledijo PLShakeXOFAbsorb, PLShakeXOFFinalize in zanka PLShakeXOFSqueeze, ki še naprej permutira za poljubno dolžino izhoda. Vsaka rutina vzorčenja z zavrnitvijo v enoti ML-DSA je napisana naravnost proti tistemu API-ju s štirimi klici

En pogon, trije nabori parametrov: TMLDSAParams

PDFlibPas opiše celoten nabor parametrov ML-DSA z enim zapisom in ga izbere po številki nabora, zato ML-DSA-44, 65 in 87 tečejo po istih poteh kode. Prva delujoča izvedba je bila fiksna izgradnja 4x4, pripeta na ML-DSA-44; posplošitev je pomenila dvig k in l, eta, tau, beta, gamma1 in gamma2, omega ter dolžine izziva v TMLDSAParams, nato pa odvajanje vsega drugega. Javni vstopi so postali ovijalniki s tremi vrsticami

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;

// Odvedena polja se izračunajo, nikoli prepišejo iz tabele
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;

Pet odvedenih polj se računa, namesto da bi bila načrtno prepisana iz tabel FIPS 204. Ročno prepisane širine bitov so ravno tista vrsta konstant, ki je v pregledu videti pravilna, v produkciji pa je zgrešena za ena, in dve od pravih napak te preselitve sta bili take oblike. Deklarirane velikosti ostanejo kot poimenovane konstante za preverjanje: 1312 / 2560 / 2420 bajtov javnega ključa, skrivnega ključa in podpisa za ML-DSA-44, 1952 / 4032 / 3309 za ML-DSA-65, 2592 / 4896 / 4627 za ML-DSA-87

PDFlibPas usmerja MLDSA44Sign, MLDSA65Sign in MLDSA87Sign skozi BuildMLDSAParams v en sam zapis TMLDSAParams, katerega odvedena polja se računajo in ne prepisujejo, zato en skupni pogon MLDSASignInternal streže vsem trem naborom parametrov FIPS 204
Trije nabori parametrov si delijo en pogon, ker številka nabora le izbere zapis, odvedene širine bitov pa se računajo namesto da bi jih prepisali iz tabel FIPS 204

Kje preselitev ML-DSA od nič prvič zaide narobe?

V expand_a, algoritmu 32 iz FIPS 204, in način odpovedi je prelepo zavajajoč. Matrika A se vzorči z osjanjem SHAKE128 z rho, ki mu sledita dva indeksna bajta, zato je serjalni medpomnilnik dolg 34 bajtov: rho(32), potem j, potem i. Zapisano v Pascalu z indeksiranjem AnsiString od 1 sta ta bajta Msg[33] in Msg[34]. Prvi osnutek preselitve ju je zapisal v Msg[34] in Msg[35], prestavljen natanko za en bajt, rezultat pa je bil par ključev, kjer se je rho popolnoma ujemal s testnim vektorjem, medtem ko je bil vsak koeficient t napačen. Onesnažena je bila le matrika, matrika pa je tisto, česar javni ključ ne nosi dobesedno

Še dve napaki sta živeli v isti rutini. Dolžina vpijanja mora biti 34, ne 35; en odveč smetni bajt spremeni celoten stisnjen tok. Notranja zanka zavrnitve pa mora porabiti vsako skupino treh bajtov, ki jo blok more dati, vključno s tisto, ki se začne pri odmiku 165 v 168-bajtnem bloku SHAKE128, kar je 56 skupin na blok. Preverjalni skript, ki je obstal pri odmiku 162, je odvrgel rep vsakega bloka in prestavil vzorčeno predpono t1 od približno trinajstega bajta naprej

SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J);          // najprej indeks stolpca
Msg[34]:= AnsiChar(I);          // nato indeks vrstice
PLShakeXOFInit(Ctx, True);      // SHAKE128, hitrost 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 obdrži skupino pri odmiku 165: 56 trojk 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 temi tremi popravljenimi so se zgoščevalne vrednosti SHA-256 celotnih javnih in skrivnih ključev ML-DSA-44 ujemale z vektorji z znanimi odgovori FIPS 204. Vredno imena je tudi ena lekcija razhroščevanja, ker je stala sejo: ko zgradite Pythonov preverjalni skript za zanko zavrnitve, ki jo poganja XOF, vrne hashlib.shake_128().digest(n) ob vsakem klicu isto predpono namesto da bi nadaljeval tok. Vzemite celotno dolžino enkrat in jo narežite na bloke velikosti hitrosti, sicer bo vaša referenca z veseljem znova porabila ravno tiste vrednosti, ki jih je vaš Pascal pravilno zavrnil

Rutina expand_a v ML-DSA za PDFlibPas osanja SHAKE128 s 34-bajtnim medpomnilnikom, ki drži rho, indeks stolpca in indeks vrstice, ob prvem osnutku, ki je oba indeksna bajta prestavil, in preverjalnem skriptu, ki je odvrgel zadnjo skupino treh bajtov vsakega bloka
Dve napaki za ena v isti rutini: indeksna bajta zapisana eno mesto prepozno in zanka zavrnitve, ki obstane pred skupino treh bajtov pri odmiku 165

Vzorčenje eta: zakaj ML-DSA-65 potrebuje svojo vejo

PDFlibPas v expand_s obdrži dve ločeni poti, ker jih algoritem 33 iz FIPS 204 resnično definira. Za eta = 2 se vsak nibble zavrne, ko doseže 15, sicer pa se zmanjša mod 5. Za eta = 4 se nibble zavrne pri 9 ali več in se nato uporabi neposredno, brez vsakršne modularne redukcije. ML-DSA-65 je edini izdani nabor z eta = 4, ponovna uporaba poti mod 5 zanjo pa zaravna s1 in s2 že pri prvem koeficientu, nastane par ključev, ki je notranjo skladen, se potrdi sam proti sebi in se ne ujema z ničemer, kar proizvaja kdo drug

Procedure StoreNibble(Nibble: Byte);
Var
  M: Integer;
  Centered: Cardinal;
Begin
  If Cnt>= N Then
    Exit;
  If Eta= 4 Then
  Begin
    If Nibble>= 9 Then        // zavrne, nato vzame nibble kot je
      Exit;
    M:= Nibble;
  End
  Else
  Begin
    If Nibble>= 15 Then       // eta = 2: zavrne 15, nato zmanjša 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;

Velikosti so preizkus: dolžina c-tilde in širina bitov gamma1

Dva parametra kodiranja se spreminjata z varnostno ravnjo na načine, ki jih je lahko spregledati, ko delujoča izgradnja ML-DSA-44 že stoji ob strani. Zgoščevalna vrednost izziva c-tilde je 2 x lambda / 8 bajtov, kar je 32 za ML-DSA-44, 48 za ML-DSA-65 in 64 za ML-DSA-87. Puščanje fiksirano na 32 da podpis ML-DSA-65 s 3293 bajti namesto standardnih 3309, predpona KAT pa takoj zaide s poti. Polje zapisa CTildaBytes obstaja ravno zato, da te številke ni mogoče pozabiti

Druga je širina pakiranja maskirnega polinoma z. PDFlibPas jo izračuna kot BitWidth(Gamma1) in ne kot eksponent: gamma1 = 2^19 za ML-DSA-65 in 87 potrebuje 20 bitov na koeficient in ne 19, ta en sam bit pa odloča, ali vsak polinom z zasede 640 bajtov ali nekaj, česar noben preverjevalnik ne bo razčlenil. Preverjevalnik je med preselitvijo nosil ujemajočo napako, kjer je bil medpomnilnik deserializacije z velik 192 bajtov namesto 576. Dolžina podpisa je najcenejši regresijski preizkus, ki ga boste kdaj napisali: uveljavite 2420, 3309 in 4627 proti Length(Signature) in večina napak parametrizacije se javi, preden pridete do ene same kriptografske trditve

PDFlibPas veže dva parametra kodiranja ML-DSA na varnostno raven: zgoščevalna vrednost izziva c-tilde raste od 32 do 48 do 64 bajtov, maskirni polinom z pa se pakuje pri BitWidth bitov gamma1, dolžina podpisa pa je regresijski preizkus
Dva parametra se spreminjata z varnostno ravnjo, podpis, ki pride ven s 3293 bajti namesto 3309, pa napako naznani, preden teče katera koli kriptografska trditev

Podpisovanje brez neomejene zanke

Podpisovanje ML-DSA temelji na zavrnitvah, zato poskuša znova s povečanim kappa, dokler kandidatski podpis ne gre čez preizkusa norme in namigov. PDFlibPas to zveže z izrecnim zunanjim proračunom 65535 poskusov; ob izčrpanju vrne MLDSASignInternal False in pusti podpis prazen, namesto da bi se vrtel znotraj niti za izdelavo dokumentov. V praksi uradni vektor ML-DSA-44 uspe pri kappa = 4 s 55 namigi proti stropu omega 80, zato je proračun varovalna tirnica in ne delovna meja

Napaka, ki je to tirnico dala občutek, da je nujna, sploh ni bila številčna. Podpisovanje je izgledalo zataknjeno, sum je padel na decompose in make_hint (algoritem 36 in 39 iz FIPS 204), pravi vzrok pa je bil obrnjen cilj seštevanja: vektor, ki hrani izračun namigov, mora akumulirati c*t0, izvirni c*t0 pa mora preživeti nedotaknjen za preizkus norme. Oboma usmerjena v isti medpomnilnik zanka zavrne za vedno, z povsem pravilno aritmetiko. Na obeh poteh, uspeha in izčrpanega proračuna, enota izniči odvedena semena, skrivne polinome, maske, izziv in medpomnilnike kodiranja; serje, skrivni ključ in rnd, ki jih da klicatelj, ostajajo odgovornost klicatelja, kar je prava delitev za knjižnico, ki ne more vedeti, od kod so ti nizi

Kje se ML-DSA danes sreča s skladom podpisov PDF?

Bodite natančni glede tega, kar obstaja. PDFlibPas izdaja ML-DSA kot preverjene podpisne primitivne operacije plus vezavo mehanizma PKCS #11, ne kot zamenjavo takoj za uporabo za vaš trenutni izhod PAdES. Pot žetona je TPDFlibPKCS11Client.SignMLDSA in je namenoma ločen vstop, ker CKM_ML_DSA porabi surovo sporočilo in ne vnaprej izračunane zgoščene vrednosti, zato obstoječih povratnih klicev SignHash in zunanje zgoščene vrednosti ni mogoče ponovno uporabiti. Odkrivanje brez potrdil zahteva, da je CertificateOptional izrecno omogočen skupaj z oznako ali ID-jem zasebnega ključa, odjemalec pa ob povezovanju preveri CKA_PARAMETER_SET proti belemu seznamu CKP_ML_DSA_44 / 65 / 87, tako da privzeto parjenje potrdil RSA in ECDSA nikoli ne popusti po nesreči

Integracija na ravni dokumentov je del, ki ga še vedno upravlja standardizacijsko delo in ne koda knjižnice. ISO 32000-2 §12.8 definira slovar podpisov in njegov tovor CMS, ISO/TS 32002 pa je vozilo za razširjanje te podpore na novejše zgoščevalne in podpisne algoritme; dokler vam preverjevalniki in nasprotne strani ne sledijo, klasično podpisovanje ostaja produkcijska pot. Praktična drža sta vzporedni tirnici: še naprej izdajajte podpise PAdES B-B do B-LTA s časovnim žigom in podatki dolgoročne validacije za vse, kar mora tretja stranka danes potrditi, medtem ko ob tem dokazujete upravljanje ključev ML-DSA in integracijo žetonov. Za lokalne poskuse vam isti potek samopodpisanih potrdil na CryptoAPI da podpisno identiteto brez vključevanja javnega CA

Spremembo nabora parametrov preizkusite tako, kot bi preizkusili katero koli drugo podpisno spremembo. Najprej velikosti, nato uradni vektorji, potem negativni primeri: alteriran bajt podpisa, neskladen niz konteksta, odrezan ključ. PDFlibPas pokrije vse te v svoji zbirki DUnitX, ista disciplina pa sodi v vaš lasten cevovod, po možnosti ob delovno mesto skladnosti in podpisovanja, ki paketno validira čez korpus dokumentov, da regresija nikoli ne pride do stranke neopažena

Pripravljenost na po kvantni dobi za programsko opremo za dokumente ne pride kot en sam stikalo. Pride kot primitivne operacije, ki jih znaš preizkusiti, kot pot žetona, ki jo znaš povezati, in kot standardizacijska tirnica, ki ji slediš, ne da bi na njo stavil trenutno izdajo. Če si želite ogledati, kako enota ML-DSA stoji ob ostalem orodju za podpisovanje, šifriranje in PDF/A v izvorni zbirki Object Pascal, produkcijska stran knjižnice PDFlibPas Delphi PDF Library našteva celoten nabor komponent in podprto matriko prevajalnikov