HotPDF podepisuje PDF dokumenty privátním klíčem drženým vzdálenou službou Cloud Signature Consortium (CSC) přes THPDFCSCSignatureProvider, signature providera, který řídí CSC API — credential info, autorizaci, signatures/signHash a polling — zatímco vaše Delphi aplikace dodává HTTP transport a OAuth access token. Klíč nikdy neopustí HSM služby
To je čím dál tím jediná cesta, jak k kvalifikovanému podepisovacímu klíči vůbec dojít. Trust service providery rozdávají CSC endpoint a OAuth klienta, ne soubor PFX ani USB token, takže není co načítat do lokálního certificate store, jako to dělá podepisování z Windows cert store přes CNG a CAPI. Naivní integrace selhává předvídatelně: volání signHash timeoutne a retry podepíše tutéž smlouvu dvakrát, nebo dávka čtyřiceti faktur spustí čtyřicet jednorázových hesel, protože každý hash se autorizoval zvlášť. Většina toho, co provider dělá, je obrana proti těm dvěma selháním
Proč nechává HotPDF HTTP na vaší aplikaci?
Protože transport je přesně to místo, kde se každé nasazení liší. Proxy, TLS pinning, klient certifikáty, firemní OAuth vaulty a logovací politika všechno bydlí v HTTP vrstvě, takže THPDFCSCSignatureProvider diriguje stav protokolu a na každý požadavek volá funkci THPDFCSCTransport. Provider vám podá THPDFCSCTransportRequest s Method (vždy POST), plným URL postaveným z ServiceBaseURL plus cestou endpointu, hotovou hlavičkou Authorization bearer, ContentType, JSON Body, IdempotencyKey, číslem Attempt a MaxResponseBytes. Vy naplníte THPDFCSCTransportResponse hodnotami StatusCode, Body a RetryAfterMS a vrátíte jedno z ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure nebo 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 // název hlavičky podle dokumentace vaší služby
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // potíže se socketem nebo DNS: opakovatelné
end;
Response.StatusCode := HttpResp.StatusCode; // hlásit 503 as-is, neklasifikovat
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 stojí za zapamatování: vracet ctsSuccess, kdykoli server skutečně odpověděl, i s 503. Provider si klasifikuje status kódy sám a transport, který 429 otočí na ctsPermanentFailure, potichu vypne retry logiku popsanou níže. Konstruktor je přísný opačným směrem — vyhodí EHPDFCSCSignatureProviderError, když transport chybí, CredentialID je prázdné, není dodán ani AccessToken ani token callback, budget je mimo rozsah, nebo ServiceBaseURL není HTTPS. Holé http:// se akceptuje jen s AllowInsecureHTTP, což patří do testovacího rigu a nikam jinam
Co je SAD a proč ji HotPDF po jednom použití zahodí?
THPDFCSCSignatureProvider bere Signature Activation Data (SAD) jako jednorázovou: maže se ze stavu providera v momentě, kdy se signatures/signHash akceptuje, i když samotný podpis dorazí později asynchronním pollingem. SAD je důkazem služby, že podepisující schválil právě tyhle hashe, a SAD visící v paměti je autorizace čekající, až se utratí za špatný dokument
S defaulty z THPDFCSCOptions.Default — RequireSAD i AutoAuthorize obě True — načte provider jednou credentials/info, zeptá se vašeho THPDFCSCAuthenticationCallback na hodnoty authData (OTP, PIN, cokoli, co žádá blok auth credentialu) a pošle credentials/authorize. 200 nese SAD přímo; 202 nese handle, který se polluje přes credentials/authorizeCheck nejvýš MaxPollAttempts (60) krát po PollIntervalMS (250 ms). Callback smí vrátit nejvýš 32 hodnot, každou s neprázdným ID do 256 bajtů a hodnotou do 4 096 bajtů. Když síť vypadne, než se signHash akceptuje, automaticky získaná SAD se podrží, aby se tatáž dávka dala opakovat, aniž by se podepisující ptal znovu
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async mód, 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
// váš OAuth klient; ForceRefresh je True, když služba odpověděla 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, kterou podáte sami přes Options.SAD, se chová jinak, a to záměrně. HotPDF nemůže vědět, pro které hashe se vystavila, takže provider použije přednastavenou SAD jen pro požadavek s jedním hashem. U dávky s vypnutým AutoAuthorize selže provider s „CSC SAD is not pinned to the requested hash batch“ místo hádání
Jak podepisuje SignHashBatch mnoho dokumentů s jednou autorizací?
SignHashBatch pošle jedno credentials/authorize a jedno signatures/signHash pro nejvýš MaxBatchSignatures (64) digestů a staví obě těla ze stejného pole, takže numSignatures, pořadí hashes a hashAlgorithmOID jsou v obou voláních identické. Ta shoda je to, co model CSC multisign vyžaduje. Loopněte single-hash metodu Sign čtyřicetkrát a dostanete čtyřicet autorizací; pošlete authorize a signHash, které si nerozumí, a služba může utratit SAD proti špatné dávce
Před jakýmkoli síťovým provozem validuje provider dávku. Každý požadavek musí být digest (sikDigest) o 1 až 1 024 bajtech s digest OID a všechny požadavky musí sdílet jeden OID podpisového algoritmu, jeden digest OID a u RSASSA-PSS jednu délku saltu. Multi-hash dávka taky načítá credentials/info a vrátí spsUnsupported, když hodnota multisign credentialu je menší než dávka. SAD se pak připíchne na fingerprint dávky — SHA-256 přes label verze, počet a na požadavek OID algoritmu, digest OID, algoritmus, délku saltu a bajty digestu, každé s délkovým prefixem. Vyměňte dva hashe a je to jiná dávka, která potřebuje čerstvou autorizaci
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256 hodnoty, které jste spočítali
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo se odvodí, když je AlgorithmOID prázdný
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] patří Digests[I]; počet se zkontroloval proti požadavku
end;
Pro RSASSA-PSS posílá provider taky signAlgoParams, strukturu RSASSA-PSS-params jako base64 DER s hash algoritmem, MGF1 a délkou saltu. Její stavba znamená kódování OID a verze 2.748.5 opravila jeden kout: X.690 §8.19.4 skládá první dva arkusy do jedné hodnoty (40 × první + druhý) a pod kořenem 2 posune druhý arkus nad 39 tu hodnotu za 127, kde už potřebuje base-128 vícebajtovou formu, kterou dřívější buildy neaplikovaly. Žádný OID SHA-2 se nedotkne — 2.16 se složí na 96 — ale deformovaný OID teď vyhodí vlastní chybu providera místo EConvertError
Proč opakovaný požadavek nevyprodukuje druhý podpis?
THPDFCSCSignatureProvider zařizuje, že každé opakovatelné volání nese deterministický idempotency key a cachuje dokončené výsledky, takže retry po ztracené odpovědi vrátí původní podpisy místo ptaní HSM po nových. Klíč je csc- následované hex SHA-256 z identifikátoru operace a fáze a fáze vkládá fingerprint dávky pro autorizaci i signHash. Hashování místo useknutí záleží: dva dlouhé operation ID sdílející prefix by se pod useknutím srazily, zatímco fixně dlouhý content-addressed klíč zůstává unikátní a stabilní napříč pokusy
Retry politika ve sdílené cestě požadavků je záměrně úzká:
- HTTP 401 vynutí přesně jednu obnovu tokenu přes access-token callback a pak se požadavek zopakuje jednou, když je access-token callback přiřazen; druhé 401 je konečné
- Ostatní odpovědi 4xx a
ctsPermanentFailureukončí volání sspsProviderErroraerror_descriptionslužby přistane vLastError - 408, 429, 5xx a
ctsTemporaryFailurese opakují doRetryLimit(defaultně 2), s čekáním naRetry-AfterneboRetryBaseDelayMS× 2pokus (základ 100 ms), zastropované naMaxRetryAfterMS(5 000 ms) - Čekání běží po 25 ms plátkách kontrolujících
Cancel, takže uživatel, který to přeruší, nesedí pětisekundový back-off signHashse opakuje jen dokud jeEnableIdempotencyzapnuté; vypněte ho a timeout po odeslání je konečný, protože nikdo nedokáže říct, jestli se klíč už použil
Asynchronní podepisování (operationMode „A“, default) přidává ještě jednu pojistku: responseID se ukládá před pollingem signatures/signPolling, takže opakované volání s tímtéž identifikátorem operace naváže pollingem místo opětovného odeslání. Dokončené dávky sedí v cache klíčované identifikátorem operace, credentialem a fingerprintem, zastropované MaxOperationCacheEntries (128) a vracené jako deep copies. Ta cache bydlí v instanci providera a nepřežije restart. Idempotency key přežije, protože se odvozuje, ne hází náhodně, takže restartovaný proces, který znovu použije svůj identifikátor operace, pošle týž klíč — jestli na něm služba deduplikuje, je slib služby, ne HotPDF
Jak dostanete CSC podpis do PDF?
Podejte provider do HPDFCMSSignPDFStreamWithProvider spolu s end-entity certifikátem z GetCertificateChain; HotPDF postaví CMS SignedData a provider podepíše digest signed attributes. Vstupní PDF potřebuje placeholder /ByteRange a /Contents, který zapisuje THPDFPage.AddSignedSignatureField, přesně jako v PAdES workflow podepisování v HotPDF, a model providera je týž, jaký pokrývá článek o pluggable signature providers HotPDF pro 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 vyjmenovává end-entity certifikát první
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;
Velikost placeholderu /Contents jde přes EstimateSignatureSize, který vrátí EstimatedSignatureBytes, když ho nastavíte, a jinak velikost RSA modulu z délky klíče credentialu. Pro ECDSA si EstimatedSignatureBytes nastavte sami, jinak odhad nahlásí spsUnsupported. Auto-size podpisová varianta podepíše znovu, když se placeholder ukáže jako malý, a dělá to jen pro providery hlásící spcSafeSignRetry — což THPDFCSCSignatureProvider dělá jen dokud je EnableIdempotency zapnuté. Pro workflow PAdES-B-T žádá TimestampDigest timestamp token od téže služby přes signatures/timestamp, zastropované na MaxTimestampBytes (1 MB)
Co CSC provider nedělá?
Nepodepisuje zprávy, jen digesty. Ed25519 a Ed448 v pure módu podají providerovi celou zprávu signed attributes (sikMessage) a batch validator ji odmítne jako deformovanou, protože signHash je z definice hash-based. Unit providera se kompiluje pod Free Pascal s obyčejnými typy funkcí místo anonymních metod, ale CMS buildery řízené providerem dnes pod FPC vyhazují, takže vkládání CSC podpisu do PDF je Delphi cesta
Nerozhoduje ani o politice. CredentialInfo hlásí status klíče, status certifikátu, autorizační mód, úroveň SCAL a limit multisign, ale provider sám neodmítne vypnutý klíč ani credential SCAL1 — zkontrolujte to, než ukážete podepisujícímu OTP prompt. A jedna instance providera podepisuje najednou jednu dávku: SignHashBatch je interně serializovaný, takže dvě vlákna si nemůžou zacházet o jednu SAD, což znamená, že throughput pochází z dávkování, ne ze sdílení providera napříč worker vlákny. Jestli výsledný podpis je kvalifikovaný, záleží na trust službě a jejím credentialu, ne na knihovně, která hash tam donesla
CSC provider, CMS a PAdES buildery i lokální a PKCS#11 providery všechny vycházejí v HotPDF Delphi PDF component