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 provajder koji pokreće CSC API — credential info, autorizaciju, signatures/signHash i polling — dok vaša Delphi aplikacija snabdeva HTTP transport i OAuth access token. Ključ nikada ne izlazi iz HSM-a servisa

To je sve češći jedini način da uopšte dobijete kvalifikovani ključ za potpisivanje. Trust service provajderi dele CSC endpoint i OAuth klijenta, a ne PFX fajl ili USB token, pa nema šta da se učita u lokalni certifikat store onako kako to radi potpisivanje kroz Windows cert store sa CNG i CAPI. Naivna integracija pada na predvidljive načine: signHash poziv istekne i retry potpiše isti ugovor dvaput, ili serija od četrdeset faktura okine četrdeset jednokratnih lozinki jer je svaki heš autorizovan posebno. Najveći deo onoga što provajder radi je odbrana od ta dva kvara

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

Jer transport je tačno mesto gde se svaka implementacija razlikuje. Proksi, TLS pinning, klijentski sertifikati, korporativni OAuth trezori i politika logovanja sve žive u HTTP sloju, pa THPDFCSCSignatureProvider orkestrira stanje protokola i zove THPDFCSCTransport funkciju za svaki zahtev. Provajder vam predaje THPDFCSCTransportRequest sa Method-om (uvek POST), punim URL-om izgrađenim od ServiceBaseURL plus putanje endpointa, gotovim Authorization bearer headerom, ContentType-om, JSON Body-jem, IdempotencyKey-em, brojem Attempt i MaxResponseBytes. Vi popunite THPDFCSCTransportResponse sa StatusCode-om, Body-jem i RetryAfterMS, i vratite jedan od ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ili ctsCancelled

Dijagram granice CSC transporta u HotPDF-u: THPDFCSCSignatureProvider orkestrira protokol i predaje vašem kodu THPDFCSCTransportRequest sa POST metodom, punim URL-om, gotovim Authorization bearer headerom, JSON telom, IdempotencyKey-em i brojem pokušaja, a vi vraćate StatusCode, Body, RetryAfterMS plus jednu od četiri cts status vrednosti dok ključ nikada ne izlazi iz HSM-a
Provajder sam klasifikuje status kodove, pa transport koji odgovoreni 503 pretvori u trajan kvar tiho isključuje retry logiku, dok proksi i TLS politika ostaju u kodu koji je vaš
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 headera kako ga vaš servis dokumentuje
          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 sa soketom ili DNS-om: ponovljivo
        end;
        Response.StatusCode := HttpResp.StatusCode;   // prijavite 503 kakav jeste, ne klasifikujte
        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 vredi zapamtiti: vraćajte ctsSuccess kad god je server zaista odgovorio, čak i sa 503. Provajder sam klasifikuje status kodove, a transport koji 429 pretvori u ctsPermanentFailure tiho isključuje retry logiku opisanu ispod. Konstruktor je strog u drugom smeru — podiže EHPDFCSCSignatureProviderError kada transport nedostaje, CredentialID je prazan, nijedan AccessToken niti token callback nije dat, budžet je van opsega, ili ServiceBaseURL nije HTTPS. Običan http:// se prihvata samo sa AllowInsecureHTTP, što pripada testnom stubu i nigde drugde

Šta je SAD i zašto HotPDF baca jednom korišćen?

THPDFCSCSignatureProvider tretira Signature Activation Data (SAD) kao jednokratnu: briše se iz stanja provajdera u trenutku kad se signatures/signHash prihvati, čak i kad sam potpis stigne kasnije kroz asinhroni polling. SAD je dokaz servisa da je potpisnik odobrio baš ove hešove, i SAD koji vise u memoriji je autorizacija koja čeka da se potroši na pogrešan dokument

Sa podrazumevanim vrednostima iz THPDFCSCOptions.Default — RequireSAD i AutoAuthorize oba True — provajder jednom učita credentials/info, traži od vašeg THPDFCSCAuthenticationCallback authData vrednosti (OTP, PIN, šta god blok auth kredencijala traži), i šalje credentials/authorize. A 200 nosi SAD direktno; a 202 nosi ručku koja se poluje kroz credentials/authorizeCheck do MaxPollAttempts (60) puta na PollIntervalMS (250 ms). Callback sme da vrati najviše 32 vrednosti, svaku sa nepraznim ID-jem do 256 bajtova i vrednošću do 4.096 bajtova. Ako mreža padne pre nego što se signHash prihvati, automatski dobijen SAD se čuva da ista serija može da se ponovi bez ponovnog pitanja potpisnika

