HotPDF semnează documente PDF cu o cheie privată deținută de un serviciu remote Cloud Signature Consortium (CSC) prin THPDFCSCSignatureProvider, un signature provider care conduce API-ul CSC — credential info, autorizare, signatures/signHash și polling — în timp ce aplicația voastră Delphi furnizează transportul HTTP și token-ul de acces OAuth. Cheia nu părăsește niciodată HSM-ul serviciului
Asta devine din ce în ce mai mult singura cale de a obține măcar o cheie de semnare calificată. Furnizorii de servicii de încredere dau un endpoint CSC și un client OAuth, nu un fișier PFX sau un token USB, deci nu există nimic de încărcat într-un magazin de certificate local așa cum face semnarea din magazinul de certificate Windows prin CNG și CAPI. Integrația naivă eșuează în moduri previzibile: un apel signHash dă timeout și retry-ul semnează același contract de două ori, sau un lot de patruzeci de facturi declanșează patruzeci de parole de unică folosință pentru că fiecare hash a fost autorizat separat. Cea mai mare parte din ce face provider-ul e apărarea împotriva celor două eșecuri
De ce lasă HotPDF HTTP-ul în seama aplicației voastre?
Pentru că transportul e exact locul în care fiecare implementare diferă. Proxy-urile, TLS pinning, certificatele de client, seifurile corporative OAuth și politica de logging trăiesc toate în stratul HTTP, deci THPDFCSCSignatureProvider orchestrează starea protocolului și apelează o funcție THPDFCSCTransport pentru fiecare cerere. Provider-ul vă dă un THPDFCSCTransportRequest cu Method (mereu POST), URL-ul complet construit din ServiceBaseURL plus calea endpoint-ului, un header Authorization bearer gata făcut, ContentType, Body-ul JSON, un IdempotencyKey, numărul Attempt și MaxResponseBytes. Voi umpleți un THPDFCSCTransportResponse cu StatusCode, Body și RetryAfterMS și întoarceți una dintre ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure sau 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 // numele header-ului așa cum îl documentează serviciul vostru
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // problemă de socket sau DNS: reîncercabil
end;
Response.StatusCode := HttpResp.StatusCode; // raportați 503 ca atare, nu clasificați
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;
Regula unică care merită memorată: întoarceți ctsSuccess de fiecare dată când un server chiar a răspuns, chiar și cu un 503. Provider-ul clasifică singur codurile de stare, iar un transport care transformă un 429 în ctsPermanentFailure dezactivează în tăcere logica de retry descrisă mai jos. Constructorul e strict în cealaltă direcție — ridică EHPDFCSCSignatureProviderError când transportul lipsește, CredentialID e gol, nu e furnizat niciun AccessToken și niciun callback de token, un buget e în afara intervalului sau ServiceBaseURL nu e HTTPS. http:// simplu e acceptat doar cu AllowInsecureHTTP, care își are locul într-un banc de test și nicăieri altundeva
Ce este SAD-ul și de ce îl aruncă HotPDF după o singură folosire?
THPDFCSCSignatureProvider tratează Signature Activation Data (SAD) ca single-use: e curățată din starea provider-ului în momentul în care signatures/signHash e acceptat, chiar dacă semnătura în sine sosește mai târziu prin polling asincron. SAD-ul e dovada serviciului că semnatarul a aprobat tocmai acești hash-i, iar o SAD care rămâne în memorie e o autorizare care așteaptă să fie cheltuită pe documentul greșit
Cu implicitele din THPDFCSCOptions.Default — RequireSAD și AutoAuthorize ambele True — provider-ul încarcă credentials/info o dată, cere callback-ului vostru THPDFCSCAuthenticationCallback valorile authData (un OTP, un PIN, orice cere blocul auth al credential-ului) și post-ează credentials/authorize. Un 200 cară SAD-ul direct; un 202 cară un handle care e poll-uit prin credentials/authorizeCheck până la MaxPollAttempts (60) de ori la PollIntervalMS (250 ms). Callback-ul poate întoarce cel mult 32 de valori, fiecare cu un ID non-gol de cel mult 256 de octeți și o valoare de cel mult 4.096 de octeți. Dacă rețeaua pică înainte ca signHash să fie acceptat, o SAD obținută automat e păstrată, ca același lot să poată fi reîncercat fără a mai întreba semnatarul
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, mod async, 2 retry-uri
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
// clientul vostru OAuth; ForceRefresh e True după ce serviciul a răspuns 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 // UI-ul vostru
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
O SAD pe care o pasați voi prin Options.SAD se comportă diferit, și asta în mod deliberat. HotPDF nu poate ști pentru ce hash-i a fost emisă, deci provider-ul folosește o SAD presetată doar pentru o cerere cu un singur hash. Pentru un lot cu AutoAuthorize oprit, provider-ul eșuează cu „CSC SAD is not pinned to the requested hash batch” în loc să ghicească
Cum semnează SignHashBatch multe documente cu o singură autorizare?
SignHashBatch trimite un singur credentials/authorize și un singur signatures/signHash pentru până la MaxBatchSignatures (64) de digest-uri și construiește ambele body-uri din același tablou, astfel încât numSignatures, ordinea hashes și hashAlgorithmOID să fie identice în cele două apeluri. Potrivirea aceea e exact ce cere modelul multisign CSC. Buclați metoda single-hash Sign de patruzeci de ori și primiți patruzeci de autorizări; trimiteți un authorize și un signHash care nu sunt de acord și serviciul poate consuma SAD-ul pe lotul greșit
Înainte de orice trafic de rețea, provider-ul validează lotul. Fiecare cerere trebuie să fie un digest (sikDigest) de 1 până la 1.024 de octeți cu un OID de digest, iar toate cererile trebuie să partajeze un OID de algoritm de semnătură, un OID de digest și, pentru RSASSA-PSS, o lungime de salt. Un lot multi-hash mai încarcă și credentials/info și întoarce spsUnsupported când valoarea multisign a credential-ului e mai mică decât lotul. SAD-ul e apoi fixat pe o amprentă a lotului — un SHA-256 peste o etichetă de versiune, numărătoarea și, per cerere, OID-ul algoritmului, OID-ul digest-ului, algoritmul, lungimea salt-ului și octeții digest-ului, fiecare prefixat cu lungimea. Schimbați doi hash-i între ei și e un alt lot, care are nevoie de o autorizare proaspătă
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: valorile SHA-256 pe care le-ați calculat
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo derivat când AlgorithmOID e gol
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] aparține lui Digests[I]; numărul a fost verificat față de cerere
end;
Pentru RSASSA-PSS, provider-ul mai trimite și signAlgoParams, o structură RSASSA-PSS-params DER în base64 cu algoritmul de hash, MGF1 și lungimea salt-ului. Construirea ei înseamnă encodare de OID-uri, iar versiunea 2.748.5 a reparat un colț din asta: X.690 §8.19.4 pliază primele două arce într-o singură valoare (40 × primul + al doilea), iar sub rădăcina 2 un al doilea arc peste 39 împinge valoarea aceea peste 127, unde are nevoie de forma multi-byte base-128 pe care build-urile anterioare nu o aplicau. Niciun OID SHA-2 nu e afectat — 2.16 se pliază la 96 — dar un OID malformat ridică acum eroarea proprie a provider-ului în loc de un EConvertError
De ce o cerere reîncercată nu produce o a doua semnătură?
THPDFCSCSignatureProvider face ca fiecare apel reîncercabil să cară o cheie de idempotență deterministă și ține cache rezultatele completate, deci un retry după un răspuns pierdut întoarce semnăturile originale în loc să ceară HSM-ului unele noi. Cheia e csc- urmat de SHA-256 hexazecimal al identificatorului de operație și al fazei, iar faza încorporează amprenta lotului atât pentru autorizare, cât și pentru signHash. Hash-ing în loc de trunchiere contează: două ID-uri de operație lungi care partajează un prefix ar colisiona la trunchiere, în timp ce o cheie de lungime fixă, adresată după conținut, rămâne unică și stabilă între încercări
Politica de retry din calea de cereri partajată e îngustă din principiu:
- Un HTTP 401 forțează exact un refresh de token prin callback-ul de access-token, apoi cererea e repetată o dată când un callback de access-token e asignat; un al doilea 401 e final
- Celelalte răspunsuri 4xx și
ctsPermanentFailuretermină apelul cuspsProviderError, iarerror_description-ul serviciului aterizează înLastError - 408, 429, 5xx și
ctsTemporaryFailuresunt reîncercate până laRetryLimit(implicit 2), așteptândRetry-AftersauRetryBaseDelayMS× 2attempt (bază 100 ms), plafonat laMaxRetryAfterMS(5.000 ms) - Așteptările rulează în felii de 25 ms care verifică
Cancel, deci un utilizator care abandonează nu stă printr-un back-off de cinci secunde signHashe reîncercat doar cât timpEnableIdempotencye pornit; opriți-l și un timeout după trimitere e final, pentru că nimeni nu poate spune dacă cheia fusese deja folosită
Semnarea asincronă (operationMode „A”, implicitul) adaugă o gardă în plus: responseID-ul e stocat înainte de polling-ul lui signatures/signPolling, deci un apel repetat cu același identificator de operație reia polling-ul în loc să re-trimite. Loturile completate stau într-un cache cheiat după identificator de operație, credential și amprentă, plafonat de MaxOperationCacheEntries (128) și întors ca copii profunde. Cache-ul acela trăiește în instanța provider-ului și nu supraviețuiește unui restart. Cheia de idempotență da, pentru că e derivată, nu aleatoare, deci un proces repornit care își refolosește identificatorul de operație trimite aceeași cheie — dacă serviciul deduplichează pe ea e promisiunea serviciului, nu a HotPDF
Cum puneți o semnătură CSC într-un PDF?
Pasați provider-ul către HPDFCMSSignPDFStreamWithProvider împreună cu certificatul end-entity de la GetCertificateChain; HotPDF construiește CMS SignedData, iar provider-ul semnează digest-ul atributelor semnate. PDF-ul de intrare are nevoie de placeholder-ele /ByteRange și /Contents pe care THPDFPage.AddSignedSignatureField le scrie, exact ca în fluxul de semnare PAdES din HotPDF, iar modelul de provider e același acoperit în provider-ele de semnătură plugabile HotPDF pentru ML-DSA și EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC listează certificatul end-entity primul
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;
Dimensionarea placeholder-ului /Contents trece prin EstimateSignatureSize, care întoarce EstimatedSignatureBytes când îl setați și, în caz contrar, dimensiunea modulului RSA din lungimea cheii credential-ului. Pentru ECDSA setați voi EstimatedSignatureBytes, sau estimarea raportează spsUnsupported. Varianta de semnare cu dimensionare automată re-semnează când un placeholder iese prea mic, și face asta doar pentru provider-e care anunță spcSafeSignRetry — lucru pe care THPDFCSCSignatureProvider îl face doar cât timp EnableIdempotency e pornit. Pentru fluxuri PAdES-B-T, TimestampDigest cere un timestamp token de la același serviciu prin signatures/timestamp, plafonat la MaxTimestampBytes (1 MB)
Ce nu face provider-ul CSC?
Nu semnează mesaje, ci doar digest-uri. Ed25519 și Ed448 în modul pur dau provider-ului tot mesajul de atribute semnate (sikMessage), iar validatorul de lot respinge asta ca malformat, pentru că signHash e prin definiție bazat pe hash. Unitatea provider-ului se compilează sub Free Pascal cu tipuri de funcții simple în locul metodelor anonime, dar builder-ele CMS conduse de provider ridică excepție sub FPC în prezent, deci încorporarea unei semnături CSC într-un PDF e o cale Delphi
Nu decide nici politica. CredentialInfo raportează starea cheii, starea certificatului, modul de autorizare, nivelul SCAL și limita multisign, dar provider-ul nu va refuza singur o cheie dezactivată sau un credential SCAL1 — verificați-le înainte să arătați semnatarului promptul de OTP. Și o instanță de provider semnează un singur lot odată: SignHashBatch e serializat intern, deci două thread-uri nu pot intra în cursă pentru o SAD, ceea ce înseamnă că throughput-ul vine din gruparea în loturi, nu din partajarea unui provider între worker threads. Dacă semnătura rezultată e calificată depinde de serviciul de încredere și de credential-ul lui, nu de biblioteca care a dus hash-ul acolo
Provider-ul CSC, builder-ele CMS și PAdES și provider-ele local și PKCS#11 se livrează în HotPDF Delphi PDF component