Tekninen artikkeli

ML-DSA Delphissä: FIPS 204 post-kvantti PDFlibPasissa

PDFlibPas toteuttaa ML-DSAn, FIPS 204:ssa standardoidun Module-Lattice-Based Digital Signature Algorithmin, kokonaan Object Pascalilla. Kaikki kolme parametrijoukkoa toimitetaan tavallisina funktioina: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign sekä vastaavat KeyGen- ja Verify-keinot. Ei OpenSSL:a, ei alustakohtaista DLL:ää, ei C-liimaa. Yksittäinen PDFlibMLDSA-yksikkö ei riipu mistään muusta kuin kirjaston SHAKE-sienestä, ja sen tuloste vastaa virallisia FIPS 204 known-answer-testivektoreita tavua tarkalleen

Se viimeinen virke on ainoa osa, joka vei oikeaa työtä. Verkkoaritmetiikan kirjoittaminen Pascalilla on mekaanista; sen saaminen sopimaan NISTin kanssa ei ole. Seuraava on siirron engineering-kertomus: miten kolme parametrijoukkoa päätyivät jakamaan yhden moottorin, ja ne täsmälliset viat, jotka erottivat kääntyy ja toimii tilanteesta vastaa KAT:ia. Jos punnitut post-kvanttivalintoja Delphi- tai C++Builder-dokumenttiputkeen, viat ovat hyödyllinen osa, koska jokainen niistä tuottaa uskottavan näköistä tulosta, joka hiljaisesti epäonnistuu yhteentoimivuudessa

Miksi kirjoittaa post-kvantti allekirjoittaja puhtaana Object Pascalina?

Koska vaihtoehto on yksi natiivi riippuvuus per kohde, ja Delphi-PDF-kirjastolla on niitä jo tarpeeksi. PDFlibPas rakentuu Delphillä, C++Builderilla ja FPC/Lazaruksella Win32-, Win64- ja Unix-kohdeille; C-post-kvanttikirjaston sitominen tarkoittaisi sen käännöksen seuraamista jokaisessa näistä paikoista sekä kutsukäytäntöjen ja muistiomistuksen pinnan niiden välillä. Puhdas Pascal-yksikkö kääntyy siellä missä kirjaston loputkin kääntyvät, ja se on koko perustelu

ML-DSA tekee tästä epätavallisen halpaa, koska sen ainoa primitiiviriippuvuus on SHAKE. Ei isoja kokonaislukukerrosta, ei elliptistä käyrää, ei erillistä tiivistyspakettia. PDFlibPas sai streaming-XOF:n julkaisussa heti ennen siirtoa: TPLShakeXOF yksikössä PDFlibDigest, jossa PLShakeXOFInit valitsee SHAKE128:n (rate 168) tai SHAKE256:n (rate 136), jota seuraavat PLShakeXOFAbsorb, PLShakeXOFFinalize ja PLShakeXOFSqueeze-silmukka, joka permutoi edelleen mielivaltaisen tulostusmitan verran. Jokainen hylkäysotantarutiini ML-DSA-yksikössä on kirjoitettu suoraan sitä neljän kutsun API:a vasten

Yksi moottori, kolme parametrijoukkoa: TMLDSAParams

PDFlibPas kuvaa kokonaisen ML-DSA-parametrijoukon yhdellä tietueella ja valitsee sen joukkonumerolla, joten ML-DSA-44, 65 ja 87 kulkevat samoja koodipolkuja. Ensimmäinen toimiva toteutus oli kiinteä 4x4-käännös, joka oli kytketty kovaa ML-DSA-44:lle; sen yleistäminen tarkoitti k:n ja l:n, etan, tauun, betan, gamma1:n ja gamma2:n, omean sekä haastepituuden nostamista TMLDSAParamsiin ja kaiken muun johtamista siitä. Julkiset sisääntulopisteet muuttuivat kolmen rivin kääreiksi

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;

// Johdetut kentät lasketaan, ei koskaan kirjoiteta taulukosta
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;

Viisi johdettua kenttää lasketaan tarkoituksella eikä kopioida FIPS 204 -taulukoista. Käsin kirjoitetut bittileveydet ovat täsmälleen se vakioluokka, joka näyttää katselmuksessa oikealta ja on yhden pielessä tuotannossa, ja kaksi todellista vikaa tässä siirrossa oli juuri sitä muotoa. Ilmoitetut mitat pysyvät nimettyinä vakioina validointia varten: 1312 / 2560 / 2420 tavua julkista avainta, salaista avainta ja allekirjoitusta ML-DSA-44:lle, 1952 / 4032 / 3309 ML-DSA-65:lle, 2592 / 4896 / 4627 ML-DSA-87:lle

