Tehnički članak

HotPDF CSC daljinsko potpisivanje: PDF potpisi u oblaku

HotPDF potpisuje PDF dokumente privatnim ključem koji drži udaljeni Cloud Signature Consortium (CSC) servis kroz THPDFCSCSignatureProvider, signature providera koji vodi CSC API — credential info, autorizaciju, signatures/signHash i polling — dok vaša Delphi aplikacija opskrbljuje HTTP transport i OAuth access token. Ključ nikad ne napušta HSM tog servisa

To je sve češće jedini način da uopće dobijete kvalificirani ključ za potpisivanje. Trust service provideri isporučuju CSC endpoint i OAuth klijenta, a ne PFX datoteku ili USB token, pa nema ničega za učitati u lokalni certifikat store onako kako to čini potpisivanje iz Windows cert storea kroz CNG i CAPI. Naivna integracija pada na predvidljive načine: signHash poziv istekne i retry potpiše isti ugovor dvaput, ili batch od četrdeset računa okine četrdeset jednokratnih lozinki jer je svaki hash autoriziran odvojeno. Najveći dio onoga što provider radi jest obrana od te dvije vrste kvara

Zašto HotPDF prepušta HTTP vašoj aplikaciji?

Zato je transport upravo ono mjesto gdje se svaki deployment razlikuje. Proxyji, TLS pinning, klijentski certifikati, korporativni OAuth trezori i logging politika svi žive u HTTP sloju, pa THPDFCSCSignatureProvider orkestrira stanje protokola i za svaki zahtjev zove funkciju THPDFCSCTransport. Provider vam preda THPDFCSCTransportRequest sa sljedećim: Method (uvijek POST), puni URL sastavljen od ServiceBaseURL plus putanje endpointa, gotovo Authorization bearer zaglavlje, ContentType, JSON Body, IdempotencyKey, broj Attempt i MaxResponseBytes. Vi napunite THPDFCSCTransportResponse s StatusCode, Body i RetryAfterMS, i vratite jedan od ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ili ctsCancelled

Dijagram granice transporta u HotPDF CSC: THPDFCSCSignatureProvider orkestrira protokol i preda vašem kodu THPDFCSCTransportRequest s POST metodom, punim URL-om, gotovim Authorization bearer zaglavljem, JSON body-em, IdempotencyKey-em i brojem pokušaja, a vi vraćate StatusCode, Body, RetryAfterMS plus jednu od četiriju cts status vrijednosti dok ključ nikad ne napušta HSM
Provider klasificira status kodove sam, pa transport koji odgovoreni 503 preklopi u trajni kvar tiho isključi retry logiku, dok proxyi i TLS politika ostaju u kodu koji posjedujete
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  // ime zaglavlja kako ga dokumentira vaš servis
          Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
            string(Request.IdempotencyKey))];
        try
          HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
        except
          on ENetHTTPClientException do
            Exit(ctsTemporaryFailure);            // problem socketa ili DNS-a: retryable
        end;
        Response.StatusCode := HttpResp.StatusCode;   // javi 503 kakav jest, ne klasificiraj
        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 pravilo vrijedi naučiti napamet: vraćajte ctsSuccess svaki put kad je server stvarno odgovorio, čak i s 503. Provider klasificira status kodove sam, i transport koji 429 preklopi u ctsPermanentFailure tiho isključi retry logiku opisanu ispod. Konstruktor je strog u drugom smjeru — baca EHPDFCSCSignatureProviderError kad transporta nema, kad je CredentialID prazan, kad nije dan ni AccessToken ni token callback, kad je budžet izvan raspona, ili ServiceBaseURL nije HTTPS. Običan http:// prima se samo s AllowInsecureHTTP, što pripada u testni postroj i nikamo drugamo

Što je SAD i zašto ga HotPDF baci nakon jedne uporabe?

THPDFCSCSignatureProvider tretira Signature Activation Data (SAD) kao jednokratnu: briše se iz stanja providera onog trenutka kad se signatures/signHash prihvati, čak i kad sam potpis stigne kasnije kroz asinkroni polling. SAD je dokaz servisa da je potpisnik odobrio baš ove hashove, i SAD koji visi u memoriji autorizacija je koja čeka da se potroši na krivi dokument

Sa zadanima iz THPDFCSCOptions.Default — RequireSAD i AutoAuthorize oba True — provider jednom učita credentials/info, pita vaš THPDFCSCAuthenticationCallback za authData vrijednosti (OTP, PIN, što god blok auth kredencijala traži), i pošalje credentials/authorize. 200 nosi SAD izravno; 202 nosi handle koji se polla kroz credentials/authorizeCheck do MaxPollAttempts (60) puta na PollIntervalMS (250 ms). Callback smije vratiti najviše 32 vrijednosti, svaka s nepraznim ID-em do 256 bajtova i vrijednošću do 4.096 bajtova. Padne li mreža prije nego se signHash prihvati, automatski dobiveni SAD čuva se da isti batch može retry bez novog pitanja potpisnika

