HotPDF اسناد PDF را با کلید خصوصیای که یک سرویس راه دور Cloud Signature Consortium (CSC) نگه میدارد امضا میکند، از طریق THPDFCSCSignatureProvider، یک signature provider که CSC API را میراند — credential info، authorization، signatures/signHash و polling — در حالی که اپلیکیشن Delphi تو ترابری HTTP و access token مربوط به OAuth را تأمین میکند. کلید هرگز HSM سرویس را ترک نمیکند
این کمکم تنها راه گرفتن یک کلید امضای qualified است. trust service providerها endpoint مربوط به CSC و یک OAuth client میدهند، نه یک فایل PFX یا یک USB token، پس چیزی نیست که مثل امضای cert store ویندوز از طریق CNG و CAPI در یک certificate store محلی بارگذاری شود. یکپارچهسازی سادهلوحانه به شیوههای قابلپیشبینی شکست میخورد: یک فراخوانی signHash timeout میخورد و retry همان قرارداد را دوبار امضا میکند، یا یک دستهٔ چهل فاکتوری چهل رمز یکبارمصرف راه میاندازد چون هر هش جداگانه مجاز شده بود. بیشتر کاری که provider میکند دفاع در برابر همین دو شکست است
چرا HotPDF کار HTTP را به اپلیکیشن تو واگذار میکند؟
چون transport دقیقاً همانجایی است که هر استقرار با بقیه فرق دارد. پراکسیها، TLS pinning، گواهیهای کلاینت، OAuth vaultهای سازمانی و سیاست لاگ همه در لایهٔ HTTP زندگی میکنند، پس THPDFCSCSignatureProvider وضعیت پروتکل را رهبری میکند و بهازای هر درخواست یک تابع THPDFCSCTransport را صدا میزند. provider یک THPDFCSCTransportRequest با Method (همیشه POST) و URL کامل ساختهشده از ServiceBaseURL بهعلاوهٔ مسیر endpoint و یک هدر bearer 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: قابل retry
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. provider کدهای وضعیت را خودش دستهبندی میکند و transportی که یک 429 را به ctsPermanentFailure تبدیل کند بیسروصدا منطق retry را که پایینتر توضیح داده شد از کار میاندازد. constructor در جهت دیگر سختگیر است — وقتی transport غایب باشد یا CredentialID خالی باشد یا نه یک AccessToken نه callback توکن داده شده باشد یا بودجه بیرون بازه باشد یا ServiceBaseURL بهجای HTTPS چیز دیگری باشد EHPDFCSCSignatureProviderError میدهد. http:// ساده فقط با AllowInsecureHTTP پذیرفته میشود که جایش یک رانر تست است و هیچجای دیگر
SAD چیست و چرا HotPDF بعد از یک بار استفاده دورش میاندازد؟
THPDFCSCSignatureProvider با Signature Activation Data (SAD) مثل یک چیز یکبارمصرف رفتار میکند: همان لحظه که signatures/signHash پذیرفته میشود از وضعیت provider پاک میشود، حتی وقتی خود امضا بعداً از طریق polling ناهمزمان میرسد. SAD مدرک سرویس است که امضاکننده این هشهای خاص را تأیید کرده، و SADی که در حافظه درنگ کند یک مجوز است که منتظر خرج شدن روی سند اشتباه مینشیند
با پیشفرضهای THPDFCSCOptions.Default — یعنی RequireSAD و AutoAuthorize هر دو True — provider یک بار credentials/info را بارگذاری میکند، از THPDFCSCAuthenticationCallback تو مقدارهای authData را میخواهد (یک OTP، یک PIN، هر چه بلوک auth مربوط به credential بخواهد) و credentials/authorize را پست میکند. یک 200 مستقیماً SAD را حمل میکند؛ یک 202 دستگیرهای حمل میکند که تا MaxPollAttempts (60) بار با فاصلهٔ PollIntervalMS (250 ms) از طریق credentials/authorizeCheck پول میشود. callback حداکثر 32 مقدار برمیگرداند، هر کدام با شناسهٔ غیرخالی حداکثر 256 بایتی و مقداری حداکثر 4,096 بایتی. اگر شبکه قبل از پذیرش signHash بریزد، یک SAD که خودکار گرفته شده نگه داشته میشود تا همان دسته بدون پرسیدن دوباره از امضاکننده قابلretry باشد
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD، AutoAuthorize، حالت ناهمزمان، 2 بار retry
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 client خودت؛ ForceRefresh بعد از جواب 401 سرویس True است
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 نمیتواند بداند برای کدام هشها صادر شده، پس provider یک SAD از پیشتنظیمشده را فقط برای یک درخواست تکهشی به کار میبرد. برای یک دسته با AutoAuthorize خاموش، provider با خطای «CSC SAD is not pinned to the requested hash batch» شکست میخورد بهجای حدس زدن
SignHashBatch چطور با یک مجوز اسناد زیادی را امضا میکند؟
SignHashBatch برای حداکثر MaxBatchSignatures (64) تا digest، یک credentials/authorize و یک signatures/signHash میفرستد و هر دو body را از همان آرایه میسازد تا numSignatures و ترتیب hashes و hashAlgorithmOID در دو فراخوانی یکسان باشند. همان تطابقی است که مدل multisign مربوط به CSC میخواهد. متد تکهشی Sign را چهل بار بچرخان و چهل مجوز میگیری؛ یک authorize و یک signHash ناسازگار بفرست و سرویس ممکن است SAD را خرج دستهٔ اشتباه کند
قبل از هر ترافیکی روی شبکه، provider دسته را اعتبارسنجی میکند. هر درخواست باید یک digest (sikDigest) از 1 تا 1,024 بایت با یک OID digest باشد و همهٔ درخواستها باید یک OID الگوریتم امضا و یک OID digest و برای RSASSA-PSS یک طول salt مشترک داشته باشند. یک دستهٔ چند-هشی credentials/info را هم بارگذاری میکند و وقتی مقدار multisign مربوط به credential از دسته کوچکتر باشد spsUnsupported برمیگرداند. بعد SAD به یک اثر انگشت دسته پین میشود — یک SHA-256 روی یک برچسب نسخه، تعداد و بهازای هر درخواست OID الگوریتم و OID digest و الگوریتم و طول salt و بایتهای digest که هر کدام با طول پیشوند میشوند. دو هش را جابهجا کن و میشود دستهٔ دیگری که مجوز تازه میخواهد
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، provider یک signAlgoParams هم میفرستد، یعنی ساختار RSASSA-PSS-params بهصورت DER و base64 با الگوریتم هش و MGF1 و طول salt. ساختنش یعنی کدگذاری OIDها، و نسخهٔ 2.748.5 گوشهای از این را fix کرد: X.690 §8.19.4 دو arc اول را در یک مقدار تاشده (40 × اول + دوم)، و زیر ریشهٔ 2 یک arc دوم بالای 39 آن مقدار را از 127 میگذراند، جایی که به شکل چندبایتی پایه-128 نیاز پیدا میکند که بیلدهای قبلی اعمالش نمیکردند. هیچ OID مربوط به SHA-2 تحتتأثیر نیست — 2.16 به 96 تاشده میشود — اما حالا یک OID بدفرم بهجای EConvertError خطای مخصوص خود provider را میدهد
چرا یک درخواست دوبارهشده امضای دومی تولید نمیکند؟
THPDFCSCSignatureProvider کاری میکند هر فراخوانی قابلretry یک کلید idempotency قطعی حمل کند و نتایج کاملشده را کش میکند، پس یک retry بعد از گم شدن جواب، امضاهای اصلی را برمیگرداند بهجای آنکه از HSM امضاهای تازه بخواهد. کلید بهشکل csc- بهعلاوهٔ هگز SHA-256 شناسهٔ عملیات و فاز است و فاز برای هر دو authorize و signHash اثر انگشت دسته را در خود جای میدهد. هش کردن بهجای بریدن مهم است: دو شناسهٔ عملیات بلند که پیشوند مشترک دارند زیر بریدن برخورد میکنند، در حالی که یک کلید محتوا-آدرس با طول ثابت در طول تلاشها یکتا و پایدار میماند
سیاست retry در مسیر درخواست مشترک عمداً باریک است:
- HTTP 401 دقیقاً یک بار refresh توکن را از طریق callback توکن دسترسی تحمیل میکند و بعد وقتی callback توکن دسترسی assign شده باشد درخواست یک بار تکرار میشود؛ یک 401 دوم قطعی است
- بقیهٔ جوابهای 4xx و
ctsPermanentFailureفراخوانی را باspsProviderErrorتمام میکنند وerror_descriptionسرویس درLastErrorمینشیند - 408 و 429 و 5xx و
ctsTemporaryFailureتاRetryLimit(پیشفرض 2) retry میشوند با انتظار برایRetry-AfterیاRetryBaseDelayMS× 2attempt (پایهٔ 100 ms) که سقفشMaxRetryAfterMS(5,000 ms) است - انتظارها در برشهای 25 میلیثانیهای اجرا میشوند که
Cancelرا بررسی میکنند، پس کاربری که لغو میکند پنج ثانیه back-off را تحمل نمیکند signHashفقط تا وقتیEnableIdempotencyروشن است retry میشود؛ خاموشش کن و یک timeout بعد از ارسال قطعی است، چون هیچکس نمیتواند بگوید کلید قبلاً استفاده شده یا نه
امضای ناهمزمان (operationMode برابر "A"، یعنی پیشفرض) یک نگهبان دیگر هم اضافه میکند: responseID قبل از پول کردن signatures/signPolling ذخیره میشود، پس یک فراخوانی تکراری با همان شناسهٔ عملیات polling را ادامه میدهد بهجای ارسال دوباره. دستههای کاملشده در یک cache به کلید شناسهٔ عملیات و credential و اثر انگشت مینشینند، با سقف MaxOperationCacheEntries (128) و بهشکل کپی عمیق برگردانده میشوند. آن cache داخل instance مربوط به provider زندگی میکند و از یک ریاستارت جان سالم به در نمیبرد. کلید idempotency میبرد، چون مشتق شده نه تصادفی، پس یک فرایند ریاستارتشده که شناسهٔ عملیاتش را دوباره استفاده کند همان کلید را میفرستد — اینکه سرویس روی آن deduplicate کند قول سرویس است، نه HotPDF
چطور یک امضای CSC را داخل PDF بگذاریم؟
provider را همراه با گواهی end-entity از GetCertificateChain به HPDFCMSSignPDFStreamWithProvider بده؛ HotPDF مربوط به CMS یعنی SignedData را میسازد و provider digestِ signed attributes را امضا میکند. PDF ورودی به جاینگهدار /ByteRange و /Contents نیاز دارد که THPDFPage.AddSignedSignatureField مینویسد، دقیقاً مثل گردشکار امضای PAdES در HotPDF، و مدل provider همان است که در signature providerهای قابلتعویض 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 گواهی end-entity را اول فهرست میکند
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 را برمیگرداند و در غیر این صورت اندازهٔ modulus مربوط به RSA را از طول کلید credential. برای ECDSA خودت EstimatedSignatureBytes را ست کن، وگرنه تخمین spsUnsupported گزارش میکند. واریانت امضای خود-اندازه وقتی جاینگهداری کوچک از آب درآمد دوباره امضا میکند و این کار را فقط برای providerهایی میکند که spcSafeSignRetry را اعلان کنند — که THPDFCSCSignatureProvider فقط تا وقتی EnableIdempotency روشن است اعلانش میکند. برای گردشکارهای PAdES-B-T، TimestampDigest از همان سرویس از طریق signatures/timestamp یک توکن timestamp میخواهد با سقف MaxTimestampBytes (1 MB)
provider مربوط به CSC چه کاری نمیکند؟
پیام امضا نمیکند، فقط digest. Ed25519 و Ed448 در حالت خالص، کل پیام signed attributes را (sikMessage) به provider میدهند و اعتبارسنج دسته آن را بهعنوان بدفرم رد میکند، چون signHash بنا به تعریف هشمحور است. واحد provider زیر Free Pascal با نوع تابع ساده بهجای متدهای بینام کامپایل میشود، اما builderهای CMS راندهشده با provider امروز زیر FPC خطا میدهند، پس embed کردن یک امضای CSC در PDF مسیر مخصوص Delphi است
سیاست هم تصمیم نمیگیرد. CredentialInfo وضعیت کلید و وضعیت گواهی و حالت مجوزدهی و سطح SCAL و سقف multisign را گزارش میکند، اما provider خودش یک کلید غیرفعال یا credential از نوع SCAL1 را رد نمیکند — قبل از اینکه OTP prompt را به امضاکننده نشان بدهی اینها را بررسی کن. و هر instance provider در هر لحظه یک دسته امضا میکند: SignHashBatch داخلی سریالایز میشود تا دو thread نتوانند سر یک SAD با هم مسابقه بدهند، یعنی توانعملیاتی از batch کردن میآید نه از به اشتراک گذاشتن یک provider بین threadهای کارگر. اینکه امضای حاصل qualified باشد به trust service و credential آن بستگی دارد، نه به کتابخانهای که هش را به آنجا رساند
provider مربوط به CSC، builderهای CMS و PAdES و providerهای محلی و PKCS#11 همه در کامپوننت PDF در Delphi از HotPDF عرضه میشوند