PDFlibPas implementerer ML-DSA, Module-Lattice-Based Digital Signature Algorithm standardisert i FIPS 204, helt i Object Pascal. Alle tre parametersettene leveres som vanlige funksjoner: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, pluss tilhørende KeyGen- og Verify-innganger. Ingen OpenSSL, ingen plattform-DLL, ingen C-lim. Den ene uniten PDFlibMLDSA avhenger av ingenting unntatt bibliotekets SHAKE-svamp, og utdataene matcher de offisielle FIPS 204 known-answer-testvektorene byte for byte
Den siste setningen er den eneste delen som tok ekte arbeid. Å skrive lattice-aritmetikk i Pascal er mekanisk; å få den til å stemme med NIST er ikke det. Det som følger er ingeniørberetningen om porteringen: hvordan de tre parametersettene endte opp med å dele én motor, og de konkrete defektene som skilte kompilerer og kjører fra matcher KAT-en. Vurderer du post-kvantum-alternativer for en Delphi- eller C++Builder-dokumentpipeline, er defektene den nyttige delen, for hver av dem produserer plausibel utdata som stille feiler interoperabilitet
Hvorfor skrive en post-kvantum-signer i ren Object Pascal?
Fordi alternativet er én innfødt avhengighet per mål, og et Delphi PDF-bibliotek har allerede nok av dem. PDFlibPas bygges på tvers av Delphi, C++Builder og FPC/Lazarus på Win32-, Win64- og Unix-mål; å binde et C post-kvantum-bibliotek ville bety å vedlikeholde et bygg av det for hver eneste av de plassene, pluss kallekonvensjons- og minneeiendoms-flaten mellom dem. En ren Pascal-unit kompilerer overalt der resten av biblioteket kompilerer, og det er hele argumentet
ML-DSA gjør dette uvanlig billig, fordi den eneste primitive avhengigheten er SHAKE. Intet big-integer-lag, ingen elliptisk kurve, ingen egen hash-samling. PDFlibPas fikk en streaming-XOF i utgivelsen rett før porteringen: TPLShakeXOF i PDFlibDigest, der PLShakeXOFInit velger SHAKE128 (rate 168) eller SHAKE256 (rate 136), etterfulgt av PLShakeXOFAbsorb, PLShakeXOFFinalize og en PLShakeXOFSqueeze-løkke som fortsetter å permutere for vilkårlig utdatalengde. Hver rejection-sampling-rutine i ML-DSA-uniten er skrevet direkte mot det firekalls-API-et
Én motor, tre parametersett: TMLDSAParams
PDFlibPas beskriver et helt ML-DSA-parametersett med én post og velger det etter settnummer, så ML-DSA-44, 65 og 87 går gjennom samme kodebaner. Den første fungerende implementeringen var et fast 4x4-bygg hardkablet for ML-DSA-44; å generalisere det betydde å løfte k og l, eta, tau, beta, gamma1 og gamma2, omega og utfordringslengden inn i TMLDSAParams, og deretter derivere alt annet. De offentlige inngangspunktene ble tre-linjers omslag
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;
// Deriverte felt beregnes, avskrives aldri fra en tabell
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 fem deriverte feltene beregnes med vilje i stedet for å kopieres ut av FIPS 204-tabellene. Håndavskrevne bitbredder er nøyaktig den klassen av konstanter som ser riktige ut i kodegjennomgang og er av med én i produksjon, og to av de virkelige defektene i denne porteringen var av den formen. De deklarerte størrelsene står som navngitte konstanter for validering: 1312 / 2560 / 2420 byte for offentlig nøkkel, hemmelig nøkkel og signatur for ML-DSA-44, 1952 / 4032 / 3309 for ML-DSA-65, 2592 / 4896 / 4627 for ML-DSA-87
Hvor går en ML-DSA-portering fra bunnen av galt først?
I expand_a, FIPS 204 Algorithm 32, og feilmoden er vakkert villedende. Matrisen A samples ved å så SHAKE128 med rho etterfulgt av to indeksbyte, så så-bufferen er 34 byte: rho(32), så j, så i. Skrevet i Pascal med 1-basert AnsiString-indeksering er de to bytene Msg[33] og Msg[34]. Det første utkastet av denne porteringen skrev dem til Msg[34] og Msg[35], forskjøvet med nøyaktig én byte, og resultatet var et nøkkelpar hvis rho matchet testvektoren perfekt mens hver koeffisient av t var feil. Bare matrisen var forurenset, og matrisen er det eneste den offentlige nøkkelen ikke bærer ordrett
To flere defekter bodde i samme rutine. Absorb-lengden må være 34, ikke 35; én ekstra søppelbyte endrer hele den pressede strømmen. Og den indre avvisningsløkken må konsumere hver tre-byte-gruppe blokken kan levere, inkludert den som starter ved offset 165 i en SHAKE128-blokk på 168 byte, som er 56 grupper per blokk. Et krysssjekk-skript som stoppet ved offset 162 droppet halen av hver blokk og forskjøvet det samplede t1-prefikset fra omtrent trettende byte og utover
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // kolonneindeksen først
Msg[34]:= AnsiChar(I); // deretter radindeksen
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 beholder gruppen ved offset 165: 56 tripler per blokk
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;
Med de tre rettet matchet SHA-256-digestene av de komplette ML-DSA-44 offentlige og hemmelige nøklene FIPS 204 known-answer-vektorene. Én feilsøkingslærdom er verdt å nevne også, for den kostet en økt: når du bygger et Python-krysssjekk for en XOF-drevet avvisningsløkke, returnerer hashlib.shake_128().digest(n) samme prefiks ved hvert kall i stedet for å fortsette strømmen. Ta hele lengden én gang, og del den deretter opp i rate-store blokker, ellers gjenkonsumerer referansen i beste velgående nettopp verdiene Pascal-koden korrekt avviste
Sampling av eta: hvorfor ML-DSA-65 trenger sin egen gren
PDFlibPas holder to separate baner i expand_s fordi FIPS 204 Algorithm 33 genuint definerer to. For eta = 2 avvises hver nibble når den når 15, ellers reduseres den mod 5. For eta = 4 avvises nibbleen ved 9 eller over og brukes deretter direkte, uten noen modulær reduksjon i det hele tatt. ML-DSA-65 er det eneste leverte settet med eta = 4, og å gjenbruke mod-5-banen for det feiljusterer s1 og s2 fra den aller første koeffisienten, noe som produserer et nøkkelpar som er internt konsistent, verifiserer mot seg selv og matcher ingenting andre produserer
Procedure StoreNibble(Nibble: Byte);
Var
M: Integer;
Centered: Cardinal;
Begin
If Cnt>= N Then
Exit;
If Eta= 4 Then
Begin
If Nibble>= 9 Then // avvis, bruk deretter nibbleen som den er
Exit;
M:= Nibble;
End
Else
Begin
If Nibble>= 15 Then // eta = 2: avvis 15, reduser deretter 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;
Størrelsene er testen: c-tilde-lengden og gamma1-bitbredden
To kodingsparametre varierer med sikkerhetsnivået på måter som er lette å bomme på når et fungerende ML-DSA-44-bygg ligger der allerede. Utfordringshashen c-tilde er 2 x lambda / 8 byte, altså 32 for ML-DSA-44, 48 for ML-DSA-65 og 64 for ML-DSA-87. Å la den stå fast på 32 gir en ML-DSA-65-signatur på 3293 byte i stedet for standard 3309, og KAT-prefikset divergerer umiddelbart. Postfeltet CTildaBytes finnes presis slik at det tallet ikke kan glemmes
Den andre er pakkebredden til maskepolynomet z. PDFlibPas beregner den som BitWidth(Gamma1), ikke som eksponenten: gamma1 = 2^19 for ML-DSA-65 og 87 trenger 20 bit per koeffisient, ikke 19, og den ene biten avgjør om hvert z-polynom opptar 640 byte eller noe ingen verifikator vil tolke. Verifikatoren bar en tilsvarende defekt under porteringen, der z-deserialiseringsbufferen ble dimensjonert til 192 byte i stedet for 576. Signaturlengden er den billigste regresjonstesten du noen gang skriver: assert 2420, 3309 og 4627 mot Length(Signature), og de fleste parameteriseringsfeil annonserer seg selv før du kommer til et eneste kryptografisk utsagn
Å signere uten en ubegrenset løkke
ML-DSA-signering er avvisningsbasert, så den prøver igjen med økt kappa til en kandidatsignatur består sine norm- og hint-sjekker. PDFlibPas setter et tak på det med et eksplisitt eksternt budsjett på 65535 forsøk; ved utmattelse returnerer MLDSASignInternal False og lar signaturen stå tom i stedet for å spinne inne i en dokumentproduserende tråd. I praksis lykkes den offisielle ML-DSA-44-vektoren ved kappa = 4 med 55 hint mot omega-taket på 80, så budsjettet er en sikringsskinne snarere enn en arbeidsgrense
Feilen som fikk den skinnen til å virke nødvendig, var ikke numerisk i det hele tatt. Signeringen så ut til å henge, mistanken falt på decompose og make_hint (FIPS 204 Algorithms 36 og 39), og den virkelige årsaken var et vendt akkumuleringsmål: vektoren som mater hint-beregningen må akkumulere c*t0, mens den opprinnelige c*t0 må overleve urørt for normsjekken. Sikte begge mot samme buffer, og løkken avviser for alltid med fullstendig korrekt aritmetikk. På både suksess- og budsjettutmattelsesbanene nullstiller uniten deriverte frø, hemmelige polynomer, masker, utfordringen og kodebufferne; frøet, den hemmelige nøkkelen og rnd levert av kalleren forblir kallerens ansvar, som er riktig splitt for et bibliotek som ikke kan vite hvor de strengene kom fra
Hvor møter ML-DSA PDF-signaturstakken i dag?
Vær presis med hva som finnes. PDFlibPas leverer ML-DSA som verifiserte signaturprimitiver pluss en PKCS #11-mekanisme-binding, ikke som en drop-in-erstatning for dagens PAdES-utdata. Token-banen er TPDFlibPKCS11Client.SignMLDSA, og den er bevisst et separat inngangspunkt fordi CKM_ML_DSA konsumerer rå meldingen i stedet for en forhåndsberegnet digest, så de eksisterende SignHash- og ekstern-digest-callbacks kan ikke gjenbrukes. Sertifikatfri oppdagelse krever at CertificateOptional aktiveres eksplisitt sammen med en privatnøkkel-etikett eller -ID, og klienten validerer CKA_PARAMETER_SET mot hvitelisten CKP_ML_DSA_44 / 65 / 87 ved tilkobling, så standard RSA- og ECDSA-sertifikatparing løsnes aldri ved et uhell
Integrasjon på dokumentnivå er delen som fortsatt styres av standardiseringsarbeid snarere enn av bibliotekskode. ISO 32000-2 §12.8 definerer signaturordboken og dens CMS-pakke, og ISO/TS 32002 er kjøretøyet for å utvide den støtten til nyere hash- og signaturalgoritmer; helt til validatorene og motpartene dine følger etter, forblir klassisk signering produksjonsbanen. Den praktiske holdningen er parallelle spor: fortsett å levere PAdES B-B til B-LTA-signaturer med tidsstempling og langsiktige valideringsdata for alt en tredjepart må validere i dag, samtidig som du prøver ut ML-DSA-nøkkelhåndtering og token-integrasjon ved siden av. For lokale eksperimenter gir samme selvsignert sertifikat-arbeidsflyt bygget på CryptoAPI deg en signeringsidentitet uten å involvere en offentlig CA
Test en parametersett-endring slik du ville testet enhver annen signeringsendring. Størrelser først, så de offisielle vektorene, så de negative tilfellene: en manipulert signaturbyte, en feilpassende kontekststreng, en avkortet nøkkel. PDFlibPas dekker alt dette i DUnitX-suiten sin, og samme disiplin hører hjemme i din egen pipeline, ideelt sett ved siden av samsvars- og signeringsbenken som bunkevaliderer på tvers av en dokumentkorpus så en regresjon aldri når en kunde ubemerket
Post-kvantum-beredskap for dokumentprogramvare kommer ikke som én enkelt bryter. Den kommer som primitiver du kan teste, en token-bane du kan koble til, og et standardspor du følger uten å satse gjeldende utgivelse på det. For å se hvordan ML-DSA-uniten ligger ved siden av resten av signerings-, krypterings- og PDF/A-verktøyet i en innfødt Object Pascal-kodebase, lister produktssiden for PDFlibPas Delphi PDF library opp hele komponentsettet og den støttede kompilatormatrisen