PDFlibPas изпълнява ML-DSA, алгоритъма за цифров подпис върху модулни решетки, стандартизиран в FIPS 204, изцяло в Object Pascal. И трите параметърни набора идват като обикновени функции: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, плюс съответните KeyGen и Verify входове. Без OpenSSL, без платформена DLL, без C лепило. Единичната единица PDFlibMLDSA не зависи от нищо друго освен SHAKE гъбата на библиотеката, а изходът ѝ съвпада байт по байт с официалните known-answer тестови вектори на FIPS 204
Последното изречение е единствената част, която отне истинска работа. Писането на решетъчна аритметика в Pascal е механично; да я накараш да съгласува с NIST — не. Следва инженерният разказ на преноса: как трите параметърни набора завършиха с един споделен двигател, и конкретните дефекти, които разделиха компилира и върви от съвпада с KAT. Ако преценявате пост-квантов вариант за документен конвейер на Delphi или C++Builder, дефектите са полезната част, защото всеки от тях ражда правдоподобно изглеждащ изход, който тихо проваля съвместимостта
Защо да пишеш пост-квантов подписващ в чист Object Pascal?
Защото алтернативата е по една естествена зависимост на цел, а една Delphi PDF библиотека вече има достатъчно от тях. PDFlibPas се строи за Delphi, C++Builder и FPC/Lazarus на цели Win32, Win64 и Unix; обвързване с C пост-квантова библиотека би означавало да следиш по едно ѝ строене за всеки от тези слотове, плюс повърхността от призивни конвенции и собственост на паметта между тях. Чиста Pascal единица се компилира навсякъде, където се компилира останалата част от библиотеката, и това е целият аргумент
ML-DSA прави това необикновено евтино, защото единствената му примитивна зависимост е SHAKE. Няма слой за големи цели числа, няма елиптична крива, няма отделен хеш пакет. PDFlibPas получи потоков XOF в изданието непосредствено преди преноса: TPLShakeXOF в PDFlibDigest, където PLShakeXOFInit избира SHAKE128 (rate 168) или SHAKE256 (rate 136), последвано от PLShakeXOFAbsorb, PLShakeXOFFinalize и цикъл PLShakeXOFSqueeze, който продължава да разбърква за произволна дължина на изхода. Всяка рутина с отхвърлящо семплиране в ML-DSA единицата е писана направо върху този API от четири повиквания
Един двигател, три параметърни набора: TMLDSAParams
PDFlibPas описва целия ML-DSA параметърен набор с един запис и го избира по номер на набор, така че ML-DSA-44, 65 и 87 вървят през едни и същи кодови пътища. Първата работеща реализация беше фиксирано 4x4 строене, вградено за ML-DSA-44; обобщаването му означаваше да вдигне k и l, eta, tau, beta, gamma1 и gamma2, omega и дължината на предизвикателството в TMLDSAParams, а после да извежда всичко останало. Публичните входни точки станаха обвивки от три реда
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;
// Извежданите полета се смятат, никога не се преписват от таблица
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;
Петте извеждани полета се смятат, а не се преписват от таблиците на FIPS 204, съвсем нарочно. Ръчно преписаните битови ширини са точно класът константи, които изглеждат верни при ревю и са с единица встрани в продукция, а две от реалните дефекти в този пренос бяха точно такива. Обявените размери остават като именувани константи за валидация: 1312 / 2560 / 2420 байта за публичен ключ, таен ключ и подпис при ML-DSA-44, 1952 / 4032 / 3309 за ML-DSA-65, 2592 / 4896 / 4627 за ML-DSA-87
Къде се проваля най-напред пренос на ML-DSA от нулата?
В expand_a, Алгоритъм 32 на FIPS 204, а начинът на провал е красиво подвеждащ. Матрицата A се семплира, като SHAKE128 се зареди с rho последвано от два индексни байта, така че буферът семе е 34 байта: rho(32), после j, после i. Написани в Pascal с индексиране на AnsiString от 1, тези два байта са Msg[33] и Msg[34]. Първата чернова на този пренос ги записа в Msg[34] и Msg[35], местени с точно един байт, а резултатът беше двойка ключове, чийто rho съвпадаше идеално с тестовия вектор, докато всеки коефициент на t беше грешен. Само матрицата беше замърсена, а матрицата е единственото нещо, което публичният ключ не носи дословно
Още два дефекта живееха в същата рутина. Дължината на поглъщане трябва да е 34, не 35; един излишен боклук байт променя целия изстискан поток. А вътрешният цикъл на отхвърляне трябва да изразходва всяка трибайтова група, която блокът може да даде, включително тази на отместване 165 в 168-байтов SHAKE128 блок, тоест 56 групи на блок. Скрипт за кръстосана проверка, спрял на отместване 162, отхвърляше опашката на всеки блок и изместваше семплирания t1 префикс от около тринадесетия байт нататък
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // първо индексът на колоната
Msg[34]:= AnsiChar(I); // после индексът на реда
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 пази групата на отместване 165: 56 тройки на блок
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;
С тези три поправени, SHA-256 дайджестите на пълните публичен и таен ключ на ML-DSA-44 съвпаднаха с known-answer векторите на FIPS 204. Един урок от дебъгването също си заслужава да се назове, защото струва една сесия: когато строите Python кръстосана проверка за цикъл на отхвърляне, воден от XOF, hashlib.shake_128().digest(n) връща същия префикс при всяко повикване вместо да продължи потока. Вземете цялата дължина веднъж, после я нарежете на блокове с размер rate, иначе вашата референция с удоволствие ще преизразходва точно стойностите, които вашият Pascal е отхвърлил правилно
Семплиране на eta: защо ML-DSA-65 се нуждае от собствен клон
PDFlibPas пази два отделни пътя в expand_s, защото Алгоритъм 33 на FIPS 204 наистина дефинира два. За eta = 2 всеки нибъл се отхвърля, когато стигне 15, и иначе се редуцира mod 5. За eta = 4 нибълът се отхвърля на 9 или повече и после се използва директно, без никакво модулено редуциране. ML-DSA-65 е единственият издаден набор с eta = 4, и преизползването на пътя mod 5 за него размества s1 и s2 от самия първи коефициент, раждайки двойка ключове, вътрешно съгласувана, проверима срещу себе си и напълно несъвместима с това, което друг произвежда
Procedure StoreNibble(Nibble: Byte);
Var
M: Integer;
Centered: Cardinal;
Begin
If Cnt>= N Then
Exit;
If Eta= 4 Then
Begin
If Nibble>= 9 Then // отхвърли, после вземи нибъла какъвто е
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: отхвърли 15, после редуцирай 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;
Размерите са тестът: дължина на c-tilde и битова ширина на gamma1
Два параметъра на кодирането се менят със степента на сигурност по начини, лесни за пропускане, когато работещо ML-DSA-44 строене стои точно там. Хешът предизвикателство c-tilde е 2 x lambda / 8 байта, тоест 32 за ML-DSA-44, 48 за ML-DSA-65 и 64 за ML-DSA-87. Оставен фиксиран на 32, той дава подпис на ML-DSA-65 от 3293 байта вместо стандартните 3309, а KAT префиксът се разминава мигновено. Полето на записа CTildaBytes съществува точно за да не може това число да се забрави
Вторият е ширината на пакетиране на масковия полином z. PDFlibPas я смята като BitWidth(Gamma1), не като степента: gamma1 = 2^19 за ML-DSA-65 и 87 изисква 20 бита на коефициент, не 19, и този единичен бит решава дали всеки полином z заема 640 байта или нещо, което никакъв проверяващ няма да разчете. Проверяващът носеше съответен дефект по време на преноса: буферът за десериализация на z беше оразмерен 192 байта вместо 576. Дължината на подписа е най-евтиният регресионен тест, който някога ще пишете: проверете 2420, 3309 и 4627 срещу Length(Signature) и повечето грешки в параметризацията се обявяват сами, преди да стигнете и до един криптографски assert
Подписване без неограничен цикъл
Подписването с ML-DSA е основано на отхвърляне, така че повтаря с увеличено kappa, докато кандидат подпис не мине проверките си за норма и hint. PDFlibPas го ограничава с изричен външен бюджет от 65535 опита; при изчерпване MLDSASignInternal връща False и оставя подписа празен, вместо да се върти вътре в нишка за производство на документи. На практика официалният вектор ML-DSA-44 успява при kappa = 4 с 55 hint срещу тавана на omega от 80, така че бюджетът е предпазна ограда, а не работещ лимит
Бъгът, който накара тази ограда да се стори необходима, изобщо не беше числов. Подписването изглеждаше заклинило, подозрението падна върху decompose и make_hint (Алгоритми 36 и 39 на FIPS 204), а истинската причина беше обърната цел на акумулиране: векторът, хранещ изчислението на hint, трябва да акумулира c*t0, докато оригиналният c*t0 трябва да оцелее недокоснат за проверката на нормата. Насочете и двете към един буфер и цикълът отхвърля вечно с перфектно правилна аритметика. И по пътя на успеха, и по пътя на изчерпан бюджет единицата занулява извежданите семета, тайни полиноми, маски, предизвикателството и буферите за кодиране; семето, тайният ключ и rnd, подадени от извикващия, остават негова отговорност, което е правилното разделение за библиотека, която не може да знае откъде идват тези низове
Къде ML-DSA се среща с подписовия стек на PDF днес?
Бъдете точни за това какво съществува. PDFlibPas издава ML-DSA като проверени подписови примитиви плюс обвързване към механизъм PKCS #11, а не като замяна директно вместо текущия ви PAdES изход. Токеновият път е TPDFlibPKCS11Client.SignMLDSA и е нарочно отделна входна точка, защото CKM_ML_DSA изразходва суровото съобщение, а не предварително смятан дайджест, така че съществуващите обратни повиквания SignHash и external-digest не могат да се преизползват. Откриването без сертификат изисква CertificateOptional да бъде включено изрично заедно с етикет или ID на частен ключ, а клиентът валидира CKA_PARAMETER_SET срещу белия списък CKP_ML_DSA_44 / 65 / 87 при свързване, така че сдвояването по подразбиране RSA и ECDSA сертификати никога не се разхлабва по невнимание
Интеграцията на ниво документ е частта, все още управлявана от стандартната работа, а не от библиотечен код. ISO 32000-2 §12.8 дефинира речника на подписа и CMS полезния му товар, а ISO/TS 32002 е превозното средство за разширяване на тази поддръжка към по-нови хеш и подписови алгоритми; докато вашите валидатори и контрагенти не последват, класическото подписване остава производственият път. Практическата позиция са успоредни линии: продължавайте да издавате подписи PAdES B-B до B-LTA с времеви печати и дългосрочни валидационни данни за всичко, което трета страна трябва да валидира днес, докато доказвате обработката на ML-DSA ключове и токен интеграцията до тях. За локални експерименти същият работен поток със самоподписан сертификат, изграден върху CryptoAPI ви дава подписващ идентитет, без да намесва публичен CA
Тествайте промяна на параметърния набор както бихте тествали всяка друга подписова промяна. Първо размерите, после официалните вектори, после негативните случаи: подправен байт на подписа, разминаващ се контекстен низ, отрязан ключ. PDFlibPas покрива всички тях в своя DUnitX пакет, а същата дисциплина принадлежи и във вашия конвейер, идеално до работната маса за съответствие и подписване, която групира валидацията върху корпус от документи, така че регресия никога да не стигне до клиент незабелязана
Пост-квантовата готовност за софтуер за документи няма да пристигне като един ключ. Тя пристига като примитиви, които можете да тествате, токенов път, който можете да свържете, и стандартна линия, която следвате, без да заложите текущото издание върху нея. За да видите как ML-DSA единицата седи до останалите инструменти за подписване, шифриране и PDF/A в естествена Object Pascal кодова база, продуктовата страница на PDFlibPas Delphi PDF library изброява пълния набор компоненти и поддържаната компилаторна матрица