Dijagram životnog ciklusa SAD u HotPDF-u: sa RequireSAD i AutoAuthorize provajder jednom učita credentials/info, traži od authentication callback-a OTP ili PIN vrednosti, šalje credentials/authorize, poluje credentials/authorizeCheck do 60 puta na 250 ms kad je odgovor 202, i briše Signature Activation Data u trenutku kad se signatures/signHash prihvati, zadržavajući dobijen SAD dok je mreža pala pre prihvatanja
SAD koji visi u memoriji je autorizacija koja čeka da se potroši na pogrešan dokument, a pretpostavljeni SAD prosleđen kroz opcije koristi se samo za zahtev sa jednim hešom
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, async mode, 2 retry-a
  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 posle 401 odgovora servisa
      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 prosledite kroz Options.SAD ponaša se drugačije, i to namerno. HotPDF ne može da zna za koje je hešove izdat, pa provajder koristi pretpostavljeni SAD samo za zahtev sa jednim hešom. Za seriju sa isključenim AutoAuthorize-om, provajder pada sa „CSC SAD is not pinned to the requested hash batch” umesto da nagađa

Kako SignHashBatch potpisuje mnogo dokumenata jednom autorizacijom?

SignHashBatch šalje jedan credentials/authorize i jedan signatures/signHash za do MaxBatchSignatures (64) digesta, i gradi oba tela iz istog niza tako da su numSignatures, redosled hashes i hashAlgorithmOID identični u ta dva poziva. To poklapanje je ono što CSC multisign model zahteva. Petljajte single-hash Sign metodu četrdeset puta i dobićete četrdeset autorizacija; pošaljite authorize i signHash koji se ne slažu i servis može da potroši SAD na pogrešnu seriju

Pre bilo kog mrežnog saobraćaja, provajder validira seriju. Svaki zahtev mora biti digest (sikDigest) od 1 do 1.024 bajta sa digest OID-om, i svi zahtevi moraju deliti jedan OID algoritma potpisa, jedan digest OID i, za RSASSA-PSS, jednu dužinu salt-a. Serija sa više hešova takođe učitava credentials/info i vraća spsUnsupported kada je multisign vrednost kredencijala manja od serije. SAD se zatim pribija na otisak serije — SHA-256 preko oznake verzije, broja i, po zahtevu, OID-a algoritma, digest OID-a, algoritma, dužine salt-a i digest bajtova, svako sa prefiksom dužine. Zamenite dva heša i to je druga serija koja treba svežu autorizaciju

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: SHA-256 vrednosti 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 proveren naspram zahteva
end;

Za RSASSA-PSS provajder šalje i signAlgoParams, base64 DER RSASSA-PSS-params strukturu sa algoritmom heša, MGF1 i dužinom salt-a. Njeno građenje znači enkodovanje OID-ova, i verzija 2.748.5 je popravila jedan ćošak toga: X.690 §8.19.4 savija prva dva luka u jednu vrednost (40 × prvi + drugi), i pod korenom 2 drugi luk iznad 39 gura tu vrednost preko 127, gde treba base-128 višebajtna forma koju raniji buildovi nisu primenjivali. Nijedan SHA-2 OID nije pogođen — 2.16 se savija u 96 — ali nepravilan OID sada podiže sopstvenu grešku provajdera umesto EConvertError

Zašto ponovljeni zahtev ne proizvodi drugi potpis?

THPDFCSCSignatureProvider čini da svaki ponovljivi poziv nosi deterministički idempotency ključ i kešira završene rezultate, pa retry posle izgubljenog odgovora vraća originalne potpise umesto da traži nove od HSM-a. Ključ je csc- pa heks SHA-256 identifikatora operacije i faze, i faza ugrađuje otisak serije za i autorizaciju i signHash. Hešovanje umesto skraćivanja je bitno: dva duga ID-ja operacija koja dele prefiks sudarila bi se pri skraćivanju, dok ključ fiksne dužine adresiran sadržajem ostaje jedinstven i stabilan kroz pokušaje

Retry politika u deljenom putu zahteva je uska namerno:

  • HTTP 401 nameće tačno jedno osvežavanje tokena kroz access-token callback, pa se zahtev ponavlja jednom kada je access-token callback dodeljen; drugi 401 je konačan
  • Ostali 4xx odgovori i ctsPermanentFailure završavaju poziv sa spsProviderError, a error_description servisa pada u LastError
  • 408, 429, 5xx i ctsTemporaryFailure se ponavljaju do RetryLimit (podrazumevano 2), uz čekanje na Retry-After ili RetryBaseDelayMS × 2attempt (osnova 100 ms), ograničeno na MaxRetryAfterMS (5.000 ms)
  • Čekanja idu u rezovima od 25 ms koji proveravaju Cancel, pa korisnik koji prekine ne sedi kroz petosekundno odugovlačenje
  • signHash se ponavlja samo dok je EnableIdempotency uključen; isključite ga i tajm-aut posle predaje je konačan, jer niko ne može da zna da li je ključ već iskorišćen
