PDFlibPas الگوریتم امضای دیجیتال ML-DSA یعنی Module-Lattice-Based Digital Signature Algorithm استانداردشده در 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 خالص مینویسیم؟
زیرا جایگزینش یک وابستگی بومی به ازای هر مقصد است، و یک کتابخانه PDF برای Delphi از اینها به اندازه کافی دارد. 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 و سپس مشتق کردن بقیه چیزها. مدخلهای عمومی به wrapperهای سهخطی تبدیل شدند
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 آن دو بایت Msg[33] و Msg[34] میشوند. پیشنویس اول این پورت آنها را در Msg[34] و Msg[35] نوشت، دقیقاً یک بایت جابهجا، و نتیجه یک جفتکلید بود که rhoاش با بردار تست کاملاً مطابقت داشت در حالی که هر ضریب t غلط بود. فقط ماتریس آلوده شده بود، و ماتریس همان چیزی است که کلید عمومی عیناً حمل نمیکند
دو نقص دیگر در همان روتین زندگی میکردند. طول absorb باید 34 باشد نه 35؛ یک بایت زباله اضافی کل جریان فشردهشده را عوض میکند. و حلقه پسزدن داخلی باید هر گروه سهبایتیای را که بلوک میتواند عرضه کند مصرف کند، از جمله گروهی که از آفست 165 بلوک 168 بایتی SHAKE128 شروع میشود؛ یعنی 56 گروه به ازای هر بلوک. یک اسکریپت cross-check که در آفست 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 مطابقت پیدا کرد. یک درس دیباگ هم ارزش نام بردن دارد، چون یک نشست خرج برد: وقتی برای یک حلقه پسزدن XOF-ران یک cross-check پایتونی میسازید، 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 را از نخستین ضریب ناهمراستا میکند؛ نتیجه یک جفتکلید است که درونیاش سازگار، روی خودش verify میشود و با هیچ چیز تولیدشده توسط دیگران مطابقت ندارد
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 را بهجای 3309 استاندارد، 3293 بایت میکند و پیشوند KAT بلافاصله واگرا میشود. فیلد رکورد CTildaBytes دقیقاً برای همین وجود دارد که این عدد فراموش نشود
دومی پهنای بستهبندی چندجملهای ماسک z است. PDFlibPas آن را بهشکل BitWidth(Gamma1) حساب میکند نه بهشکل توان: gamma1 = 2^19 برای ML-DSA-65 و 87 به 20 بیت به ازای هر ضریب نیاز دارد نه 19، و همان تک بیت تعیین میکند که هر چندجملهای z روی هم 640 بایت اشغال میکند یا چیزی که هیچ verifierای تجزیهاش نخواهد کرد. Verifier در طول پورت نقص متناظری داشت: بافر deserialization z بهجای 576 بایت، 192 بایت اندازه گرفته شده بود. طول امضا ارزانترین تست رگرسیونیای است که تا به حال نوشتهاید: 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 منتشر میکند، نه بهشکل جایگزین drop-in برای خروجی 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 و یکپارچهسازی توکن در کنارش. برای آزمایشهای محلی همان گردش کار گواهی self-signed بنا شده روی CryptoAPI بدون دخالت یک CA عمومی به شما هویت امضا میدهد
تغییر مجموعه پارامتر را همانطور تست کنید که هر تغییر امضای دیگری را تست میکنید. اول اندازهها، بعد بردارهای رسمی، بعد حالتهای منفی: یک بایت امضای دستکاریشده، یک رشته context ناهمخوان، یک کلید ناقص. PDFlibPas همه اینها را در مجموعه DUnitX خودش پوشش میدهد و همین نظم جای خودش در خط لوله شماست، ترجیحاً در کنار میز کار انطباق و امضا که اعتبارسنجی را روی یک پیکره سند دستهبندی میکند تا یک رگرسیون هرگز بیخبر به مشتری نرسد
آمادگی پساکوانتومی برای نرمافزار سند بهشکل یک کلید واحد نمیرسد. بهشکل پیشفرضهایی میرسد که میتوانید تستشان کنید، یک مسیر توکن که میتوانید وصلش کنید، و یک مسیر استاندارد که دنبالش میکنید بدون اینکه انتشار فعلی را روی آن شرطبندی کنید. برای دیدن اینکه واحد ML-DSA در کنار بقیه ابزار امضا، رمزنگاری و PDF/A در یک کدبیس بومی Object Pascal چطور مینشیند، صفحه محصول PDFlibPas Delphi PDF library مجموعه کامل کامپوننتها و ماتریس کامپایلرهای پشتیبانیشده را فهرست میکند