Żądanie signatures/signHash do zdalnej usługi podpisywania może wypaść na timeout, gdy HSM już wyprodukował podpis, i klient nie umie powiedzieć, po której stronie tej granicy wylądował. Ponawiaj ślepo i jedno zatwierdzenie może dać dwa podpisy na tej samej umowie. Odmów ponowienia i nocna partia padnie na jednej 503. Podpisuj czterdzieści faktur po jednym hashu naraz, a podpisujący wpisze czterdzieści jednorazowych haseł
Każda integracja Cloud Signature Consortium (CSC) musi rozwiązać oba. Dostawcy usług zaufanych wydający kwalifikowane klucze coraz częściej wręczają punkt końcowy CSC API i klienta OAuth zamiast pliku PFX albo tokena USB, więc nie ma czego importować do lokalnego sklepu tak, jak działa podpisywanie ze sklepu certyfikatów Windows przez CNG i CAPI. Klucz zostaje w HSM dostawcy, a każdy podpis to podróż tam i z powrotem po HTTPS
Dlaczego HotPDF zostawia HTTP twojej aplikacji?
HotPDF podpisuje przez zdalną usługę CSC przez THPDFCSCSignatureProvider — providera podpisów, który prowadzi protokół CSC v2 (credential info, autoryzacja, signatures/signHash, pollowanie), podczas gdy Twój kod Delphi posiada transport HTTP i token OAuth. Proxy, przypinanie TLS, certyfikaty klienta, sejfy tokenów i logowanie żądań mieszkają wszystkie w warstwie HTTP i różnią się w każdym wdrożeniu
Dla każdego żądania provider buduje THPDFCSCTransportRequest i woła Twoją funkcję THPDFCSCTransport. Rekord niesie pełny URL, gotowy nagłówek bearer Authorization, Body JSON, IdempotencyKey, OperationIdentifier i numer Attempt. Twoja funkcja wypełnia THPDFCSCTransportResponse polami StatusCode, Body i RetryAfterMS, po czym zwraca ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure albo ctsCancelled
Transport zbudowany na RTL-owym THTTPClient potrzebuje około czterdziestu linii. Ciało wychodzi jako surowe bajty, więc JSON nigdy nie przechodzi przez konwersję strony kodowej:
uses
System.SysUtils, System.Classes, 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, Reply: TMemoryStream;
Headers: TNetHeaders;
HttpResp: IHTTPResponse;
begin
Response := Default(THPDFCSCTransportResponse);
Body := TMemoryStream.Create;
Reply := TMemoryStream.Create;
try
if Request.Body <> '' then
Body.WriteBuffer(Request.Body[1], Length(Request.Body));
Body.Position := 0;
Headers := [TNameValuePair.Create('Authorization', string(Request.Authorization)),
TNameValuePair.Create('Content-Type', string(Request.ContentType))];
if Request.IdempotencyKey <> '' then // nazwa nagłówka tak, jak dokumentuje ją Twoja usługa
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łopoty DNS, TLS albo socketa: do ponowienia
end;
Response.StatusCode := HttpResp.StatusCode; // przepuść 429 i 503 nietknięte
SetLength(Response.Body, Reply.Size);
if Reply.Size > 0 then
Move(Reply.Memory^, Response.Body[1], Reply.Size);
// Retry-After w sekundach; wartość HTTP-date spada tu do 0
Response.RetryAfterMS := StrToIntDef(HttpResp.HeaderValue['Retry-After'], 0) * 1000;
Result := ctsSuccess;
finally
Reply.Free;
Body.Free;
end;
end;
end;
Jedna reguła do zapamiętania: zwracaj ctsSuccess zawsze, gdy serwer odpowiedział, nawet 503. Provider sam klasyfikuje kody statusów, a transport mapujący 429 na ctsPermanentFailure wyłącza logikę ponowień opisaną niżej. Ciała większe niż MaxResponseBytes (domyślnie 2 MB) są odrzucane z spsMalformed, cokolwiek transport zwróci
Konstruktor jest surowy w drugą stronę. Podnosi EHPDFCSCSignatureProviderError, gdy brakuje transportu, ServiceBaseURL albo CredentialID jest puste, nie podano ani AccessToken, ani callbacka tokena, budżet jest poza zakresem albo URL nie jest HTTPS. Gołe http:// przechodzi tylko z ustawionym AllowInsecureHTTP, a ta flaga należy do stanowiska testowego celującego w lokalną atrapę usługi
Co to jest SAD i czemu jest jednorazowy?
THPDFCSCSignatureProvider traktuje Signature Activation Data (SAD) jako jednorazową: jest czyszczona ze stanu providera w chwili przyjęcia signatures/signHash, nawet gdy sam podpis przychodzi później przez asynchroniczne pollowanie. SAD to dowód usługi, że podpisujący zatwierdził właśnie te hashe. SAD zalegająca w pamięci to autoryzacja czekająca na wydanie na złym dokumencie
Z domyślnymi z THPDFCSCOptions.Default (RequireSAD i AutoAuthorize oba True) provider wczytuje raz credentials/info, pyta Twój THPDFCSCAuthenticationCallback o wartości authData, których żada blok auth poświadczenia — zwykle OTP albo PIN — i wysyła credentials/authorize. Odpowiedź 200 niesie SAD wprost. 202 niesie uchwyt, który provider polluje przez credentials/authorizeCheck do MaxPollAttempts (60) razy co PollIntervalMS (250 ms), chyba że usługa wyśle Retry-After. Callback może zwrócić najwyżej 32 wartości, każdą z niepustym ID do 256 bajtów i wartością do 4096 bajtów
Czyszczenie dzieje się po przyjęciu. Jeśli sieć padnie, zanim signHash zostanie przyjęty, automatycznie zdobyta SAD zostaje na miejscu, nadal związana z tą samą partią, więc ponowienie wychodzi bez drugiego pytania o OTP. Podpisujący zatwierdził raz; provider wydaje to zatwierdzenie raz
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, tryb async, 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
// TokenVault to Twój własny klient OAuth; ForceRefresh jest True po 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 // Twoje własne UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Callback tokena odpala, gdy cached token brakuje albo jest w promieniu TokenRefreshSkewSeconds (30) od zadeklarowanego przez Ciebie wygaśnięcia; ExpiresAtUTC równe zero znaczy, że token jest reużywany, aż 401 wymusi odświeżenie
SAD, którą podajesz sam przez Options.SAD, zachowuje się inaczej — i celowo. Nic nie mówi providerowi, które hashe ta SAD pokrywa, więc wstępnie ustawiona SAD jest używana wyłącznie dla żądania pojedynczego hasha. Dla wielohashowej partii z włączonym AutoAuthorize zrzuca wartość wstępną i autoryzuje partię; z wyłączonym AutoAuthorize pada z „CSC SAD is not pinned to the requested hash batch” zamiast zgadywać. Usługi autoryzujące samym zakresem OAuth i nigdy nie wystawiające SAD działają z RequireSAD ustawionym na False — wówczas człon SAD jest pomijany w ciele signHash
Podpisywanie czterdziestu faktur jednym pytaniem o OTP
SignHashBatch wysyła jedno credentials/authorize i jedno signatures/signHash dla maksymalnie MaxBatchSignatures (64) skrótów i buduje oba ciała z tej samej tablicy, więc numSignatures, kolejność hashes i hashAlgorithmOID zgadzają się między dwoma wywołaniami. Zapętl metodę pojedynczego hasha Sign czterdzieści razy, a dostaniesz czterdzieści autoryzacji. Wyślij authorize i signHash, które się różnią, a usługa może wydać SAD przeciw złej partii
Walidacja biegnie przed jakimkolwiek ruchem sieciowym. Każde żądanie musi być skrótem (sikDigest) od 1 do 1024 bajtów z OID-em skrótu, a wszystkie żądania muszą dzielić jeden algorytm podpisu, jeden OID skrótu i, dla RSASSA-PSS, jedną długość soli. Wielohashowa partia wczytuje też credentials/info i zwraca spsUnsupported, gdy wartość multisign poświadczenia jest mniejsza od rozmiaru partii. Autoryzacja jest potem związana z wrażliwym na kolejność odciskiem SHA-256 listy żądań, więc zamiana dwóch hashów daje inną partię, która potrzebuje świeżego zatwierdzenia
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
OpId: AnsiString;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: wartości SHA-256, które policzyłeś
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;
// Wyprowadź ID z własnego rekordu biegu, nigdy ze świeżego GUID-a na wywołanie
OpId := AnsiString('invoices-' + RunRecord.BatchNumber);
Status := Provider.SignHashBatch(Requests, OpId, Signatures);
if Status = spsProviderError then
// Ten sam OpId: skończona partia wraca z cache, oczekująca
// praca async wraca do pollowania, niewysłane żądanie reużywa ten sam klucz
Status := Provider.SignHashBatch(Requests, OpId, 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]; licznik był sprawdzony względem żądania
end;
Dla RSASSA-PSS provider dokleja signAlgoParams, strukturę base64 DER RSASSA-PSS-params niosącą algorytm hasza, MGF1 i długość soli. X.690 §8.19.4 składa pierwsze dwa łuki OID-a w jedną wartość (40 × pierwszy + drugi), i pod korzeniem 2 drugi łuk powyżej 39 wypycha ją ponad 127, gdzie potrzebuje wielobajtowej formy base-128. Żaden OID SHA-2 nie ćwiczy tej ścieżki, bo 2.16 składa się do 96, więc koder może przejść każdy test SHA-2 i być wciąż zły. Provider zapisuje formę wielobajtową i zgłasza zniekształcony OID własnym EHPDFCSCSignatureProviderError zamiast EConvertError
Dlaczego ponowione żądanie nie produkuje drugiego podpisu?
THPDFCSCSignatureProvider daje każdemu ponawialnemu wywołaniu 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- po czym hex SHA-256 identyfikatora operacji i fazy protokołu, a dla autoryzacji i signHash faza zawiera odcisk partii. Haszowanie zamiast obcinania trzyma klucz stałej długości i blokuje kolizje dwóch długich ID operacji ze wspólnym prefiksem
Polityka ponowień we wspólnej ścieżce żądań jest wąska:
- HTTP 401 wymusza dokładnie jedno odświeżenie tokena przez callback access-tokena, i żądanie jest powtarzane raz; druga 401 jest finalna, a bez callbacka tokena pierwsza już jest
- Inne odpowiedzi 4xx i
ctsPermanentFailurekończą wywołanie zspsProviderError, aerror_description(alboerror) usługi ląduje wLastError - 408, 429, 5xx i
ctsTemporaryFailuresą ponawiane doRetryLimit(domyślnie 2), z czekaniem naRetry-AfteralboRetryBaseDelayMS(100 ms) podwajane przy każdej próbie, z sufitemMaxRetryAfterMS(5 000 ms) - Czekania biegą w plasterkach 25 ms sprawdzających
Cancel, więc użytkownik, który przerwał, nie siedzi przez pięciosekundowy back-off signHashjest ponawiany tylko przy włączonymEnableIdempotency; wyłącz to, a timeout po wysłaniu jest finalny, bo nikt nie umie powiedzieć, czy hash był już podpisany
Podpisywanie asynchroniczne (operationMode „A”, domyślne, z validity_period równym AsyncValidityPeriodMS, 60 000 ms) dokłada jedną straż więcej. responseID jest zapisywany, zanim provider ruszy pollować signatures/signPolling, więc powtórzone wywołanie z tym samym identyfikatorem operacji podnosi oczekującą pracę zamiast wysyłać hash ponownie. Skończone partie są cacheowane per instancja providera, do MaxOperationCacheEntries (128), i ten cache znika po restarcie. Klucz idempotencji przeżywa, bo jest wyprowadzany, a nie losowy: zrestartowany proces, który reużywa swój identyfikator operacji, wysyła ten sam klucz, a to, czy usługa deduplikuje po nim, jest częścią kontraktu usługi
Całe to wisi na identyfikatorze operacji, a parametr łatwo zostawić pustym. Podaj '' do SignHashBatch, a provider spada z powrotem na OperationIdentifier żądań, potem na nowy GUID. Nowy GUID na wywołanie znaczy, że Twoja własna pętla ponowień zaczyna nową operację za każdym razem: nowy slot cache, nowy klucz idempotencji i być może drugie pytanie o OTP. Znaczy też, że Provider.Cancel nie ma czego celować, bo Twój kod nigdy nie widział GUID-a. Wyprowadź identyfikator z czegoś, co już trwale zapisujesz, jak numer partii albo ID dokumentu, i zostaw EnableIdempotency na domyślnym True. Jeśli usługa odrzuca nagłówek, zrzuć go w transporcie zamiast wyłączać opcję
Osadzanie podpisu CSC w PDF
Podaj providera do HPDFCMSSignPDFStreamWithProvider razem z certyfikatem końcowym z GetCertificateChain; HotPDF buduje CMS SignedData, a provider podpisuje skrót atrybutów podpisanych. Wejściowy PDF potrzebuje placeholdera /ByteRange i /Contents, który zapisuje THPDFPage.AddSignedSignatureField — dokładnie jak w przepływie podpisywania PAdES w HotPDF — a model providera to ten sam, który stoi za wtykowymi providerami podpisów 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;
Do wymiarowania placeholdera EstimateSignatureSize zwraca EstimatedSignatureBytes, gdy go ustawisz, w przeciwnym razie rozmiar modułu RSA wyprowadzony z długości klucza w credentials/info. Dla kluczy ECDSA odpowiada spsUnsupported, a estymator placeholdera spada z powrotem na certyfikat, więc ustawienie EstimatedSignatureBytes samodzielnie daje przewidywalny wynik. HPDFCMSSignPDFStreamWithProviderAutoSize przebudowuje i podpisuje ponownie, gdy placeholder okaże się za mały, i robi to tylko dla providerów reklamujących spcSafeSignRetry. THPDFCSCSignatureProvider go reklamuje przy włączonym EnableIdempotency — to kolejny powód, by zostawić ten domyślny w spokoju
Dla PAdES-B-T TimestampDigest prosi tę samą usługę o token RFC 3161 przez signatures/timestamp, z opcjonalnym nonce i sufitem rozmiaru MaxTimestampBytes (1 MB)
Decyzje dotyczące polityki, które zostają w Twoim kodzie
Provider podpisuje hashe. Ed25519 i Ed448 w trybie pure wręczają providerowi podpisu całą wiadomość atrybutów podpisanych (sikMessage), a walidator partii odrzuca to jako zniekształcone, bo signHash jest z definicji oparty o hash. Pod Free Pascal jednostka providera kompiluje z gołymi typami funkcji zamiast metod anonimowych, więc SignHashBatch i TimestampDigest są tam wywoływalne; osadzanie wyniku w PDF przez HPDFCMSSignPDFStreamWithProvider to ścieżka Delphi
Provider zostawia też politykę Tobie. CredentialInfo raportuje KeyStatus, CertificateStatus, AuthorizationMode, SCAL i MultiSignatures, ale wyłączony klucz albo poświadczenie SCAL1 to Twoja decyzja o odrzuceniu. Sprawdź te pola zaraz po RefreshCredentialInfo i zanim podpisujący kiedykolwiek zobaczy pytanie o OTP; odmowa od usługi po wpisaniu kodu przez podpisującego to gorsze doświadczenie niż jasny błąd z góry
Jedna instancja providera biegnie jedną partię naraz, bo SignHashBatch, RefreshCredentialInfo i TimestampDigest dzielą lock, więc dwa wątki nie wyścigują się o jedną SAD. Przepustowość bierze się z większych partii, do limitu multisign poświadczenia. Czy wynikowy podpis liczy się jako kwalifikowany, rozstrzyga usługa zaufana i poświadczenie, które wydała
Provider CSC jest dostarczany obok budowniczych CMS i PAdES oraz lokalnych providerów podpisów i PKCS#11 w komponencie HotPDF Delphi PDF, więc podpisywanie zdalne, tokenowe i plikowe wszystkie karmią te same wywołania podpisywania PDF