Dijagram retry politike u HotPDF-u: svaki ponovljivi poziv nosi deterministički csc- idempotency ključ hešovan iz identifikatora operacije i faze, HTTP 401 nameće tačno jedno osvežavanje tokena, ostali 4xx odgovori završavaju sa spsProviderError, a 408, 429, 5xx ili privremeni transport kvar se ponavljaju do RetryLimit od 2 uz čekanje na Retry-After ili eksponencijalni backoff ograničen na 5.000 ms
Završene serije se keširaju po identifikatoru operacije, kredencijalu i otisku, i u asinhronom režimu sačuvani responseID dozvoljava ponovljenom pozivu da nastavi polling umesto da ponovo predaje heš

Asinhrono potpisivanje (operationMode „A”, podrazumevano) dodaje još jednu zaštitu: responseID se čuva pre pollanja signatures/signPolling, pa ponovljen poziv sa istim identifikatorom operacije nastavlja polling umesto da ponovo predaje. Završene serije sede u kešu sa ključem od identifikatora operacije, kredencijala i otiska, ograničen na MaxOperationCacheEntries (128) i vraćaju se kao duboke kopije. Taj keš živi u instanci provajdera i ne preživljava restart. Idempotency ključ preživljava, jer se izvodi a nije nasumičan, pa ponovo pokrenut proces koji koristi isti identifikator operacije šalje isti ključ — da li servis na njemu deduplicira je obećanje servisa, ne HotPDF-a

Kako CSC potpis ugraditi u PDF?

Prosledite provajder HPDFCMSSignPDFStreamWithProvider zajedno sa end-entity sertifikatom iz GetCertificateChain; HotPDF gradi CMS SignedData a provajder potpisuje digest potpisanih atributa. Ulazni PDF treba /ByteRange i /Contents rezervisano mesto koje THPDFPage.AddSignedSignatureField upisuje, tačno kao u PAdES toku potpisivanja u HotPDF-u, a model provajdera je isti koji pokriva HotPDF priključivi signature provajderi 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 navodi end-entity sertifikat prvi
  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;

Dimenzionisanje /Contents rezervisanog mesta ide kroz EstimateSignatureSize, koji vraća EstimatedSignatureBytes kad ga postavite, a inače veličinu RSA modula iz dužine ključa kredencijala. Za ECDSA postavite EstimatedSignatureBytes sami, ili procena prijavljuje spsUnsupported. Auto-size varijanta potpisivanja potpisuje ponovo kad se rezervisano mesto ispostavi premalo, i to radi samo za provajdere koji reklamiraju spcSafeSignRetry — što THPDFCSCSignatureProvider radi samo dok je EnableIdempotency uključen. Za PAdES-B-T tokove, TimestampDigest traži timestamp token od istog servisa kroz signatures/timestamp, ograničeno na MaxTimestampBytes (1 MB)

Šta CSC provajder ne radi?

Ne potpisuje poruke, samo digeste. Ed25519 i Ed448 u čistom režimu predaju provajderu celu poruku potpisanih atributa (sikMessage), i validator serije to odbija kao nepravilno, jer je signHash po definiciji zasnovan na hešu. Jedinica provajdera se kompajlira pod Free Pascal-om sa običnim tipovima funkcija umesto anonimnih metoda, ali CMS builderi vođeni provajderom danas podižu izuzetak pod FPC-om, pa je ugradnja CSC potpisa u PDF Delphi putanja

Ne odlučuje ni o politici. CredentialInfo prijavljuje status ključa, status sertifikata, režim autorizacije, SCAL nivo i multisign granicu, ali provajder sam neće odbiti isključen ključ ili SCAL1 kredencijal — proverite to pre nego što potpisniku pokažete OTP upit. I jedna instanca provajdera potpisuje jednu seriju istovremeno: SignHashBatch je interno serializovan pa dve niti ne mogu da trkuju za jednim SAD-om, što znači da propusnost dolazi iz grupisanja, a ne iz deljenja provajdera preko radnih niti. Da li je rezultujući potpis kvalifikovan zavisi od trust servisa i njegovog kredencijala, ne od biblioteke koja je heš tamo odnela

CSC provajder, CMS i PAdES builderi i lokalni i PKCS#11 provajderi isporučuju se svi u HotPDF Delphi PDF component-i