PDFlibPas implementuje ML-DSA, algoritmus digitálního podpisu založený na modulových mřížkách standardizovaný ve FIPS 204, celý v Object Pascalu. Všechny tři sady parametrů se dodávají jako obyčejné funkce: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, plus odpovídající vstupy KeyGen a Verify. Žádný OpenSSL, žádná platformová DLL, žádné C lepidlo. Jediná jednotka PDFlibMLDSA nezávisí na ničem jiném než na SHAKE houbě knihovny a její výstup odpovídá oficiálním testovacím vektorům known-answer z FIPS 204 bajt za bajt
Ta poslední věta je jediná část, která stála skutečnou práci. Napsat mřížkovou aritmetiku v Pascalu je mechanika; dostat ji k souladu s NIST není. Co následuje, je inženýrský rozbor portu: jak tři sady parametrů skončily u jednoho sdíleného enginu a jaké konkrétní vady dělaly rozdíl mezi přeloží se a běží a sedí na KAT. Pokud hodnotíte postkvantové volby pro dokumentovou pipeline v Delphi nebo C++Builderu, vady jsou ta užitečná část, protože každá z nich produkuje věrohodně vypadající výstup, který potichu selhává v interoperabilitě
Proč psát postkvantového podepisujícího v čistém Object Pascalu?
Protože alternativa je jedna nativní závislost na každý cíl a PDF knihovna pro Delphi jich už má dost. PDFlibPas staví napříč Delphi, C++Builder a FPC/Lazarus na cílech Win32, Win64 a Unix; vazba na C postkvantovou knihovnu by znamenala sledovat její build pro každý z těch slotů, plus povrch konvencí volání a vlastnictví paměti mezi nimi. Čistá Pascal jednotka se přeloží všude, kde se přeloží zbytek knihovny, a to je celý argument
ML-DSA to činí neobvykle levným, protože jeho jediná primitivní závislost je SHAKE. Není tu vrstva velkých celých čísel, žádná eliptická křivka, žádný samostatný balík hashů. PDFlibPas získal streamovací XOF ve vydání těsně před portem: TPLShakeXOF v PDFlibDigest, kde PLShakeXOFInit volí SHAKE128 (rate 168) nebo SHAKE256 (rate 136), následované PLShakeXOFAbsorb, PLShakeXOFFinalize a smyčkou PLShakeXOFSqueeze, která permutuje pro libovolnou délku výstupu. Každá rutina rejection samplingu v jednotce ML-DSA je psaná přímo proti tomuto čtyřvolánímu API
Jeden engine, tři sady parametrů: TMLDSAParams
PDFlibPas popisuje celou sadu parametrů ML-DSA jedním záznamem a vybírá ji číslem sady, takže ML-DSA-44, 65 a 87 běží stejnými cestami kódu. První funkční implementace byla pevná 4x4 sestava natvrdo zapojená pro ML-DSA-44; zobecnit to znamenalo vyzvednout k a l, eta, tau, beta, gamma1 a gamma2, omega a délku challenge do TMLDSAParams a všechno ostatní z toho odvodit. Veřejné vstupní body se staly třířádkovými obálkami
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;
// Odvozená pole se počítají, nikdy se neopsávají z tabulky
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ět odvozených polí se počítá místo kopírování z tabulek FIPS 204 záměrně. Ručně přepsané bitové šířky jsou přesně třída konstant, které v revizi vypadají správně a v produkci se pletou o jedničku, a dvě skutečné vady tohoto portu byly přesně takového tvaru. Deklarované velikosti zůstávají jako pojmenované konstanty pro validaci: 1312 / 2560 / 2420 bajtů veřejného klíče, soukromého klíče a podpisu pro ML-DSA-44, 1952 / 4032 / 3309 pro ML-DSA-65, 2592 / 4896 / 4627 pro ML-DSA-87
Kde se port ML-DSA od nuly pokazí nejdřív?
V expand_a, FIPS 204 algoritmus 32, a režim selhání je nádherně klamavý. Matice A se vzorkuje osazením SHAKE128 hodnotou rho následovanou dvěma indexovými bajty, takže seed buffer má 34 bajtů: rho(32), pak j, pak i. Zapsané v Pascalu s indexováním AnsiString od jedničky jsou ty dva bajty Msg[33] a Msg[34]. První návrh tohoto portu je zapsal do Msg[34] a Msg[35], posunuté přesně o jeden bajt, a výsledkem byl pár klíčů, jehož rho sedělo na testovacím vektoru dokonale, zatímco každý koeficient t byl špatně. Znečištěná byla jen matice a matice je právě ta věc, kterou veřejný klíč nosí doslova
Dvě další vady žily ve stejné rutině. Délka absorb musí být 34, ne 35; jeden navíc bajt odpadu změní celý vytlačený proud. A vnitřní odmítací smyčka musí spotřebovat každou tříbajtovou skupinu, kterou blok může podat, včetně té začínající na offsetu 165 168bajtového bloku SHAKE128, což je 56 skupin na blok. Křížový kontrolní skript, který skončil na offsetu 162, odepsal ocas každého bloku a posunul vzorkovaný prefix t1 zhruba od třináctého bajtu
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // nejdřív index sloupce
Msg[34]:= AnsiChar(I); // pak index řádku
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 drží skupinu na offsetu 165: 56 trojic 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ěmi třemi opravami seděly SHA-256 digesty kompletních veřejných a soukromých klíčů ML-DSA-44 na vektorech known-answer FIPS 204. Jedna lekcí ladění stojí za pojmenování, protože stála session: když stavíte Python křížovou kontrolu pro odmítací smyčku řízenou XOF, hashlib.shake_128().digest(n) vrací při každém volání stejný prefix místo pokračování v proudu. Vezměte celou délku jednou a pak ji rozkrájejte na bloky velikosti rate, jinak vaše reference ochotně znovu spotřebuje přesně ty hodnoty, které váš Pascal správně odmítl
Vzorkování eta: proč ML-DSA-65 žádá vlastní větev
PDFlibPas drží dvě oddělené cesty v expand_s, protože FIPS 204 algoritmus 33 je skutečně definuje dvě. Pro eta = 2 se každý nibble odmítne, když dosáhne 15, a jinak se redukuje mod 5. Pro eta = 4 se nibble odmítne na 9 a výš a pak se použije přímo, bez jakékoli modulární redukce. ML-DSA-65 je jediná dodávaná sada s eta = 4 a znovupoužití cesty mod 5 pro ni rozsadí s1 a s2 od prvního koeficientu, takže vznikne pár klíčů, který je vnitřně konzistentní, ověří se sám proti sobě a nesedí s ničím, co vyprodukují 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 // odmítnout, pak vzít nibble tak, jak je
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: odmítnout 15, pak redukovat 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 jsou test: délka c-tilde a bitová šířka gamma1
Dva kódovací parametry se mění s bezpečnostní úrovní způsoby, které snadno přehlédnete, když funkční sestava ML-DSA-44 sedí hned vedle. Challenge hash c-tilde má 2 x lambda / 8 bajtů, což je 32 pro ML-DSA-44, 48 pro ML-DSA-65 a 64 pro ML-DSA-87. Nechat ho fixní na 32 dá podpis ML-DSA-65 o 3293 bajtech místo standardních 3309 a prefix KAT diverguje okamžitě. Pole záznamu CTildaBytes existuje přesně proto, aby se na tohle číslo nemohlo zapomenout
Druhým je balicí šířka maskového polynomu z. PDFlibPas ji počítá jako BitWidth(Gamma1), ne jako exponent: gamma1 = 2^19 pro ML-DSA-65 a 87 žádá 20 bitů na koeficient, ne 19, a ten jediný bit rozhoduje, zda každý polynom z zabere 640 bajtů, nebo něco, co žádný verifikátor neparsovat. Verifikátor nesl odpovídající vadu během portu, kde se buffer deserializace z rozměřoval na 192 bajtů místo 576. Délka podpisu je nejlevnější regresní test, jaký kdy napíšete: asertujte 2420, 3309 a 4627 proti Length(Signature) a většina chyb parametrizace se přizná, dřív než dojdete k jediné kryptografické aserci
Podepisování bez neomezené smyčky
Podepisování ML-DSA je odmítací, takže opakuje s inkrementovaným kappa, dokud kandidátní podpis neprojde kontrolou normy a hintů. PDFlibPas to stropí výslovným externím rozpočtem 65535 pokusů; při vyčerpání vrátí MLDSASignInternal False a nechá podpis prázdný místo točení uvnitř vlákna výroby dokumentů. V praxi oficiální vektor ML-DSA-44 uspěje na kappa = 4 s 55 hinty proti stropu omega 80, takže rozpočet je zábradlí, ne pracovní limit
Chyba, kvůli které se to zábradlí zdálo nutné, nebyla vůbec číselná. Podepisování vypadalo jako zatuhnutí, podezření padlo na decompose a make_hint (FIPS 204 algoritmy 36 a 39) a skutečnou příčinou byl obrácený akumulační cíl: vektor krmící výpočet hintů musí akumulovat c*t0, zatímco původní c*t0 musí pro kontrolu normy přežít nedotčený. Namiřte oba na stejný buffer a smyčka odmítá navždy s dokonale správnou aritmetikou. Na cestě úspěchu i vyčerpaného rozpočtu jednotka vynuluje odvozené seed, soukromé polynomy, masky, challenge a kódovací buffery; seed, soukromý klíč a rnd dodané volajícím zůstávají v odpovědnosti volajícího, což je správné rozdělení pro knihovnu, která nemůže vědět, odkud ty řetězce přišly
Kde se ML-DSA dnes setkává se zásobníkem PDF podpisů?
Buďte přesní v tom, co existuje. PDFlibPas dodává ML-DSA jako ověřené kryptografické primitivy plus vazbu na mechanismus PKCS #11, ne jako drop-in náhradu vašeho současného PAdES výstupu. Tokenová cesta je TPDFlibPKCS11Client.SignMLDSA a záměrně je samostatným vstupním bodem, protože CKM_ML_DSA spotřebovává syrovou zprávu místo předpočítaného digestu, takže stávající SignHash a callbacky externího digestu nelze znovu použít. Objev bez certifikátu žádá výslovně povolené CertificateOptional spolu s labelem nebo ID soukromého klíče a klient validuje CKA_PARAMETER_SET proti whitelistu CKP_ML_DSA_44 / 65 / 87 už při připojení, takže výchozí párování certifikátů RSA a ECDSA se nikdy neutrhne náhodou
Integrace na úrovni dokumentu je část stále řízená standardizační prací místo kódu knihovny. ISO 32000-2 §12.8 definuje slovník podpisu a jeho CMS payload a ISO/TS 32002 je vozidlo pro rozšíření této podpory na novější hash a podpisové algoritmy; dokud vaši verifikátoři a protistrany nepodají, klasické podepisování zůstává produkční cestou. Praktický postoj jsou dvě koleje: dál dodávejte podpisy PAdES B-B až B-LTA s časovým razítkem a daty dlouhodobé validace pro cokoli, co dnes musí třetí strana ověřit, a bokem prověřujte manipulaci s klíči ML-DSA a tokenovou integraci. Pro lokální experimenty vám stejný workflow samopodepsaných certifikátů postavený na CryptoAPI dá podpisovou identitu bez zapojení veřejné CA
Testujte změnu sady parametrů tak, jak byste testovali jakoukoli jinou změnu podepisování. Nejdřív velikosti, pak oficiální vektory, pak negativní případy: poškrábaný bajt podpisu, nesedící kontextový řetězec, zkrácený klíč. PDFlibPas pokrývá všechno tohle ve své sadě DUnitX a stejná disciplína patří i do vaší vlastní pipeline, ideálně vedle workbenche shody a podepisování, který dávkově validuje přes korpus dokumentů, takže regrese nikdy nedojde k zákazníkovi nepovšimnutá
Postkvantová připravenost pro dokumentový software nepřijde jako jediný přepínač. Přijde jako primitivy, které můžete testovat, tokenová cesta, kterou můžete propojit, a standardizační kolej, kterou sledujete, aniž byste na ni vsadili aktuální vydání. Kdybyste chtěli vidět, jak jednotka ML-DSA sedí vedle zbytku nástrojů podepisování, šifrování a PDF/A v nativním kódu Object Pascalu, produktová stránka knihovny PDFlibPas Delphi PDF vypisuje celou sadu komponent a podporovanou matici kompilátorů