مقال تقني

التحقق من توقيع ECDSA لملفات PDF في Delphi: من DER إلى P1363

قيمة signatureValue الخاصة بـECDSA داخل حاوية CMS هي SEQUENCE { INTEGER r, INTEGER s } بترميز DER. دالة CNG الخاصة بويندوز BCryptVerifySignature لا تقبل أيًا منهما: فهي تريد صيغة IEEE P1363 ثابتة العرض r || s، بلا وسوم tags وبلا أطوال. HotPDF، مكوّن VCL الأصلي لملفات PDF في Delphi وC++Builder، يحوّل بين الصيغتين وفق قواعد DER صارمة قبل استيراد المفتاح

الفشل الذي يمنعه هذا محدد ومحبِط. يفتح Acrobat المستند ويُظهر علامة صح خضراء. أما مدقّقك الخاص، وهو يجتاز البايتات نفسها، فيُعيد غير صالح، أو تُعيد CNG رمز STATUS_INVALID_SIGNATURE دون أي تفسير إضافي. لا خطأ في التوقيع. الخطأ أن نحو سبعين بايتًا من ASN.1 مُرِّرت إلى واجهة برمجية تتوقع أربعة وستين بايتًا من عدد صحيح خام، والتباين غير مرئي ما لم تعرف أن تبحث عنه

لماذا ترفض BCryptVerifySignature توقيع ECDSA صحيحًا؟

لأن طرفي الاستدعاء يتحدثان ترميزي توقيع مختلفين، ولا يُعلن أي منهما عن ذلك. ينص ISO 32000-1 §12.8 على أن قاموس توقيع يحمل كتلة CMS في /Contents؛ وينص RFC 5652 §5.3 على أن signatureValue في كل SignerInfo هو OCTET STRING محتواه أيًا كان ما تُعرّفه خوارزمية التوقيع. بالنسبة لـECDSA ذلك المحتوى هو بنية DER من SEC 1: SEQUENCE يحمل عددين صحيحين INTEGER. طولهما متغيّر بالتصميم، لأن r وs عددان صحيحان و DER تحذف ثُمانيات الصفر البادئة من الأعداد الصحيحة

يتخذ IEEE P1363 الرأي المعاكس. فهو يعرّف التوقيع كسلسلة اتصال concatenation للإحداثيين، محشوًا كل منهما من اليسار بأصفار حتى يبلغ بالضبط عرض بايت حقل المنحنى field. توقيع P-256 دومًا 64 بايتًا. أما ترميز DER للتوقيع نفسه فعادة 70 أو 71 بايتًا ويمكن أن يتراوح بين نحو 8 و72. سلّم صيغة DER إلى BCryptVerifySignature وسيحكم فحص الطول وحده على الاستدعاء بالفشل، ولهذا تُطبّع HotPDF قبل التحقق لا بعده

uses
  HPDFECDSA;

// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

قواعد DER التي يجب ألا يخفّفها محلّل التوقيع

كل رفض مذكور هنا هو رفض تنفّذه HotPDF عمدًا، وكل واحد منها يغلق مسارًا كان محلّل متسامح سيتركه مفتوحًا. الإغراء عند كتابة محوّل هو إيجاد عقدتي INTEGER، ونسخ محتواهما، والمتابعة. هذا ينجح على مدخلات صحيحة التكوين ويقبل بصمت عائلة من إعادات الترميز القابلة للتطويع malleable على مدخلات عدائية. لذا يرفض HPDFECDSANormalizeSignature أي عدد صحيح سالب، أي أي r أو s تكون أول ثُمانية محتوى فيه ذات البت العالي مضبوطًا، لأن العددية الاسمية لـECDSA يجب أن تكون موجبة. ويرفض قيمة تساوي صفرًا بالكامل، إذ r = 0 أو s = 0 ليست أبدًا توقيعًا شرعيًا. ويرفض ثُمانية صفر بادئة زائدة عن الحاجة: يسمح X.690 §8.3 بواحدة بالضبط، وفقط حين تُقرأ الثُمانية التالية سالبة لولا ذلك، فـ00 يتبعه ثُمانية دون 0x80 هي إعادة ترميز، لا توقيع. ويرفض ترويسة طول غير دنيا، لأن X.690 §10.1 يفرض الصيغة المحددة مرمَّزة بأقل عدد من الثُمانيات، وطول من الصيغة الطويلة كان يمكن أن يكون قصيرًا هو سلسلة بايتات مختلفة تحمل المعنى نفسه. ويرفض عددًا صحيحًا أعرض من حجم إحداثية المنحنى، لأن تلك القيمة لا يمكن أن تكون عنصر حقل. ويرفض أي عقدة زائدة بعد s، إلى جانب SEQUENCE خارجي لا يساوي طوله الإجمالي طول الكتلة بأكملها

