PDFlibPas implementuje ML-DSA, czyli Module-Lattice-Based Digital Signature Algorithm ustandaryzowany w FIPS 204, w całości w Object Pascal. Wszystkie trzy zestawy parametrów są dostępne jako zwykłe funkcje: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, plus pasujące wejścia KeyGen i Verify. Żadnego OpenSSL, żadnej DLL platformowej, żadnego kleju w C. Pojedyncza jednostka PDFlibMLDSA nie zależy od niczego poza gąbką SHAKE biblioteki, a jej wyjście zgadza się z oficjalnymi wektorami testowymi known-answer z FIPS 204 bajt w bajt
To ostatnie zdanie to jedyna część, która zajęła prawdziwą robotę. Napisanie arytmetyki kratowej w Pascalu jest mechaniczne; doprowadzenie jej do zgody z NIST już nie. Dalej jest inżynierska relacja z portowania: jak trzy zestawy parametrów skończyły na współdzieleniu jednego silnika i jakie konkretnie defekty dzieliły kompiluje się i działa od zgadza się z KAT. Jeśli oceniasz opcje postkwantowe dla potoku dokumentów w Delphi albo C++Builder, defekty są użyteczną częścią, bo każdy z nich daje wynik wyglądający wiarygodnie, który po cichu obywa się na interoperacyjności
Dlaczego pisać postkwantowy podpisujący w czystym Object Pascal?
Bo alternatywą jest jedna natywna zależność na cel, a biblioteka PDF dla Delphi ma takich i tak dość. PDFlibPas buduje się w Delphi, C++Builder i FPC/Lazarus na celach Win32, Win64 i Unix; związanie z postkwantową biblioteką C oznaczałoby śledzenie jej builda dla każdego z tych slotów, plus powierzchnię konwencji wywołań i własności pamięci między nimi. Czysta jednostka Pascal kompiluje się wszędzie, gdzie kompiluje się reszta biblioteki, i to jest cały argument
ML-DSA robi to niezwykle tanio, bo jego jedyna prymitywna zależność to SHAKE. Nie ma warstwy wielkich liczb całkowitych, krzywej eliptycznej ani osobnego zestawu skrótów. PDFlibPas dostał strumieniowy XOF w wydaniu tuż przed portem: TPLShakeXOF w PDFlibDigest, gdzie PLShakeXOFInit wybiera SHAKE128 (rate 168) albo SHAKE256 (rate 136), po czym idą PLShakeXOFAbsorb, PLShakeXOFFinalize i pętla PLShakeXOFSqueeze, która permuuje dalej dla dowolnej długości wyjścia. Każda procedura próbkowania z odrzucaniem w jednostce ML-DSA jest napisana wprost na tym API z czterech wywołań
Jeden silnik, trzy zestawy parametrów: TMLDSAParams
PDFlibPas opisuje cały zestaw parametrów ML-DSA jednym rekordem i wybiera go numerem zestawu, więc ML-DSA-44, 65 i 87 biegną tymi samymi ścieżkami kodu. Pierwsza działająca implementacja była sztywnym buildem 4x4 zahardzowanym pod ML-DSA-44; uogólnienie jej znaczyło podniesienie k i l, eta, tau, beta, gamma1 i gamma2, omega oraz długości wyzwania do TMLDSAParams, a potem wyprowadzenie wszystkiego innego. Publiczne punkty wejścia stały się trzyliniowymi opakowaniami
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;
// Pola pochodne są wyliczane, nigdy przepisywane z tabeli
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;
Pięć pól pochodnych jest wyliczanych, a nie kopiowanych z tabel FIPS 204, i to celowo. Ręcznie przepisywane szerokości bitowe to dokładnie ta klasa stałych, które w przeglądzie wyglądają dobrze, a w produkcji mylą się o jeden, i dwa z realnych defektów tego portu miały właśnie taki kształt. Zadeklarowane rozmiary zostają jako nazwane stałe do walidacji: 1312 / 2560 / 2420 bajtów klucza publicznego, klucza tajnego i podpisu dla ML-DSA-44, 1952 / 4032 / 3309 dla ML-DSA-65, 2592 / 4896 / 4627 dla ML-DSA-87
Gdzie port ML-DSA pisany od zera wali się najpierw?
W expand_a, algorytmie 32 FIPS 204, a tryb awarii jest pięknie mylący. Macierz A jest próbkowana przez zasilanie SHAKE128 rho i dwoma bajtami indeksu, więc bufor ziarna ma 34 bajty: rho(32), potem j, potem i. Zapisane w Pascalu z indeksowaniem AnsiString od 1 te dwa bajty to Msg[33] i Msg[34]. Pierwszy szkic tego portu pisał je do Msg[34] i Msg[35], przesunięte dokładnie o jeden bajt, a wynikiem była para kluczy, której rho zgadzało się z wektorem testowym idealnie, podczas gdy każdy współczynnik t był zły. Zanieczyszczona była tylko macierz, a macierz to jedyna rzecz, której klucz publiczny nie niesie dosłownie
Dwa kolejne defekty mieszkały w tej samej procedurze. Długość absorb musi wynosić 34, nie 35; jeden nadmiarowy śmieciowy bajt zmienia cały wyciskany strumień. A wewnętrzna pętla odrzucania musi skonsumować każdą trójbajtową grupę, jaką blok może dać, włącznie z tą zaczynającą się na offsecie 165 168-bajtowego bloku SHAKE128, czyli 56 grup na blok. Skrypt krzyżowej kontroli, który zatrzymał się na offsecie 162, gubił ogon każdego bloku i przesuwał próbkowany prefiks t1 mniej więcej od trzynastego bajta
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // najpierw indeks kolumny
Msg[34]:= AnsiChar(I); // potem indeks wiersza
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 zatrzymuje grupę na offsecie 165: 56 trójek 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;
Po poprawieniu tych trzech miejsc skróty SHA-256 kompletnych kluczy publicznego i tajnego ML-DSA-44 zgadzały się z wektorami known-answer FIPS 204. Warta nazwania jest też jedna lekcja debugowania, bo kosztowała sesję: gdy budujesz kontrolę krzyżową w Pythonie dla pętli odrzucania napędzanej XOF, hashlib.shake_128().digest(n) zwraca ten sam prefiks przy każdym wywołaniu zamiast kontynuować strumień. Weź całą długość raz, potem potnij ją na bloki wielkości rate, inaczej twoja referencja chętnie skonsumuje ponownie dokładnie te wartości, które twój Pascal słusznie odrzucił
Próbkowanie eta: czemu ML-DSA-65 potrzebuje własnej gałęzi
PDFlibPas trzyma dwie osobne ścieżki w expand_s, bo algorytm 33 FIPS 204 faktycznie definiuje dwie. Dla eta = 2 każdy nibble jest odrzucany, gdy dobije do 15, a poza tym redukowany mod 5. Dla eta = 4 nibble jest odrzucany od 9 wzwyż, a potem używany bezpośrednio, bez żadnej redukcji modularnej. ML-DSA-65 to jedyny dostarczany zestaw z eta = 4, a ponowne użycie ścieżki mod 5 rozjeżdża s1 i s2 od pierwszego współczynnika, dając parę kluczy wewnętrznie spójną, weryfikującą się z samą sobą i niepasującą do niczego, co produkuje ktokolwiek inny
Procedure StoreNibble(Nibble: Byte);
Var
M: Integer;
Centered: Cardinal;
Begin
If Cnt>= N Then
Exit;
If Eta= 4 Then
Begin
If Nibble>= 9 Then // odrzuć, potem weź nibble bez zmian
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: odrzuć 15, potem zredukuj 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;
Rozmiary są testem: długość c-tilde i szerokość bitowa gamma1
Dwa parametry kodowania zmieniają się z poziomem bezpieczeństwa w sposób łatwy do przegapienia, gdy działający build ML-DSA-44 siedzi tuż obok. Skrót wyzwania c-tilde ma 2 x lambda / 8 bajtów, czyli 32 dla ML-DSA-44, 48 dla ML-DSA-65 i 64 dla ML-DSA-87. Zostawienie go sztywno na 32 daje podpis ML-DSA-65 o 3293 bajty zamiast standardowych 3309, a prefiks KAT rozjeżdża się natychmiast. Pole rekordu CTildaBytes istnieje właśnie po to, by tej liczby nie dało się zapomnieć
Drugi to szerokość pakowania wielomianu maski z. PDFlibPas wylicza ją jako BitWidth(Gamma1), a nie jako wykładnik: gamma1 = 2^19 dla ML-DSA-65 i 87 potrzebuje 20 bitów na współczynnik, nie 19, i ten pojedynczy bit rozstrzyga, czy każdy wielomian z zajmuje 640 bajtów, czy coś, czego żaden weryfikator nie sparsuje. Weryfikator niósł w trakcie portu pasujący defekt: bufor deserializacji z był wymiarowany na 192 bajty zamiast 576. Długość podpisu to najtańszy test regresyjny, jaki kiedykolwiek napiszesz: asertuj 2420, 3309 i 4627 na Length(Signature), a większość błędów parametryzacji zgłosi się sama, zanim dojdziesz do jednej kryptograficznej asercji
Podpisywanie bez nieograniczonej pętli
Podpisywanie ML-DSA bazuje na odrzucaniu, więc powtarza próbę z podniesionym kappa, dopóki kandydujący podpis nie przejdzie kontroli normy i hintów. PDFlibPas ogranicza to jawnym zewnętrznym budżetem 65535 prób; przy wyczerpaniu MLDSASignInternal zwraca False i zostawia podpis pusty, zamiast kręcić się w wątku produkcji dokumentów. W praktyce oficjalny wektor ML-DSA-44 odnosi sukces przy kappa = 4 z 55 hintami wobec sufitu omega równego 80, więc budżet to poręcz bezpieczeństwa, a nie roboczy limit
Błąd, który sprawił, że ta poręcz wydawała się konieczna, nie był liczbowy wcale. Podpisywanie pozornie wisiało, podejrzenie padło na decompose i make_hint (algorytmy 36 i 39 FIPS 204), a prawdziwą przyczyną był odwrócony cel akumulacji: wektor zasilający obliczenie hintów musi akumulować c*t0, podczas gdy pierwotne c*t0 musi przetrwać nietknięte do kontroli normy. Wycelowanie obu w ten sam bufor i pętla odrzuca w nieskończoność przy idealnie poprawnej arytmetyce. Na obu ścieżkach, sukcesu i wyczerpania budżetu, jednostka zeruje wyprowadzone ziarna, wielomiany tajne, maski, wyzwanie i bufory kodowania; dostarczone przez wywołującego ziarno, klucz tajny i rnd pozostają odpowiedzialnością wywołującego, co jest właściwym podziałem dla biblioteki, która nie może wiedzieć, skąd te łańcuchy pochodzą
Gdzie ML-DSA spotyka dziś stos podpisów PDF?
Bądź precyzyjny co do tego, co istnieje. PDFlibPas dostarcza ML-DSA jako zweryfikowane prymitywy podpisu plus wiązanie mechanizmu PKCS #11, nie jako gotowy zamiennik twojego obecnego wyjścia PAdES. Ścieżka tokenu to TPDFlibPKCS11Client.SignMLDSA i jest celowo osobnym punktem wejścia, bo CKM_ML_DSA konsumuje surową wiadomość, a nie wcześniej wyliczony skrót, więc istniejących zwrotnych callbacków SignHash i zewnętrznego skrótu nie da się ponownie użyć. Odkrywanie bez certyfikatu wymaga jawnego włączenia CertificateOptional razem z etykietą albo identyfikatorem klucza prywatnego, a klient waliduje CKA_PARAMETER_SET względem białej listy CKP_ML_DSA_44 / 65 / 87 w momencie łączenia, więc domyślne parowanie certyfikatów RSA i ECDSA nigdy nie zostaje rozluźnione przez przypadek
Integracja na poziomie dokumentu to część wciąż rządzona pracą nad standardami, a nie kodem biblioteki. ISO 32000-2 §12.8 definiuje słownik podpisu i jego ładunek CMS, a ISO/TS 32002 to pojazd rozszerzenia tego wsparcia na nowsze algorytmy skrótów i podpisów; dopóki twoje walidatory i kontrahenci nie nadążą, klasyczne podpisywanie pozostaje ścieżką produkcyjną. Praktyczna postawa to tory równoległe: dalej dostarczaj podpisy PAdES B-B do B-LTA ze znacznikami czasu i danymi walidacji długoterminowej dla wszystkiego, co strona trzecia musi dziś zwalidować, przy równoległym sprawdzaniu obsługi kluczy ML-DSA i integracji tokenów. Do lokalnych eksperymentów ten sam przepływ certyfikatu samopodpisanego oparty na CryptoAPI daje tożsamość podpisującą bez angażowania publicznego CA
Zmianę zestawu parametrów testuj tak, jak testowałbyś każdą inną zmianę podpisywania. Najpierw rozmiary, potem oficjalne wektory, potem przypadki negatywne: podmieniony bajt podpisu, niedopasowany łańcuch kontekstu, obcięty klucz. PDFlibPas pokrywa to wszystko w swojej suicie DUnitX, a ta sama dyscyplina należy się w twoim własnym potoku, najlepiej obok warsztatu zgodności i podpisu, który grupowo waliduje korpus dokumentów, żeby regresja nigdy nie dotarła do klienta niezauważona
Gotowość postkwantowa oprogramowania dokumentowego nie przyjdzie jako jeden przełącznik. Przyjdzie jako prymitywy, które możesz testować, ścieżka tokenów, którą możesz poprowadzić, i tor standardów, który śledzisz bez obstawiania obecnego wydania. Żeby zobaczyć, jak jednostka ML-DSA siedzi obok reszty narzędzi podpisu, szyfrowania i PDF/A w natywnej bazie Object Pascal, strona produktowa biblioteki PDFlibPas Delphi PDF wylicza pełny zestaw komponentów i wspieraną macierz kompilatorów