PDFlibPas įgyvendina ML-DSA — FIPS 204 standartizuotą Module-Lattice-Based Digital Signature Algorithm — visiškai Object Pascal kalba. Visi trys parametrų rinkiniai tiekiami kaip paprastos funkcijos: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, plius atitinkami KeyGen ir Verify įėjimai. Jokio OpenSSL, jokios platformos DLL, jokių C klijų. Vienintelis modulis PDFlibMLDSA nepriklauso nuo nieko, išskyrus bibliotekos SHAKE kempinę, ir jo išvestis baitas į baitą sutampa su oficialiais FIPS 204 known-answer testų vektoriais
Ta paskutinė frazė yra vienintelė dalis, pareikalavusi tikro darbo. Tinklų aritmetikos rašymas Pascal kalba yra mechaniškas; jos sutaikymas su NIST — ne. Toliau eina porto inžinerinė atskaita: kaip trys parametrų rinkiniai galiausiai pradėjo dalintis vienu varikliu, ir konkrečios klaidos, kurios skyrė sukompiliuoja ir veikia nuo atitinka KAT. Jei vertinate postkvantinius variantus Delphi arba C++Builder dokumentų grandynei, klaidos yra naudingiausia dalis, nes kiekviena iš jų duoda įtikinamai atrodančią išvestį, tyliai žlugdančią sąsają su kitais
Kodėl rašyti postkvantinį pasirašytoją gryna Object Pascal?
Nes alternatyva yra po vieną natyvią priklausomybę kiekvienam tikslui, o Delphi PDF biblioteka jau turi jų per daug. PDFlibPas statomas Delphi, C++Builder ir FPC/Lazarus aplinkose Win32, Win64 ir Unix tikslams; C postkvantinės bibliotekos susiejimas reikštų jos statymo sekimą kiekvienam iš tų vietų, plius iškvietimo konvencijų ir atminties nuosavybės paviršių tarp jų. Grynas Pascal modulis kompiliuojasi ten, kur kompiliuojasi visa likusi biblioteka, ir tai visas argumentas
ML-DSA tai daro neįprastai pigu, nes vienintelė jo primityvi priklausomybė yra SHAKE. Nėra didelių sveikųjų skaičių sluoksnio, nėra elipsinės kreivės, nėra atskiros maišos komplekto. PDFlibPas gavo srautinį XOF iškart prieš portą pasirodžiusiame leidime: TPLShakeXOF modulyje PDFlibDigest, kur PLShakeXOFInit pasirenka SHAKE128 (rate 168) arba SHAKE256 (rate 136), po to seka PLShakeXOFAbsorb, PLShakeXOFFinalize ir PLShakeXOFSqueeze ciklas, permaišantis tiek, kiek reikia savavalės išvesties ilgiui. Kiekviena atmetimo atrankos rutina ML-DSA modulyje parašyta tiesiogiai prieš tą keturių kvietimų API
Vienas variklis, trys parametrų rinkiniai: TMLDSAParams
PDFlibPas aprašo visą ML-DSA parametrų rinkinį vienu įrašu ir pasirenka jį pagal rinkinio numerį, todėl ML-DSA-44, 65 ir 87 eina tais pačiais kodo keliais. Pirmas veikiantis įgyvendinimas buvo fiksuotas 4x4 statymas, kietai sujungtas su ML-DSA-44; jo apibendrinimas reiškė k ir l, eta, tau, beta, gamma1 ir gamma2, omega bei iššūkio ilgį perkėlimą į TMLDSAParams, o viską kitą — išvedimą. Viešieji įėjimo taškai tapo trijų eilučių įvyniojais
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;
// Išvestiniai laukai skaičiuojami, niekada nenukopijuojami iš lentelės
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;
Penki išvestiniai laukai skaičiuojami, o ne nukopijuojami iš FIPS 204 lentelių, sąmoningai. Rankomis perkelti bitų pločiai yra būtent ta konstantų klasė, kuri peržiūroje atrodo teisingai, o produkcijoje klysta vienetu, ir dvi iš tikrųjų šio porto klaidų buvo tokios formos. Deklaruoti dydžiai lieka kaip pavadintos konstantos patikrai: 1312 / 2560 / 2420 baitų viešajam raktui, privačiajam raktui ir parašui ML-DSA-44, 1952 / 4032 / 3309 ML-DSA-65, 2592 / 4896 / 4627 ML-DSA-87
Kur nuo nulio rašomas ML-DSA portas pirmiausia suklysta?
expand_a viduje, FIPS 204 32 algoritme, ir gedimo forma nuostabiai apgaulinga. Matrica A imta, sėjant SHAKE128 su rho, po kurio seka du indeksų baitai, todėl sėklos buferis yra 34 baitai: rho(32), tada j, tada i. Parašyta Pascal kalba su 1-pradžios AnsiString indeksacija tie du baitai yra Msg[33] ir Msg[34]. Pirmasis šio porto juodraštis juos rašė į Msg[34] ir Msg[35], paslinktus lygiai vienu baitu, ir rezultatas buvo raktų pora, kurios rho tobulai sutapo su testo vektoriumi, kol visi t koeficientai buvo neteisingi. Užteršta buvo tik matrica, o matrica yra vienintelis dalykas, kurio viešasis raktas neperneša žodis į žodį
Dar dvi klaidos gyveno toje pačioje rutinoje. Absorb ilgis turi būti 34, o ne 35; vienas papildomas šiukšlinis baitas pakeičia visą išspaustą srautą. Ir vidinis atmetimo ciklas turi suvartoti kiekvieną trijų baitų grupę, kurią blokas gali duoti, įskaitant tą, kuri prasideda 168 baitų SHAKE128 bloko poslinkyje 165 — tai 56 grupės blokui. Kryžminės patikros scenarijus, sustojęs poslinkyje 162, numetė kiekvieno bloko uodegą ir paslinko imtą t1 priešdėlį maždaug nuo tryliktų baito
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // stulpelio indeksas pirmas
Msg[34]:= AnsiChar(I); // tada eilutės indeksas
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 išlaiko grupę poslinkyje 165: 56 triplės blokui
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;
Ištaisius tuos tris, visų ML-DSA-44 viešojo ir privataus rakto SHA-256 maišos sutapo su FIPS 204 known-answer vektoriais. Viena derinimo pamoka taip pat verta įvardinti, nes kainavo visą sesiją: kai statote Python kryžminę patikrą XOF valdomam atmetimo ciklui, hashlib.shake_128().digest(n) grąžina tą patį priešdėlį kiekvieną kartą, vietoj to, kad tęstų srautą. Paimkite visą ilgį vieną kartą, tada supjaustykite į rate dydžio blokus, kitaip jūsų etalonas noriai suvartos iš naujo būtent tas reikšmes, kurias jūsų Pascal teisingai atmetė
eta atranka: kodėl ML-DSA-65 reikalauja savo šakos
PDFlibPas laiko du atskirus kelius expand_s viduje, nes FIPS 204 33 algoritmas tikrai apibrėžia du. Eta = 2 atveju kiekvienas nibble atmetamas, kai pasiekia 15, ir kitu atveju redukuojamas mod 5. Eta = 4 atveju nibble atmetamas nuo 9 į viršų, o tada naudojamas tiesiogiai, be jokios modulinės redukcijos. ML-DSA-65 yra vienintelis tiekiamas rinkinys su eta = 4, ir mod 5 kelio pakartotinis naudojimas jam išderina s1 ir s2 nuo pat pirmojo koeficiento, duodamas raktų porą, kuri viduje nuosekli, patvirtina pati save ir nesutampa su niekuo, ką gamina kas nors kitas
Procedure StoreNibble(Nibble: Byte);
Var
M: Integer;
Centered: Cardinal;
Begin
If Cnt>= N Then
Exit;
If Eta= 4 Then
Begin
If Nibble>= 9 Then // atmesti, tada imti nibble tokį, koks yra
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: atmesti 15, tada redukuoti 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;
Dydžiai yra testas: c-tilde ilgis ir gamma1 bitų plotis
Du kodavimo parametrai kinta su saugumo lygiu taip, kad lengva nepastebėti, kai šalia stovi veikiantis ML-DSA-44 statymas. Iššūkio maiša c-tilde yra 2 x lambda / 8 baitų, tai 32 ML-DSA-44, 48 ML-DSA-65 ir 64 ML-DSA-87. Palikus ją fiksuotą 32, gaunamas ML-DSA-65 parašas 3293 baitų vietoje standartinių 3309, ir KAT priešdėlis iškart išsiskiria. Įrašo laukas CTildaBytes egzistuoja būtent tam, kad tas skaičius nebūtų užmirštas
Antrasis yra kaukės daugianario z pakavimo plotis. PDFlibPas jį skaičiuoja kaip BitWidth(Gamma1), o ne kaip eksponentą: gamma1 = 2^19 ML-DSA-65 ir 87 reikalauja 20 bitų koeficientui, o ne 19, ir tas vienas bitas nusprendžia, ar kiekvienas z daugianaris užima 640 baitų, ar kažką, ko joks patikrintojas neišanalizuos. Patikrintojas porto metu nešė atitinkamą klaidą, kur z deserializacijos buferis buvo 192 baitų vietoje 576. Parašo ilgis yra pigiausias regresijos testas, kokį kada parašysite: tikrinkite 2420, 3309 ir 4627 prieš Length(Signature), ir dauguma parametrizavimo klaidų apie save praneš, kol dar nesate pasiekę nė vienos kriptografinės teiginio patikros
Pasirašymas be neriboto ciklo
ML-DSA pasirašymas grindžiamas atmetimu, todėl jis kartojasi su padidintu kappa, kol kandidatas parašas praeina normos ir hint patikras. PDFlibPas tai riboja atvira išorine 65535 bandymų kvota; išsekus MLDSASignInternal grąžina False ir palieka parašą tuščią, vietoj to, kad suktųsi dokumentų gamybos gijoje. Praktikoje oficialus ML-DSA-44 vektorius sėkmingas ties kappa = 4 su 55 hint prieš omega lubas 80, todėl kvota yra saugos turėklas, o ne veikianti riba
Klaida, dėl kurios tas turėklas pasirodė būtinas, visai nebuvo skaitinė. Pasirašymas rodėsi stringantis, įtarimas krito ant decompose ir make_hint (FIPS 204 36 ir 39 algoritmai), o tikroji priežastis buvo apsuktas kaupimo tikslas: vektorius, maitinantis hint skaičiavimą, turi kaupti c*t0, kol originalus c*t0 turi išlikti nepaliestas normos patikrai. Nukreipkite abu į tą patį buferį, ir ciklas atmeta amžinai su tobulai teisinga aritmetika. Tiek sėkmės, tiek kvotos išsekimo keliuose modulis užnulina išvestines sėklas, slaptus daugianarius, kaukes, iššūkį ir kodavimo buferius; kvietėjo pateikta sėkla, privatus raktas ir rnd lieka kvietėjo atsakomybe — teisingas padalijimas bibliotekai, negalinčiai žinoti, iš kur tos eilutės atkeliavo
Kur ML-DSA šiandien susitinka su PDF parašų krūva?
Būkite tikslūs dėl to, kas egzistuoja. PDFlibPas tiekia ML-DSA kaip patikrintus parašo primitivus plius PKCS #11 mechanizmo susiejimą, o ne kaip paruoštą pakeitimą jūsų dabartinei PAdES išvesčiai. Tokeno kelias yra TPDFlibPKCS11Client.SignMLDSA, ir jis sąmoningai atskiras įėjimo taškas, nes CKM_ML_DSA vartoja žalią pranešimą, o ne iš anksto apskaičiuotą maišą, todėl esami SignHash ir išorinės maišos atgaliniai kvietimai negali būti pakartotinai panaudoti. Be liudijimų veikiantis aptikimas reikalauja CertificateOptional atvirai įjungti kartu su privačiojo rakto etikete arba ID, o klientas prieš jungdamasis patikrina CKA_PARAMETER_SET prieš CKP_ML_DSA_44 / 65 / 87 baltąjį sąrašą, todėl numatytasis RSA ir ECDSA liudijimų susiejimas niekada nenusilpnėja atsitiktinai
Dokumentų lygio integracija yra dalis, kol kas valdoma standartų darbo, o ne bibliotekos kodo. ISO 32000-2 §12.8 apibrėžia parašo žodyną ir jo CMS turinį, o ISO/TS 32002 yra priemonė tą palaikymą išplėsti naujesnėms maišos ir parašų algoritmams; kol jūsų patikrintojai ir partneriai to neseką, klasikinis pasirašymas lieka produkcijos keliu. Praktinė pozicija — lygiagretūs keliai: toliau tiekite PAdES B-B iki B-LTA parašus su laiko žymėjimu ir ilgalaikio tikrinimo duomenimis viskam, ką trečioji šalis šiandien privalo patikrinti, o ML-DSA rakto apdorojimą ir tokeno integraciją įrodinėkite šalia. Vietiniams eksperimentams ta pati savarankiškai pasirašytų liudijimų seka, sukurta ant CryptoAPI, duoda pasirašymo tapatybę be viešosios CA
Parametrų rinkinio pakeitimą tikrinkite taip, kaip tikrintumėte bet kokį kitą pasirašymo pakeitimą. Pirmiausia dydžiai, tada oficialūs vektoriai, tada neigiami atvejai: pakeistas parašo baitas, nesutampanti konteksto eilutė, nukirptas raktas. PDFlibPas dengia visus juos savo DUnitX rinkinyje, ir ta pati drausmė priklauso jūsų pačių grandynei, idealiu atveju šalia atitikties ir pasirašymo darbo aplinkos, tikrinančios dokumentų korpusą partijomis, kad regresija niekada nepasiektų kliento nepastebėta
Postkvantinis pasirengimas dokumentų programinei įrangai neateis kaip vienas jungiklis. Jis ateis kaip primitivai, kuriuos galite testuoti, tokeno kelias, kurį galite prijungti, ir standartų kelias, kurį sekate nestatydami ant jo dabartinio leidimo. Norėdami pamatyti, kaip ML-DSA modulis guli šalia kitos pasirašymo, šifravimo ir PDF/A įrankių natyvioje Object Pascal kodų bazėje, PDFlibPas Delphi PDF library produkto puslapyje išvardytas visas komponentų rinkinys ir palaikomų kompiliatorių matrica