هذان الأخيران أهم مما يبدوان. البايتات الزائدة بعد SEQUENCE هي الحيلة الكلاسيكية لقابلية تطويع التوقيع signature malleability: ألحق بيانات عشوائية، ولا يزال المدقق المتساهل يقول صالح بينما سلسلة البايتات التي تحقق منها ليست سلسلة البايتات التي وُقّعت. الحدس نفسه يقود تصليب طول ASN.1 الموصوف في الملاحظة عن تحليل PKCS#12، وهو الحدس نفسه هنا. في مسار تحقق، البنية المقبولة التي لم يُصدرها أي موقّع متوافق قط عيب، لا مجاملة

عرض الإحداثية ملك المنحنى، لا التوقيع

تشتق HotPDF عرض المُخرَج من OID المنحنى المسمّى، لا من طول DER الذي حللته للتو. هذا هو النصف الثاني من التحويل والنصف الذي يسهل إخطاؤه بدقة. يعرّف RFC 5480 §2.1.1 المنحنى في معاملات SubjectPublicKeyInfo الخاصة بالشهادة، ويربط HPDFECDSACurveFromOID ثلاثة معرّفات OID تدعمها HotPDF: 1.2.840.10045.3.1.7 لـP-256، و1.3.132.0.34 لـP-384، و1.3.132.0.35 لـP-521. ثم يُعيد HPDFECDSACoordinateSize 32 أو 48 أو 66 بايتًا، ومخزن P1363 ضعف ذلك: 64 أو 96 أو 132. كل عدد صحيح مفكوك الترميز يُحاذى إلى اليمين داخل نصفه، فـr القصير يُحشى بأصفار من اليسار لا يُزاح. P-521 هو ما يوقع الناس في الخطأ، لأن 521 بت يساوي 65.125 بايتًا ويُقرَّب إلى 66، ما يعطي توقيعًا من 132 بايتًا ما كان أي حدس بأس القوة power-of-two ليتوقعه. المفتاح العام يسافر إلى جانبه كنقطة EC غير مضغوطة وفق RFC 5480 §2.2، وهي 0x04 تتبعها X ثم Y، فتتحقق HotPDF أنها بالضبط 1 + 2 * CoordinateSize بايتًا وتبدأ بـ0x04 قبل لمس CNG

var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
  Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);

  case Res of
    evrValid:
      Memo1.Lines.Add('signature verifies');
    evrInvalid:
      Memo1.Lines.Add('signature does not match the digest');
    evrMalformed:
      Memo1.Lines.Add('DER encoding or public point rejected');
    evrUnsupported:
      Memo1.Lines.Add('curve or algorithm not supported here');
    evrProviderUnavailable:
      Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
    evrProviderError:
      Memo1.Lines.Add('CNG returned an unexpected status');
  end;
end;

لاحظ المعامل الأخير. يقبل HPDFECDSAVerifyDigest أيضًا eseP1363 للمستدعين الذين يملكون فعلًا توقيعًا ثابت العرض، من رمز أجهزة أو خدمة توقيع بعيدة تُعيد r || s خامًا. هذا المسار ما يزال يفرض فحص الطول وفحص عدم الصفرية على كلا النصفين، فمخزن مؤقت بالحجم الصحيح مليء بالأصفار يُرفَض بدل تمريره إلى الموفّر

