HotPDF potpisuje PDF dokumente privatnim ključem koji drži udaljeni Cloud Signature Consortium (CSC) servis kroz THPDFCSCSignatureProvider, signature provajder koji pokreće CSC API — credential info, autorizaciju, signatures/signHash i polling — dok vaša Delphi aplikacija snabdeva HTTP transport i OAuth access token. Ključ nikada ne izlazi iz HSM-a servisa
To je sve češći jedini način da uopšte dobijete kvalifikovani ključ za potpisivanje. Trust service provajderi dele CSC endpoint i OAuth klijenta, a ne PFX fajl ili USB token, pa nema šta da se učita u lokalni certifikat store onako kako to radi potpisivanje kroz Windows cert store sa CNG i CAPI. Naivna integracija pada na predvidljive načine: signHash poziv istekne i retry potpiše isti ugovor dvaput, ili serija od četrdeset faktura okine četrdeset jednokratnih lozinki jer je svaki heš autorizovan posebno. Najveći deo onoga što provajder radi je odbrana od ta dva kvara
Zašto HotPDF prepušta HTTP vašoj aplikaciji?
Jer transport je tačno mesto gde se svaka implementacija razlikuje. Proksi, TLS pinning, klijentski sertifikati, korporativni OAuth trezori i politika logovanja sve žive u HTTP sloju, pa THPDFCSCSignatureProvider orkestrira stanje protokola i zove THPDFCSCTransport funkciju za svaki zahtev. Provajder vam predaje THPDFCSCTransportRequest sa Method-om (uvek POST), punim URL-om izgrađenim od ServiceBaseURL plus putanje endpointa, gotovim Authorization bearer headerom, ContentType-om, JSON Body-jem, IdempotencyKey-em, brojem Attempt i MaxResponseBytes. Vi popunite THPDFCSCTransportResponse sa StatusCode-om, Body-jem i RetryAfterMS, i vratite jedan od ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ili 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 // ime headera kako ga vaš servis dokumentuje
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 sa soketom ili DNS-om: ponovljivo
end;
Response.StatusCode := HttpResp.StatusCode; // prijavite 503 kakav jeste, ne klasifikujte
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;
Jedno pravilo vredi zapamtiti: vraćajte ctsSuccess kad god je server zaista odgovorio, čak i sa 503. Provajder sam klasifikuje status kodove, a transport koji 429 pretvori u ctsPermanentFailure tiho isključuje retry logiku opisanu ispod. Konstruktor je strog u drugom smeru — podiže EHPDFCSCSignatureProviderError kada transport nedostaje, CredentialID je prazan, nijedan AccessToken niti token callback nije dat, budžet je van opsega, ili ServiceBaseURL nije HTTPS. Običan http:// se prihvata samo sa AllowInsecureHTTP, što pripada testnom stubu i nigde drugde
Šta je SAD i zašto HotPDF baca jednom korišćen?
THPDFCSCSignatureProvider tretira Signature Activation Data (SAD) kao jednokratnu: briše se iz stanja provajdera u trenutku kad se signatures/signHash prihvati, čak i kad sam potpis stigne kasnije kroz asinhroni polling. SAD je dokaz servisa da je potpisnik odobrio baš ove hešove, i SAD koji vise u memoriji je autorizacija koja čeka da se potroši na pogrešan dokument
Sa podrazumevanim vrednostima iz THPDFCSCOptions.Default — RequireSAD i AutoAuthorize oba True — provajder jednom učita credentials/info, traži od vašeg THPDFCSCAuthenticationCallback authData vrednosti (OTP, PIN, šta god blok auth kredencijala traži), i šalje credentials/authorize. A 200 nosi SAD direktno; a 202 nosi ručku koja se poluje kroz credentials/authorizeCheck do MaxPollAttempts (60) puta na PollIntervalMS (250 ms). Callback sme da vrati najviše 32 vrednosti, svaku sa nepraznim ID-jem do 256 bajtova i vrednošću do 4.096 bajtova. Ako mreža padne pre nego što se signHash prihvati, automatski dobijen SAD se čuva da ista serija može da se ponovi bez ponovnog pitanja potpisnika
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async mode, 2 retry-a
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
// vaš OAuth klijent; ForceRefresh je True posle 401 odgovora servisa
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 // vaš UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
SAD koji sami prosledite kroz Options.SAD ponaša se drugačije, i to namerno. HotPDF ne može da zna za koje je hešove izdat, pa provajder koristi pretpostavljeni SAD samo za zahtev sa jednim hešom. Za seriju sa isključenim AutoAuthorize-om, provajder pada sa „CSC SAD is not pinned to the requested hash batch” umesto da nagađa
Kako SignHashBatch potpisuje mnogo dokumenata jednom autorizacijom?
SignHashBatch šalje jedan credentials/authorize i jedan signatures/signHash za do MaxBatchSignatures (64) digesta, i gradi oba tela iz istog niza tako da su numSignatures, redosled hashes i hashAlgorithmOID identični u ta dva poziva. To poklapanje je ono što CSC multisign model zahteva. Petljajte single-hash Sign metodu četrdeset puta i dobićete četrdeset autorizacija; pošaljite authorize i signHash koji se ne slažu i servis može da potroši SAD na pogrešnu seriju
Pre bilo kog mrežnog saobraćaja, provajder validira seriju. Svaki zahtev mora biti digest (sikDigest) od 1 do 1.024 bajta sa digest OID-om, i svi zahtevi moraju deliti jedan OID algoritma potpisa, jedan digest OID i, za RSASSA-PSS, jednu dužinu salt-a. Serija sa više hešova takođe učitava credentials/info i vraća spsUnsupported kada je multisign vrednost kredencijala manja od serije. SAD se zatim pribija na otisak serije — SHA-256 preko oznake verzije, broja i, po zahtevu, OID-a algoritma, digest OID-a, algoritma, dužine salt-a i digest bajtova, svako sa prefiksom dužine. Zamenite dva heša i to je druga serija koja treba svežu autorizaciju
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256 vrednosti koje ste izračunali
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo se izvodi kad je AlgorithmOID prazan
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] pripada Digests[I]; broj je proveren naspram zahteva
end;
Za RSASSA-PSS provajder šalje i signAlgoParams, base64 DER RSASSA-PSS-params strukturu sa algoritmom heša, MGF1 i dužinom salt-a. Njeno građenje znači enkodovanje OID-ova, i verzija 2.748.5 je popravila jedan ćošak toga: X.690 §8.19.4 savija prva dva luka u jednu vrednost (40 × prvi + drugi), i pod korenom 2 drugi luk iznad 39 gura tu vrednost preko 127, gde treba base-128 višebajtna forma koju raniji buildovi nisu primenjivali. Nijedan SHA-2 OID nije pogođen — 2.16 se savija u 96 — ali nepravilan OID sada podiže sopstvenu grešku provajdera umesto EConvertError
Zašto ponovljeni zahtev ne proizvodi drugi potpis?
THPDFCSCSignatureProvider čini da svaki ponovljivi poziv nosi deterministički idempotency ključ i kešira završene rezultate, pa retry posle izgubljenog odgovora vraća originalne potpise umesto da traži nove od HSM-a. Ključ je csc- pa heks SHA-256 identifikatora operacije i faze, i faza ugrađuje otisak serije za i autorizaciju i signHash. Hešovanje umesto skraćivanja je bitno: dva duga ID-ja operacija koja dele prefiks sudarila bi se pri skraćivanju, dok ključ fiksne dužine adresiran sadržajem ostaje jedinstven i stabilan kroz pokušaje
Retry politika u deljenom putu zahteva je uska namerno:
- HTTP 401 nameće tačno jedno osvežavanje tokena kroz access-token callback, pa se zahtev ponavlja jednom kada je access-token callback dodeljen; drugi 401 je konačan
- Ostali 4xx odgovori i
ctsPermanentFailurezavršavaju poziv saspsProviderError, aerror_descriptionservisa pada uLastError - 408, 429, 5xx i
ctsTemporaryFailurese ponavljaju doRetryLimit(podrazumevano 2), uz čekanje naRetry-AfteriliRetryBaseDelayMS× 2attempt (osnova 100 ms), ograničeno naMaxRetryAfterMS(5.000 ms) - Čekanja idu u rezovima od 25 ms koji proveravaju
Cancel, pa korisnik koji prekine ne sedi kroz petosekundno odugovlačenje signHashse ponavlja samo dok jeEnableIdempotencyuključen; isključite ga i tajm-aut posle predaje je konačan, jer niko ne može da zna da li je ključ već iskorišćen
Asinhrono potpisivanje (operationMode „A”, podrazumevano) dodaje još jednu zaštitu: responseID se čuva pre pollanja signatures/signPolling, pa ponovljen poziv sa istim identifikatorom operacije nastavlja polling umesto da ponovo predaje. Završene serije sede u kešu sa ključem od identifikatora operacije, kredencijala i otiska, ograničen na MaxOperationCacheEntries (128) i vraćaju se kao duboke kopije. Taj keš živi u instanci provajdera i ne preživljava restart. Idempotency ključ preživljava, jer se izvodi a nije nasumičan, pa ponovo pokrenut proces koji koristi isti identifikator operacije šalje isti ključ — da li servis na njemu deduplicira je obećanje servisa, ne HotPDF-a
Kako CSC potpis ugraditi u PDF?
Prosledite provajder HPDFCMSSignPDFStreamWithProvider zajedno sa end-entity sertifikatom iz GetCertificateChain; HotPDF gradi CMS SignedData a provajder potpisuje digest potpisanih atributa. Ulazni PDF treba /ByteRange i /Contents rezervisano mesto koje THPDFPage.AddSignedSignatureField upisuje, tačno kao u PAdES toku potpisivanja u HotPDF-u, a model provajdera je isti koji pokriva HotPDF priključivi signature provajderi za 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 navodi end-entity sertifikat prvi
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;
Dimenzionisanje /Contents rezervisanog mesta ide kroz EstimateSignatureSize, koji vraća EstimatedSignatureBytes kad ga postavite, a inače veličinu RSA modula iz dužine ključa kredencijala. Za ECDSA postavite EstimatedSignatureBytes sami, ili procena prijavljuje spsUnsupported. Auto-size varijanta potpisivanja potpisuje ponovo kad se rezervisano mesto ispostavi premalo, i to radi samo za provajdere koji reklamiraju spcSafeSignRetry — što THPDFCSCSignatureProvider radi samo dok je EnableIdempotency uključen. Za PAdES-B-T tokove, TimestampDigest traži timestamp token od istog servisa kroz signatures/timestamp, ograničeno na MaxTimestampBytes (1 MB)
Šta CSC provajder ne radi?
Ne potpisuje poruke, samo digeste. Ed25519 i Ed448 u čistom režimu predaju provajderu celu poruku potpisanih atributa (sikMessage), i validator serije to odbija kao nepravilno, jer je signHash po definiciji zasnovan na hešu. Jedinica provajdera se kompajlira pod Free Pascal-om sa običnim tipovima funkcija umesto anonimnih metoda, ali CMS builderi vođeni provajderom danas podižu izuzetak pod FPC-om, pa je ugradnja CSC potpisa u PDF Delphi putanja
Ne odlučuje ni o politici. CredentialInfo prijavljuje status ključa, status sertifikata, režim autorizacije, SCAL nivo i multisign granicu, ali provajder sam neće odbiti isključen ključ ili SCAL1 kredencijal — proverite to pre nego što potpisniku pokažete OTP upit. I jedna instanca provajdera potpisuje jednu seriju istovremeno: SignHashBatch je interno serializovan pa dve niti ne mogu da trkuju za jednim SAD-om, što znači da propusnost dolazi iz grupisanja, a ne iz deljenja provajdera preko radnih niti. Da li je rezultujući potpis kvalifikovan zavisi od trust servisa i njegovog kredencijala, ne od biblioteke koja je heš tamo odnela
CSC provajder, CMS i PAdES builderi i lokalni i PKCS#11 provajderi isporučuju se svi u HotPDF Delphi PDF component-i