مقال تقني

SHAKE256 XOF في Delphi: فصل نطاقات Keccak

تنفذ PDFlibPas SHA-3 و SHAKE256 على تبديل Keccak-f[1600] واحد في Object Pascal. تختلف العائلتان في مكانين بالضبط: بايت نطاق pad10*1، وهو $06 لـ SHA-3 و $1F لـ SHAKE بموجب FIPS 202 §6.2 و §6.3، وقاعدة المعدل. تثبّت SHAKE256 معدلها عند 136 بايتاً مهما كان طول المخرج

الفرق الثاني هو الذي يخطئ فيه الناس، لأنه لا يبدو فرقاً أصلاً. تعمل SHA3-256 أيضاً بمعدل 136، لذا فإن SHAKE256 المبنية بنسخ مسار كود SHA-3 تجتاز كل اختبار 32 بايت تقذفه بها ثم تنتج قمامة في أول مرة يطلب فيها أحدهم 64. ما يلي هو كيف انتهى الأمر بالعائلتين إلى مشاركة سجل سياق واحد في PDFlibDigest، وأين تتوقف نقطة الدخول أحادية الاستدعاء، ولماذا كان على XOF المتدفق أن يوجد قبل أن تستطيع Ed448 أو ML-DSA الظهور

لماذا تتشارك SHA-3 و SHAKE256 سياق Keccak واحداً؟

لأن الحالة الوحيدة الخاصة بكل عائلة هي بايت واحد، وتخزنه PDFlibPas في TSHA3Context كـ Suffix. بمجرد أن يعيش فاصل النطاق في السياق بدلاً من ترميزه ثابتاً في روتين الإنهاء، تخدم SHA3AbsorbBlock و SHA3Update و SHA3Final العائلتين معاً دون تغيير، ويوجد في المكتبة نسخة واحدة بالضبط من التبديل ذي 24 جولة. المُهيّئات هي المكان الذي يتباعد فيه المساران، ويتباعدان تماماً

سياق Keccak واحد يخدم عائلتين في PDFlibPas: يخزن TSHA3Context فاصل النطاق في بايت Suffix واحد، ويضبط SHA3Init اللاحقة على $06 مع اشتقاق Rate كـ 200 ناقص ضعف طول المخرج، ويضبط SHAKE256Init اللاحقة $1F مع تثبيت المعدل عند 136 بايتاً، لذا تخدم SHA3AbsorbBlock و SHA3Update و SHA3Final الاثنتين دون تغيير
لأن الحالة الوحيدة الخاصة بكل عائلة هي بايت واحد، تتشارك عائلتا التجزئة نسخة واحدة من التبديل ذي 24 جولة، والمهيّئان هما المكان الوحيد الذي يتباعد فيه المساران
Type
  TSHA3Context= Record
    State: Array [0.. 24] Of UInt64;
    Buffer: Array [0.. 135] Of Byte;
    BufferLength: Integer;
    Rate: Integer;
    OutputLength: Integer;
    // فاصل النطاق: $06 لـ SHA3، $1F لـ SHAKE (FIPS 202 §6.2/6.3)
    Suffix: Byte;
  End;

Procedure SHA3Init(Var Context: TSHA3Context; OutputBits: Integer);
Begin
  FillChar(Context, SizeOf(Context), 0);
  Context.OutputLength:= OutputBits div 8;
  Context.Rate:= 200- 2* Context.OutputLength;   // المعدل يتبع حجم الملخص
  Context.Suffix:= $06;
End;

// SHAKE256: المعدل مثبت عند 136 بايتاً مهما طلب المتصل،
// لأن SHAKE دالة مخرجات قابلة للتمديد
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
  FillChar(Context, SizeOf(Context), 0);
  Context.OutputLength:= OutputBytes;
  Context.Rate:= 136;
  Context.Suffix:= $1F;
End;

تعطي الصيغة في SHA3Init معدلات 136 و 104 و 72 بايتاً لـ SHA3-256 و SHA3-384 و SHA3-512، لذا فإن المعدل ومستوى الأمان ملحومان معاً للعائلة ثابتة الطول. يكسر SHAKE256Init ذلك الرابط عمداً: يصبح OutputLength طلباً للبايتات لا إعلاناً عن قوة أمان، وتبقى سعة الإسفنجة عند 512 بتاً مهما سحبت من المخرجات. ذلك الفصل هو تعريف دالة المخرجات القابلة للتمديد، وكتابته كمهيّئين على سياق واحد يجعل التمييز مرئياً في المصدر لا مدفوناً في تعليق

سقف 136 بايتاً على نقطة الدخول أحادية الاستدعاء