لماذا يفشل الاسم العام لخوارزمية ECDSA على إصدارات ويندوز الأقدم؟

لأن الاسم العام أحدث من قاعدة النشر التي تشحن إليها. تعرض CNG معرّف خوارزمية ECDSA يستنتج المنحنى من المفتاح المستورَد، وهو الطريقة النظيفة لكتابة هذا الكود، لكن BCryptOpenAlgorithmProvider مضمون حلّه فقط على إصدارات ويندوز الأحدث. على جهاز أقدم يفشل استدعاء الفتح، ويبقى مقبض provider handle فارغًا nil، ويُبلغ كل تحقق ECDSA في تطبيقك عن عدم دعم لتوقيع سليم تمامًا. تتجنب HotPDF هذا الجرف بفتح المعرّفات الخاصة بكل منحنى بدلًا من ذلك. فهي تحلّ ECDSA_P256 وECDSA_P384 وECDSA_P521 مرة واحدة، وتخزّن مقبض موفّر واحد لكل منحنى مؤقتًا، وتغلقها عند إنهاء الوحدة unit finalization. بعدها كل تحقق ينفّذ العمل الرخيص فقط: استيراد مفتاح عام مؤقت من ECCPUBLICBLOB، استدعاء BCryptVerifySignature، إتلاف المفتاح. لا LoadLibrary متكررة، ولا GetProcAddress متكررة، ولا فتح وإغلاق موفّر لكل توقيع. التحقق الدفعي batch لبضع مئات من المستندات يشعر بالفارق، وكذلك عملية خدمة service process كانت ستستهلك مقابض موفّرين تحت الحمل لولا ذلك

تبقى رموز النتيجة صادقة بشأن التمييز. evrProviderUnavailable تعني أن الجهاز لم يستطع منح HotPDF موفّرًا؛ وevrInvalid تعني أن CNG أجابت بـSTATUS_INVALID_SIGNATURE. دمج هذين في فشل واحد هو كيف تُبلَّغ مشكلة نشر خطأً على أنها مستند مزوَّر. الفصل نفسه بين فشل البيئة والفشل التشفيري يجري عبر معالجة CNG وCAPI في جانب التوقيع، المشروحة في المقال عن التوقيع من مخزن الشهادات وترتيب البايتات

أي شهادة وقّعت هذا؟ SignerIdentifier شيئان مختلفان

يجعل RFC 5652 §5.3 من SignerIdentifier نوع CHOICE، والمدقق الذي يتعامل مع فرع واحد فقط سيتحقق بصمت من مفتاح خاطئ. الفرع الأول هو issuerAndSerialNumber، وهو SEQUENCE يحمل اسم المُصدِر Issuer بترميز DER خام والرقم التسلسلي INTEGER، ومطابقته مقارنة بايتات مقابل كل شهادة في مجموعة certificates الخاصة بـCMS. الفرع الثاني هو [0] subjectKeyIdentifier، وهو OCTET STRING موسوم ضمنيًا، ومطابقته تتطلب التنقيب داخل الشهادة لا مقارنة حقول ترويستها

