HotPDF podpisuje PDF dokumenty privátnym kľúčom držaným vzdialenou službou Cloud Signature Consortium (CSC) cez THPDFCSCSignatureProvider, signature provider, ktorý riadi CSC API — credential info, autorizáciu, signatures/signHash a polling — kým vaša Delphi aplikácia dodáva HTTP transport a OAuth access token. Kľúč nikdy neopustí HSM služby
To je čoraz častejšie jediný spôsob, ako sa k kvalifikovanému podpisovému kľúču vôbec dostať. Trust service providers rozdávajú CSC endpoint a OAuth clienta, nie PFX súbor alebo USB token, takže nie je čo načítať do lokálneho certifikátového úložiska, ako to robí podpisovanie cez Windows cert store s CNG a CAPI. Naivná integrácia zlyháva predvídateľnými spôsobmi: volanie signHash vyprší a opakovanie podpíše tú istú zmluvu dvakrát, alebo dávka štyridsiatich faktúr spustí štyridsať jednorazových hesiel, lebo každý hash sa autorizoval osobitne. Väčšina toho, čo provider robí, je obrana proti týmto dvom zlyhaniam
Prečo necháva HotPDF HTTP na vašej aplikácii?
Pretože transport je presne to miesto, kde sa každé nasadenie líši. Proxy, TLS pinning, klientské certifikáty, firemné OAuth trezory a logovacia politika všetky bývajú vo HTTP vrstve, takže THPDFCSCSignatureProvider riadi stav protokolu a na každú požiadavku volá funkciu THPDFCSCTransport. Provider vám podá THPDFCSCTransportRequest s Method (vždy POST), plným URL postaveným z ServiceBaseURL plus cesty endpointu, hotovou hlavičkou bearer Authorization, ContentType, JSON Body, IdempotencyKey, číslom Attempt a MaxResponseBytes. Vy naplníte THPDFCSCTransportResponse hodnotami StatusCode, Body a RetryAfterMS a vrátite jedno z ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure alebo 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 // meno hlavičky ako ho dokumentuje vaša služba
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // trable so socketom alebo DNS: opakovateľné
end;
Response.StatusCode := HttpResp.StatusCode; // hláste 503 taký, aký je, neklasifikujte
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 pravidlo, ktoré stojí za zapamätanie: vracajte ctsSuccess vždy, keď server naozaj odpovedal, aj s 503. Provider klasifikuje stavové kódy sám a transport, ktorý zmení 429 na ctsPermanentFailure, potichu vypne logiku opakovaní popísanú nižšie. Konštruktor je prísny opačným smerom — vyhodí EHPDFCSCSignatureProviderError, keď chýba transport, keď je CredentialID prázdne, keď nie je dodaný ani AccessToken, ani token callback, keď je budget mimo rozsahu alebo keď ServiceBaseURL nie je HTTPS. Holé http:// sa prijíma len s AllowInsecureHTTP, čo patrí do testovacieho rigu a nikam inam
Čo je SAD a prečo ho HotPDF vyhodí po jednom použití?
THPDFCSCSignatureProvider berie Signature Activation Data (SAD) ako jednorazovú: čistí sa zo stavu providera v momente, keď sa signatures/signHash prijme, aj keď samotný podpis dorazí neskôr cez asynchrónny polling. SAD je dôkazom služby, že podpisovateľ schválil práve tieto hashe, a SAD voľne ležiaca v pamäti je autorizácia čakajúca, kedy sa minie na nesprávny dokument
S predvolenými z THPDFCSCOptions.Default — RequireSAD aj AutoAuthorize oboje True — provider raz načíta credentials/info, poprosí váš THPDFCSCAuthenticationCallback o hodnoty authData (OTP, PIN, čokoľvek, čo žiada blok auth credentialu) a odošle credentials/authorize. 200 nesie SAD priamo; 202 nesie handle, ktorý sa polluje cez credentials/authorizeCheck až MaxPollAttempts (60) krát po PollIntervalMS (250 ms). Callback môže vrátiť najviac 32 hodnôt, každú s neprázdnym ID do 256 bajtov a hodnotou do 4 096 bajtov. Ak sieť padne skôr, než sa signHash prijme, automaticky získané SAD sa ponechá, takže tú istú dávku možno opakovať bez nového pýtania podpisovateľa
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async režim, 2 opakovania
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
// váš OAuth client; ForceRefresh je True po tom, čo služba odpovedala 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 // vaše UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
SAD, ktoré podáte sami cez Options.SAD, sa správa inak, a to zámerne. HotPDF nevie, pre ktoré hashe ho služba vydala, takže provider používa prednastavené SAD len pre požiadavku s jedným hashom. Pri dávke s vypnutým AutoAuthorize zlyhá provider s „CSC SAD is not pinned to the requested hash batch" namiesto hádania
Ako SignHashBatch podpíše mnoho dokumentov jednou autorizáciou?
SignHashBatch pošle jedno credentials/authorize a jedno signatures/signHash pre až MaxBatchSignatures (64) digestov a stavia obidve telá z toho istého poľa, takže numSignatures, poradie hashes aj hashAlgorithmOID sú v oboch volaniach identické. Presne tú zhodu vyžaduje multisign model CSC. Preloopujte jednohashovú metódu Sign štyridsaťkrát a dostanete štyridsať autorizácií; pošlite authorize a signHash, ktoré si odporujú, a služba môže minúť SAD na nesprávnu dávku
Pred akoukoľvek sieťovou prevádzkou validuje provider dávku. Každá požiadavka musí byť digest (sikDigest) o 1 až 1 024 bajtoch s digest OID a všetky požiadavky musia zdieľať jeden OID podpisového algoritmu, jeden digest OID a pri RSASSA-PSS jednu dĺžku soli. Viachashová dávka tiež načíta credentials/info a vráti spsUnsupported, keď je hodnota multisign credentialu menšia než dávka. SAD sa potom pripne na fingerprint dávky — SHA-256 cez verziu štítka, počet a pri každej požiadavke OID algoritmu, digest OID, algoritmus, dĺžku soli a bajty digestu, všetko s dĺžkovým prefixom. Vymeňte dva hashe a je to iná dávka, ktorá potrebuje čerstvú autorizáciu
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: hodnoty SHA-256, ktoré ste spočítali
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo sa odvodí, keď je AlgorithmOID prázdne
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] patrí k Digests[I]; počet sa skontroloval proti požiadavke
end;
Pre RSASSA-PSS provider tiež posiela signAlgoParams, štruktúru RSASSA-PSS-params v base64 DER s hash algoritmom, MGF1 a dĺžkou soli. Jej stavba znamená kódovanie OID a verzia 2.748.5 opravila roh toho: X.690 §8.19.4 skladá prvé dva oblúky do jednej hodnoty (40 × prvý + druhý) a pod koreňom 2 tlačí druhý oblúk nad 39 tú hodnotu nad 127, kde potrebuje viacbajtovú formu base-128, ktorú staršie zostavenia neaplikovali. Žiadny OID SHA-2 nie je dotknutý — 2.16 sa skladá na 96 — ale deformované OID teraz vyhodí vlastnú chybu providera namiesto EConvertError
Prečo opakovaná požiadavka nevyrobí druhý podpis?
THPDFCSCSignatureProvider dáva každému opakovateľnému volaniu deterministický idempotency kľúč a kešuje dokončené výsledky, takže opakovanie po strate odpovede vráti pôvodné podpisy namiesto pýtania nových od HSM. Kľúč je csc- nasledované hex SHA-256 identifikátora operácie a fázy a fáza vkladá fingerprint dávky pre autorizáciu aj signHash. Hashovanie namiesto skracovania má význam: dva dlhé identifikátory operácií zdieľajúce prefix by sa pod skracovaním zrazili, kým kľúč s pevnou dĺžkou adresovaný obsahom ostáva unikátny a stabilný cez pokusy
Politika opakovaní v zdieľanej ceste požiadaviek je účelovo úzka:
- HTTP 401 vynúti presne jednu obnovu tokenu cez access-token callback a potom sa požiadavka zopakuje raz, keď je access-token callback priradený; druhá 401 je definitívna
- Ostatné odpovede 4xx a
ctsPermanentFailureukončia volanie sspsProviderErroraerror_descriptionslužby pristane vLastError - 408, 429, 5xx a
ctsTemporaryFailuresa opakujú až doRetryLimit(predvolené 2) s čakaním naRetry-AfteraleboRetryBaseDelayMS× 2pokus (základ 100 ms), kapované naMaxRetryAfterMS(5 000 ms) - Čakania bežia v 25 ms plátkoch, ktoré kontrolujú
Cancel, takže používateľ, ktorý preruší, nesedí päťsekundové back-off signHashsa opakuje len pri zapnutomEnableIdempotency; vypnite ho a timeout po odoslaní je definitívny, lebo nikto nepovie, či sa kľúč už použil
Asynchrónne podpisovanie (operationMode „A", predvolené) pridáva ešte jednu stráž: responseID sa uchová skôr, než sa polluje signatures/signPolling, takže opakované volanie s tým istým identifikátorom operácie pokračuje v polling namiesto znovuodoslania. Dokončené dávky sedia v cache kľúčovanej identifikátorom operácie, credentialom a fingerprintom, kapovanej MaxOperationCacheEntries (128) a vracanej ako hlboké kópie. Tá cache býva v inštancii providera a neprežije reštart. Idempotency kľúč prežije, lebo sa odvodzuje, nie losuje, takže reštartovaný proces znovu používajúci svoj identifikátor operácie pošle ten istý kľúč — či na ňom služba deduplikuje, je sľub služby, nie HotPDF
Ako dostanete CSC podpis do PDF?
Podajte providera do HPDFCMSSignPDFStreamWithProvider spolu s end-entity certifikátom z GetCertificateChain; HotPDF postaví CMS SignedData a provider podpíše digest podpisovaných atribútov. Vstupné PDF potrebuje placeholder /ByteRange a /Contents, ktorý zapíše THPDFPage.AddSignedSignatureField, presne ako v workflow podpisovania PAdES v HotPDF, a provider model je ten istý, ktorý popisuje článok o zásuvných signature provideroch HotPDF pre ML-DSA a EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC listuje end-entity certifikát ako prvý
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;
Veľkosť placeholderu /Contents ide cez EstimateSignatureSize, ktorý vráti EstimatedSignatureBytes, keď ho nastavíte, a inak veľkosť RSA modulu z dĺžky kľúča credentialu. Pri ECDSA nastavte EstimatedSignatureBytes sami, inak odhad hlási spsUnsupported. Autosize podpisovacia varianta sa podpíše znovu, keď sa placeholder ukáže ako príliš malý, a robí to len pre providerov reklamujúcich spcSafeSignRetry — čo THPDFCSCSignatureProvider robí len pri zapnutom EnableIdempotency. Pre workflow PAdES-B-T žiada TimestampDigest timestamp token od tej istej služby cez signatures/timestamp, kapovaný na MaxTimestampBytes (1 MB)
Čo CSC provider nerobí?
Nepodpisuje správy, len digesty. Ed25519 a Ed448 v pure mode podajú providerovi celú správu podpisovaných atribútov (sikMessage) a validátor dávky to zamietne ako deformované, lebo signHash je z definície hash-based. Jednotka providera sa preloží pod Free Pascal s obyčajnými typmi funkcií namiesto anonymných metód, ale CMS buildery riadené providerom dnes pod FPC vyhodia výnimku, takže vloženie CSC podpisu do PDF je Delphi cesta
Nerozhoduje ani politiku. CredentialInfo hlási stav kľúča, stav certifikátu, režim autorizácie, úroveň SCAL a limit multisign, ale provider sám neodmietne neaktívny kľúč ani credential SCAL1 — skontrolujte to, skôr než ukážete podpisovateľovi OTP prompt. A jedna inštancia providera podpisuje jednu dávku naraz: SignHashBatch sa interne serializuje, takže dve vlákna nemôžu pretekať o jedno SAD, čo znamená, že priepustnosť pochádza z dávkovania, nie zo zdieľania providera cez worker vlákna. Či je výsledný podpis kvalifikovaný, závisí od trust služby a jej credentialu, nie od knižnice, ktorá tam hash doniesla
CSC provider, buildery CMS a PAdES aj lokálni a PKCS#11 provideri prichádzajú všetky v HotPDF Delphi PDF component