ترفض دوال التيسير في PDFlibPas أي شيء خارج 1..136 بايتاً، وذلك الحد صادق لا اعتباطي. يمرر كل من SHAKE256StreamRange و SHAKE256ByteArray عبر SHA3Final، التي تحشو الكتلة الأخيرة وتُبدّل مرة واحدة وتقرأ المخرج مباشرة من الحالة. أول Rate بايتاً فقط من تلك الحالة مخرجات قانونية، لذا تعصر واحدة تعطي 136 بايتاً على الأكثر؛ وما بعد ذلك تحتاج تبديلاً جديداً لكل كتلة. على أي حال، يقع المستهلكان الحقيقيان داخل النافذة: يهضم ملف تعريف توقيع PDF 32 بايتاً، وتحتاج Ed448 إلى 114 بموجب RFC 8032 §5.2. يأخذ SHAKE256StreamRange نفس الثلاثية FirstLength و SecondStart و SecondLength التي يأخذها SHA256StreamRange و SHA3_256StreamRange، لذا يمشي كود التحقق /ByteRange الخاص بتوقيع بنفس الطريقة لكل خوارزمية ولا يخصّ أبداً دالة XOF

ماذا يحدث في pad10*1 عندما يقع الموضع على معدل ناقص 1؟

لا شيء، بشرط أن تجمع بايتي الحشو بـ XOR في الحالة بدلاً من تعيينهما. يضع FIPS 202 §B.2 لاحقة النطاق عند موضع الامتصاص الحالي و $80 عند آخر بايت من الكتلة. عندما تصادف الرسالة ترك الموضع عند Rate- 1، يستهدف هذان الكتابان نفس البايت ويجب أن يتحدا في $9F. خزّن بدل أن تجمع XOR وسيربح أحدهما، فيكون الحشو خاطئاً بصمت لطول رسالة واحد في 136 بالضبط، وتنجح كل متجهات الانحدار التي تملكها لأن أياً منها لا يقع هناك. يجمع PLShakeXOFFinalize في PDFlibPas الاثنين بـ XOR، ما يجعل حالة التداخل تخرج من الحالة العامة دون فرع

حالة تداخل pad10*1 في PDFlibPas: يضع PLShakeXOFFinalize لاحقة النطاق عند موضع الامتصاص الحالي و $80 عند آخر بايت من الكتلة؛ عندما يقع الموضع على معدل ناقص 1 يستهدف الكتابان بايتاً واحداً، فيدمجهما XOR في $9F بينما كان تخزين عادي سيفسد طول رسالة واحد في 136
التخزين بدل XOR يترك بايت حشو واحداً يفوز، ولأن العيب يظهر لطول رسالة واحد في 136 بالضبط يمكن لكل متجهات الانحدار التي تملكها أن تنجح بينما الحشو خاطئ بصمت
// يحشو الكتلة الجزئية الأخيرة ويدخل مرحلة العصر
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
  If Not Ctx.Absorbing Then
    Exit;
  Ctx.Absorbing:= False;
  // اللاحقة عند SqueezePos، و $80 عند Rate-1؛ عندما يكون SqueezePos = Rate-1
  // يهبط عمليا XOR على نفس البايت ويتحدان بشكل صحيح إلى $9F
  Ctx.State[Ctx.SqueezePos div 8]:= Ctx.State[Ctx.SqueezePos div 8]xor
    (UInt64(Ctx.Suffix)shl (8* (Ctx.SqueezePos and 7)));
  Ctx.State[(Ctx.Rate- 1)div 8]:= Ctx.State[(Ctx.Rate- 1)div 8]xor
    (UInt64($80)shl (8* ((Ctx.Rate- 1)and 7)));
  SHA3Permute(Ctx.State);
  Ctx.SqueezePos:= 0;
End;

TPLShakeXOF: امتصاص مباشرة في الإسفنجة

تكشف PDFlibPas عن المخرجات غير المحدودة عبر سجل منفصل، TPLShakeXOF، يحمل فقط State كـ 25 كلمة UInt64 و Rate و Suffix وعلماً Absorbing و SqueezePos. لا مخزن رسائل: يجمع PLShakeXOFAbsorb البايتات الواردة مباشرة في الحالة ويُبدّل كلما ملأ كتلة، مع خدمة SqueezePos كإزاحة في المرحلتين. يختار PLShakeXOFInit النمط بمنطقي واحد، True لـ SHAKE128 بمعدل 168 و False لـ SHAKE256 بمعدل 136، ويواصل PLShakeXOFSqueeze التبديل حتى يحصل المتصل على عدد البايتات الذي طلبه. فُحص المخرج العابر للكتل عند 200 بايت، أي ما بعد حد الكتلة الأولى في كلا النمطين

SHAKE المتدفق في PDFlibPas عبر TPLShakeXOF، سجل يحمل State و Rate و Suffix وعلماً Absorbing و SqueezePos لكن بلا مخزن رسائل: يختار PLShakeXOFInit SHAKE128 بمعدل 168 أو SHAKE256 بمعدل 136، ويجمع PLShakeXOFAbsorb البايتات مباشرة في الإسفنجة، ويحشو PLShakeXOFFinalize ويُبدّل، ويواصل PLShakeXOFSqueeze التبديل حتى يحصل المتصل على كل بايت
الامتصاص عبر عدة استدعاءات دون تجميع مسبق هو ما يتيح لـ Ed448 سحب 114 بايتاً ومنفذ ML-DSA العصر بلا حدود، تغطية لا تستطيع أي نقطة دخول أحادية الاستدعاء بسقفها 136 بايتاً دعمها
Var
  Ctx: TPLShakeXOF;
  Digest: Array [0.. 113] Of Byte;   // Ed448 تريد 114 بايتاً، RFC 8032 §5.2