هذا التنقيب فيه طبقة تفاجئ الناس. معرّف المفتاح يعيش في امتداد X.509v3، فتجتاز HotPDF حقل الامتدادات [3] الخاص بـtbsCertificate، وتجد الامتداد الذي معرّفه OID هو 2.5.29.14، وتتخطى القيمة المنطقية BOOLEAN الاختيارية للحرجية، وتأخذ OCTET STRING الخاص بـextnValue. تلك السلسلة الثُمانية ليست المعرّف. فوفق RFC 5280 §4.2.1.2 محتواها نفسه DER، ونوع KeyIdentifier هو OCTET STRING آخر، فتُحلِّل مرة ثانية للوصول إلى البايتات الفعلية. توقف طبقة واحدة مبكرًا وستقارن غلافًا wrapper من 22 بايتًا بمعرّف من 20 بايتًا، فلا تطابق أي شهادة أبدًا، ويسقط المدقق إلى أي منطق ابتدائي heuristic كتبته بعد ذلك، وهذا هو الخطر الحقيقي. أخذ الشهادة الأولى في المجموعة اختصار مُغرٍ وهو خاطئ كلما حملت CMS سلسلة شهادات، وهذا معظم الوقت، لأن الورقة leaf ليست مُلزَمة بأن تأتي أولًا. لا تقبل HotPDF شهادة غير مطابَقة إلا حين تحمل الحاوية شهادة واحدة بالضبط؛ ومع وجود شهادات متعددة، تكون المطابقة الدقيقة لـSignerIdentifier إلزامية. التحقق من ملخّص digest مقابل مفتاح عام لسلطة إصدار وسيطة intermediate CA لا ينتج خطأ ودودًا، بل ينتج غير صالح واثقًا على مستند سليم تمامًا

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('signed.pdf') > 0 then
      for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
        if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
          Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
            [String(Info.FieldName), String(Info.PublicKeyAlgorithm),
             String(Info.CurveName), String(Info.HashAlgorithm),
             String(Info.SignerName)]))
        else
          Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
  finally
    Pdf.Free;
  end;
end;

THPDFSignatureInfo.CurveName يُبلغ عن P-256 أو P-384 أو P-521 بحيث يسجّل سجل تدقيق أي منحنى استُخدم فعلًا لا كلمة ECDSA فقط. الأنابيب على مستوى المستند حول هذا الاستدعاء، وتحديدًا كيف تُحسَب أجزاء /ByteRange ولماذا يجب حساب الملخّص digest فوق الملف لا فوق شجرة الكائنات المحلَّلة، هو موضوع المقال المرافق عن التحقق من توقيعات PDF الرقمية

ما الذي لا يمنحك إياه هذا

نتيجة خضراء من HPDFECDSAVerifyDigest تجيب عن سؤال واحد فقط: هذه البايتات وقّعها المفتاح الخاص المطابق لهذا المفتاح العام. لا تقول شيئًا عمّا إذا كان ذلك المفتاح ملكًا لأحد ينبغي أن تثق به. بناء سلسلة الثقة chain building إلى مرساة ثقة، والإبطال عبر CRL أو OCSP، وفحوص السياسة كلها عمل منفصل، وأي منتج يُبلّغ عن توقيع صالح دون هذه العناصر يُبلّغ بأقل مما يفترضه المستخدم. تُعرَض تواريخ صلاحية الشهادة بشكل منفصل في THPDFSignatureInfo لهذا السبب بالتحديد: قد يتحقق توقيع تشفيريًا بينما انتهت صلاحية الشهادة التي أنشأته منذ عامين. دعم المنحنيات ضيّق عمدًا أيضًا. تُعالَج ثلاثة منحنيات أولية من NIST، وأي توقيع فوق أي منحنى آخر يُعيد غير مدعوم بدل تخمين. مسار CNG مقتصر على ويندوز فقط، وهي مقايضة صحيحة لمكوّن VCL لكنها تستحق الذكر قبل التخطيط لخدمة عابرة للمنصات حولها. والصرامة غير قابلة للتهيئة: لا يوجد وضع متساهل يقبل طول DER غير دنيا لأن موقّعًا قديمًا أصدر واحدًا. إن صادفت ملفًا كهذا في الإنتاج، فالاستجابة الصادقة هي تسجيله ومطاردة الجهة المُصدرة، لا توسيع المحلّل حتى يجتاز الملف

مسار التحقق من ECDSA الموصوف هنا يُشحن ضمن HotPDF Component القياسي لـ Delphi وC++Builder، إلى جانب مساري RSA PKCS#1 v1.5 وRSA-PSS وسجل معلومات التوقيع الكامل؛ وتحمل صفحة المنتج المرجع الكامل للتوقيع الرقمي