PDFlibPas implementeert ML-DSA, het in FIPS 204 gestandaardiseerde Module-Lattice-Based Digital Signature Algorithm, volledig in Object Pascal. Alle drie de parametersets worden geleverd als gewone functies: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, plus bijpassende KeyGen- en Verify-ingangen. Geen OpenSSL, geen platform-DLL, geen C-lijm. De ene unit PDFlibMLDSA hangt van niets anders af dan de SHAKE-sponge van de library, en zijn output matcht de officiële FIPS 204 known-answer-testvectoren byte voor byte
Die laatste zin is het enige deel dat echt werk kostte. Lattice-rekensommen in Pascal schrijven is mechanisch; ze met NIST laten overeenkomen is dat niet. Wat volgt is het technische verslag van de port: hoe de drie parametersets op één gedeelde engine uitkwamen, en de specifieke defecten die compileert en draait scheidden van matcht de KAT. Wie post-quantum-opties afweegt voor een documentpijplijn in Delphi of C++Builder, heeft vooral aan de defecten iets, want elk van hen produceert plausibel ogende output die stilletjes op interoperabiliteit faalt
Waarom een post-quantum-ondertekenaar in zuiver Object Pascal schrijven?
Omdat het alternatief één native afhankelijkheid per doelplatform is, en een Delphi-PDF-library heeft er al genoeg. PDFlibPas bouwt over Delphi, C++Builder en FPC/Lazarus heen op Win32-, Win64- en Unix-doelen; een C-post-quantum-library binden zou betekenen dat u voor elk van die hokjes een build moet bijhouden, plus het calling-convention- en geheugeneigendom-oppervlak ertussen. Een zuivere Pascal-unit compileert overal waar de rest van de library compileert, en dat is het hele argument
ML-DSA maakt dit ongewoon goedkoop, want zijn enige primitieve afhankelijkheid is SHAKE. Er is geen big-integer-laag, geen elliptische kromme, geen aparte hash-suite. PDFlibPas kreeg een streaming XOF in de release vlak vóór de port: TPLShakeXOF in PDFlibDigest, waarin PLShakeXOFInit SHAKE128 (rate 168) of SHAKE256 (rate 136) kiest, gevolgd door PLShakeXOFAbsorb, PLShakeXOFFinalize en een PLShakeXOFSqueeze-lus die blijft permuteren voor willekeurige uitvoerlengte. Elke rejection-sampling-routine in de ML-DSA-unit is rechtstreeks tegen die vier-aanroepen-API geschreven
Eén engine, drie parametersets: TMLDSAParams
PDFlibPas beschrijft een hele ML-DSA-parameterset met één record en kiest hem op setnummer, dus ML-DSA-44, 65 en 87 lopen door dezelfde codepaden. De eerste werkende implementatie was een vaste 4x4-build hard verdraad voor ML-DSA-44; generaliseren betekende k en l, eta, tau, beta, gamma1 en gamma2, omega en de challengelengte tillen naar TMLDSAParams, en daarna alles wat overblijft afleiden. De publieke ingangspunten werden wrappers van drie regels
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;
// Afgeleide velden worden berekend, nooit uit een tabel overgetypt
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;
De vijf afgeleide velden worden bewust berekend in plaats van uit de FIPS 204-tabellen overgeschreven. Met de hand overgetypte bitbreedten zijn precies de klasse constante die er in review goed uitziet en in productie er één naast zit, en twee van de echte defecten in deze port hadden die vorm. De opgegeven groottes blijven als benoemde constanten bestaan voor validatie: 1312 / 2560 / 2420 bytes aan publieke sleutel, geheime sleutel en handtekening voor ML-DSA-44, 1952 / 4032 / 3309 voor ML-DSA-65, 2592 / 4896 / 4627 voor ML-DSA-87
Waar gaat een ML-DSA-port vanaf nul het eerst de mist in?
In expand_a, FIPS 204 Algorithm 32, en de faalmodus is prachtig misleidend. De matrix A wordt bemonsterd door SHAKE128 te zaaien met rho gevolgd door twee indexbytes, dus de seedbuffer is 34 bytes: rho(32), dan j, dan i. Geschreven in Pascal met 1-gebaseerde AnsiString-indexering zijn die twee bytes Msg[33] en Msg[34]. De eerste versie van deze port schreef ze naar Msg[34] en Msg[35], precies één byte verschoven, en het resultaat was een sleutelpaar waarvan rho perfect matchte met de testvector terwijl elke coëfficient van t verkeerd was. Alleen de matrix was vervuild, en de matrix is precies dat ene wat de publieke sleutel niet letterlijk meedraagt
Nog twee defecten woonden in dezelfde routine. De absorblengte moet 34 zijn, niet 35; één extra rommelbyte verandert de hele geperste stroom. En de binnenste weigeringslus moet elke driegroep verbruiken die het block kan leveren, inclusief de groep die op offset 165 van een SHAKE128-block van 168 bytes begint, wat 56 groepen per block is. Een cross-check-script dat op offset 162 stopte verloor de staart van elk block en verschoof de bemonsterde t1-prefix vanaf grofweg de dertiende byte
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // eerst de kolomindex
Msg[34]:= AnsiChar(I); // dan de rijindex
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 houdt de groep op offset 165 vast: 56 drietallen per block
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;
Met die drie gecorrigeerd matchten de SHA-256-digests van de complete ML-DSA-44-publieke en geheime sleutels de FIPS 204-known-answer-vectoren. Nog een debugles is het benoemen waard, want die kostte een sessie: als u een Python-cross-check bouwt voor een door een XOF gedreven weigeringslus, geeft hashlib.shake_128().digest(n) bij elke aanroep dezelfde prefix terug in plaats van de stroom voort te zetten. Neem de hele lengte één keer, snijd hem daarna in rategrote blocks, anders consumeert uw referentie enthousiast opnieuw precies de waarden die uw Pascal terecht had geweigerd
Eta bemonsteren: waarom ML-DSA-65 zijn eigen tak nodig heeft
PDFlibPas houdt twee aparte paden aan in expand_s omdat FIPS 204 Algorithm 33 er werkelijk twee definieert. Voor eta = 2 wordt elke nibble geweigerd zodra hij 15 bereikt en anders gereduceerd mod 5. Voor eta = 4 wordt de nibble geweigerd bij 9 of hoger en daarna direct gebruikt, zonder enige modulaire reductie. ML-DSA-65 is de enige geleverde set met eta = 4, en het hergebruiken van het mod-5-pad verschuift s1 en s2 vanaf de allereerste coëfficient, wat een sleutelpaar oplevert dat intern consistent is, tegen zichzelf verifieert, en met niets matcht dat iemand anders produceert
Procedure StoreNibble(Nibble: Byte);
Var
M: Integer;
Centered: Cardinal;
Begin
If Cnt>= N Then
Exit;
If Eta= 4 Then
Begin
If Nibble>= 9 Then // weigeren, daarna de nibble ongewijzigd overnemen
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: 15 weigeren, daarna mod 5 reduceren
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;
De groottes zijn de test: c-tilde-lengte en de gamma1-bitbreedte
Twee encodingparameters variëren mee met het beveiligingsniveau op manieren die makkelijk worden gemist wanneer er een werkende ML-DSA-44-build klaarstaat. De challenge-hash c-tilde is 2 x lambda / 8 bytes, dus 32 voor ML-DSA-44, 48 voor ML-DSA-65 en 64 voor ML-DSA-87. Hem vast op 32 laten staan levert een ML-DSA-65-handtekening op van 3293 bytes in plaats van de standaard 3309, en de KAT-prefix wijkt meteen af. Het recordveld CTildaBytes bestaat juist zodat dat getal niet vergeten kan worden
De tweede is de verpakkingsbreedte van het maskerpolynoom z. PDFlibPas berekent hem als BitWidth(Gamma1), niet als de exponent: gamma1 = 2^19 voor ML-DSA-65 en 87 vraagt 20 bits per coëfficient, niet 19, en die ene bit bepaalt of elk z-polynoom 640 bytes inneemt of iets dat geen enkele verifier zal parsen. De verifier droeg tijdens de port een spiegelbeeldig defect mee, waarin de z-deserialisatiebuffer op 192 bytes was gemeten in plaats van 576. Handtekeningslengte is de goedkoopste regressietest die u ooit schrijft: beweer 2420, 3309 en 4627 tegen Length(Signature) en de meeste parametrisatiefouten melden zich voordat u ook maar één cryptografische assertie bereikt
Ondertekenen zonder eindloze lus
ML-DSA-ondertekenen is rejection-gebaseerd, dus het probeert opnieuw met een verhoogde kappa tot een kandidaat-handtekening zijn norm- en hinttoetsen doorstaat. PDFlibPas begrenst dat met een expliciet extern budget van 65535 pogingen; bij uitputting geeft MLDSASignInternal False terug en laat hij de handtekening leeg in plaats van binnen een documentproductiethread te blijven ronddraaien. In de praktijk slaagt de officiële ML-DSA-44-vector bij kappa = 4 met 55 hints tegen het omega-plafond van 80, dus het budget is een vangrail in plaats van een werkende limiet
De bug die die vangrail noodzakelijk deed voelen was helemaal niet numeriek. Ondertekenen leek te hangen, het vermoeden viel op decompose en make_hint (FIPS 204 Algorithms 36 en 39), en de echte oorzaak was een omgekeerd accumulatiedoel: de vector die de hintberekening voedt moet c*t0 accumuleren, terwijl de originele c*t0 onaangetast moet overleven voor de normtoets. Richt beide op dezelfde buffer en de lus weigert eeuwig met volmaakt correcte rekenkunde. Op zowel het succespad als het pad met uitgeput budget nult de unit afgeleide seeds, geheime polynomen, maskers, de challenge en de encodingbuffers; de door de aanroeper geleverde seed, geheime sleutel en rnd blijven de verantwoordelijkheid van de aanroeper, wat de juiste verdeling is voor een library die niet kan weten waar die strings vandaan kwamen
Waar raakt ML-DSA vandaag de PDF-handtekeningenstack?
Wees precies over wat er bestaat. PDFlibPas levert ML-DSA als geverifieerde handtekeningsprimitieven plus een PKCS #11-mechanismebinding, niet als drop-in vervanging van uw huidige PAdES-uitvoer. Het tokenpad is TPDFlibPKCS11Client.SignMLDSA, en het is bewust een apart ingangspunt omdat CKM_ML_DSA het ruwe bericht verbruikt in plaats van een vooraf berekende digest, dus de bestaande callbacks SignHash en external-digest kunnen niet worden hergebruikt. Certificaatloze discovery vereist dat CertificateOptional expliciet wordt aangezet samen met een label of ID voor een private key, en de client valideert CKA_PARAMETER_SET bij het verbinden tegen de witte lijst CKP_ML_DSA_44 / 65 / 87, zodat de standaardkoppeling van RSA- en ECDSA-certificaten nooit per ongeluk wordt versoepeld
Integratie op documentniveau is het deel dat nog door standaardiseringswerk wordt bepaald in plaats van door librarycode. ISO 32000-2 §12.8 definieert het handtekeningwoordenboek en zijn CMS-payload, en ISO/TS 32002 is het voertuig om die ondersteuning uit te breiden naar nieuwere hash- en handtekeningalgoritmen; totdat uw validators en tegenpartijen meebewegen blijft klassiek ondertekenen het productiepad. De praktische houding is parallelspoor: blijf PAdES B-B tot B-LTA-handtekeningen met timestamping en long-term-validatiegegevens leveren voor alles wat een derde partij vandaag moet valideren, terwijl u er naast ML-DSA-sleutelafhandeling en tokenintegratie uitproveert. Voor lokale experimenten geeft dezelfde zelfondertekende certificaatworkflow op CryptoAPI-basis u een ondertekenidentiteit zonder een openbare CA erbij te halen
Toets een parametersetswijziging zoals u elke andere ondertekenwijziging zou toetsen. Eerst de groottes, dan de officiële vectoren, dan de negatieve gevallen: een gemanipuleerde handtekeningbyte, een contextstring die niet matcht, een afgekapte sleutel. PDFlibPas dekt ze allemaal in zijn DUnitX-suite, en dezelfde discipline hoort thuis in uw eigen pijplijn, bij voorkeur naast de compliance- en ondertekenwerkbank die validatie batchgewijs over een documentcorpus uitvoert, zodat een regressie een klant nooit ongemerkt bereikt
Post-quantum-gereedheid voor documentsoftware komt niet als één schakelaar. Ze komt als primitieven die u kunt testen, een tokenpad dat u kunt aansluiten, en een standaardenspoor dat u volgt zonder de huidige release erop te verwedden. Om te zien hoe de ML-DSA-unit naast de rest van de signing-, encryptie- en PDF/A-tooling in een native Object Pascal-codebasis past, somt de productpagina PDFlibPas Delphi PDF library de volledige componentset en de ondersteunde compilermatrix op