PDFlibPas ohjaa MLDSA44Signin, MLDSA65Signin ja MLDSA87Signin BuildMLDSAParamsin kautta yhteen TMLDSAParams-tietueeseen, jonka johdetut kentät lasketaan eikä kirjoiteta yli, joten yksi jaettu MLDSASignInternal-moottori palvelee kaikkia kolmea FIPS 204 -parametrijoukkoa
Kolme parametrijoukkoa jakaa yhden moottorin, koska joukkonumero vain valitsee tietueen, ja johdetut bittileveydet lasketaan sen sijaan, että ne kirjoitettaisiin yli FIPS 204 -taulukoista

Missä tyhjästä tehty ML-DSA-siirto menee ensin pieleen?

expand_assa, FIPS 204 algoritmi 32, ja vikatila on kauniisti harhaanjohtava. Matriisi A otannetaan kylvämällä SHAKE128 rho:lla ja sen jälkeen kahdella indeksitavulla, joten siemenpuskuri on 34 tavua: rho(32), sitten j, sitten i. Pascalilla kirjoitettuna 1-pohjaisella AnsiString-indeksoinnilla ne kaksi tavua ovat Msg[33] ja Msg[34]. Tämän siirron ensimmäinen luonnos kirjoitti ne Msg[34]ään ja Msg[35]ään, siirrettynä täsmälleen yhdellä tavulla, ja tulos oli avainpari, jonka rho vastasi testivektoria täydellisesti, kun taas jokainen t:n kerroin oli väärä. Vain matriisi oli saastunut, ja matriisi on se ainoa asia, jota julkinen avain ei kanna sanatarkasti

Kaksi muuta vikaa asui samassa rutiinissa. Absorb-pituuden on oltava 34, ei 35; yksi ylimääräinen roskatavu muuttaa koko puristetun virran. Ja sisimmän hylkäyssilmukan on kulutettava jokainen kolmen tavun ryhmä, jonka lohko voi tarjota, mukaan lukien se, joka alkaa 168-tavuisen SHAKE128-lohkon siirtymästä 165, eli 56 ryhmää per lohko. Siirtymässä 162 pysähtynyt ristitarkistusskripti pudotti jokaisen lohkon hännän ja siirsi otannetun t1-etuliitteen suunnilleen kolmannestoista tavusta alkaen

SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J);          // sarakeindeksi ensin
Msg[34]:= AnsiChar(I);          // sitten riviindeksi
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 säilyttää ryhmän siirtymässä 165: 56 kolmikkoa per lohko
  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;

Kun ne kolme oli korjattu, täydellisen ML-DSA-44 julkisen ja salaisen avaimen SHA-256-tiivistelmät vastasivat FIPS 204 known-answer-vektoreita. Yksi vianetsintäoppi kannattaa nimetäkin, koska se maksoi istunnon: kun rakennat Python-ristitarkistusta XOF-ohjatulle hylkäyssilmukalle, hashlib.shake_128().digest(n) palauttaa saman etuliitteen jokaisella kutsulla virran jatkamisen sijaan. Ota koko mitta kerralla ja viipaloi se ratemittaisiin lohkoihin, tai viiteyksikkösi kuluttaa mielellään uudelleen täsmälleen ne arvot, jotka Pascalisi hylkäsi oikein

PDFlibPasin ML-DSA expand_a-rutiini kylvää SHAKE128:n 34-tavuisella puskurilla, joka pitää rhon, sarakeindeksin ja riviindeksin, vierellä ensimmäinen luonnos, joka siirsi molemmat indeksitavut, ja ristitarkistus, joka pudotti jokaisen lohkon viimeisen kolmen tavun ryhmän
Kaksi yhden askelen virhettä samassa rutiinissa: indeksitavut kirjoitettu yhden paikan myöhässä ja hylkäyssilmukka, joka pysähtyy ennen siirtymän 165 kolmen tavun ryhmää

Etan otanta: miksi ML-DSA-65 tarvitsee oman haaransa

