مقال تقني

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

التحقق من صحة توقيع PAdES واحد يعني التحقق من ثلاثة أشياء مستقلة، وتخبرك علامة الاختيار الخضراء في العارض عن الثالث فقط. أولاً، يجب أن تغطي مصفوفة /ByteRange البايتات الصحيحة: يجب أن تعيد النطاقات التي تسميها بناء الإدخال الدقيق الذي تم أخذ ملخص CMS عليه، دون ترك أي بايتات موقعة خارجها. ثانيًا، يجب أن تتسلسل الشهادة الموجودة داخل CMS إلى جذر تثق به وتحمل سمة شهادة التوقيع الموقعة التي يتطلبها PAdES. ثالثًا، إذا ادعى الملف الشخصي وجود طابع زمني، فيجب أن يربط رمز RFC 3161 المميز قيمة التوقيع بنقطة زمنية قبل انتهاء صلاحية الشهادة. يطوي Acrobat الثلاثة جميعًا في رمز واحد؛ يبقيهم مدقق التوافق منفصلين، وكذلك يجب أن تفعل التعليمات البرمجية التي تنتج هذه الملفات. تمنحك losLab PDF Library (PDFlibPas) الجانب التوقيعي من ذلك، وإعادة تضمين الطابع الزمني، واستدعاءات التدقيق لفحص ByteRange قبل الوثوق به

تمييز واحد يعرقل تقريبًا كل أول تنفيذ لـ PAdES، لذلك يجدر ذكره قبل أي رمز. التوقيع المكتوب بـ /SubFilter /adbe.pkcs7.detached هو توقيع ISO 32000-1 §12.8 سليم تمامًا وسيبلغ Acrobat أنه صالح. وهو أيضًا ليس توقيع PAdES، لأن ETSI EN 319 142-1 يتطلب ETSI.CAdES.detached عند كل مستوى أساسي. يرفض مدقق التوافق eIDAS الأول ويقبل الثاني على الرغم من أن التشفير متطابق. الملف الشخصي هو ادعاء يقدمه المستند عن نفسه، والحصول على هذا الادعاء بشكل صحيح هو استدعاء واحد في PDFlibPas

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

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

يقوم PDFlibPas بتعيين هذه المفاهيم على واجهة برمجة تطبيقات عملية التوقيع الخاصة به. علامة الملف الشخصي هي SetSignProcessCustomSubFilter. إذا كانت سياستك تحتاج إلى إشارة لنوع الالتزام (إثبات المنشأ، إثبات الموافقة، أو أحد معرفات ETSI الأخرى المرقمة من 1 إلى 6)، فإن ذلك يمر عبر SetSignProcessCommitmentType. سياسة التوقيع الصريحة ترفق بـ SetSignProcessSignaturePolicy، والتي تأخذ معرف الكائن (OID) للسياسة وملخصه. يستحق افتراضي واحد الاهتمام: مع ترك خوارزمية الملخص في الوضع التلقائي، تختار المكتبة SHA-256 لتوقيعات ETSI و adbe.pkcs7.detached وتتراجع إلى SHA-1 فقط على مسار adbe.pkcs7.sha1 القديم. قم بتعيينها صراحة على أي حال. يسأل المدققون عن التجزئة التي استخدمتها، والقيمة الصريحة في التعليمات البرمجية أسهل في الدفاع عنها من الافتراضي الذي يجب أن تذهب لقراءة الدليل لشرحه

إنتاج توقيع الأساس (baseline signature)

تدفع واجهة برمجة التطبيقات المسطحة التوقيع كآلة حالة من طلقة واحدة: افتح عملية على الملف المصدر، وقم بتكوينها، والانتهاء إلى ملف إخراج، واقرأ رمز النتيجة. ينتج التسلسل أدناه توقيع PAdES-B-B مع SHA-256. السطر الأكثر أهمية لا علاقة له بالتوقيع نفسه. إنه حجز /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 الذي يضيف اسميًا سمات غير موقعة، CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR، مع CRYPT_E_INVALID_INDEX على تخطيط SignedData المنفصل الذي يستخدمه PAdES. لذلك يجب أن يأتي CMS المحسن من مشفر CMS تحت سيطرتك الخاصة. لا يمكن لأي مكتبة أن تطوي الرمز المميز بهدوء من خلال استدعاء نظام واحد، وأي شخص يدعي القيام بذلك يقوم بإجراء الجراحة في مكان لا يمكنك رؤيته

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 الموسع لم يعد يتناسب مع العنصر النائب /Contents المحجوز. الرمز 13 هو الرمز الذي يؤلم، لأن الإصلاح الوحيد هو إعادة التوقيع: رمز مميز نموذجي للطابع الزمني مع سلسلة الشهادات الخاصة به يعمل من 4 إلى 6 كيلوبايت، وحجز 8192 بايت الذي تم إجراؤه أثناء خطوة B-B موجود على وجه التحديد حتى يكون لهذه الخطوة مساحة للهبوط

