يوقّع HotPDF مستندات PDF بمفتاح خاص يحتفظ به خدمة Cloud Signature Consortium (CSC) بعيدة عبر THPDFCSCSignatureProvider، مزود تواقيع يقود واجهة CSC — معلومات الاعتماد، والتصريح، و signatures/signHash، والاستقصاء — بينما يوفر تطبيق Delphi لديك نقل HTTP ورمز وصول OAuth. المفتاح لا يغادر HSM الخدمة أبداً
ذلك صار يوماً بعد يوم الطريقة الوحيدة للحصول على مفتاح توقيع مؤهل أصلاً. مزودو خدمات الثقة يسلّمون نقطة نهاية CSC وعميل OAuth، لا ملف PFX ولا رمزاً USB، فلا شيء يُحمَّل في مخزن شهادات محلي بالطريقة التي يفعلها التوقيع من مخزن شهادات Windows عبر CNG و CAPI. التكامل الساذج يفشل بطرق متوقعة: استدعاء signHash ينقضي الوقت وإعادة المحاولة توقّع العقد نفسه مرتين، أو دفعة من أربعين فاتورة تطلق أربعين كلمة سر أحادية الاستخدام لأن كل هُضم وُضّح منفرداً. معظم ما يفعله المزود هو الدفاع عن هاتي الحالتين
لماذا يترك HotPDF الـ HTTP لتطبيقك؟
لأن النقل بالضبط هو حيث تختلف كل عملية نشر. الوكلاء وتثبيت TLS وشهادات العميل وخزائن OAuth المؤسسية وسياسة التسجيل كلها تسكن طبقة HTTP، لذا ينظم THPDFCSCSignatureProvider حالة البروتوكول ويستدعي دالة THPDFCSCTransport عن كل طلب. يسلّمك المزود THPDFCSCTransportRequest بـ Method (POST دائماً)، و URL كاملاً مبني من ServiceBaseURL زائد مسار نقطة النهاية، وترويسة Authorization حاملة جاهزة، و ContentType، و Body بصيغة JSON، و IdempotencyKey، ورقم Attempt، و MaxResponseBytes. وتملأ أنت THPDFCSCTransportResponse بـ StatusCode و Body و RetryAfterMS، وتعيد واحدة من ctsSuccess أو ctsTemporaryFailure أو ctsPermanentFailure أو ctsCancelled
uses
System.Net.HttpClient, System.Net.URLClient, HPDFSignatureProvider,
HPDFCSCSignatureProvider;
function MakeCSCTransport(Client: THTTPClient): THPDFCSCTransport;
begin
Result :=
function(const Request: THPDFCSCTransportRequest;
out Response: THPDFCSCTransportResponse): THPDFCSCTransportStatus
var
Body: TStringStream;
Reply: TMemoryStream;
Headers: TNetHeaders;
HttpResp: IHTTPResponse;
begin
Response := Default(THPDFCSCTransportResponse);
Body := TStringStream.Create(string(Request.Body), TEncoding.UTF8);
Reply := TMemoryStream.Create;
try
Headers := [TNameValuePair.Create('Authorization', string(Request.Authorization)),
TNameValuePair.Create('Content-Type', string(Request.ContentType))];
if Request.IdempotencyKey <> '' then // اسم الترويسة كما توثقه خدمتك
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // عطل مقبس أو DNS: قابل لإعادة المحاولة
end;
Response.StatusCode := HttpResp.StatusCode; // ابلّغ 503 كما هي، لا تصنّف
SetLength(Response.Body, Reply.Size);
if Reply.Size > 0 then
Move(Reply.Memory^, Response.Body[1], Reply.Size);
Response.RetryAfterMS := StrToIntDef(HttpResp.HeaderValue['Retry-After'], 0) * 1000;
Result := ctsSuccess;
finally
Reply.Free;
Body.Free;
end;
end;
end;
القاعدة الواحدة التي تستحق الحفظ: أعِد ctsSuccess كلما أجاب الخادم فعلاً، ولو بـ 503. المزود يصنف أكواد الحالة بنفسه، ونقل يحول 429 إلى ctsPermanentFailure يعطّل بصمت منطق إعادة المحاولة الموصوف أدناه. الباني صارم في الاتجاه المعاكس — يرفع EHPDFCSCSignatureProviderError حين يكون النقل مفقوداً، أو CredentialID فارغاً، أو لم يُسلَّم AccessToken ولا نداء الرمز معاً، أو كانت الميزانية خارج المدى، أو ServiceBaseURL ليس HTTPS. و http:// العادي يقبل فقط مع AllowInsecureHTTP، ومكانها منصة اختبار لا غير
ما هو الـ SAD، ولماذا يرميه HotPDF بعد استخدام واحد؟
يعامل THPDFCSCSignatureProvider بيانات تفعيل التوقيع (SAD) بوصفها أحادية الاستخدام: تُمسح من حالة المزود لحظة قبول signatures/signHash، حتى حين يصل التوقيع نفسه لاحقاً عبر استقصاء غير متزامن. الـ SAD إثبات الخدمة بأن الموقّع وافق على هذه الهضمات بعينها، و SAD تتمدد في الذاكرة تصريح ينتظر أن يُصرف على المستند الخطأ
وبالإعدادات الافتراضية من THPDFCSCOptions.Default — RequireSAD و AutoAuthorize كلاهما True — يحمل المزود credentials/info مرة واحدة، ويسأل THPDFCSCAuthenticationCallback لديك عن قيم authData (كلمة سر أحادية، أو رقم PIN، أو ما يتطلبه كتلة auth للاعتماد)، ويرسل credentials/authorize. 200 تحمل الـ SAD مباشرة؛ و 202 تحمل معرفاً يُستقصى عبر credentials/authorizeCheck حتى MaxPollAttempts (60) مرة بفواصل PollIntervalMS (250 ms). وقد يعيد النداء 32 قيمة على الأكثر، كل منها بمعرف غير فارغ حتى 256 بايتاً وقيمة حتى 4,096 بايتاً. وإن انقطعت الشبكة قبل قبول signHash، حُفظ SAD مُحصَّل تلقائياً حتى تُعاد الدفعة نفسها دون سؤال الموقّع مجدداً
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD و AutoAuthorize ووضع غير متزامن وإعادتان
Options.ServiceBaseURL := 'https://csc.example.com/csc/v2';
Options.CredentialID := 'contracts-signing-01';
Options.ClientData := 'invoice-run-2026-09';
Provider := THPDFCSCSignatureProvider.Create(Options, MakeCSCTransport(HttpClient),
function(ForceRefresh: Boolean; const OperationIdentifier: AnsiString;
out AccessToken: AnsiString; out ExpiresAtUTC: TDateTime): THPDFSignatureProviderStatus
begin
// عميل OAuth لديك؛ تصير ForceRefresh صحيحة بعد أن أجابت الخدمة 401
if not TokenVault.Acquire(ForceRefresh, AccessToken, ExpiresAtUTC) then
Exit(spsProviderError);
Result := spsValid;
end,
function(const CredentialID, CredentialInfoJSON, OperationIdentifier: AnsiString;
out Values: THPDFCSCAuthenticationValues): THPDFSignatureProviderStatus
var
Otp: string;
begin
if not AskSignerForOtp(Otp) then // واجهتك
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
أما SAD تمررها بنفسك عبر Options.SAD فتسلك سلوكاً مختلفاً، وعمداً. لا تستطيع HotPDF معرفة أي هضمات صدرت لأجلها، فيستخدم المزود SAD سابقة الضبط لطلب هضم واحد فقط. ولدفعة مع AutoAuthorize مطفأ يفشل المزود بـ «CSC SAD is not pinned to the requested hash batch» بدل التخمين
كيف توقّع SignHashBatch مستندات كثيرة بتصريح واحد؟
ترسل SignHashBatch credentials/authorize واحدة و signatures/signHash واحدة حتى MaxBatchSignatures (64) هضمة، وتبني الجسمين من المصفوفة نفسها بحيث يكون numSignatures وترتيب hashes و hashAlgorithmOID متطابقة في الاستدعاءين. ذلك التطابق هو ما يتطلبه نموذج multisign في CSC. ودّر طريقة Sign أحادية الهضم أربعين مرة وستحصل على أربعين تصريحاً؛ وأرسل authorize و signHash يخالفان بعضهما وقد تستهلك الخدمة الـ SAD ضد الدفعة الخطأ
قبل أي حركة شبكة يتحقق المزود من الدفعة. كل طلب يجب أن يكون هضمة (sikDigest) من 1 إلى 1,024 بايتاً بمعرف OID لهضم، وكل الطلبات يجب أن تتشارك OID خوارزمية توقيع واحدة، و OID هضم واحداً، وطول ملح واحداً لـ RSASSA-PSS. والدفعة متعددة الهضمات تحمل كذلك credentials/info وتعيد spsUnsupported حين تكون قيمة multisign للاعتماد أصغر من الدفعة. ثم تثبَّت الـ SAD إلى بصمة دفعة — SHA-256 فوق وسم نسخة، والعدد، ولكل طلب: OID الخوارزمية، و OID الهضم، والخوارزمية، وطول الملح، وبايتات الهضم، كلها مسبوقة بطولها. بدّل هضمين وصار ذلك دفعة مختلفة تحتاج تصريحاً جديداً
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: قيم SHA-256 التي حسبتها
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // يُشتق signAlgo حين يكون AlgorithmOID فارغاً
Requests[I].DigestAlgorithmOID := '2.16.840.1.101.3.4.2.1';
Requests[I].InputKind := sikDigest;
Requests[I].Input := Digests[I];
end;
Status := Provider.SignHashBatch(Requests, 'invoices-2026-09-25-a', Signatures);
if Status <> spsValid then
raise Exception.CreateFmt('CSC batch failed (HTTP %d): %s',
[Provider.LastHTTPStatus, Provider.LastError]);
// Signatures[I] يعود إلى Digests[I]؛ ووُفحص العد مقابل الطلب
end;
ولـ RSASSA-PSS يرسل المزود كذلك signAlgoParams، بنية RSASSA-PSS-params بترميز base64 DER تحوي خوارزمية الهضم و MGF1 وطول الملح. وبناؤها يعني ترميز معرفات OID، وأصلح الإصدار 2.748.5 زاوية من ذلك: تطوي X.690 §8.19.4 القوسين الأولين في قيمة واحدة (40 × الأول + الثاني)، وتحت جذر 2 يدفع القوس الثاني فوق 39 تلك القيمة إلى ما بعد 127، حيث تحتاج صيغة الأساس 128 متعددة البايتات التي لم تكن البنيات الأقدم تطبقها. لا OID من عائلة SHA-2 يتأثر — فـ 2.16 يتطوى إلى 96 — لكن OID مشوه يرفع الآن خطأ المزود الخاص بدل EConvertError
لماذا لا ينتج عن طلب معاد محاولته توقيع ثانٍ؟
يجعل THPDFCSCSignatureProvider كل استدعاء قابل لإعادة المحاولة يحمل مفتاح idempotency حتمياً ويخزن النتائج المنجزة، فإعادة محاولة بعد استجابة ضائعة تعيد التواقيع الأصلية بدل طلب تواقيع جديدة من الـ HSM. المفتاح csc- يليه SHA- hex للمعرف العملي والطور، ويضمّن الطريق بصمة الدفعة للتصريح ولـ signHash كليهما. البصم بدل البتر يهم: معرفان عمليان طويلان يتشاركان بادئة سيتصادمان تحت البتر، بينما مفتاح ذو طول ثابت معنون بالمحتوى يبقى فريداً ومستقراً عبر المحاولات
سياسة إعادة المحاولة في مسار الطلب المشترك ضيقة عمداً:
- HTTP 401 يفرض تجديد رمز واحداً بالضبط عبر نداء رمز الوصول، ثم يكرر الطلب مرة واحدة حين يكون نداء رمز الوصول مسنداً؛ و 401 ثانية نهائية
- بقية ردود 4xx و
ctsPermanentFailureتنهيان الاستدعاء بـspsProviderError، وتحلerror_descriptionمن الخدمة فيLastError - 408 و 429 و 5xx و
ctsTemporaryFailureتعاد المحاولة حتىRetryLimit (الافتراضي 2)، بانتظارRetry-AfterأوRetryBaseDelayMS× 2attempt (أساس 100 ms)، بسقفMaxRetryAfterMS (5,000 ms) - تجري الانتظارات شرائح 25 ms تفحص
Cancel، فمستخدم يجهض لا يجلس خمسة ثوانٍ كاملة -
signHashتعاد محاولتها فقط ما دامEnableIdempotencyمفعلاً؛ أطفئه وصار انقضاء الوقت بعد الإرسال نهائياً، لأن أحداً لا يستطيع أن يعرف هل استُخدم المفتاح فعلاً
التوقيع غير المتزامن (operationMode بقيمة "A"، وهو الافتراضي) يضيف حراساً آخر: يخزن responseID قبل استقصاء signatures/signPolling، فاستدعاء مكرر بالمعرف العملي نفسه يستأنف الاستقصاء بدل إعادة الإرسال. وتجلس الدفعات المنجزة في ذاكرة مفاتيحها المعرف العملي والاعتماد والبصمة، بسقف MaxOperationCacheEntries (128) وتعاد نسخاً عميقة. تلك الذاكرة تعيش في نسخة المزود ولا تنجو من إعادة تشغيل. أما مفتاح idempotency فينجو، لأنه مشتق لا عشوائي، فعملية معاد تشغيلها تعيد استخدام معرفها العملي ترسل المفتاح نفسه — وهل تزيل الخدمة التكرار عليه وعد الخدمة، لا وعد HotPDF
كيف تضع توقيع CSC في ملف PDF؟
مرر المزود إلى HPDFCMSSignPDFStreamWithProvider مع شهادة الكيان النهائي من GetCertificateChain؛ يبني HotPDF CMS SignedData والمزود يوقّع هضم الخصائص الموقعة. يحتاج ملف PDF المدخل العنصرين النائبين /ByteRange و /Contents الذي تكتبه THPDFPage.AddSignedSignatureField، تماماً كما في سير عمل التوقيع PAdES في HotPDF، ونموذج المزود هو نفسه المغطى في مزودات تواقيع HotPDF القابلة للوصل لـ ML-DSA و EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // تسرد CSC شهادة الكيان النهائي أولاً
if Length(Chain) = 0 then
raise Exception.Create('Credential returned no certificate');
SignOpts := HPDFCMSDefaultOptions(palBaseline_B_B);
SignOpts.DigestAlgorithm := cmsdaSHA256;
SignOpts.SignatureScheme := cmsRSAPKCS1v15;
Src := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
Dst := TFileStream.Create('contract-signed.pdf', fmCreate);
try
if not HPDFCMSSignPDFStreamWithProvider(Src, Dst, Chain[0], Provider, '', SignOpts) then
raise Exception.Create('PDF signing failed');
finally
Dst.Free;
Src.Free;
end;
end;
وتحجيم العنصر النائب /Contents يمر عبر EstimateSignatureSize، التي تعيد EstimatedSignatureBytes حين تضبطها وإلا حجم معامل RSA من طول مفتاح الاعتماد. لـ ECDSA اضبط EstimatedSignatureBytes بنفسك، وإلا بلّغ التقدير spsUnsupported. وصيغة التوقيع ذاتية الحجم تعيد التوقيع حين يتضح أن العنصر النائب صغير جداً، ولا تفعل ذلك إلا لمزودات تعلن spcSafeSignRetry — وهو ما يفعله THPDFCSCSignatureProvider فقط ما دام EnableIdempotency مفعلاً. ولأسير العمل PAdES-B-T يطلب TimestampDigest رمز طابع زمني من الخدمة نفسها عبر signatures/timestamp، بسقف MaxTimestampBytes (1 MB)
ماذا لا يفعله مزود CSC؟
لا يوقّع رسائل، هضمات فقط. Ed25519 و Ed448 في الوضع الصافي يسلّمان المزود رسالة الخصائص الموقعة كاملة (sikMessage)، ويتحقق الدفعات يرفضها بوصفها مشوهة، لأن signHash بالتعريف قائم على الهضم. ووحدة المزود تُترجم تحت Free Pascal بأنواع دوال عادية مكان الطرق المجهولة، لكن باني CMS المدفوع بالمزود يرفع استثناء تحت FPC اليوم، فتضمين توقيع CSC في PDF مسار Delphi
وهو كذلك لا يقرر السياسة. يبلّغ CredentialInfo حالة المفتاح وحالة الشهادة ونمط التصريح ومستوى SCAL وحد multisign، لكن المزود لن يرفض مفتاحاً معطلاً أو اعتماد SCAL1 من تلقاء نفسه — افحص ذلك قبل أن تري موقّعاً نافذة OTP. ونسخة مزود واحدة توقّع دفعة واحدة في كل لحظة: SignHashBatch تسلسل داخلياً حتى لا يتنافس خيطان على SAD واحدة، أي أن معدل النقل يأتي من التجميع لا من مشاركة مزود بين خيوط عاملة. وهل يكون التوقيع الناتج مؤهلاً يعتمد على خدمة الثقة واعتمادها، لا على المكتبة التي حملت الهضم إليها
مزود CSC وباني CMS و PAdES والمزودات المحلية و PKCS#11 كلها تشحن في مكون PDF من HotPDF لـ Delphi