تنفذ PDFlibPas، مكتبة PDF لـ Delphi و C++Builder، حزمة التشفير التجاري الصينية في Object Pascal خالص: تجزئة SM3 (GB/T 32905) وتشفير كتل SM4 (GB/T 32907) وتواقيع المنحنيات الإهليلجية SM2 (GB/T 32918). وبهما توقّع ملفات PDF بتواقيع CMS من نوع SM2-with-SM3 عبر SetSignProcessSM2Identity وتشفر المستندات بمرشح تشفير SM4 عبر Encrypt بقوة 6، دون أي مزود من نظام التشغيل أو DLL خارجي
السبب المعتاد للرغبة في هذا هو متطلب شراء لا فضول. من المتوقع بصورة متزايدة أن تستخدم الأنظمة المباعة للحكومة الصينية والقطاع المالي والرعاية الصحية الخوارزميات الوطنية، و Windows CryptoAPI و CNG لا توقعان بـ SM2 ولا تجزئان بـ SM3. إن مر مسار التوقيع عبر المنصة فالجواب ببساطة لا. يمنحك التنفيذ الباسكالي الخالص الجواب نفسه على Win32 و Win64 وأهداف FPC، ويمنحه دون تبعية أصلية متحركة تحت أنبوب معالجة المستندات
لماذا بُنيت حزمة GB/T في Object Pascal خالص؟
تبني PDFlibPas خوارزميات SM2 و SM3 و SM4 في باسكال لأنه لا يوجد مزود من المنصات التي تستهدفها أصلاً قادر على إنجاز العمل. لا تدعم CryptoAPI و CNG تواقيع SM3، فقبل وجود هوية SM2 كانت المكتبة ترفض التوقيع عمداً عندما تضبط خوارزمية التجزئة على SM3: فالبديل كان SignerInfo يعلن معرف OID لتجزئة واحدة ويحمل توقيعاً على شيء آخر، وهو توقيع خاطئ صامت لا خطأ ظاهر
وللخوارزميات الثلاث ترتيب بناء طبيعي اتبعته المكتبة. جاءت SM3 أولاً، لأن SM2 تحتاجها لبصمة الهوية ZA ولبصمة الرسالة. وجاءت SM4 ثانية بوصفها وحدة مستقلة. وجاءت SM2 أخيراً، أولاً كبدائيات خام تحققت مقابل منحنى المثال المنشور في ملحق المواصفة، ثم كهوية توقيع موصولة بباني CMS نفسه الذي يعالج أصلاً Ed25519 و Ed448 و ML-DSA. ويوصف النهج نفسه في منحنيات Ed448 و Brainpool ECDSA في باسكال خالص وفي موقّع ما بعد الكم FIPS 204 ML-DSA، وتوصل SM2 إلى ذلك الإطار كنوع مفاتيح إضافي
البدائيات دوال عامة، فيمكنك استخدامها خارج توقيع PDF. تعيد SM3String و SM3ByteArray في PDFlibDigest البصمة الخام بطول 32 بايت (بايت واحد لكل محرف لا hex)، وتصدّر PDFlibSM4 مساعدات ECB و CBC بنكهتين: نسخ محاذاة للكتل تعيد سلسلة فارغة عند طول خاطئ، ونسخ PKCS#7 مثل SM4EncryptCBCPKCS7 لبيانات بطول اعتباطي
uses
SysUtils, PDFlibDigest, PDFlibSM4;
function RawToHex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2);
end;
var
Digest, Key, IV, Cipher, Plain: AnsiString;
begin
// مثال GB/T 32905: SM3("abc")
Digest := SM3String('abc');
Assert(LowerCase(RawToHex(Digest)) =
'66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0');
// تحتاج SM4 مفتاحاً من 16 بايت؛ وتحتاج CBC متجه تهيئة من 16 بايت
Key := 'SixteenByteKey!!';
IV := 'InitVector16byte';
Cipher := SM4EncryptCBCPKCS7(Key, IV, 'Invoice 2026-0042 approved');
Plain := SM4DecryptCBCPKCS7(Key, IV, Cipher);
Assert(Plain = 'Invoice 2026-0042 approved');
end;
وعُدّ تلك السلاسل ذات 16 بايت يدوياً على مسؤوليتك. سلسلتا اختبار في مجموعة اختبارات SM4 كان من المفترض أن يكون طولهما 16 بايتاً فتبيّن أنه 17، ولأن المساعدات المحاذاة للكتل تعيد سلسلة فارغة عند طول خاطئ عن قصد، ظهر الفشل بوصفه تأكيداً (assertion) محيراً لا استثناء
توقيع ملف PDF بهوية SM2
تستخدم عملية توقيع في PDFlibPas مفتاح SM2 بمجرد أن يرفق SetSignProcessSM2Identity أو SetSignProcessSM2IdentityFromFile شهادة ومفتاحاً خاصاً بها. الشهادة بصيغة DER، ويكون المفتاح الخاص عدداً خاماً من 32 بايت أو حاوية PKCS#8 غير مشفرة أو ECPrivateKey وفق SEC 1. إرفاق هوية SM2 يمسح أي هوية Ed25519 أو Ed448 أو ML-DSA أو Brainpool على العملية نفسها ويطفئ التوقيع من مخزن الشهادات و CNG، فعملية توقيع واحدة لا تحمل مفتاحين متنافسين أبداً
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('contract.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Signature1');
if Pdf.SetSignProcessSM2IdentityFromFile(SignId,
'signer-sm2.cer', 'signer-sm2.key') <> 1 then
raise Exception.Create('SM2 certificate or key file missing');
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Shanghai', '[email protected]');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 8); // 8 = SM3
Pdf.EndSignProcessToFile(SignId, 'contract-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('SM2 signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
لاحظ ما تخبرك به القيمة المعادة من SetSignProcessSM2IdentityFromFile وما لا تخبرك به. العودة بـ 1 تعني أن كلا الملفين قُرئا وخُزنا على عملية التوقيع. أما الفحص الحقيقي، اشتقاق المفتاح العام من الـ scalar الخاص ومقارنته بالشهادة، فيجري عند تطبيق التوقيع، فزوج مفاتيح غير متطابق يظهر بوصفه GetSignProcessResult لا تساوي 1، لا عند استدعاء الهوية
الرمز 8 في SetSignProcessDigestAlgorithm يختار SM3، ومع هوية SM2 يكون إلزامياً فعلياً: باني CMS يفرض SM3 لمفتاح SM2 بغض النظر عما طلبته العملية، بالطريقة نفسها التي يُفرض بها مفتاح Ed25519 على SHA-512. والتركيبة العكسية محجوبة أيضاً. مع adbe.pkcs7.detached أو ETSI.CAdES.detached، تخرج عملية توقيع مضبوطة على SM3 بلا هوية SM2 دون توقيع، لأنه لا يوجد موقّع آخر في المكتبة يستطيع إنتاج توقيع قائم على SM3 يطابق الـ OID المعلن
ما يدخل في SignerInfo من نوع SM2-with-SM3؟
يحسب توقيع SM2 في PDFlibPas على الخصائص الموقعة المرمزة DER، وهي بنية CMS القياسية، لكن SM2 تضيف خطوة لا تملكها ECDSA. تجزئ GB/T 32918.2 الرسالة مع ZA، وهي بصمة هوية معرفة بـ ZA = SM3(ENTL || ID || a || b || Gx || Gy || xA || yA)، حيث يكون كل جزء من المنحنى قيمة big-endian بطول 32 بايت و ID هو المعرف المميز للموقّع. ويكون التوقيع عندها r = (e + x1) mod n و s = ((1 + d)-1(k - r·d)) mod n، بقيمة nonce جديدة k من مولد CSPRNG للمكتبة وإعادة محاولة عند الجولات المنحلة
تستخدم PDFlibPas المعرف المميز الافتراضي من المواصفة، السلسلة ذات 16 محرفاً 1234567812345678، على جانبي التوقيع والتحقق. لا توجد API لتغييره. هذا يطابق الغالبية الساحقة من شهادات SM2 المنتشرة، لكن إذا وقّع أحد الأطراف بمعرف مخصص اختلفت قيم ZA ولن ينجح التحقق من التوقيع، ولو كان كل بايت آخر صحيحاً
يدخل الزوج (r, s) إلى CMS بوصفه SEQUENCE بصيغة DER من قيمتين INTEGER، التخطيط الذي تستخدمه كل SignerInfo من عائلة DSA. تكتب PDFlibPas معرفات الخوارزميات المسجلة في GM/T 0006: 1.2.156.10197.1.301 لمفتاح SM2 و 1.2.156.10197.1.401 لـ SM3 و 1.2.156.10197.1.501 لـ SM2-with-SM3. وطلبات الأختام الزمنية RFC 3161 المنشأة بـ SM3 (SetTimeStampOptions تجزئة 8) تحمل معرف SM3 نفسه. أما الإصدارات قبل 3.589.1 فقد كتبت فرع 1.2.156.101.1 غير مسجل؛ وما يزال التحقق يقبله، فالمستندات الموقعة بتلك الإصدارات تظل تتحقق داخل PDFlibPas، لكن مدققات الأطراف الثالثة لن تعرفها، وهذه سبب وجيه لإعادة التوقيع لأي شيء يغادر أنبوبك الخاص
أين انكسر التحقق من SM2: بايت إشارة DER وحمل ضائع
ذهب معظم جهد الهندسة في كود SM2 إلى عيّبين كانا ينتجان مخرجات تبدو معقولة. كلاهما نمطي في التشفير ذي الأعداد الكبيرة المكتوب يدوياً، وكلاهما رخيص التجنب بمجرد معرفتهما
الأول كان بايت إشارة في DER. قيم INTEGER في ASN.1 موقعة، فيسبق المرمّز بشكل صحيح أي r أو s يكون بتها العلوي مفعلاً بـ 0x00 فيصبح الطول 33 بايتاً. حوّل المدقق كل عدد إلى عرض ثابت 32 بايتاً دون إزالة ذلك البايت أولاً. ونحو نصف قيم r و s لها البت العلوي مفعّل، ففشلت نسبة كبيرة من المستندات الموقعة حديثاً في التحقق. صادف أول تشغيل استقصائي قيماً قصيرة فنجح؛ أما مجموعة Delphi Win32، وهي توقع بقيم nonce عشوائية، فكشفت المشكلة فوراً. يحذف المدقق الآن صفراً بادئاً من أي عدد ذي 33 بايتاً قبل الحشو إلى 32 بايتاً، ويرفض ما هو أطول
الثاني كان حمالاً ضائعاً في جمع 256 بتاً. عناصر الحقل ثمانية أطراف UInt32 بطرفية صغيرة (little-endian)، وكُتب الجمع النمطي "اجمع، ثم اطرح p إذا لم تكن النتيجة دون p". وللمدخلات المخفضة يكون المجموع دون 2p لكنه قد يتجاوز 2256 مع ذلك، ومتى تجاوزه كان الحمل المسقوط يطوي القيمة عائدة إلى البتات الـ 256 الدنيا، حيث تقول المقارنة مع p إن كل شيء سليم. كانت مضاعفة نقطة بإحداثية كبيرة تسير خطأ؛ والمضاعفات الصغيرة لا. وكان العرَض الظاهر أن 1G حتى 8G طابقت المرجع بينما 9G و 10G و 11G لم تطابق، وعُثر على الإصلاح بمقارنة مدخلات ومخرجات جمع عددها 1280 مع قيم محسوبة باستقلال وحفر في أول زوج متباعد
الجمع المصحح في PDFlibSM2 يحفظ الحمل المعاد من جمع الأطراف الخام ويطرح p مرة واحدة عند تفعّل الحمل أو كون النتيجة ليست دون p؛ وطرح واحد يكفي لأن p تتجاوز 2255. ينطبق النمط على أي حقل أولي يقع بقليل فوق قوة اثنين. إن كان مساعد الجمع عندك يرمي حمله الأخير، فاختبر المضاعفات إلى ما بعد النقطة التي تعبر فيها المضاعفة حد الكلمة أول مرة، لا 1G و 2G و 3G فحسب
لماذا أخطأت SM3 مرتين قبل أن تصيب
فشل أول تنفيذ لـ SM3 في كل متجه اختبار قياسي، وكذلك فعل تنفيذ مرجعي مستقل كتب إلى جانبه. تنفيذان يتفقان معاً ويعارضان المعيار يشيران إلى فهم مشترك خاطئ للمواصفة لا إلى خطأ نسخ. وكشفت مقارنة الصيغ سطراً بسطر مع نص SM3 المنشور ثلاثة مواضع كُتب فيها الكود من الذاكرة
- توسيع الرسالة يطبق الدوران ذا 7 بتات على W[j-13]؛ أما W[j-6] فحامل XOR عادي
- يجمع SS1 بين rotl(A, 12) و E وثابت الجولة المدوّر بجمع نمطي على 232 لا بـ XOR
- في الجولات من 16 إلى 63، تكون FF دالة الأغلبية (X AND Y) OR (X AND Z) OR (Y AND Z)؛ أما دالة الاختيار بنمط SHA فتخص GG
بعد تلك التصحيحات الثلاثة طابقت السلسلة الفارغة و abc والمتجه ذو الكتلتين بطول 64 بايتاً. ودخلت SM4 من أول محاولة، لأن صندوق S ومتجهات الاختبار أُخذا من نص المواصفة قبل كتابة أي كود: مفتاح الجولة rk0 = F12186F9، والمتجه ذو الكتلة الواحدة 681EDF34D206965E86B3E94F536E4246، ومتجه المليون تكرار 595298C7C6FD271F0402F804C33D3F66. وأضاف عمل SM2 قاعدة شقيقة: الثوابت الست عشرية الطويلة تولّد بالتجميع وتفحص طولاً، ولا تكتب يداً أبداً، بعد أن أرسلت إحداثية نقطة أساس منسوخة يدوياً ناقصة خانة واحدة جلسة تنقيح تطارد أخطاء تنفيذ غير موجودة أصلاً
تشفير ملف PDF بمرشح التشفير SM4
يشفر TPDFlib.Encrypt بقوة 6 المستند المحدد بـ SM4-128 عبر إطار مرشحات التشفير في ISO 32000-1 §7.6.5. يستخدم قاموس التشفير معالج الأمان القياسي مع /V 4 و /R 4 و /Length 128، ويحمل مرشح التشفير /StdCF القيمة /CFM /SM4. والحفظ بقوة 6 يرفع الملف إلى PDF 1.6 على الأقل، ويبلّغ GetEncryptionLevel بالقيمة 7 لمثل هذا الملف، ويحصل تشفير المفتاح العام على الخيار نفسه عبر EncryptPublicKey بقوة 4
var
Pdf: TPDFlib;
Perm: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('cannot load PDF');
// الطباعة والنسخ وتعبئة النماذج مسموحة؛ لا تحرير ولا تجميع
Perm := Pdf.EncodePermissions(1, 1, 0, 0, 1, 1, 0, 1);
if Pdf.Encrypt('owner-secret', 'reader-pass', 6, Perm) <> 1 then
raise Exception.Create('SM4 encryption failed');
Pdf.SaveToFile('report-sm4.pdf');
finally
Pdf.Free;
end;
end;
يُشفر كل سلسلة وتدفق بوصفه متجه تهيئة IV عشوائياً من 16 بايتاً يليه نص SM4-CBC المشفر بحشو PKCS#7، التخطيط نفسه الذي يستخدمه AESV2. ويتبع مفتاح كل كائن الخوارزمية 1 من ISO 32000-1 §7.6.2: مفتاح الملف، وثلاثة بايتات دنيا من رقم الكائن وبايتان من رقم التوليد، مع لاحقة sAlT، تُجزأ بـ MD5 وتقتطع إلى 16 بايتاً. واشتقاق المفتاح من كلمة المرور هو ذاك الخاص بالمراجعة 4، فقوة 6 ترث حدوده: كلمات المرور لا تعالج بـ SASLprep (فقط قوى AES-256 وهي 3 و4 و5 تفعل)، واشتقاق المفتاح ليس أقوى من ذاك الخاص بـ AESV2
حدود التواصل البيني ينبغي معرفتها قبل النشر
الخلاصة الصادقة أن توقيع SM2 يتعامل بينياً ضمن حدود معرفات OID والمعرف الافتراضي الموصوفين أعلاه، بينما لا يتعامل تشفير SM4 إلا مع برامج تعرف الاصطلاح نفسه. لا /SM4 ضمن قيم /CFM المعرفة في الجدول 25 من ISO 32000-1 أو في ISO 32000-2، وهي تعرف None و V2 و AESV2 و AESV3. قارئ PDF رئيسي يصطدم بـ /CFM /SM4 لن يفك تشفير الملف. وتقرأ PDFlibPas نفسها /SM4 والصيغة /SM4V1 عبر معالج أمانها، فالأنبوب المغلق حيث تكتب PDFlibPas وتقرأ يعمل؛ أما ملف للعموم فينبغي أن يستخدم AES-256 بدلاً من ذلك
أما التواقيع فالحد هو تخطيط الشهادة والمدقق في الطرف الآخر. تقبل PLValidateSM2Identity، وهي الفحص خلف عملية التوقيع، تخطيط GM/T 0015 حيث يحمل SubjectPublicKeyInfo id-ecPublicKey مع منحنى SM2 1.2.156.10197.1.301 بوصفه معامله، وكذلك الشهادات التي تضع معرف SM2 في حقل الخوارزمية نفسه، وترفض أي شيء لا تطابق نقطته العامة المشتقة الـ scalar الخاص. وقبل الاعتماد على تواقيع SM2 في الإنتاج، وقّع مستنداً بشهادة من CA ستستخدمه فعلاً وتحقق منه بالمدقق الذي يشغله طرفك المقابل. ويمكنك إجراء فحص الهوية بنفسك قبل بدء عملية توقيع:
uses
SysUtils, PDFlibModernSign;
var
CertDER, KeyDER, Scalar: AnsiString;
begin
// حمّل CertDER و KeyDER من القرص أو من تصدير HSM
// احتفظ بمعامل Out في متغير خاص به: تمرير KeyDER بوصفه
// المدخل Const والمخرج Out معاً سيمسح المدخل أولاً
if not PLValidateSM2Identity(CertDER, KeyDER, Scalar) then
raise Exception.Create('certificate and SM2 private key do not match');
FillChar(Pointer(Scalar)^, Length(Scalar), 0);
end;
يوثق ذلك التعليق عيباً حقيقياً من عمل التكامل. عندما مُرّر متغير AnsiString نفسه بوصفه مدخلاً Const ومخرجاً Out، أنهى المصرف متغير Out عند الدخول، فصار المدخل فارغاً قبل أن تقرأه الروتين، وفشل فحص الهوية على مفتاح سليم تماماً. أي API يأخذ مفتاحاً بوصفه Const ويعيد مفتاحاً مشتقاً بوصفه Out يحتاج متغيرات منفصلة لدى المستدعي
FAQ
هل تحتاج PDFlibPas إلى Windows CNG أو DLL خارجي من أجل SM2 و SM3 و SM4؟
لا. تنفذ PDFlibPas الخوارزميات الثلاث كلها في Object Pascal داخل PDFlibDigest و PDFlibSM4 و PDFlibSM2، وتُبنى تواقيع SM2 ويُتحقق منها بكود CMS في المكتبة. لا دور لـ Windows CryptoAPI و CNG في إنتاج توقيع SM2
هل يمكن استخدام SM2 مع SHA-256 بدلاً من SM3؟
لا. عندما تحمل عملية توقيع هوية SM2، يستخدم باني CMS في PDFlibPas دائماً SM3، لأن GB/T 32918.2 تعرف تواقيع SM2 على تجزئة SM3 تتضمن بصمة هوية ZA. وضبط التجزئة على SHA-256 في عملية توقيع SM2 لا يغير النتيجة
هل سيفتح Adobe Acrobat ملف PDF شُفر بـ SM4 بواسطة PDFlibPas؟
لا تتوقع ذلك. مرشح التشفير /CFM /SM4 خارج القيم التي يعرفها ISO 32000، فالعارضات الرئيسية لا تعرفه. استخدم تشفير SM4 للأنظمة المغلقة التي تقرأ الملفات بـ PDFlibPas أو تنفيذ متوافق، و AES-256 للمستندات الموجهة للتوزيع العام
توقيع SM2 وتجزئة SM3 وتشفير SM4 وكامل مجموعة API للتوقيع PAdES متوفرة مع PDFlibPas، مكتبة PDF لـ Delphi، التي تُصرَّف مع Delphi و C++Builder و FPC/Lazarus على أهداف Windows و Unix