يبدأ التحقق من الصحة من ByteRange، وليس سلسلة الشهادات

علامة الاختيار الخضراء في العارض هي قرار ثقة مقابل مخزن شهادات ذلك الجهاز، وليست حكمًا هيكليًا حول الملف. يجب أن يبدأ التحقق البرمجي بشكل أقل، مع السؤال الذي تجعله التحديثات التدريجية خفيًا: ما هي البايتات التي يغطيها كل توقيع بالفعل؟ كل تحسين تمت مناقشته هنا، سواء كان توقيعًا ثانيًا، أو قاموس DSS، أو طابع زمني للمستند، يصل عبر التحديث التدريجي، ويلحق كل تحديث بايتات خارج /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 بينما الإزاحات الأساسية هي Int64، لذلك في ملف يتجاوز 2 جيجابايت يقتطع الاستدعاء المسطح دون شكوى، وهذا هو السبب في أن هذا المثال يسحب الإزاحات من خلال فئة التدقيق بدلاً من ذلك. غياب واحد يستحق التسمية أيضًا. لا تحتوي الطبقة المسطحة على أي غلاف VerifySignature على الإطلاق. تأتي أحكام التشفير من TPDFlibSignatureVerifier على مستوى الفئة، والذي يعيد vsValid، vsInvalid، أو vsUnknown، أو من مدقق خارجي تثق به سياسة الامتثال الخاصة بك بالفعل

التحقق من الصحة على المدى الطويل: DSS، VRI، وطابع المستند الزمني

يوجد PAdES-B-LT لأن البنية التحتية للإلغاء فانية. يحدد ETSI EN 319 142-1 §5.4.2.2 مخزن أمان المستند (Document Security Store): قاموس على مستوى المستند يحمل شهادات وقوائم CRL واستجابات OCSP، تتم فهرسته اختياريًا لكل توقيع من خلال إدخالات VRI مرتبطة بتجزئة /Contents لكل توقيع. يعكس تدفق PDFlibPas تصميم الطابع الزمني. يفتح NewPAdESDSSProcessFromFile العملية؛ يقبل AddPAdESDSSCertificate و AddPAdESDSSCRL و AddPAdESDSSOCSP كتل DER؛ يربط AddPAdESDSSVRI المواد المحددة بتوقيع واحد؛ يكتب EndPAdESDSSProcessToFile كل شيء كتحديث تدريجي. الجزء الصعب يبقى في صفك. جلب مواد الإلغاء، والحكم على ما إذا كانت جديدة بما يكفي لتستحق التضمين، هي مهمة المتصل. تضمن المكتبة أن القواميس متوافقة من الناحية الهيكلية؛ ولا يمكنها ضمان أن مستجيب OCSP الخاص بك قد قال الحقيقة

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

رد فعل واحد على كل هذا يستحق التوجيه، لأنه يبدو وكأنه خطأ وهو ليس كذلك. غالبًا ما يبلغ العارض عن "صلاحية غير معروفة" (validity unknown) في ملف بنية PAdES الخاصة به صحيحة تمامًا. الثقة والهيكل محوران مستقلان. لا يستطيع العارض ببساطة ربط الموقّع بجذر يثق به على هذا الجهاز، وهو أمر روتيني مع المراجع المصدقة (CAs) الخاصة وشهادات الاختبار، حتى مع نجاح تدقيق ByteRange والتحقق من CMS. الإصلاح هو توزيع شهادة الجذر بشكل صحيح، أو التقييم مقابل قوائم الاتحاد الأوروبي الموثوقة عندما يكون وضع eIDAS المؤهل هو الهدف الفعلي، بدلاً من لمس رمز التوقيع

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