PDFlibPas pitää kaksi erillistä polkua expand_sissä, koska FIPS 204 algoritmi 33 todella määrittää kaksi. Kun eta = 2, kukin niitti hylätään, kun se saavuttaa 15:n, ja muuten se pienennetään mod 5. Kun eta = 4, niitti hylätään 9:ssä tai ylempänä ja otetaan sitten käyttöön suoraan, ilman mitään modulaarista pienennystä. ML-DSA-65 on ainoa toimitettu joukko, jolla eta = 4, ja mod 5 -polun uudelleenkäyttö sille väristää s1:n ja s2:n jo ensimmäisestä kertoimesta alkaen tuottaen avainparin, joka on sisäisesti johdonmukainen, verifioituu itseään vastaan ja ei vastaa mitään, mitä kukaan muu tuottaa

Procedure StoreNibble(Nibble: Byte);
Var
  M: Integer;
  Centered: Cardinal;
Begin
  If Cnt>= N Then
    Exit;
  If Eta= 4 Then
  Begin
    If Nibble>= 9 Then        // hylkää, ota sitten niitti sellaisenaan
      Exit;
    M:= Nibble;
  End
  Else
  Begin
    If Nibble>= 15 Then       // eta = 2: hylkää 15, pienennä sitten 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;

Mitat ovat testi: c-tilde-pituus ja gamma1-bittileveys

Kaksi koodausparametria vaihtelee turvallisuustason mukana tavoin, jotka on helppo ohittaa, kun toimiva ML-DSA-44-käännös on juuri siinä. Haastetiiviste c-tilde on 2 x lambda / 8 tavua, eli 32 ML-DSA-44:lle, 48 ML-DSA-65:lle ja 64 ML-DSA-87:lle. Sen jättäminen kiinteäksi 32:ksi tuottaa 3293 tavun ML-DSA-65-allekirjoituksen vakio 3309:n sijaan, ja KAT-etuliite haarautuu heti. Tietuekenttä CTildaBytes on olemassa täsmälleen siksi, ettei sitä numeroa unohdettaisi

Toinen on maskipolynomin z pakkausleveys. PDFlibPas laskee sen BitWidth(Gamma1)na, ei eksponenttina: gamma1 = 2^19 ML-DSA-65:llä ja 87 tarvitsee 20 bittiä per kerroin, ei 19, ja se yksi bitti päättää, viekö kukin z-polynomi 640 tavua vai jotain, mitä mikään verifioija ei jäsennä. Verifioijalla oli siirron aikana vastaava vika, jossa z-deserialisointipuskuri mitoitettiin 192 tavuun 576:n sijaan. Allekirjoituksen pituus on halvin regressiotesti, jonka koskaan kirjoitat: väitä 2420, 3309 ja 4627 Length(Signature)ia vastaan, ja useimmat parametrisointivirheet ilmoittavat itsestään ennen kuin saavutat yhdenkään kryptografisen väitteen

PDFlibPas sitoo kaksi ML-DSA-koodausparametria turvallisuustasoon: c-tilde-haastetiiviste kasvaa 32:sta 48:aan ja 64:ään tavuun, ja maskipolynomi z pakataan gamma1:n BitWidth-bittimäärään, allekirjoituksen pituus regressiotestinä
Kaksi parametria vaihtelee turvallisuustason mukaan, ja 3293 tavuun asti päätynyt allekirjoitus vakio 3309:n sijaan ilmoittaa virheen ennen minkään kryptografisen väitteen suorittamista

Allekirjoittaminen ilman rajatonta silmukkaa

ML-DSA-allekirjoitus on hylkäyspohjainen, joten se yrittää uudelleen kasvatetulla kappalla, kunnes ehdokasallekirjoitus läpäisee normi- ja hint-tarkistuksensa. PDFlibPas rajoittaa sen eksplisiittisellä ulkoisella budjetilla 65535 yritystä; uupumuksen yhteydessä MLDSASignInternal palauttaa Falserin ja jättää allekirjoituksen tyhjäksi pyörimisen sijaan dokumentintuontisäikeen sisällä. Käytännössä virallinen ML-DSA-44-vektori onnistuu kappalla 4 ja 55 hintillä omega-kattoon 80 verrattuna, joten budjetti on turvakaide eikä toimiva raja