Dijagram životnog ciklusa SAD-a u HotPDF-u: uz RequireSAD i AutoAuthorize provider jednom učita credentials/info, pita autentikacijski callback za OTP ili PIN vrijednosti, šalje credentials/authorize, polla credentials/authorizeCheck do 60 puta na 250 ms kad je odgovor 202, i briše Signature Activation Data trenutka kad se signatures/signHash prihvati, čuvajući dobiveni SAD ako je mreža pala prije prihvaćanja
SAD koji visi u memoriji autorizacija je koja čeka da se potroši na krivi dokument, a pretpostavljeni SAD predan kroz opcije koristi se samo za zahtjev s jednim hashem
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, async mod, 2 retryja
  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
      // vaš OAuth klijent; ForceRefresh je True nakon što je servis odgovorio 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š UI
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

SAD koji sami predate kroz Options.SAD ponaša se drugačije, i to namjerno. HotPDF ne može znati za koje je hashove izdan, pa provider pretpostavljeni SAD koristi samo za zahtjev s jednim hashem. Za batch s isključenim AutoAuthorize, provider pada s "CSC SAD is not pinned to the requested hash batch" umjesto da nagađa

Kako SignHashBatch potpisuje mnogo dokumenata s jednom autorizacijom?

SignHashBatch šalje jedan credentials/authorize i jedan signatures/signHash za do MaxBatchSignatures (64) digesta, i oba bodya gradi iz istog polja tako da su numSignatures, redoslijed hashes i hashAlgorithmOID identični u dva poziva. To poklapanje traži CSC multisign model. Vrtite li jednohash metodu Sign četrdeset puta, dobit ćete četrdeset autorizacija; pošaljite li authorize i signHash koji se ne slažu, servis može potrošiti SAD na krivi batch

Prije bilo kakvog mrežnog prometa, provider validira batch. Svaki zahtjev mora biti digest (sikDigest) od 1 do 1.024 bajtova s digest OID-om, i svi zahtjevi moraju dijeliti jedan OID signature algoritma, jedan digest OID i, za RSASSA-PSS, jednu duljinu sola. Multi-hash batch učita i credentials/info i vrati spsUnsupported kad je multisign vrijednost kredencijala manja od batcha. SAD se zatim prikove uz fingerprint batcha — SHA-256 nad oznakom verzije, brojačem i, po zahtjevu, OID-om algoritma, digest OID-om, algoritmom, duljinom sola i digest bajtovima, svaki s prefiksom duljine. Zamijenite li dva hasha, to je drugi batch koji treba svježu autorizaciju

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: SHA-256 vrijednosti koje ste izračunali
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;           // signAlgo se izvodi kad je AlgorithmOID prazan
    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] pripada Digests[I]; broj je provjeren protiv zahtjeva
end;

Za RSASSA-PSS provider šalje i signAlgoParams, base64 DER strukturu RSASSA-PSS-params s hash algoritmom, MGF1 i duljinom sola. Njezina gradnja znači kodiranje OID-ova, i verzija 2.748.5 popravila je jedan kutak toga: X.690 §8.19.4 savija prva dva luka u jednu vrijednost (40 × prvi + drugi), a pod korijenom 2 drugi luk iznad 39 gurne tu vrijednost preko 127, gdje treba base-128 višebajtna forma koju raniji buildovi nisu primjenjivali. Nijedan SHA-2 OID nije pogođen — 2.16 se savija u 96 — ali neispravan OID sada baca providerovu vlastitu grešku umjesto EConvertError

Zašto ponovljeni zahtjev ne proizvede drugi potpis?

THPDFCSCSignatureProvider tjera svaki retryable poziv da nosi deterministički idempotency ključ i kešira završene rezultate, pa retry nakon izgubljenog odgovora vrati izvorne potpise umjesto da od HSM-a traži nove. Ključ je csc- iza kojeg slijedi hex SHA-256 identifikatora operacije i faze, i faza ugrađuje fingerprint batcha i za autorizaciju i za signHash. Hashiranje umjesto skraćivanja ima veze: dva duga ID-ja operacija koja dijele prefiks sudarila bi se pod skraćivanjem, dok fiksne duljine ključ adresiran sadržajem ostaje jedinstven i stabilan kroz pokušaje

