Το PDFlibPas υλοποιεί το ML-DSA, τον Module-Lattice-Based Digital Signature Algorithm που τυποποιήθηκε στο FIPS 204, εξ ολοκλήρου σε Object Pascal. Και τα τρία parameter sets έρχονται ως συνηθισμένες συναρτήσεις: MLDSA44Sign, MLDSA65Sign, MLDSA87Sign, μαζί με αντίστοιχες εισόδους KeyGen και Verify. Χωρίς OpenSSL, χωρίς DLL πλατφόρμας, χωρίς κόλλα C. Η μοναδιαία unit PDFlibMLDSA δεν εξαρτάται από τίποτα πέρα από το SHAKE sponge της βιβλιοθήκης, και η έξοδός της ταιριάζει με τα επίσημα known-answer test vectors του FIPS 204 byte προς byte
Η τελευταία εκείνη πρόταση είναι το μόνο κομμάτι που θέλησε πραγματική δουλειά. Το να γράψετε αριθμητική πλεγμάτων σε Pascal είναι μηχανικό· το να την κάνετε να συμφωνεί με το NIST όχι. Αυτό που ακολουθεί είναι η μηχανική απολογία της μεταφοράς: πώς τα τρία parameter sets κατέληξαν να μοιράζονται μία μηχανή, και τα συγκεκριμένα ελαττώματα που χώρισαν το μεταγλωττίζεται και τρέχει από το ταυτίζεται με το KAT. Αν αξιολογείτε post-quantum επιλογές για μια γραμμή εγγράφων σε Delphi ή C++Builder, τα ελαττώματα είναι το χρήσιμο κομμάτι, γιατί το καθένα τους παράγει έξοδο που φαίνεται πειστική και σιωπηλά αποτυγχάνει στη διαλειτουργικότητα
Γιατί να γράψετε post-quantum υπογράφοντα σε καθαρό Object Pascal;
Επειδή η εναλλακτική είναι μία εγγενής εξάρτηση ανά στόχο, και μια βιβλιοθήκη PDF για Delphi έχει ήδη αρκετές. Το PDFlibPas χτίζεται σε Delphi, C++Builder και FPC/Lazarus σε στόχους Win32, Win64 και Unix· το binding μιας C βιβλιοθήκης post-quantum θα σήμαινε να παρακολουθείτε ένα build της για καθεμία από αυτές τις θέσεις, συν την επιφάνεια calling-convention και ιδιοκτησίας μνήμης ανάμεσά τους. Μια καθαρή Pascal unit μεταγλωττίζεται όπου μεταγλωττίζεται η υπόλοιπη βιβλιοθήκη, και αυτό είναι όλο το επιχείρημα
Το ML-DSA κάνει αυτό ασυνήθιστα φθηνό, γιατί η μόνη primitive εξάρτησή του είναι το SHAKE. Δεν υπάρχει στρώμα big-integer, καμία ελλειπτική καμπύλη, κανένα ξεχωριστό hash suite. Το PDFlibPas απέκτησε streaming XOF στην έκδοση αμέσως πριν τη μεταφορά: το TPLShakeXOF στο PDFlibDigest, όπου το PLShakeXOFInit επιλέγει SHAKE128 (rate 168) ή SHAKE256 (rate 136), ακολουθούμενο από PLShakeXOFAbsorb, PLShakeXOFFinalize και βρόχο PLShakeXOFSqueeze που συνεχίζει να permute για αυθαίρετο μήκος εξόδου. Κάθε ρουτίνα rejection-sampling στη unit ML-DSA είναι γραμμένη απευθείας πάνω σε αυτό το API τεσσάρων κλήσεων
Μία μηχανή, τρία parameter sets: TMLDSAParams
Το PDFlibPas περιγράφει ολόκληρο parameter set ML-DSA με ένα record και το επιλέγει με αριθμό set, ώστε ML-DSA-44, 65 και 87 να τρέχουν στα ίδια code paths. Η πρώτη δουλεύουσα υλοποίηση ήταν σταθερό build 4x4 σκληροδεμένο για ML-DSA-44· η γενίκευσή της σήμαινε να ανέβουν k και l, eta, tau, beta, gamma1 και gamma2, omega και το μήκος challenge στο TMLDSAParams, και μετά να παραχθεί όλο το υπόλοιπο. Οι δημόσιες είσοδοι έγιναν wrappers τριών γραμμών
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 επί τούτου. Τα χειρόγραφα bit widths είναι ακριβώς η κατηγορία σταθεράς που φαίνεται σωστή στο review και είναι κατά ένα εκτός στην παραγωγή, και δύο από τα πραγματικά ελαττώματα σε αυτή τη μεταφορά ήταν τέτοιου είδους. Τα δηλωμένα μεγέθη μένουν ως επώνυμες σταθερές για επικύρωση: 1312 / 2560 / 2420 bytes δημόσιου κλειδιού, μυστικού κλειδιού και υπογραφής για ML-DSA-44, 1952 / 4032 / 3309 για ML-DSA-65, 2592 / 4896 / 4627 για ML-DSA-87
Πού πάει λάθος πρώτα μια μεταφορά ML-DSA από το μηδέν;
Στο expand_a, FIPS 204 Algorithm 32, και η μορφή αποτυχίας είναι όμορφα παραπλανητική. Ο πίνακας A δειγματοληπτείται με seeding του SHAKE128 από rho ακολουθούμενο από δύο index bytes, οπότε ο seed buffer είναι 34 bytes: rho(32), μετά j, μετά i. Γραμμένο σε Pascal με indexing AnsiString βάσης 1, αυτά τα δύο bytes είναι το Msg[33] και το Msg[34]. Το πρώτο προσχέδιο της μεταφοράς τα έγραψε στο Msg[34] και στο Msg[35], μετατοπισμένα ακριβώς ένα byte, και το αποτέλεσμα ήταν ζεύγος κλειδιών του οποίου το rho ταυτιζόταν τέλεια με το test vector ενώ κάθε συντελεστής του t ήταν λάθος. Μόνο ο πίνακας είχε ρυπανθεί, και ο πίνακας είναι το ένα πράγμα που το δημόσιο κλειδί δεν κουβαλά αυτούσιο
Δύο ακόμα ελαττώματα ζούσαν στην ίδια ρουτίνα. Το μήκος absorb πρέπει να είναι 34, όχι 35· ένα παραπάνω garbage byte αλλάζει ολόκληρη τη squeezed ροή. Και ο εσωτερικός βρόχος απόρριψης πρέπει να καταναλώνει κάθε ομάδα τριών bytes που μπορεί να δώσει το block, συμπεριλαμβανομένης αυτής που ξεκινά στο offset 165 ενός block SHAKE128 των 168 bytes, που είναι 56 ομάδες ανά block. Ένα script cross-check που σταματούσε στο offset 162 έχανε την ουρά κάθε block και μετατοπούσε το δειγματοληπτημένο πρόθεμα t1 από περίπου το δέκατο τρίτο byte και μετά
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 κρατά την ομάδα στο offset 165: 56 τριπλέτες ανά 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;
Με αυτά τα τρία διορθωμένα, οι digest SHA-256 των πλήρων δημόσιων και μυστικών κλειδιών ML-DSA-44 ταυτίστηκαν με τα known-answer vectors του FIPS 204. Ένα μάθημα debugging αξίζει και αυτό να ονομαστεί, γιατί κόστισε μια συνεδρία: όταν χτίζετε cross-check σε Python για βρόχο απόρριψης με XOF, η hashlib.shake_128().digest(n) επιστρέφει το ίδιο πρόθεμα σε κάθε κλήση αντί να συνεχίζει τη ροή. Πάρτε ολόκληρο το μήκος μία φορά, και κόψτε το μετά σε blocks μεγέθους rate, αλλιώς η αναφορά σας θα ξανακαταναλώσει με χαρά τις ίδιες τιμές που το Pascal σας απέρριψε σωστά
Δειγματοληψία eta: γιατί το ML-DSA-65 θέλει δικό του κλάδο
Το PDFlibPas κρατά δύο ξεχωριστές διαδρομές στο expand_s επειδή το FIPS 204 Algorithm 33 ορίζει πράγματι δύο. Για eta = 2 κάθε nibble απορρίπτεται όταν φτάσει 15 και αλλιώς ανάγεται mod 5. Για eta = 4 το nibble απορρίπτεται στα 9 και πάνω και μετά χρησιμοποιείται απευθείας, χωρίς καθόλου modular αναγωγή. Το ML-DSA-65 είναι το μόνο shipped set με 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 // απόρριψη, μετά το nibble ως έχει
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 και bit width του gamma1
Δύο παράμετροι κωδικοποίησης μεταβάλλονται με το επίπεδο ασφάλειας με τρόπους που χάνετε εύκολα όταν ένα δουλεύον build ML-DSA-44 κάθεται δίπλα. Το challenge hash c-tilde είναι 2 x lambda / 8 bytes, δηλαδή 32 για ML-DSA-44, 48 για ML-DSA-65 και 64 για ML-DSA-87. Αν το αφήσετε σταθερό στα 32, βγαίνει υπογραφή ML-DSA-65 των 3293 bytes αντί για το τυπικό 3309, και το πρόθεμα KAT αποκλίνει αμέσως. Το πεδίο record CTildaBytes υπάρχει ακριβώς ώστε ο αριθμός αυτός να μην ξεχνιέται
Το δεύτερο είναι το packing width του mask polynomial z. Το PDFlibPas το υπολογίζει ως BitWidth(Gamma1), όχι ως εκθέτη: gamma1 = 2^19 για ML-DSA-65 και 87 θέλει 20 bits ανά συντελεστή, όχι 19, και αυτό το μοναδικό bit αποφασίζει αν κάθε polynomial z καταλαμβάνει 640 bytes ή κάτι που κανένας verifier δεν θα parse. Ο verifier κουβαλούσε αντίστοιχο ελάττωμα κατά τη μεταφορά, όπου ο buffer deserialization του z μετριόταν 192 bytes αντί για 576. Το μήκος υπογραφής είναι το φθηνότερο regression test που θα γράψετε ποτέ: assert 2420, 3309 και 4627 πάνω στο Length(Signature) και τα περισσότερα λάθη παραμετροποίησης ανακοινώνονται μόνα τους πριν φτάσετε σε έστω έναν κρυπτογραφικό έλεγχο
Υπογραφή χωρίς απεριόριστο βρόχο
Η υπογραφή ML-DSA είναι βασισμένη σε απόρριψη, οπότε ξαναπροσπαθεί με αυξημένο kappa μέχρι μια υποψήφια υπογραφή να περάσει τους ελέγχους norm και hint. Το PDFlibPas το οριοθετεί με ρητό εξωτερικό ανώτατο όριο 65535 προσπαθειών· σε εξάντληση η MLDSASignInternal επιστρέφει False και αφήνει την υπογραφή κενή αντί να στριφογυρίζει μέσα σε thread παραγωγής εγγράφων. Στην πράξη το επίσημο vector ML-DSA-44 πετυχαίνει στο kappa = 4 με 55 hints απέναντι στο ανώτατο omega των 80, οπότε ο προϋπολογισμός είναι ράγα ασφαλείας και όχι λειτουργικό όριο
Το bug που έκανε εκείνη τη ράγα να μοιάζει απαραίτητη δεν ήταν καθόλου αριθμητικό. Η υπογραφή φαινόταν να κρέμεται, η υποψία έπεσε στα decompose και make_hint (FIPS 204 Algorithms 36 και 39), και η πραγματική αιτία ήταν αντιστραμμένος στόχος συσσώρευσης: το vector που τροφοδοτεί τον υπολογισμό hint πρέπει να συσσωρεύει c*t0, ενώ το αρχικό c*t0 πρέπει να επιβιώσει ανέγγιχτο για τον έλεγχο norm. Στοχεύστε και τα δύο στον ίδιο buffer και ο βρόχος απορρίπτει για πάντα με απολύτως σωστή αριθμητική. Και στις δύο διαδρομές, επιτυχίας και εξάντλησης προϋπολογισμού, η unit μηδενίζει τα παράγωγα seeds, τα μυστικά polynomials, τα masks, το challenge και τους buffers κωδικοποίησης· το seed, το μυστικό κλειδί και το rnd που δίνει ο καλών μένουν ευθύνη του καλούντος, που είναι ο σωστός διαχωρισμός για βιβλιοθήκη που δεν μπορεί να ξέρει από πού ήρθαν αυτά τα strings
Πού συναντά το ML-DSA τη στοίβα υπογραφών PDF σήμερα;
Να είστε ακριβείς για το τι υπάρχει. Το PDFlibPas παραδίδει το ML-DSA ως επαληθευμένα signature primitives συν ένα binding μηχανισμού PKCS #11, όχι ως drop-in αντικατάσταση της τρέχουσας εξόδου PAdES. Η διαδρομή token είναι η TPDFlibPKCS11Client.SignMLDSA, και είναι σκόπιμα ξεχωριστή είσοδος επειδή το CKM_ML_DSA καταναλώνει το ακατέργαστο μήνυμα αντί για προϋπολογισμένο digest, οπότε τα υπάρχοντα callbacks SignHash και external-digest δεν μπορούν να ξαναχρησιμοποιηθούν. Η ανακάλυψη χωρίς πιστοποιητικό απαιτεί το CertificateOptional να ενεργοποιηθεί ρητά μαζί με label ή ID ιδιωτικού κλειδιού, και ο client επαληθεύει το CKA_PARAMETER_SET απέναντι στη whitelist CKP_ML_DSA_44 / 65 / 87 τη στιγμή της σύνδεσης, οπότε το default ζευγάρωμα πιστοποιητικών RSA και ECDSA δεν χαλαρώνει ποτέ κατά λάθος
Η ενοποίηση σε επίπεδο εγγράφου είναι το κομμάτι που ακόμα κυβερνάται από εργασία προδιαγραφών και όχι από κώδικα βιβλιοθήκης. Το ISO 32000-2 §12.8 ορίζει το signature dictionary και το CMS payload του, και το ISO/TS 32002 είναι το όχημα για να επεκταθεί εκείνη η υποστήριξη σε νεότερους αλγορίθμους hash και υπογραφής· μέχρι να ακολουθήσουν οι validators και οι αντισυμβαλλόμενοί σας, η κλασική υπογραφή μένει η διαδρομή παραγωγής. Η πρακτική στάση είναι παράλληλες γραμμές: συνεχίστε να παραδίδετε υπογραφές PAdES B-B έως B-LTA με timestamping και δεδομένα μακροχρόνιας επικύρωσης για ό,τι πρέπει να επικυρώσει σήμερα τρίτος, αποδεικνύοντας παράλληλα τον χειρισμό κλειδιών ML-DSA και την ενοποίηση token. Για τοπικά πειράματα, η ίδια ροή αυτοϋπογεγραμμένου πιστοποιητικού πάνω στο CryptoAPI σας δίνει ταυτότητα υπογραφής χωρίς δημόσια CA
Δοκιμάστε μια αλλαγή parameter set όπως θα δοκιμάζατε κάθε άλλη αλλαγή υπογραφής. Πρώτα μεγέθη, μετά τα επίσημα vectors, μετά οι αρνητικές περιπτώσεις: ένα παραποιημένο byte υπογραφής, ένα context string που δεν ταιριάζει, ένα κομμένο κλειδί. Το PDFlibPas καλύπτει όλα αυτά στη σουίτα DUnitX του, και η ίδια πειθαρχία ανήκει και στη δική σας γραμμή, ιδανικά πλάι στο workbench συμμόρφωσης και υπογραφής που ομαδοποιεί επικυρώσεις πάνω σε σώμα εγγράφων ώστε ένα regression να μη φτάνει ποτέ σε πελάτη απαρατήρητο
Η ετοιμότητα post-quantum για λογισμικό εγγράφων δεν θα έρθει ως ένας μοναδικός διακόπτης. Έρχεται ως primitives που μπορείτε να τεστάρετε, μια διαδρομή token που μπορείτε να συνδέσετε, και μια γραμμή προδιαγραφών που ακολουθείτε χωρίς να ποντάρετε την τρέχουσα έκδοση πάνω της. Για να δείτε πώς κάθεται η unit ML-DSA δίπλα στο υπόλοιπο tooling υπογραφής, κρυπτογράφησης και PDF/A σε εγγενή codebase Object Pascal, η σελίδα προϊόντος PDFlibPas Delphi PDF library απαριθμεί το πλήρες σύνολο components και τον υποστηριζόμενο compiler matrix