Odborný článok

HotPDF CSC vzdialený podpis: cloudové PDF podpisy v Delphi

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

Diagram transportnej hranice HotPDF CSC: THPDFCSCSignatureProvider riadi protokol a podá vášmu kódu THPDFCSCTransportRequest s metódou POST, plným URL, hotovou hlavičkou Authorization bearer, JSON telom, IdempotencyKey a číslom pokusu a vy vrátite StatusCode, Body, RetryAfterMS plus jedno zo štyroch cts stavov, kým kľúč nikdy neopustí HSM
Provider klasifikuje stavové kódy sám, takže transport, ktorý zodpovedané 503 premení na trvalé zlyhanie, potichu vypne logiku opakovaní, kým proxy a TLS politika zostávajú vo vašom kóde
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

Diagram životného cyklu SAD v HotPDF: s RequireSAD a AutoAuthorize provider raz načíta credentials/info, poprosí autentifikačný callback o hodnoty OTP alebo PIN, odošle credentials/authorize, polluje credentials/authorizeCheck až 60 krát po 250 ms, keď odpoveď je 202, a čistí Signature Activation Data v momente, keď sa signatures/signHash prijme, pričom získané SAD sa ponechá, keď sieť padla pred prijatím
SAD voľne ležiaca v pamäti je autorizácia čakajúca, kedy sa minie na nesprávny dokument, a prednastavené SAD podané cez options sa použije len pre požiadavku s jedným hashom
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 ctsPermanentFailure ukončia volanie s spsProviderError a error_description služby pristane v LastError
  • 408, 429, 5xx a ctsTemporaryFailure sa opakujú až do RetryLimit (predvolené 2) s čakaním na Retry-After alebo RetryBaseDelayMS × 2pokus (základ 100 ms), kapované na MaxRetryAfterMS (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
  • signHash sa opakuje len pri zapnutom EnableIdempotency; vypnite ho a timeout po odoslaní je definitívny, lebo nikto nepovie, či sa kľúč už použil
Diagram politiky opakovaní HotPDF: každé opakovateľné volanie nesie deterministický idempotency kľúč csc- hashovaný z identifikátora operácie a fázy, HTTP 401 vynúti presne jednu obnovu tokenu, ostatné odpovede 4xx končia s spsProviderError a 408, 429, 5xx alebo dočasné zlyhanie transportu sa opakujú až do RetryLimit 2 s čakaním na Retry-After alebo exponenciálny back-off kapovaný na 5 000 ms
Dokončené dávky sa kešujú podľa identifikátora operácie, credentialu a fingerprintu a v asynchrónnom režime uložené responseID nechá opakované volanie pokračovať v polling namiesto znovuodoslania hashu

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