Retry politika u zajedničkoj putanji zahtjeva uska je namjerno:

  • HTTP 401 sili točno jedno osvježavanje tokena kroz access-token callback, pa se zahtjev ponovi jednom kad je access-token callback dodijeljen; drugi 401 je konačan
  • Ostali 4xx odgovori i ctsPermanentFailure završe poziv s spsProviderError, a error_description servisa završi u LastError
  • 408, 429, 5xx i ctsTemporaryFailure retryaju se do RetryLimit (zadano 2), uz čekanje na Retry-After ili RetryBaseDelayMS × 2attempt (baza 100 ms), s plafonom na MaxRetryAfterMS (5.000 ms)
  • Čekanja idu u rezovima od 25 ms koji provjeravaju Cancel, pa korisnik koji prekine ne sjedi kroz pet sekundi back-offa
  • signHash se retrya samo dok je EnableIdempotency upaljen; isključite li ga, timeout nakon predaje je konačan, jer nitko ne može znati je li ključ već iskorišten
Dijagram retry politike u HotPDF-u: svaki retryable poziv nosi deterministički csc- idempotency ključ hashiran iz identifikatora operacije i faze, HTTP 401 sili točno jedno osvježavanje tokena, ostali 4xx odgovori završavaju sa spsProviderError, a 408, 429, 5xx ili privremeni transportni kvar retryaju se do RetryLimit od 2 uz čekanje na Retry-After ili eksponencijalni backoff s plafonom na 5.000 ms
Završeni batchevi keširaju se po identifikatoru operacije, kredencijalu i fingerprintu, a u asinkronom modu pohranjeni responseID dopušta ponovljenom pozivu da nastavi polling umjesto da ponovno pošalje hash

Asinkrono potpisivanje (operationMode "A", zadano) dodaje još jednu zaštitu: responseID sprema se prije pollanja signatures/signPolling, pa ponovljeni poziv s istim identifikatorom operacije nastavi polling umjesto da ponovno pošalje. Završeni batchevi sjede u cacheu s ključem od identifikatora operacije, kredencijala i fingerprinta, s plafonom MaxOperationCacheEntries (128) i vraćaju se kao duboke kopije. Taj cache živi u instanci providera i ne preživi restart. Idempotency ključ preživi, jer se izvodi a nije nasumičan, pa ponovno pokrenut proces koji ponovi svoj identifikator operacije šalje isti ključ — deduplicira li servis po njemu, to je obećanje servisa, ne HotPDF-a

Kako ubaciti CSC potpis u PDF?

Predajte provider HPDFCMSSignPDFStreamWithProvideru zajedno s end-entity certifikatom iz GetCertificateChain; HotPDF gradi CMS SignedData, a provider potpisuje digest potpisanih atributa. Ulazni PDF treba placeholder /ByteRange i /Contents koji zapisuje THPDFPage.AddSignedSignatureField, točno kao u PAdES tijeku potpisivanja u HotPDF-u, a model providera isti je onaj iz članka o HotPDF priključivim signature providerima za 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 nabraja end-entity certifikat prvim
  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;

Dimenzioniranje placeholdera /Contents ide kroz EstimateSignatureSize, koji vraća EstimatedSignatureBytes kad ga postavite, a inače veličinu RSA modula iz duljine ključa kredencijala. Za ECDSA postavite EstimatedSignatureBytes sami, ili procjena javi spsUnsupported. Auto-size varijanta potpisivanja ponovno potpisuje kad se pokaže da je placeholder premalen, i to radi samo za providere koji oglašavaju spcSafeSignRetry — što THPDFCSCSignatureProvider čini samo dok je EnableIdempotency upaljen. Za PAdES-B-T tijekove, TimestampDigest traži timestamp token od istog servisa kroz signatures/timestamp, s plafonom MaxTimestampBytes (1 MB)

Što CSC provider ne radi?

Ne potpisuje poruke, nego digeste. Ed25519 i Ed448 u čistom modu preda provideru cijelu poruku potpisanih atributa (sikMessage), i batch validator to odbija kao neispravno, jer je signHash po definiciji temeljen na hashu. Provider unit se kompajlira pod Free Pascalom s običnim funkcionalnim tipovima umjesto anonimnih metoda, ali CMS builderi vođeni providerom danas pod FPC-om bacaju iznimku, pa je ugrađivanje CSC potpisa u PDF putanja za Delphi

Ne odlučuje ni o politici. CredentialInfo javlja status ključa, status certifikata, mod autorizacije, SCAL razinu i multisign limit, ali provider neće sam odbiti onemogućen ključ ili SCAL1 kredencijal — provjerite to prije nego potpisniku pokažete OTP prompt. I jedna instanca providera potpisuje jedan batch istovremeno: SignHashBatch serializiran je interno pa dva navoja ne mogu utrkovati za jedan SAD, što znači da propusnost dolazi iz grupiranja, ne iz dijeljenja providera među radnim navojima. Je li rezultirajući potpis kvalificiran, ovisi o trust servisu i njegovu kredencijalu, a ne o biblioteci koja je hash tamo donijela

CSC provider, CMS i PAdES builderi te lokalni i PKCS#11 provideri svi isporučuju se u HotPDF Delphi PDF komponenti