وقّع ملف PDF باستخدام PAdES B-B، ثم وسّعه إلى PAdES B-T
لمحة سريعة
قسم بعنوان «لمحة سريعة»استخدم هذه الوصفة لإنتاج توقيع Portable Document Format (PDF) Advanced Electronic Signatures (PAdES) B-B: بنية Cryptographic Message Syntax (CMS) SignedData ذات سمات موقّعة (content-type، message-digest، signing-time). ثم وسّع التوقيع نفسه إلى PAdES B-T بإضافة signature-time-stamp واحد وفق RFC 3161. B-T هو B-B مضافًا إليه طابع زمني واحد؛ وهو ليس فئة توقيع منفصلة. حدود الثقة صريحة: إنتاج التوقيع لا يعادل قرار جهة التحقق بأنه صالح.
تنبيه U-1. لا يؤكّد NextPDF أي اعتماد مستقل وفق ETSI EN 319 142-1 بخصوص PAdES B-T. EN 319 142-1 ليس ضمن مجموعة التحقق؛ جرى التحقق من متطلّب B-T
signature-time-stampمقابل ETSI EN 319 122-1 §5.3 مع RFC 3161، RFC 5652، RFC 5816 و ISO 32000-2 §12.8. دعم ملف تعريف B-T لا يعني مطابقةً أو اعتمادًا للصلاحية القانونية؛ بل جهة تحقّق مستقلة تتخذ ذلك القرار.
B-LT وB-LTA (مواد التحقق في Document Security Store (DSS)، وحلقة الطابع الزمني الأرشيفي) خارج نطاق هذه الوصفة وليسا جزءًا من واجهة التوقيع في Core/Pro المغطّاة هنا.
التثبيت
قسم بعنوان «التثبيت»composer require nextpdf/core:^3فعّل ext-openssl لأن CertificateInfo يحلّل المفاتيح عبر OpenSSL. ويحتاج B-T أيضًا إلى نقطة نهاية وفق RFC 3161 لجهة Time Stamping Authority (TSA) قابلة للوصول، وعميل HTTP من نوع PHP Standards Recommendation (PSR)-18.
نظرة مفاهيمية
قسم بعنوان «نظرة مفاهيمية»يخزّن توقيع PAdES B-B بنية CMS SignedData مرمّزة وفق Distinguished Encoding Rules (DER) في مدخل Contents من قاموس التوقيع؛ وقيمة Contents سلسلة سداسية عشرية محشوّة فوق مُلخّص نطاق البايتات (ISO 32000-2 §12.8.1). يغطّي المُلخّص الملف ويستثني قيمة التوقيع نفسها (ISO 32000-2 §12.8.1).
يضيف PAdES B-T signature-time-stamp واحدًا بالضبط وفق RFC 3161. بصمة رسالة الطابع الزمني هي تجزئة بايتات قيمة توقيع SignerInfo، دون أي بادئة وسم أو طول من Abstract Syntax Notation One (ASN.1) (ETSI EN 319 122-1 §5.3؛ RFC 3161 Appendix A). يُدرَج الرمز كسمة غير موقّعة id-aa-timeStampToken، بمُعرّف الكائن (OID) 1.2.840.113549.1.9.16.2.14 (RFC 3161 Appendix A)، ويُوضَع في SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). وبما أن السمات غير الموقّعة غير محميّة بالتوقيع (RFC 5652 §5.4)، فإن مُلخّص B-B الموقّع و/ByteRange وبايتات توقيع B-B تبقى دون تغيير — إذ لا يضيف B-T سوى الطابع الزمني. تُعرَّف شهادة TSA باستخدام ESSCertIDv2 (RFC 5816 يحدّث RFC 3161).
تنبيه U-1 (مُعاد ذكره عند مطلب B-T). لا يؤكّد NextPDF أي اعتماد مستقل وفق ETSI EN 319 142-1 بخصوص PAdES B-T. EN 319 142-1 ليس ضمن مجموعة التحقق؛ متطلّب B-T
signature-time-stampجرى التحقق منه مقابل ETSI EN 319 122-1 §5.3 مع RFC 3161، RFC 5652، RFC 5816، وISO 32000-2 §12.8. دعم ملف تعريف B-T ليس مطابقةً أو اعتمادًا للصلاحية القانونية؛ بل جهة تحقّق مستقلة تتخذ ذلك القرار.
SignatureLevel::PAdES_B_T متاح في Core: SignatureLevel::PAdES_B_T->requiresTimestamp() قيمته true، و->isAvailableInEnvironment() قيمته true، و->requiresDss() قيمته false — فـ B-T لا يستدعي Document Security Store. B-T ≠ B-LT ≠ B-LTA: الطابع الزمني للتوقيع لا يضيف مادة تحقّق ولا طابعًا زمنيًا أرشيفيًا؛ تلك مستويات منفصلة أعلى لا تُنتَج هنا.
يعرض المخطّط أدناه تسلسل B-B ثم B-T بالترتيب الذي يستخدمه المحرّك. يُحسَب ByteRange بعد كتابة الملف بأكمله فقط، بحيث لا يمكن للإزاحات النهائية أن تغيّر البايتات التي ستُجزّأ. ثم يُلحِق B-T رمزًا واحدًا وفق RFC 3161 كسمة غير موقّعة، تاركًا مُلخّص B-B الموقّع دون مساس.
واجهة الـ API
قسم بعنوان «واجهة الـ API»نقطة الدخول للإعداد هي Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). يسجّل هذا الاستدعاء نية التوقيع على المستند. محرّك توقيع PAdES في Core (NextPDF\Security\Signature\DigitalSigner) هو الذي يُنتج التوقيع التشفيري. ولأن مجموعة اختبارات التكامل تشغّل هذا المحرّك ويقوده المثال القابل للتشغيل مباشرةً، فإن المُخرَج كائن CMS حقيقي قابل للتحليل. SignatureLevel::PAdES_B_T يتطلّب TsaClient غير فارغ؛ وإنشاء موقّع B-T دونه يرمي SignatureException.
الـ API عالي المستوى — استدعاء واحد، مُخرَج موقّع
قسم بعنوان «الـ API عالي المستوى — استدعاء واحد، مُخرَج موقّع»أسرع مسار هو الـ API عالي المستوى: اضبط التوقيع على المستند، ثم سلسِل المستند. يشغّل ذلك محرّك PAdES نفسه في Core (DigitalSigner) داخليًا. هذه طبقة تيسير رقيقة فوق الشرح الأدنى مستوى الوارد أدناه، وليست مسار تنفيذ منفصلًا.
<?php
declare(strict_types=1);
use NextPDF\Core\Document;use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\SignatureLevel;use NextPDF\Security\Timestamp\TsaClient;
$certInfo = CertificateInfo::fromPkcs12( p12Path: __DIR__ . '/signer.p12', password: 'p12-passphrase',);
// PAdES B-B end to end: configure, then serialise.$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', '', 12);$doc->cell(0, 10, 'Signed end to end.', newLine: true);$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);$doc->save(__DIR__ . '/signed.pdf'); // or output() to stream, getPdfData() for bytes
// PAdES B-T: pass a TsaClient on the same call — one RFC 3161// signature-time-stamp is added (see the TsaClient hardening notes below).$doc->setSignature( certInfo: $certInfo, level: SignatureLevel::PAdES_B_T, tsaClient: $tsa,);$doc->save(__DIR__ . '/signed-bt.pdf');مثل output() وgetPdfData()، يكتب save() مدخل /Contents كبنية CMS SignedData مرمّزة وفق DER تحت SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8، §12.7.5.5؛ RFC 5652). المُخرَج قابل للتحقق بصفته CMS — كائن CMS SignedData جيّد التكوين يستطيع محلّل CMS قراءته — وهذا ليس مطابقةً لملف التعريف الأساسي وفق ETSI EN 319 142-1 ولا صلاحيةً قانونية؛ بل جهة تحقّق مستقلة تتخذ تلك القرارات (راجع تنبيه U-1 أعلاه). بالنسبة إلى B-T، يضيف الاستدعاء عالي المستوى بالضبط الطابع الزمني المفرد وفق RFC 3161 (signature-time-stamp) الموصوف في النظرة المفاهيمية؛ وتمرير TsaClient هو الفرق الوحيد عن B-B.
استخدم شرح DigitalSigner الأدنى مستوى الوارد أدناه عندما تحتاج تحكّمًا مباشرًا في الخوارزمية أو بيانات نطاق البايتات أو SignatureResult.
عيّنة برمجية — بداية سريعة
قسم بعنوان «عيّنة برمجية — بداية سريعة»<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\DigitalSigner;use NextPDF\Security\Signature\SignatureAlgorithm;use NextPDF\Security\Signature\SignatureLevel;
$certInfo = CertificateInfo::fromPkcs12( p12Path: __DIR__ . '/signer.p12', password: 'p12-passphrase',);
// PAdES B-B — a CMS SignedData, no timestamp.$signer = new DigitalSigner( certInfo: $certInfo, level: SignatureLevel::PAdES_B_B, algorithm: SignatureAlgorithm::Pkcs1v15,);$result = $signer->sign($byteRangeData);
echo $result->hasTimestamp() ? "B-T\n" : "B-B (no timestamp)\n";عيّنة برمجية — للإنتاج
قسم بعنوان «عيّنة برمجية — للإنتاج»يعمل هذا البرنامج المكتفي ذاتيًا ضمن مجموعة أدوات الـ cookbook. وهو يحاكي examples/36-sign-pades-b-b-and-b-t.php. يبني مستندًا، ويعدّه لتوقيع PAdES، ثم يوقّع عند B-B ومجددًا عند B-T باستخدام عميل TSA. في الإنتاج، وجّه TsaClient إلى نقطة نهاية حقيقية وفق RFC 3161 عبر عميل PSR-18 مُحصَّن: عميل HTTP واعٍ أمنيًا يثبّت SubjectPublicKeyInfo (SPKI) الخاص بـ TSA ويحلّ Domain Name System (DNS) بأمان. ولإبقاء هذا البرنامج دون اتصال وحتميًا، يحقن عميل TSA الزائف المرفق في المستودع للاختبار. يُعيد عميل TSA الزائف، وفق RFC 3161، TimeStampResp سليمًا بنيويًا.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Security\Signature\CertificateInfo;use NextPDF\Security\Signature\DigitalSigner;use NextPDF\Security\Signature\SignatureAlgorithm;use NextPDF\Security\Signature\SignatureLevel;use NextPDF\Security\Timestamp\TsaClient;use NextPDF\Tests\Support\FakeTsaHttpClient;
// In your application, build CertificateInfo from your own signing material:// CertificateInfo::fromPkcs12($p12Path, $passphrase) — a .p12/.pfx bundle// CertificateInfo::fromFiles($certPem, $keyPem, $pass) — separate PEM files// This program uses the repository RSA-2048 test fixtures so it is offline.$certDir = __DIR__ . '/tests/Fixtures/Certificates';$certPath = $certDir . '/test-rsa-2048-cert.pem';$keyPath = $certDir . '/test-rsa-2048-key.pem';
if (!is_file($certPath) || !is_file($keyPath)) { fwrite(STDERR, "Certificate fixtures absent. Run tests/Fixtures/Certificates/generate.sh\n"); exit(1);}
$certInfo = new CertificateInfo( certificate: (string) file_get_contents($certPath), privateKey: (string) file_get_contents($keyPath),);
// Build the document and record the signing intent on it. The ByteRange// digest input is the document bytes with the /Contents placeholder// excluded (ISO 32000-2 §12.8); getPdfData() yields the bytes to hash.$doc = Document::createStandalone();$doc->setTitle('Signed Invoice 2026-0042');$doc->setAuthor('NextPDF Cookbook');$doc->addPage();$doc->setFont('helvetica', '', 12);$doc->cell(0, 10, 'This document is configured for a PAdES signature.', newLine: true);$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$byteRangeData = $doc->getPdfData();
// --- PAdES B-B: a CMS SignedData, no timestamp ---$bb = (new DigitalSigner( certInfo: $certInfo, level: SignatureLevel::PAdES_B_B, algorithm: SignatureAlgorithm::Pkcs1v15,))->sign($byteRangeData);
// --- PAdES B-T: B-B + one RFC 3161 signature-time-stamp ---// In production, build the TsaClient with your TSA endpoint and a hardened// PSR-18 client (use the security-aware HTTP client for SSRF/DNS pinning):// $tsa = new TsaClient(// tsaUrl: 'https://tsa.example.com/timestamp',// httpClient: $hardenedPsr18Client,// );// Here the offline fake TSA client keeps the program network-free.$tsa = new TsaClient( tsaUrl: 'https://tsa.example.com/timestamp', httpClient: new FakeTsaHttpClient(),);$bt = (new DigitalSigner( certInfo: $certInfo, tsaClient: $tsa, level: SignatureLevel::PAdES_B_T, algorithm: SignatureAlgorithm::Pkcs1v15,))->sign($byteRangeData);
// B-T = B-B + a single timestamp token. The B-B signed digest is unchanged;// $bt->timestampToken holds the DER-encoded RFC 3161 token.printf("PAdES B-B CMS: %d bytes, timestamp=%s\n", $bb->getSize(), $bb->hasTimestamp() ? 'yes' : 'no');printf( "PAdES B-T CMS: %d bytes, timestamp=%s (%d-byte RFC 3161 token)\n", $bt->getSize(), $bt->hasTimestamp() ? 'yes' : 'no', strlen($bt->timestampToken),);echo "B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the// semantic profile (the signed CMS/timestamp bytes are inherently// non-reproducible and are asserted by the PHPUnit harness, not a byte hash).$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/signed-invoice.pdf', $byteRangeData);الـ STDOUT المتوقّع (تتفاوت الأحجام بحسب الشهادة ورمز TSA):
PAdES B-B CMS: <n> bytes, timestamp=noPAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).تنبيه U-1 (مُلازم لمطلب الإنتاج الخاص بـ B-T). لا يؤكّد NextPDF أي اعتماد مستقل وفق ETSI EN 319 142-1 لأجل PAdES B-T. EN 319 142-1 ليس ضمن مجموعة التحقق؛ متطلّب B-T
signature-time-stampجرى التحقق منه مقابل ETSI EN 319 122-1 §5.3 مع RFC 3161، RFC 5652، RFC 5816، وISO 32000-2 §12.8. دعم ملف تعريف B-T لا يعني مطابقةً أو صلاحية قانونية معتمَدة؛ بل جهة تحقّق مستقلة تتخذ ذلك القرار.
حالات حدّية ومزالق
قسم بعنوان «حالات حدّية ومزالق»- B-T دون عميل TSA. إنشاء B-T
DigitalSignerدونTsaClientيرميSignatureException(TSA مطلوب لأجل B-T). تحقّق من إعداد TSA قبل التوقيع. - قابلية الوصول إلى TSA. يؤدّي B-T رحلة ذهاب وإياب حيّة وفق RFC 3161 لكل توقيع. انقطاع TSA يعني عدم وجود توقيع B-T. استخدم قاطع دائرة واتفاقية مستوى خدمة (SLA) لـ TSA تناسب معدل المعالجة لديك؛ ويقبل
TsaClientقاطع دائرة. - تحصين عميل HTTP الخاص بـ TSA. وجّه
TsaClientإلى عميل PSR-18 يثبّت SubjectPublicKeyInfo (SPKI) الخاص بـ TSA بصيغة RFC 7469 ويحلّ Domain Name System (DNS) بأمان؛ ويشتقّTsaClient::extractPublicKeyPin()التثبيت من شهادة TSA. - B-T ليس B-LT/B-LTA. الطابع الزمني للتوقيع لا يضمّن مادة تحقّق (شهادات، Online Certificate Status Protocol (OCSP)، certificate revocation list (CRL)) ولا طابعًا زمنيًا أرشيفيًا. تلك هي مستويات B-LT/B-LTA ولا تُنتَج بهذه الوصفة.
- تعارض الخطّية.
enableLinearization()وتوقيع مُعدّ متنافيان — يرمي أي من الاستدعاءينInvalidConfigExceptionحين يكون الآخر قد ضُبط مسبقًا. - مفاتيح HSM. عند استخدام مفتاح محفوظ في hardware security module (HSM)، أنشئ
CertificateInfoباستخدامCertificateInfo::fromHsm()؛ فلا يدخل المفتاح الخاص ذاكرة العملية أبدًا. عقد موقّع PKCS#11 موجود في Core؛ أما المزوّد العامل فهو Premium.
الأداء
قسم بعنوان «الأداء»توقيع B-B عملية CMS محلية. يضيف B-T رحلة ذهاب وإياب HTTP متزامنة واحدة وفق RFC 3161 إلى TSA لكل توقيع. خصّص ميزانية لزمن استجابة TSA وحدود المعدّل في أحمال العمل الدُفعية. استخدم TsaClient محميًا بقاطع دائرة.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»لا يصبح التوقيع المُنتَج موثوقًا تلقائيًا. تحقّق التوقيع يتوقّف على الشهادة، ومرتكز الثقة الخاص بها، وسياسة جهة التحقق، وكلها تقع خارج هذه المكتبة. يحمي التشفير السرّية لا السلامة؛ ويحمي التوقيع السلامة والأصالة لا السرّية. عامِل حفظ المفتاح بوصفه الخطر الأساسي: المفتاح البرمجي في ذاكرة العملية آمن فقط بقدر أمان المضيف.
إقامة البيانات وتخفيف مخاطر الـ PII
قسم بعنوان «إقامة البيانات وتخفيف مخاطر الـ PII»تعمل عملية التوقيع داخل العملية؛ ولا تغادر بايتات المستند والمفتاح الخاص المضيف إلا في رحلة ذهاب وإياب B-T الخاصة بـ TSA، التي ترسل فقط بصمة الرسالة (تجزئة قيمة التوقيع)، ولا ترسل محتوى المستند أبدًا (RFC 3161 §2.4.1 MessageImprint). لا يُرسَل أي نص مستند أو معلومات تعريف شخصية (PII) إلى الـ TSA. اختر TSA تتوافق ولايتها القضائية مع سياسة إقامة بياناتك.
القياس الآمن وتنقية السجلّات
قسم بعنوان «القياس الآمن وتنقية السجلّات»يقبل DigitalSigner مُسجِّلًا اختياريًا من نوع PSR-3. يسجّل الخوارزمية والمستوى، لا مادة المفتاح ولا بايتات التوقيع. مُعاملات password على CertificateInfo وTsaClient مُعلَّمة بـ #[SensitiveParameter]، فتُحجَب عبارات المرور من تتبّعات المكدّس. لا تسجّل SignatureResult::$cmsSignedData ولا $timestampToken.
نموذج التهديد
قسم بعنوان «نموذج التهديد»ما أُخذ في الحسبان: العبث بالمدخل بعد التوقيع (يكتشفه مُلخّص نطاق البايتات)، وتعرّض المفتاح للاختراق (خارج نطاق المكتبة لأن حفظ المفتاح مسؤولية المُدمِج)، وانتحال TSA (يخفّفه تثبيت SPKI على عميل HTTP الخاص بـ TSA)، والخفض بين المستويات (تعداد المستوى صريح؛ ولا يخفض المحرّك B-T إلى B-B بصمت). ما لا يُؤكَّد: انعدام الثغرات، أو أن أي توقيع ناتج صالح قانونيًا.
سلوك وضع FIPS
قسم بعنوان «سلوك وضع FIPS»تُوفَّر بدائيات التوقيع من OpenSSL. على بنية OpenSSL مُعتمَدة وفق Federal Information Processing Standards (FIPS)، تجري عمليات RSA/ECDSA و SHA-256 عبر مزوّد FIPS؛ ولا يؤكّد NextPDF بذاته اعتماد FIPS. يبلّغ CryptoCapabilities عن البدائيات المتاحة على المضيف؛ تحقّق من سلسلة مزوّدي OpenSSL في بيئة النشر لديك.
المطابقة
قسم بعنوان «المطابقة»| البيان | المواصفة | البند | reference_id |
|---|---|---|---|
| يغطّي مُلخّص نطاق البايتات الملف ويستثني قيمة التوقيع. | ISO 32000-2 | §12.8.1 | |
Contents يحمل DER CMS SignedData؛ وContents لطابع زمني للمستند يحمل TimeStampToken. | ISO 32000-2 | §12.8.1 | |
Contents سلسلة سداسية عشرية محشوّة فوق مُلخّص نطاق البايتات. | ISO 32000-2 | §12.8.1 | |
| بصمة signature-time-stamp هي تجزئة بايتات قيمة توقيع SignerInfo (دون وسم/طول tag/length من ASN.1). | ETSI EN 319 122-1 | §5.3 | |
| قيمة signature-time-stamp هي SignatureTimeStampToken. | ETSI EN 319 122-1 | §6 | |
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }. | RFC 3161 | §2.4.1 | |
بصمة الطابع الزمني للتوقيع هي تجزئة حقل توقيع SignerInfo؛ SignatureTimeStampToken ::= TimeStampToken. | RFC 3161 | App. A | |
مُعرّف OID لـ id-aa-timeStampToken هو 1.2.840.113549.1.9.16.2.14. | RFC 3161 | App. A | |
SignerInfo يحمل unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL. | RFC 5652 | §5.3 | |
| السمات غير الموقّعة غير محميّة بالتوقيع؛ ومُلخّص B-B الموقّع دون تغيير. | RFC 5652 | §5.4 | |
| RFC 5816 يحدّث RFC 3161؛ ESSCertIDv2 يعرّف شهادة TSA دون SHA-1. | RFC 5816 | §1 |
تصف هذه الوصفة كيف يُنتج NextPDF توقيع B-B وتوقيع B-T. ولا تؤكّد أن أي توقيع ناتج صالح قانونيًا ولا أن مطابقة PAdES متحقّقة؛ بل جهة تحقّق مستقلة تتخذ تلك القرارات.
السياق التجاري
قسم بعنوان «السياق التجاري»PAdES B-LT وB-LTA (مواد التحقق في DSS وحلقة الطابع الزمني الأرشيفي) وحفظ مفاتيح HSM عبر PKCS#11 تتوفر في إصداري Pro و Enterprise. تغطّي هذه الوصفة B-B وB-T فقط؛ فالمستويات الأعلى قدرات متمايزة يُتحقَّق منها على حدة وهي خارج النطاق هنا.