HotPDF podpisuje dokumenty PDF kluczem prywatnym trzymanym przez zdalną usługę Cloud Signature Consortium (CSC) za pośrednictwem THPDFCSCSignatureProvider, provider podpisu, który prowadzi CSC API — credentials/info, autoryzację, signatures/signHash i polling — podczas gdy twoja aplikacja Delphi dostarcza transport HTTP i token dostępowy OAuth. Klucz nigdy nie opuszcza HSM usługi
Corczęściej to w ogóle jedyny sposób, by dostać kwalifikowany klucz podpisujący. Dostawcy usług zaufania wydaje punkt końcowy CSC i klienta OAuth, a nie plik PFX albo token USB, więc nie ma czego wczytać do lokalnego magazynu certyfikatów tak, jak robi to podpisywanie z magazynu certyfikatów Windows przez CNG i CAPI. Naiwna integracja pada w przewidywalny sposób: wywołanie signHash dostaje timeout i ponowienie podpisuje ten sam kontrakt dwa razy, albo partia czterdziestu faktur odpala czterdzieści jednorazowych haseł, bo każdy hash był autoryzowany osobno. Większość tego, co robi provider, to obrona przed tymi dwiema porażkami
Dlaczego HotPDF zostawia HTTP twojej aplikacji?
Bo transport to dokładnie to miejsce, gdzie każde wdrożenie się różni. Proxy, przypinanie TLS, certyfikaty klienta, firmowe sejfy OAuth i polityka logowania mieszkają w warstwie HTTP, więc THPDFCSCSignatureProvider orkiestruje stan protokołu i woła funkcję THPDFCSCTransport dla każdego żądania. Provider wręcza ci THPDFCSCTransportRequest z Method (zawsze POST), pełnym URL złożonym z ServiceBaseURL plus ścieżki punktu końcowego, gotowym nagłówkiem Authorization bearer, ContentType, JSON-owym Body, IdempotencyKey, numerem Attempt i MaxResponseBytes. Ty wypełniasz THPDFCSCTransportResponse StatusCode, Body i RetryAfterMS i zwracasz jedno z ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure albo 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 // nazwa nagłówka zgodnie z dokumentacją twojej usługi
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // kłopot z gniazdem albo DNS: do ponowienia
end;
Response.StatusCode := HttpResp.StatusCode; // raportuj 503 jak leci, nie klasyfikuj
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;
Jedna reguła warta zapamiętania: zwracaj ctsSuccess, ilekroć serwer faktycznie odpowiedział, nawet z 503. Provider sam klasyfikuje kody statusów, a transport, który zamienia 429 na ctsPermanentFailure, po cichu wyłącza logikę ponowień opisaną niżej. Konstruktor jest rygorystyczny w drugą stronę — rzuca EHPDFCSCSignatureProviderError, gdy brakuje transportu, CredentialID jest puste, nie dostarczono ani AccessToken, ani callbacka tokenu, budżet jest poza zakresem albo ServiceBaseURL nie jest HTTPS. Gołe http:// jest przyjmowane tylko z AllowInsecureHTTP, co należy do stanowiska testowego i nigdzie indziej
Czym jest SAD i dlaczego HotPDF wyrzuca go po jednym użyciu?
THPDFCSCSignatureProvider traktuje Signature Activation Data (SAD) jako jednorazowe: jest czyszczone ze stanu provider w chwili, gdy signatures/signHash zostanie przyjęte, nawet gdy sam podpis przychodzi później przez asynchroniczny polling. SAD to dowód usługi, że podpisujący zaaprobował właśnie te hashe, a SAD zalegający w pamięci to autoryzacja czekająca na wydanie na niewłaściwy dokument
Z domyślnymi ustawieniami z THPDFCSCOptions.Default — RequireSAD i AutoAuthorize oba True — provider wczytuje credentials/info raz, pyta twój THPDFCSCAuthenticationCallback o wartości authData (OTP, PIN, cokolwiek żąda blok auth poświadczenia) i wysyła credentials/authorize. 200 niesie SAD wprost; 202 niesie uchwyt, który jest pollowany przez credentials/authorizeCheck do MaxPollAttempts (60) razy co PollIntervalMS (250 ms). Callback może zwrócić najwyżej 32 wartości, każda z niepustym ID do 256 bajtów i wartością do 4 096 bajtów. Jeśli sieć pada, zanim signHash zostanie przyjęty, automatycznie zdobyte SAD zostaje zachowane, żeby tę samą partię dało się ponowić bez pytania podpisującego jeszcze raz
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, tryb asynchroniczny, 2 ponowienia
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
// twój klient OAuth; ForceRefresh jest True po 401 od usługi
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 // twoje UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
SAD, który przekazujesz sam przez Options.SAD, zachowuje się inaczej i to celowo. HotPDF nie może wiedzieć, dla jakich hashy został wydany, więc provider używa wstępnie ustawionego SAD tylko dla żądania pojedynczego hasha. Dla partii z wyłączonym AutoAuthorize provider pada z „CSC SAD is not pinned to the requested hash batch”, zamiast zgadywać
Jak SignHashBatch podpisuje wiele dokumentów jedną autoryzacją?
SignHashBatch wysyła jedno credentials/authorize i jedno signatures/signHash dla do MaxBatchSignatures (64) skrótów i buduje oba ciała z tej samej tablicy, tak by numSignatures, kolejność hashes i hashAlgorithmOID były identyczne w obu wywołaniach. Ta zgodność jest tym, czego wymaga model multisign CSC. Zapętlij jednorazową metodę Sign czterdzieści razy i dostaniesz czterdzieści autoryzacji; wyślij authorize i signHash, które się nie zgadzają, a usługa może wydać SAD na niewłaściwą partię
Zanim ruszy jakikolwiek ruch sieciowy, provider waliduje partię. Każde żądanie musi być skrótem (sikDigest) od 1 do 1 024 bajtów z OID-em skrótu, a wszystkie żądania muszą dzielić jeden OID algorytmu podpisu, jeden OID skrótu i, dla RSASSA-PSS, jedną długość soli. Partia wielohashowa wczytuje też credentials/info i zwraca spsUnsupported, gdy wartość multisign poświadczenia jest mniejsza od partii. SAD jest potem przypinane do odcisku partii — SHA-256 z etykiety wersji, liczby i, na żądanie, OID algorytmu, OID skrótu, algorytmu, długości soli i bajtów skrótu, każdy z prefiksem długości. Zamień dwa hashe i to inna partia, która potrzebuje świeżej autoryzacji
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: wyliczone przez ciebie wartości SHA-256
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo wyprowadzane, gdy AlgorithmOID jest puste
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] należy do Digests[I]; liczba była sprawdzona względem żądania
end;
Dla RSASSA-PSS provider wysyła też signAlgoParams, strukturę base64 DER RSASSA-PSS-params z algorytmem skrótu, MGF1 i długością soli. Jej zbudowanie znaczy kodowanie OID-ów, a wersja 2.748.5 naprawiła róg tej sprawy: X.690 §8.19.4 składa pierwsze dwa łuki w jedną wartość (40 × pierwszy + drugi), a pod korzeniem 2 drugi łuk powyżej 39 wypycha tę wartość poza 127, gdzie potrzebuje formy wielobajtowej base-128, której wcześniejsze budowy nie stosowały. Żaden OID SHA-2 nie jest dotknięty — 2.16 składa się do 96 — ale zniekształcony OID rzuca teraz własny błąd provider zamiast EConvertError
Dlaczego ponowione żądanie nie produkuje drugiego podpisu?
THPDFCSCSignatureProvider każe każdemu ponawialnemu wywołaniu nieść deterministyczny klucz idempotencji i cacheuje skończone wyniki, więc ponowienie po zgubionej odpowiedzi zwraca oryginalne podpisy, zamiast prosić HSM o nowe. Klucz to csc- plus szesnastkowy SHA-256 identyfikatora operacji i fazy, a faza osadza odcisk partii zarówno dla autoryzacji, jak i signHash. Haszowanie zamiast ucinania ma znaczenie: dwa długie ID operacji dzielące prefiks kolidowałyby przy ucinaniu, podczas gdy stałej długości klucz adresowany treścią pozostaje unikalny i stabilny między próbami
Polityka ponowień we wspólnej ścieżce żądań jest wąska z premedytacją:
- HTTP 401 wymusza dokładnie jedno odświeżenie tokenu przez callback tokenu dostępowego, po czym żądanie jest powtarzane raz, gdy callback tokenu dostępowego jest przypisany; druga 401 jest ostateczna
- Inne odpowiedzi 4xx i
ctsPermanentFailurekończą wywołaniespsProviderError, aerror_descriptionusługi ląduje wLastError - 408, 429, 5xx i
ctsTemporaryFailuresą ponawiane doRetryLimit(domyślnie 2), z oczekiwaniem naRetry-AfteralboRetryBaseDelayMS× 2próby (baza 100 ms), z limitemMaxRetryAfterMS(5 000 ms) - Oczekiwania biegną w plastrach po 25 ms sprawdzających
Cancel, więc użytkownik, który przerwie, nie siedzi przez pięciosekundowy back-off signHashjest ponawiany tylko przy włączonymEnableIdempotency; wyłącz je, a timeout po wysłaniu jest ostateczny, bo nikt nie potrafi powiedzieć, czy klucz został już użyty
Asynchroniczne podpisywanie (operationMode „A”, domyślny) dokłada jedną strażnicę: responseID jest zapisywane przed pollowaniem signatures/signPolling, więc powtórzone wywołanie z tym samym identyfikatorem operacji wraca do pollowania, zamiast wysyłać od nowa. Skończone partie siedzą w cache kluczowanym identyfikatorem operacji, poświadczeniem i odciskiem, z limitem MaxOperationCacheEntries (128) i zwracane jako głębokie kopie. Ten cache żyje w instancji provider i nie przeżywa restartu. Klucz idempotencji przeżywa, bo jest wyprowadzany, a nie losowy, więc zrestartowany proces, który reużywa swojego identyfikatora operacji, wysyła ten sam klucz — to, czy usługa deduplikuje po nim, to obietnica usługi, nie HotPDF
Jak włożyć podpis CSC do PDF?
Przekaż provider do HPDFCMSSignPDFStreamWithProvider razem z certyfikatem końcowym z GetCertificateChain; HotPDF buduje CMS SignedData, a provider podpisuje skrót podpisanych atrybutów. Wejściowy PDF potrzebuje placeholderów /ByteRange i /Contents, które pisze THPDFPage.AddSignedSignatureField, dokładnie jak w przepływie podpisywania PAdES w HotPDF, a model provider jest ten sam, co opisany w artykule o podłączanych providerach podpisu HotPDF dla 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 wypisuje certyfikat końcowy jako pierwszy
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;
Wymiarowanie placeholdera /Contents idzie przez EstimateSignatureSize, które zwraca EstimatedSignatureBytes, gdy go ustawisz, a w przeciwnym razie rozmiar modulusa RSA z długości klucza poświadczenia. Dla ECDSA ustaw EstimatedSignatureBytes sam, albo estymacja zgłosi spsUnsupported. Wariant podpisu z autoskalowaniem podpisuje ponownie, gdy placeholder okaże się za mały, i robi to tylko dla provider reklamujących spcSafeSignRetry — co THPDFCSCSignatureProvider robi tylko przy włączonym EnableIdempotency. Dla przepływów PAdES-B-T TimestampDigest prosi tę samą usługę o token znacznika czasu przez signatures/timestamp, z limitem MaxTimestampBytes (1 MB)
Czego provider CSC nie robi?
Nie podpisuje wiadomości, tylko skróty. Ed25519 i Ed448 w trybie pure wręczają provider całą wiadomość podpisanych atrybutów (sikMessage), a walidator partii odrzuca to jako zniekształcone, bo signHash jest z definicji oparty na hashach. Jednostka provider kompiluje się pod Free Pascal ze zwykłymi typami funkcyjnymi zamiast metod anonimowych, ale buildery CMS sterowane providerem rzucają dziś pod FPC, więc osadzenie podpisu CSC w PDF to ścieżka Delphi
Nie decyduje też o polityce. CredentialInfo raportuje status klucza, status certyfikatu, tryb autoryzacji, poziom SCAL i limit multisign, ale provider sam z siebie nie odmówi wyłączonego klucza ani poświadczenia SCAL1 — sprawdź to, zanim pokażesz podpisującemu prompt o OTP. I jedna instancja provider podpisuje jedną partię naraz: SignHashBatch jest serializowany wewnętrznie, więc dwa wątki nie ścigają się o jedno SAD, co znaczy, że przepustowość bierze się z partii, a nie ze współdzielenia provider między wątkami roboczymi. To, czy wynikowy podpis jest kwalifikowany, zależy od usługi zaufania i jej poświadczenia, nie od biblioteki, która zawiozła tam hash
Provider CSC, buildery CMS i PAdES oraz providery lokalny i PKCS#11 trafiają razem do komponentu PDF HotPDF dla Delphi