ينفذ PDFlibPas خوارزمية ML-DSA، وهي خوارزمية التوقيع الرقمي القائمة على الشبكات الوحدوية (Module-Lattice) الموحَّدة في FIPS 204، بالكامل بلغة Object Pascal۔ مجموعات المعاملات الثلاث كلها تُشحن كدوال عادية: MLDSA44Sign وMLDSA65Sign وMLDSA87Sign، إضافة إلى مدخلات KeyGen وVerify مطابقة۔ بلا OpenSSL، وبلا DLL للمنصة، وبلا طبقة لصق من C۔ الوحدة الوحيدة PDFlibMLDSA لا تعتمد إلا على إسفنجة SHAKE في المكتبة، وناتجها يطابق متجهات الاختبار الرسمية المعروفة الإجابة في FIPS 204 بايتا ببايت
تلك الجملة الأخيرة هي الجزء الوحيد الذي تطلب عملا حقيقيا۔ كتابة حساب الشبكات بلغة Pascal ميكانيكية؛ أما جعله يتفق مع NIST فليس كذلك۔ وما يلي هو السرد الهندسي للنقل: كيف انتهت مجموعات المعاملات الثلاث إلى مشاركة محرك واحد، والعيوب المحددة التي فصلت يُترجَم ويعمل عن يطابق KAT۔ إذا كنت تقيّم خيارات ما بعد الكم لخط أنابيب مستندات في Delphi أو C++Builder، فالعيوب هي الجزء المفيد، لأن كلا منها ينتج ناتجا يبدو معقولا ويفشل في التشغيل البيني بصمت
لماذا تكتب مُوقِّعا ما بعد الكم بلغة Object Pascal نقية؟
لأن البديل هو اعتماد أصلي واحد لكل هدف، ومكتبة PDF لـ Delphi لديها من ذلك ما يكفي بالفعل۔ يُبنى PDFlibPas عبر Delphi وC++Builder وFPC/Lazarus على أهداف Win32 وWin64 وUnix؛ وربط مكتبة C لما بعد الكم يعني تتبع بناء لها لكل واحدة من تلك الخانات، إضافة إلى سطح اتفاقية النداء وملكية الذاكرة بينها۔ وحدة Pascal نقية تُترجَم أينما تُترجَم بقية المكتبة، وهذه هي الحجة كاملة
تجعل ML-DSA هذا رخيصا بشكل غير معتاد، لأن اعتمادها البدائي الوحيد هو SHAKE۔ لا طبقة أعداد صحيحة ضخمة، ولا منحنى إهليلجي، ولا مجموعة بصمات منفصلة۔ اكتسب PDFlibPas دالة توسيع XOF تدفقية في الإصدار السابق للنقل مباشرة: TPLShakeXOF في PDFlibDigest، حيث يختار PLShakeXOFInit بين SHAKE128 (المعدل 168) وSHAKE256 (المعدل 136)، يتبعها PLShakeXOFAbsorb وPLShakeXOFFinalize وحلقة PLShakeXOFSqueeze تستمر في التقليب لأي طول إخراج۔ وكل روتين لأخذ العينات بالرفض في وحدة ML-DSA مكتوب مباشرة ضد واجهة النداءات الأربع تلك
محرك واحد، ثلاث مجموعات معاملات: 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 من كتلة SHAKE128 ذات الـ168 بايت، وهي 56 مجموعة لكل كتلة۔ وسيناريو تحقق متقاطع توقف عند الإزاحة 162 أسقط ذيل كل كتلة وزاح بادئة t1 المأخوذة بالعينات من البايت الثالث عشر تقريبا فصاعدا
SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J); // فهرس العمود أولا
Msg[34]:= AnsiChar(I); // ثم فهرس الصف
PLShakeXOFInit(Ctx, True); // SHAKE128، المعدل 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 العام والخاص كاملين متجهات FIPS 204 المعروفة الإجابة۔ وثمة درس تصحيح واحد يستحق التسمية أيضا، لأنه كلف جلسة: عندما تبني تحققا متقاطعا بلغة Python لحلقة رفض مدفوعة بـ XOF، فإن hashlib.shake_128().digest(n) تُرجع البادئة نفسها في كل نداء بدلا من مواصلة التيار۔ خذ الطول كاملا مرة واحدة، ثم قطّعه إلى كتل بحجم المعدل، وإلا فمرجعك سيعيد استهلاك القيم نفسها التي رفضها Pascal عندك بشكل صحيح وهو سعيد
أخذ عينات eta: لماذا تحتاج ML-DSA-65 فرعها الخاص
يُبقي PDFlibPas مسارين منفصلين في expand_s لأن الخوارزمية 33 في FIPS 204 تحدد اثنين فعلا۔ بالنسبة إلى eta = 2، تُرفض كل نِبلة (nibble) عندما تبلغ 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) ومعظم أخطاء إعداد المعاملات تعلن عن نفسها قبل أن تبلغ أي تأكيد تشفيري واحد
التوقيع بلا حلقة غير محدودة
توقيع ML-DSA قائم على الرفض، لذا يعيد المحاولة مع kappa متزايدا حتى تجتاز علامة توقيع مرشحة اختبارات المعيار والتلميحات۔ يضع PDFlibPas سقفا لذلك بميزانية خارجية صريحة قدرها 65535 محاولة؛ وعند النفاد تُرجع MLDSASignInternal False وتترك التوقيع فارغا بدلا من الدوران داخل خيط إنتاج مستندات۔ وفي الممارسة، ينجح متجه ML-DSA-44 الرسمي عند kappa = 4 بـ 55 تلميحا مقابل سقف omega البالغ 80، لذا الميزانية درابزين أمان لا حد تشغيل
الخلل الذي جعل ذلك الدرابزين يبدو ضروريا لم يكن عدديا إطلاقا۔ بدا التوقيع وكأنه يتجمد، ووقع الشك على decompose وmake_hint (الخوارزميتان 36 و39 في FIPS 204)، والسبب الحقيقي كان هدف تراكم معكوسا: المتجه الذي يغذي حساب التلميح يجب أن يتراكم عليه c*t0، بينما يجب أن ينجو c*t0 الأصلي دون مساس لاختبار المعيار۔ وجّه كليهما إلى المخزن نفسه وسترفض الحلقة إلى الأبد بحساب صحيح تماما۔ وفي مساري النجاح ونفاد الميزانية معا، تُصفّر الوحدة البذور المشتقة وكثيرات الحدود السرية والأقنعة والتحدي ومخازن الترميز؛ أما البذرة والمفتاح السري وrnd التي يوفرها المستدعي فتبقى مسؤولية المستدعي، وهو التقسيم الصحيح لمكتبة لا يمكنها معرفة مصدر تلك النصوص
أين تلتقي ML-DSA بمكدس توقيع PDF اليوم؟
كن دقيقا بشأن الموجود۔ يشحن PDFlibPas ML-DSA كبدائيات توقيع متحققة منها إضافة إلى ربط آلية PKCS #11، لا كبديل جاهز للإفلات محل ناتج PAdES الحالي۔ مسار الرمز المميز هو TPDFlibPKCS11Client.SignMLDSA، وهو عمدا نقطة دخول منفصلة لأن CKM_ML_DSA تستهلك الرسالة الخام لا بصمة محسوبة مسبقا، لذا لا يمكن إعادة استخدام نداءات SignHash والبصمة الخارجية القائمة۔ الاكتشاف بلا شهادات يتطلب تمكين CertificateOptional صراحة مع تسمية أو معرّف لمفتاح خاص، ويتحقق العميل من 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 هوية توقيع دون إشراك مرجع مصدق عام
اختبر تغيير مجموعة المعاملات بالطريقة التي تختبر بها أي تغيير توقيع آخر۔ الأحجام أولا، ثم المتجهات الرسمية، ثم الحالات السالبة: بايت توقيع متلاعب به، ونص سياق غير مطابق، ومفتاح مبتور۔ يغطي PDFlibPas كل ذلك في مجموعة DUnitX لديه، والانضباط نفسه ينتمي إلى خط أنابيبك، من الناحية المثالية إلى جانب منضدة الامتثال والتوقيع التي تجمّع التحقق عبر مجموعة مستندات بحيث لا يصل انحدار إلى عميل دون ملاحظة
الجاهزية لما بعد الكم لبرمجيات المستندات لن تأتي كمفتاح واحد۔ تأتي كبدائيات يمكنك اختبارها، ومسار رموز مميزة يمكنك توصيله، ومسار معايير تتبعه دون المراهنة بالإصدار الحالي عليه۔ ولرؤية كيفية تموضع وحدة ML-DSA إلى جانب بقية أدوات التوقيع والتشفير وPDF/A في قاعدة كود Object Pascal أصيلة، تسرد صفحة منتج مكتبة PDFlibPas لـ PDF في Delphi مجموعة المكونات الكاملة ومصفوفة المترجمات المدعومة