مقال تقني

توقيعات PAdES الرقمية في Delphi: التوقيع والتحقق باستخدام PDFlibPas

أبلغ Adobe Acrobat أن كل توقيع في الدفعة صالح. ورفض مدقق eIDAS الخاص بالعميل كل واحد منها. اختُصر الفرق إلى اسم واحد في قاموس التوقيع: كانت الملفات تحمل /SubFilter /adbe.pkcs7.detached، وهذا ينتج توقيعا سليما تماما وفق ISO 32000-1 §12.8 لكنه غير مطابق لـ PAdES، لأن ETSI EN 319 142-1 يتطلب ETSI.CAdES.detached في كل مستوى baseline. كانت cryptography بلا عيب؛ المستند ببساطة لم يكن يدعي profile الذي طلبه المنظم. إذا كان تطبيق Delphi لديك يوقع فواتير أو عقودا أو تقارير مختبر يجب أن تنجو من سياسة تحقق على النمط الأوروبي، فهذا الفرق هو أول ما يجب ضبطه — وهو استدعاء واحد في losLab PDF Library (PDFlibPas)، التي تتناول هذه المقالة سلسلة التوقيع والطوابع الزمنية و DSS فيها من توقيع baseline إلى long-term validation.

ما الذي يحول توقيع PDF إلى توقيع PAdES

يعرّف ETSI EN 319 142-1 أربعة مستويات baseline متراكبة على صيغة CMS. يمثل PAdES-B-B نقطة الدخول: توقيع CAdES في حقل توقيع PDF مع SubFilter باسم ETSI.CAdES.detached وسمة signing-certificate موقعة. يضيف PAdES-B-T طابعا زمنيا RFC 3161 فوق قيمة التوقيع، لإثبات أن التوقيع وُجد قبل نقطة زمنية لا يستطيع أحد إرجاعها. يضمن PAdES-B-LT الشهادات و CRLs و OCSP responses اللازمة للتحقق داخل Document Security Store، فيبقى الملف قابلا للتحقق بعد أن توقف CA المصدرة بنيتها التحتية. ويختم PAdES-B-LTA السلسلة بطابع زمني للمستند يعيد حماية الأدلة المتراكمة مع ضعف الخوارزميات.

يرسم PDFlibPas هذه المفاهيم على sign-process API. علامة profile هي SetSignProcessCustomSubFilter؛ ومؤشرات commitment-type في ETSI — proof of origin و proof of approval وبقية المعرّفات التي يحددها ETSI من 1 إلى 6 — تمر عبر SetSignProcessCommitmentType؛ وتُرفق سياسة توقيع صريحة عبر SetSignProcessSignaturePolicy، التي تأخذ OID الخاص بالسياسة و digest الخاص بها. يستحق افتراض واحد الانتباه: عندما تُترك خوارزمية digest على auto، تختار المكتبة SHA-256 لتوقيعات ETSI و adbe.pkcs7.detached ولا تعود إلى SHA-1 إلا في مسار adbe.pkcs7.sha1 القديم. اضبطها صراحة على أي حال؛ فالمدققون يسألون، والصريح أفضل من المستنتج في كل مراجعة امتثال.

إنتاج توقيع baseline

تقود API المسطحة التوقيع كآلة حالة one-shot: افتح عملية على ملف المصدر، اضبطها، أنهها إلى ملف إخراج، اقرأ رمز النتيجة. ينتج التسلسل أدناه توقيع PAdES-B-B مع SHA-256 — ويحجز عمدا مساحة إضافية داخل placeholder الخاص بـ /Contents، وهو السطر الوحيد الذي لا تستطيع ترقيعه لاحقا إذا احتاج طابع زمني إلى الإضافة إلى هذا التوقيع.

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

تعيد NewSignProcessFromFile القيمة 0 عندما لا يمكن فتح المصدر إطلاقا. بعد ذلك، تفصل GetSignProcessResult أنماط الفشل التي تحدث فعلا في الإنتاج: 4 تعني كلمة مرور PDF خاطئة، و7 كلمة مرور PFX خاطئة، و9 ملف شهادة بلا مفتاح خاص، و10 مسار إخراج غير قابل للكتابة، و11 فشلا أثناء تطبيق بايتات التوقيع. تسجيل الرمز العددي بجانب اسم ملف الإدخال يحول تذكرة دعم غامضة إلى تشخيص في دقيقة.

إضافة طابع RFC 3161 الذي لن تجلبه المكتبة نيابة عنك

لا يشحن PDFlibPas عميل TSA، وهذا حد مقصود لا فجوة. تحسب المكتبة الهاش الذي يجب أن توقعه سلطة الطوابع الزمنية وتعيد تضمين CMS المعزز بعد ذلك؛ أما تبادل HTTP وجراحة CMS بينهما فهي مسؤولية المستدعي. للانقسام سبب تقني صلب: التحكم Windows CryptoAPI الذي يفترض أن يضيف unsigned attributes، وهو CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR، يفشل مع CRYPT_E_INVALID_INDEX على تخطيط SignedData المنفصل الذي يستخدمه PAdES، لذلك يجب أن يأتي CMS المعزز من CMS encoder تحت تحكمك — لا تستطيع أي مكتبة أن تفعله بصمت باستدعاء نظام واحد.

var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

راقب رموز النتائج هنا: 12 تعني أن حقل التوقيع المسمى غير موجود، و11 أن CMS القائم لم يمكن تحليله، و13 أن CMS المعزز لم يعد يناسب placeholder الخاص بـ /Contents. الرمز 13 هو المؤلم، لأن الإصلاح الوحيد هو إعادة التوقيع: طابع زمني نموذجي مع سلسلة شهاداته يشغل 4 إلى 6 KB، والحجز 8192-byte الذي أُجري في خطوة B-B موجود تحديدا كي تجد هذه الخطوة مكانا.

