HotPDF חותם מסמכי PDF עם מפתח פרטי שמוחזק על ידי שירות Cloud Signature Consortium (CSC) מרוחק דרך THPDFCSCSignatureProvider, ספק חתימות שמניע את ה-CSC API — credential info, authorization, signatures/signHash ו-polling — בזמן שיישום הדלפי שלכם מספק את תעבורת ה-HTTP ואת access token של ה-OAuth. המפתח לעולם לא עוזב את ה-HSM של השירות
זו הולכת ונעשית הדרך היחידה בכלל להשיג מפתח חתימה מאושר. ספקי שירותי אמון מחלקים נקודת קצה של CSC ולקוח OAuth, לא קובץ PFX ולא token USB, ולכן אין מה לטעון אל חנות תעודות מקומית כפי שחתימה מחנות התעודות של Windows דרך CNG ו-CAPI עושה. האינטגרציה הנאיבית נכשלת בדרכים צפויות: קריאת signHash פגה בתוקף והניסיון החוזר חותם את אותו חוזה פעמיים, או שאצווה של ארבעים חשבוניות מציתה ארבעים סיסמאות חד-פעמיות כי כל hash אושר בנפרד. רוב מה שהספק עושה הוא להגן מפני שני הכשלים האלה
למה HotPDF משאירה את ה-HTTP ליישום שלכם?
כי התעבורה היא בדיוק המקום שבו כל פריסה שונה. Proxies, TLS pinning, תעודות לקוח, כספות OAuth ארגוניים ומדיניות לוגים כולם חיים בשכבת ה-HTTP, ולכן THPDFCSCSignatureProvider מתזמר את מצב הפרוטוקול וקורא לפונקציית THPDFCSCTransport עבור כל בקשה. הספק מוסר לכם THPDFCSCTransportRequest עם Method (תמיד POST), ה-URL המלא שנבנה מ-ServiceBaseURL בתוספת נתיב נקודת הקצה, כותרת 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); // תקלת socket או 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 משתקת בשקט את לוגיקת הניסיונות החוזרים שמתוארת להלן. ה-constructor קפדני בכיוון השני — הוא מעלה EHPDFCSCSignatureProviderError כשהתעבורה חסרה, כשה-CredentialID ריק, כשלא סופק לא AccessToken ולא callback של token, כשתקציב מחוץ לטווח, או כש-ServiceBaseURL אינו HTTPS. http:// פשוט מתקבל רק עם AllowInsecureHTTP, ששייך לריג בדיקות ולשום מקום אחר
מהו ה-SAD, ולמה HotPDF זורקת אותו אחרי שימוש אחד?
THPDFCSCSignatureProvider מתייחס אל Signature Activation Data (SAD) כחד-פעמי: הוא נמחק ממצב הספק ברגע ש-signatures/signHash מתקבל, גם כשהחתימה עצמה מגיעה מאוחר יותר דרך polling אסינכרוני. ה-SAD הוא ההוכחה של השירות שהחותם אישר את ה-hash-ים הספציפיים האלה, ו-SAD שנשאר לו בזיכרון הוא הרשאה שממתינה להיות מוצאת על המסמך הלא נכון
עם ברירות המחדל מ-THPDFCSCOptions.Default — RequireSAD ו-AutoAuthorize שניהם True — הספק טוען credentials/info פעם אחת, שואל את ה-THPDFCSCAuthenticationCallback שלכם עבור ערכי ה-authData (OTP, PIN, מה שבלוק ה-auth של ה-credential דורש), ושולח credentials/authorize. 200 נושא את ה-SAD ישירות; 202 נושא handle שנשאל דרך credentials/authorizeCheck עד MaxPollAttempts (60) פעמים בקצב של PollIntervalMS (250 ms). ה-callback רשאי להחזיר לכל היותר 32 ערכים, כל אחד עם מזהה לא ריק של עד 256 בתים וערך של עד 4,096 בתים. אם הרשת נופלת לפני ש-signHash מתקבל, SAD שהושג אוטומטית נשמר כדי שאותה אצווה תוכל לנסות שוב בלי לשאול את החותם שוב
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, מצב אסינכרוני, 2 ניסיונות חוזרים
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 הוא True אחרי שהשירות ענה 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 לא יכולה לדעת עבור אילו hash-ים הוא הונפק, ולכן הספק משתמש ב-SAD מוגדר מראש רק עבור בקשת hash יחיד. עבור אצווה עם AutoAuthorize כבוי, הספק נכשל עם "CSC SAD אינו נעוץ לאצוות ה-hash המבוקשת" במקום לנחש
איך SignHashBatch חותם מסמכים רבים עם הרשאה אחת?
SignHashBatch שולח credentials/authorize אחד ו-signatures/signHash אחד עבור עד MaxBatchSignatures (64) digest-ים, ובונה את שני הגופים מאותו מערך כך ש-numSignatures, סדר ה-hashes וה-hashAlgorithmOID זהים בשתי הקריאות. ההתאמה הזאת היא מה שמודל ה-multisign של CSC דורש. לולאה של מתודת ה-Sign החד-hash ארבעים פעמים מניבה ארבעים הרשאות; לשלוח authorize ו-signHash שאינם מסכימים יכול לגרום לשירות לצרוך את ה-SAD על האצווה הלא נכונה
לפני כל תנועת רשת, הספק מאמת את האצווה. כל בקשה חייבת להיות digest (sikDigest) בן בין 1 ל-1,024 בתים עם OID של digest, וכל הבקשות חייבות לחלוק OID אחד של אלגוריתם חתימה, OID אחד של digest, ועבור RSASSA-PSS, אורך מלח אחד. אצווה מרובת hash גם טוענת credentials/info ומחזירה spsUnsupported כשערך ה-multisign של ה-credential קטן מהאצווה. ה-SAD נעוץ אז אל טביעת אצבע של האצווה — SHA-256 על תווית גרסה, המספר, ולכל בקשה: OID האלגוריתם, OID ה-digest, האלגוריתם, אורך המלח ובתי ה-digest, כל אחד עם קידומת אורך. להחליף שני hash-ים וזו אצווה אחרת שזקוקה להרשאה טרייה
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 של DER ב-base64 עם אלגוריתם ה-hash, MGF1 ואורך המלח. לבנות אותו פירושו לקודד OID-ים, וגרסה 2.748.5 תיקנה פינה של זה: X.690 §8.19.4 מקפל את שני הקשתות הראשונות לערך אחד (40 × הראשונה + השנייה), ותחת השורש 2 קשת שנייה מעל 39 דוחפת את הערך מעבר ל-127, שם הוא זקוק לצורה מרובת הבתים של base-128 ש-builds קודמים לא החילו. אף OID של SHA-2 לא מושפע — 2.16 מתקפל ל-96 — אבל OID פגום עכשיו מעלה את השגיאה המשלה של הספק במקום EConvertError
למה בקשה שנוסתה שוב לא מניבה חתימה שנייה?
THPDFCSCSignatureProvider גורם לכל קריאה שניתנת לניסיון חוזר לשאת מפתח idempotency דטרמיניסטי ומקבץ ל-cache תוצאות שהושלמו, כך שניסיון חוזר אחרי תשובה שאבדה מחזיר את החתימות המקוריות במקום לבקש מה-HSM חדשות. המפתח הוא csc- ואחריו ה-SHA-256 ההקסדצימלי של מזהה הפעולה והשלב, והשלב מטמיע את טביעת האצבע של האצווה גם עבור ה-authorize וגם עבור ה-signHash. לחשב hash במקום לחתוך חשוב: שני מזהי פעולה ארוכים שחולקים קידומת היו מתנגשים תחת חיתוך, בזמן שמפתח באורך קבוע הממוען-תוכן נשאר ייחודי ויציב בין ניסיונות
מדיניות הניסיונות החוזרים בנתיב הבקשה המשותף מצומצמת במכוון:
- HTTP 401 כופה בדיוק רענון token אחד דרך callback של access-token, ואז הבקשה חוזרת פעם אחת כשcallback של access-token מוקצה; 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, כך שקריאה חוזרת עם אותו מזהה פעולה מחדשת את ה-polling במקום לשלוח מחדש. אצוות שהושלמו יושבות ב-cache הממופתח לפי מזהה פעולה, credential וטביעת אצבע, מוגבל ב-MaxOperationCacheEntries (128) ומוחזר כעותקים עמוקים. ה-cache הזה חי במופע הספק ולא שורד הפעלה מחדש. מפתח ה-idempotency כן שורד, כי הוא נגזר ולא אקראי, ולכן תהליך שהופעל מחדש שמשתמש במזהה הפעולה שלו שולח את אותו מפתח — האם השירות מדיופל על זה היא ההבטחה של השירות, לא של HotPDF
איך מכניסים חתימת CSC לתוך PDF?
מעבירים את הספק אל HPDFCMSSignPDFStreamWithProvider יחד עם תעודת ה-end-entity מ-GetCertificateChain; HotPDF בונה את ה-CMS SignedData והספק חותם את ה-digest של התכונות החתומות. קובץ ה-PDF הקלט זקוק ל-placeholder של ה-/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 מפרט את תעודת ה-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;
כיוון ה-placeholder של ה-/Contents עובר דרך EstimateSignatureSize, שמחזיר EstimatedSignatureBytes כשמגדירים אותו ואחרת את גודל ה-modulus של ה-RSA מאורך המפתח של ה-credential. עבור ECDSA מגדירים את EstimatedSignatureBytes בעצמכם, או שההערכה מדווח spsUnsupported. וריאנט החתימה בגודל אוטומטי חותם מחדש כש-placeholder מתגלה קטן מדי, והוא עושה זאת רק עבור ספקים שמכריזים על spcSafeSignRetry — מה שה-THPDFCSCSignatureProvider עושה רק בזמן שה-EnableIdempotency דלוק. עבור תהליכי PAdES-B-T, ה-TimestampDigest מבקש token חותמת-זמן מאותו שירות דרך signatures/timestamp, מוגבל ב-MaxTimestampBytes (1 MB)
מה ספק ה-CSC לא עושה?
הוא לא חותם הודעות, רק digest-ים. Ed25519 ו-Ed448 במצב pure מוסרים לספק את כל הודעת התכונות החתומות (sikMessage), ומאמת האצוות דוחה זאת כפגום, כי signHash מוגדר כמבוסס-hash. יחידת הספק מתקמפלת תחת Free Pascal עם סוגי פונקציה פשוטים במקום מתודות אנונימיות, אבל בוני ה-CMS המונעים-ספק מעלים חריגה תחת FPC כיום, ולכן הטמעת חתימת CSC ב-PDF היא נתיב דלפי
הוא גם לא מחליט מדיניות. ה-CredentialInfo מדווח את מצב המפתח, מצב התעודה, מצב האימות, רמת ה-SCAL ומגבלת ה-multisign, אבל הספק לא יסרב בעצמו למפתח מנוטרל או ל-credential SCAL1 — בדקו את אלה לפני שאתם מציגים לחותם את בקשת ה-OTP. ומופע ספק אחד חותם אצווה אחת בכל רגע: ה-SignHashBatch מסודר בפנים כך ששני תהליכונים לא יתחרו על SAD אחד, מה שאומר שהתפוקה באה מאצוות, לא משיתוף ספק בין תהליכוני עבודה. האם החתימה המתקבלת מאושרת תלויה בשירות האמון וב-credential שלו, לא בספרייה שהובילה את ה-hash לשם
ספק ה-CSC, בוני ה-CMS וה-PAdES והספקים המקומיים ושל PKCS#11 מגיעים כולם ברכיב HotPDF ל-Delphi