Vika, joka sai tuen kaiteen tuntumaan tarpeelliselta, ei ollut lainkaan numeerinen. Allekirjoittaminen näytti jumittuvan, epäily osui decomposeiin ja make_hintiin (FIPS 204 algoritmit 36 ja 39), ja todellinen syy oli käänteinen kertymiskohde: vektorin, joka syöttää hint-laskentaa, on kerrytettävä c*t0, kun taas alkuperäisen c*t0:n on säilyttävä koskemattomana normitarkistusta varten. Suuntaa molemmat samaan puskuriin, ja silmukka hylkää ikuisesti täysin oikealla aritmetiikalla. Sekä onnistumisen että budjetin uupumisen poluilla yksikkö nollaa johdetut siemenet, salaiset polynomit, maskit, haasteen ja koodauspuskurit; kutsujan toimittama siemen, salainen avain ja rnd pysyvät kutsujan vastuulla, mikä on oikea jako kirjastolle, joka ei voi tietää, mistä ne merkkijonot tulevat

Missä ML-DSA kohtaa PDF-allekirjoituspinon tänä päivänä?

Ole täsmällinen siitä, mitä on olemassa. PDFlibPas toimittaa ML-DSAn verifioituina allekirjoitusprimitiiveinä plus PKCS #11 -mekanismisidoksena, ei valmiina korvikkeena nykyiselle PAdES-tuotoksellesi. Token-polku on TPDFlibPKCS11Client.SignMLDSA, ja se on tahallaan erillinen sisääntulopiste, koska CKM_ML_DSA kuluttaa raakaviestin valmiiksi lasketun tiivistelmän sijaan, joten olemassa olevia SignHash- ja ulkoisen tiivistelmän takaisinkutsuja ei voi käyttää uudelleen. Sertifikaatiton löytäminen edellyttää CertificateOptionalin eksplisiittistä käyttöönottoa yksityisen avaimen tunnisteen tai ID:n kanssa, ja asiakas validoi CKA_PARAMETER_SETin CKP_ML_DSA_44 / 65 / 87 -valkoluetteloa vastaan yhdistämisen hetkellä, joten oletus-RSA- ja ECDSA-sertifikaattiparitus ei koskaan pääse höllentymään vahingossa

Dokumenttitason integraatio on se osa, jota yhä hallitsee standardityö kirjastokoodin sijaan. ISO 32000-2 §12.8 määrittää allekirjoitussanakirjan ja sen CMS-payloadin, ja ISO/TS 32002 on ajoneuvo, jolla se tuki laajennetaan uudempiin tiiviste- ja allekirjoitusalgoritmeihin; kunnes validoijasi ja vastapuolesi seuraavat, klassinen allekirjoittaminen pysyy tuotantopolulla. Käytännön asenne on rinnakkaiset raiteet: toimita edelleen PAdES B-B:stä B-LTA:aan ulottuvia allekirjoituksia aikaleimalla ja pitkän aikavälin validointidatalla kaikkeen, mikä on kolmannen osapuolen validoituva tänään, ja todista samalla ML-DSA-avainkäsittely ja token-integraatio. Paikallisiin kokeiluihin sama CryptoAPI:lle rakennettu itse allekirjoitettujen sertifikaattien työnkulku antaa allekirjoitusidentiteetin ilman julkista CA:ta

Testaa parametrijoukon muutos niin kuin testaisit minkä tahansa muun allekirjoitusmuutoksen. Ensin mitat, sitten viralliset vektorit, sitten negatiiviset tapaukset: muokattu allekirjoitustavu, ei-yhteensopiva kontekstimerkkijono, katkaistu avain. PDFlibPas kattaa ne kaikki DUnitX-testisarjassaan, ja sama kuri kuuluu omaan putkeesi, ihanteellisesti vaatimustenmukaisuus- ja allekirjoitustyöpöydän rinnalla, joka niputtaa validoinnin dokumenttikorpuksen yli, jotta regressio ei koskaan tavoita asiakasta huomaamatta

Post-kvanttivalmius dokumenttiohjelmistolle ei saavu yhtenä kytkimenä. Se saavuu primitiiveinä, joita voit testata, token-polkuna, jonka voit kytkeä, ja standardiraitana, jota seuraat panostamatta nykyistä julkaisua siihen. Nähdäksesi, miten ML-DSA-yksikkö istuu allekirjoituksen, salauksen ja PDF/A-työkalujen lopun rinnalla natiivissa Object Pascal -koodikannassa, PDFlibPas Delphi PDF library -tuotesivu luetteloi koko komponenttijoukon ja tuettujen kääntäjien matriisin