Begin
  PLShakeXOFInit(Ctx, False);        // False = SHAKE256، معدل 136
  PLShakeXOFAbsorb(Ctx, @Dom4[1], Length(Dom4));
  PLShakeXOFAbsorb(Ctx, @Prefix[1], Length(Prefix));
  PLShakeXOFAbsorb(Ctx, @Msg[1], Length(Msg));
  PLShakeXOFFinalize(Ctx);
  PLShakeXOFSqueeze(Ctx, @Digest[0], SizeOf(Digest));
End;

// أو، عندما يكفيك مخزن واحد للداخل ومخزن واحد للخارج:
//   PLShakeXOF(False, Input, Output, 114)

الامتصاص عبر عدة استدعاءات أهم مما يبدو. تسبق Ed448 الرسالة بسلسلة dom4 وقيمة من 57 بايتاً قبل التجزئة، وعيّان الرفض يريد تغذية بايتات البذرة والفهرس دون تجميع السلاسل أولاً. كان وصول السجل المتدفق قبل أي من المستهلكين هو النقطة: عمل منحنى Ed448 و Brainpool بـ Pascal النقي يحتاج 114 بايتاً في دفعة واحدة، ومنفذ FIPS 204 ML-DSA يعصر بلا حدود من كلا النمطين. لا يمكن كتابة أي منهما ضد الواجهة أحادية الاستدعاء

لماذا ترفض PDFlibPas كشف SHAKE256 كمحدد ملخص؟

لأنه لا توجد طريقة قانونية لاختياره. يشغّل TPDFlibDigestAlgorithm في PDFlibDigitalSign daAuto و daSHA1 و daSHA256 و daSHA384 و daSHA512 و daSHA3_256 و daSHA3_384 و daSHA3_512، ويتوقف. لا يوجد daSHAKE256، والاستبعاد قرار لا سهو. يسجّل RFC 8702 SHAKE في CMS، لكن داخل توقيع PDF المكان الوحيد الذي يضع فيه ISO/TS 32002 SHAKE256 هو كملخص داخلي لـ Ed448، حيث تختاره خوارزمية التوقيع ولا يختاره المتصل. نشره كقيمة DigestAlgorithm مستقلة سيسمح لأي شخص بإقران SHAKE256 بـ RSA أو ECDSA وإنتاج قاموس توقيع لن يقبله أي مدقق. القدرة تُشحن، والمحدد لا يُشحن، وكود جهة التحقق يستدعي SHAKE256StreamRange فوق تخطيط /ByteRange مباشرة. إذا كنت تختار ملخصاً لتوقيع يجب أن يتحقق منه طرف مقابل اليوم، فتبقى daSHA256 حتى daSHA3_512 داخل سير عمل PAdES B-B حتى B-LTA مع الطابع الزمني وبيانات التحقق طويلة الأمد هي الجواب؛ و ISO/TS 32001 هو ما يحكم متى يصبح SHA-3 روتينياً في PDF أصلاً

اختبار دالة XOF: المتجه الذي يبدو ناجحاً

قارن طول المخرج الكامل، واستخدم أكثر من متجه واحد. نتيجة SHAKE256 للرسالة الفارغة هي التي يحفظها الجميع، وهي اختبار ضعيف تحديداً لأنه مقتبس على نطاق واسع. مخرج SHAKE256 البالغ 32 بايتاً لـ abc أسوأ: أول 16 بايتاً منه، 483366601360a8771c6863080cc4114d، تتطابق مع عدة متجهات مجاورة، لذا يبلّغ المقارنة المبتورة عن النجاح بينما النصف المميز، 8db44530f8f1e1ee4f94ea37e78b5739، يبقى دون فحص. يقارن اختبار الانحدار في PDFlibPas المتجهين على طولهما الكامل. أضف حالة عابرة للكتل أيضاً، لأن حلقة عصر لا تعبر حد معدل أبداً لم تُختبر أصلاً

الدرس الأوسع من طي SHAKE في تنفيذ SHA-3 قائم هو أن الكود المشترك لم يكن الخطر أبداً. Keccak-f[1600] هو نفس التبديل للعائلتين، وإما أن يطابق المتجهات وإما لا. الخطر يعيش في البايتين والعدد الصحيح الواحد اللذين يختلفان، وفي السقف الذي عليك ذكره بصدق بدلاً من ترك المتصلين يكتشفونه. لترى كيف تنسجم طبقة الملخص مع أدوات التوقيع والتشفير و PDF/A في قاعدة أكواد Object Pascal أصلية، صفحة منتج مكتبة PDFlibPas Delphi للـ PDF تسرد مجموعة المكونات الكاملة ومصفوفة المترجمات المدعومة