يبدأ التحقق عند ByteRange لا عند سلسلة الشهادات

علامة خضراء في عارض قرار ثقة مقابل مخزن شهادات ذلك الجهاز، لا حكم بنيوي على الملف. يجب أن يبدأ التحقق البرمجي من مستوى أدنى، بالسؤال الذي تجعله incremental updates دقيقا: أي بايتات يغطيها كل توقيع فعلا؟ كل تعزيز ناقشناه هنا — توقيعات ثانية، وقواميس DSS، وطوابع زمنية للمستند — يصل عبر incremental update، وكل تحديث يضيف بايتات خارج /ByteRange الخاص بالتوقيع السابق. هذه البايتات المضافة مشروعة، لكن على المدقق تصنيفها مقابل سياسة تعديل المستند؛ يمكن قراءة مستوى DocMDP لكل حقل عبر GetSignatureDocMDPLevelByName.

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 means actually signed
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

يعيش فخان في مسار التدقيق هذا. تحتفظ TPDFlibSignDoc.Open بالملف بقفل مشاركة حصري، لذلك يجب على المدقق الذي يريد أيضا تجزئة بايتات الملف الخام للتحقق من CMS أن يقرأ الملف إلى الذاكرة قبل فتحه للتدقيق — فالترتيب مهم. كما أن نظير API المسطحة GetSignProcessByteRange يعيد Integer بينما offsets الحقيقية Int64: بعد 2 GB يقتطع الاستدعاء المسطح بصمت، ولهذا يسحب المثال offsets عبر فئة التدقيق بدلا منه. لاحظ أيضا ما يغيب عمدا عن الطبقة المسطحة: لا يوجد wrapper باسم VerifySignature إطلاقا. أحكام cryptographic تأتي من مستوى الفئات عبر TPDFlibSignatureVerifier، الذي يعيد vsValid أو vsInvalid أو vsUnknown، أو من مدقق خارجي تثق به سياسة الامتثال لديك أصلا.

التحقق طويل الأمد: DSS و VRI والطابع الزمني للمستند

يوجد PAdES-B-LT لأن بنية الإبطال التحتية فانية. يحدد ETSI EN 319 142-1 §5.4.2.2 Document Security Store: قاموسا على مستوى المستند يحمل الشهادات و CRLs و OCSP responses، ويمكن فهرسته اختياريا لكل توقيع عبر VRI entries مفتاحية بهاش /Contents الخاص بكل توقيع. يعكس تدفق PDFlibPas تصميم الطابع الزمني: تفتح NewPAdESDSSProcessFromFile العملية؛ وتقبل AddPAdESDSSCertificate و AddPAdESDSSCRL و AddPAdESDSSOCSP blobs من DER؛ وتربط AddPAdESDSSVRI المادة المختارة بتوقيع واحد؛ وتكتب EndPAdESDSSProcessToFile كل شيء كـ incremental update. يبقى جلب مادة الإبطال — والحكم على حداثتها الكافية للتضمين — مسؤولية المستدعي؛ تضمن المكتبة أن القواميس مطابقة بنيويا، لا أن OCSP responder قال الحقيقة.

نقطة النهاية الأرشيفية، B-LTA، تضيف طابعا زمنيا للمستند: حقل توقيع مستقل نوعه DocTimeStamp لا Sig، ينتج عبر SetSignProcessDocTimeStamp مع طول توقيع محجوز. وبالنسبة إلى القراء الأقدم من هذه البنى، تسجل TPDFlibSignDoc.EnsurePAdESExtensions امتداد ESIC developer في catalog المستند، معلنة أن الملف يستخدم ميزات يحددها ETSI.

أسئلة شائعة

لماذا يقول Acrobat إن "validity unknown" بينما بنية PAdES صحيحة؟

لأن الثقة والبنية مستقلتان. العارض لا يستطيع وصل الموقّع إلى جذر يثق به على ذلك الجهاز — وهو أمر شائع مع CAs خاصة وشهادات اختبار — بينما ينجح تدقيق ByteRange والتحقق من CMS في اللحظة نفسها. وزّع شهادة الجذر بشكل صحيح، أو قيّم مقابل قوائم EU trusted lists عندما يكون تأهيل eIDAS هو الهدف الحقيقي.

هل يمكن إضافة طابع زمني إلى توقيع لم يحجز مساحة Contents إضافية؟

عادة لا. يجب أن يناسب CMS المعزز placeholder الأصلي، و placeholder بالحجم الافتراضي يناسب التوقيع الأصلي بإحكام. توقع رمز النتيجة 13، وخطط لإعادة التوقيع مع SetSignProcessReserveContentsBytes منذ البداية.

هل يحل الطابع الزمني للمستند محل طابع التوقيع الزمني؟

لا. يثبت طابع التوقيع الزمني متى وُجد توقيع واحد؛ أما طابع المستند الزمني فيحمي الملف كله بما فيه أدلة DSS، وهو العنصر الذي يتجدد عبر العقود. تنتهي profiles الأرشيفية عادة إلى حمل الاثنين.

لمنظور التدقيق — تعداد حقول التوقيع عبر corpus، وتفريغ تخطيطات ByteRange، وقراءة مستويات DocMDP على دفعات — راجع المقالة المرافقة عن بيئة عمل الامتثال والتوقيع. المستندات الموقعة التي يجب أن تستوفي سياسة أرشيفية أيضا تنتمي إلى workflow الموضح في PDF/A و PDF/UA preflight في Delphi. التوثيق الكامل لـ API وتنزيلات التقييم موجودة في صفحة منتج losLab PDF Library for Delphi.