Tehnični članak

HotPDF CSC oddaljeno podpisovanje: podpisi PDF v oblaku

HotPDF podpisuje dokumente PDF z zasebnim ključem, ki ga hrani oddaljena storitev Cloud Signature Consortium (CSC), prek THPDFCSCSignatureProvider, ponudnika podpisov, ki poganja API CSC — credential info, avtorizacijo, signatures/signHash in anketiranje — medtem ko vaša aplikacija Delphi dovaja prevoz HTTP in dostopni žeton OAuth. Ključ nikoli ne zapusti HSM storitve

To je vse bolj edini način, da sploh dobite kvalificiran podpisni ključ. Ponudniki storitev zaupanja izročijo končno točko CSC in odjemalca OAuth, ne datoteke PFX ali žetona USB, zato ni ničesar, kar bi se naložilo v krajevno shrambo certifikatov, kot to počne podpisovanje iz shrambe certifikatov Windows skozi CNG in CAPI. Naivna integracija odpove na predvidljive načine: klic signHash zamudi in ponovitev podpiše isti pogodbo dvakrat, ali pa serija štiridesetih računov sproži štirideset enkratnih gesel, ker je bil vsak hash avtoriziran posebej. Večina tega, kar ponudnik počne, je obramba pred tema dvema odpovedema

Zakaj HotPDF pusti HTTP vaši aplikaciji?

Ker je prevoz točno tisto mesto, kjer se vsaka namestitev razlikuje. Posredniki, pripenjanje TLS, certifikati odjemalcev, podjetjaški sef OAuth in politika beleženja vsi živijo v plasti HTTP, zato THPDFCSCSignatureProvider orkestrira stanje protokola in za vsako zahtevo pokliče funkcijo THPDFCSCTransport. Ponudnik vam izroči THPDFCSCTransportRequest z Method (vselej POST), polnim URL, zgrajenim iz ServiceBaseURL plus poti končne točke, pripravljeno glavo Authorization bearer, ContentType, telesom JSON Body, IdempotencyKey, številko Attempt in MaxResponseBytes. Vi napolnite THPDFCSCTransportResponse s StatusCode, Body in RetryAfterMS ter vrnete enega od ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ali ctsCancelled

Diagram meje prevoza HotPDF CSC: THPDFCSCSignatureProvider orkestrira protokol in izroči vaši kodi THPDFCSCTransportRequest z metodo POST, polnim URL, pripravljeno glavo Authorization bearer, telesom JSON, IdempotencyKey in številko poskusa, vi pa vrnete StatusCode, Body, RetryAfterMS plus eno od štirih vrednosti stanja cts, medtem ko ključ nikoli ne zapusti HSM
Ponudnik sam klasificira kode stanj, zato prevoz, ki spremeni odgovorjeni 503 v trajno odpoved, tiho onemogoči logiko ponovitev, posredniki in politika TLS pa ostanejo v kodi, ki je vaša
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 glave, kot ga dokumentira vaša storitev
          Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
            string(Request.IdempotencyKey))];
        try
          HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
        except
          on ENetHTTPClientException do
            Exit(ctsTemporaryFailure);            // težava vtičnice ali DNS: ponovljivo
        end;
        Response.StatusCode := HttpResp.StatusCode;   // poročajte 503 kot je, ne klasificirajte
        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;

Edino pravilo, vredno naučenja: vrnite ctsSuccess, kadarkoli je strežnik dejansko odgovoril, tudi z 503. Ponudnik sam klasificira kode stanj, prevoz, ki spremeni 429 v ctsPermanentFailure, pa tiho onemogoči logiko ponovitev, opisano spodaj. Konstruktor je strog v drugo smer — sproži EHPDFCSCSignatureProviderError, ko prevoz manjka, CredentialID je prazen, ni podan ne AccessToken ne povratni klic žetona, proračun je izven obsega ali ServiceBaseURL ni HTTPS. Goli http:// je sprejet le z AllowInsecureHTTP, ki pripada preskusni postaji in nikamor drugam

Kaj je SAD in zakaj ga HotPDF vrže proč po eni uporabi?

THPDFCSCSignatureProvider obravnava Signature Activation Data (SAD) kot enkratno: počiščena je iz stanja ponudnika v trenutku, ko je signatures/signHash sprejet, tudi kadar podpis sama pride pozneje skozi asinhrono anketiranje. SAD je dokaz storitve, da je podpisovalec odobril točno te hashe, SAD, ki muli v spominu, pa je avtorizacija, ki čaka, da bo zapravljena na napačnem dokumentu

S privzetimi vrednostmi iz THPDFCSCOptions.Default — RequireSAD in AutoAuthorize oba True — ponudnik enkrat naloži credentials/info, od vašega THPDFCSCAuthenticationCallback povpraša po vrednostih authData (OTP, PIN, kar koli zahteva blok auth poverilnice) in pošlje credentials/authorize. 200 nosi SAD neposredno; 202 nosi ročaj, ki se anketira skozi credentials/authorizeCheck do MaxPollAttempts (60) krat pri PollIntervalMS (250 ms). Povratni klic lahko vrne največ 32 vrednosti, vsaka z nepraznim ID do 256 bajtov in vrednostjo do 4.096 bajtov. Če se mreža prekine, preden je signHash sprejet, se samodejno pridobljeni SAD obdrži, tako da se lahko ista serija ponovi, brez da bi ponovno vprašali podpisovalca

Diagram življenjskega cikla SAD v HotPDF: z RequireSAD in AutoAuthorize ponudnik enkrat naloži credentials/info, povpraša povratni klic avtentikacije po vrednostih OTP ali PIN, pošlje credentials/authorize, anketrira credentials/authorizeCheck do 60 krat pri 250 ms, ko je odgovor 202, in počisti Signature Activation Data v trenutku, ko je signatures/signHash sprejet, ter obdrži pridobljeni SAD, ko je mreža padla pred sprejemom
SAD, ki muli v spominu, je avtorizacija, ki čaka, da bo zapravljena na napačnem dokumentu, vnaprej nastavljeni SAD, podan skozi možnosti, pa se uporabi le za zahtevo z enim hashem
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, asinhroni način, 2 ponovitvi
  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š odjemalec OAuth; ForceRefresh je resničen, potem ko je storitev odgovorila 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š vmesnik
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

SAD, ki ga sami podate prek Options.SAD, se obnaša drugače in namenoma. HotPDF ne more vedeti, za katere hashe je bil izdan, zato ponudnik uporabi vnaprej nastavljeni SAD le za zahtevo z enim hashem. Za serijo z izklopljenim AutoAuthorize ponudnik odpove z »CSC SAD is not pinned to the requested hash batch«, namesto da bi ugibal

Kako SignHashBatch podpiše veliko dokumentov z eno avtorizacijo?

SignHashBatch pošlje en credentials/authorize in en signatures/signHash za do MaxBatchSignatures (64) povzetkov in zgradi obe telesi iz istega polja, tako da so numSignatures, vrstni red hashes in hashAlgorithmOID identični v obeh klicih. To ujemanje zahteva model multisign CSC. Zankajte metodo Sign z enim hashom štirideset krat in dobili boste štirideset avtorizacij; pošljite authorize in signHash, ki se ne strinjata, in storitev lahko porabi SAD za napačno serijo

Pred kakršnim koli omrežnim prometom ponudnik preveri serijo. Vsaka zahteva mora biti povzetek (sikDigest) od 1 do 1.024 bajtov z OID povzetka, vse zahteve pa si morajo deliti en OID podpisnega algoritma, en OID povzetka in, pri RSASSA-PSS, eno dolžino soli. Večhashna serija prav tako naloži credentials/info in vrne spsUnsupported, ko je vrednost multisign poverilnice manjša od serije. SAD je nato pripet na prstni odtis serije — SHA-256 nad oznako različice, številom in, na zahtevo, OID algoritma, OID povzetka, algoritmom, dolžino soli in bajti povzetka, vsak s predpono dolžine. Zamenjajte dva hasha in to je drugačna serija, ki potrebuje svežo avtorizacijo

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: vrednosti SHA-256, ki ste jih izračunali
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;           // signAlgo izpeljan, kadar je AlgorithmOID prazen
    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]; število je bilo preverjeno proti zahtevi
end;

Za RSASSA-PSS ponudnik pošlje še signAlgoParams, strukturo RSASSA-PSS-params v DER, kodirano base64, z algoritmom hasha, MGF1 in dolžino soli. Njena gradnja pomeni kodiranje OID-jev, različica 2.748.5 pa je popravila kot takega: X.690 §8.19.4 prepogne prva dva loka v eno vrednost (40 × prvi + drugi), pod korenom 2 pa drugi lok nad 39 potisne to vrednost čez 127, kjer potrebuje večbajtno obliko osnova 128, ki je starejše gradnje niso uporabile. Noben OID SHA-2 ni prizadet — 2.16 se prepogne v 96 — deformiran OID pa zdaj sproži lastno napako ponudnika namesto EConvertError

Zakaj ponovljena zahteva ne izdela drugega podpisa?

THPDFCSCSignatureProvider naredi, da vsak ponovljiv klic nosi deterministični ključ idempotentnosti, in predpomni opravljene rezultate, tako da ponovitev po izgubljenem odgovoru vrne izvirne podpise, namesto da bi HSM vprašal za nove. Ključ je csc-, ki mu sledi šestnajstiški SHA-256 identifikatorja operacije in faze, faza pa vgradi prstni odtis serije tako za avtorizacijo kot za signHash. Hashiranje namesto krajšanja je pomembno: dva dolga ID operacij, ki imata skupno predpono, bi trčila pod krajšanjem, fiksno dolg ključ po vsebini pa ostane enkraten in stabilen čez poskuse

Politika ponovitev v skupni poti zahtev je namenoma ozka:

  • HTTP 401 vsili točno eno osvežitev žetona skozi povratni klic dostopnega žetona, nato se zahteva ponovi enkrat, kadar je povratni klic dostopnega žetona dodeljen; drugi 401 je končen
  • Drugi odgovori 4xx in ctsPermanentFailure zaključita klic z spsProviderError, error_description storitve pa pristane v LastError
  • 408, 429, 5xx in ctsTemporaryFailure so ponovljeni do RetryLimit (privzeto 2), s čakanjem na Retry-After ali RetryBaseDelayMS × 2poskus (osnova 100 ms), omejeno na MaxRetryAfterMS (5.000 ms)
  • Čakanja tečejo v rezinah po 25 ms, ki preverjajo Cancel, tako da uporabnik, ki prekine, ne sedi skozi umik petih sekund
  • signHash je ponovljen le, medtem ko je EnableIdempotency vklopljen; izklopite ga in zamuda po oddaji je končna, ker nihče ne more vedeti, ali je bil ključ že uporabljen
Diagram politike ponovitev v HotPDF: vsak ponovljiv klic nosi deterministični ključ idempotentnosti csc-, hashiran iz identifikatorja operacije in faze, HTTP 401 vsili točno eno osvežitev žetona, drugi odgovori 4xx se končajo s spsProviderError, 408, 429, 5xx ali začasna odpoved prevoza pa se ponovita do RetryLimit 2, medtem ko se čaka na Retry-After ali eksponentni umik, omejen na 5.000 ms
Opravljene serije so predpomnjene po identifikatorju operacije, poverilnici in prstnemu odtisu, v asinhronem načinu pa shranjeni responseID omogoča ponovljenemu klicu nadaljevati anketiranje, namesto da bi znova oddal hash

Asinhrono podpisovanje (operationMode »A«, privzeto) dodaja še eno varovalo: responseID je shranjen, preden se anketrira signatures/signPolling, tako da ponovljen klic z istim identifikatorjem operacije nadaljuje anketiranje, namesto da bi oddal znova. Opravljene serije sedijo v predpomnilniku s ključi iz identifikatorja operacije, poverilnice in prstnega odtisa, omejenem z MaxOperationCacheEntries (128) in vračanem kot globoke kopije. Ta predpomnilnik živi v primerku ponudnika in ne preživi ponovnega zagona. Ključ idempotentnosti pa preživi, ker je izpeljan in ne naključen, zato ponovno zagnan proces, ki ponovno uporabi svoj identifikator operacije, pošlje isti ključ — ali storitev na njem odpravlja dvojnosti, je obljuba storitve in ne HotPDF

Kako vstavite podpis CSC v PDF?

Podajte ponudnika HPDFCMSSignPDFStreamWithProvider skupaj s končnim certifikatom iz GetCertificateChain; HotPDF zgradi CMS SignedData, ponudnik pa podpiše povzetek podpisanih lastnosti. Vhodni PDF potrebuje ograda /ByteRange in /Contents, ki ju zapiše THPDFPage.AddSignedSignatureField, točno kot v poteku podpisovanja PAdES v HotPDF, model ponudnika pa je isti, kot ga pokriva vstavljanje ponudnikov podpisov HotPDF za ML-DSA in EdDSA

var
  Chain: THPDFCSCCertificateChain;
  SignOpts: THPDFCMSSignOptions;
  Src, Dst: TFileStream;
begin
  if Provider.RefreshCredentialInfo <> spsValid then
    raise Exception.Create(Provider.LastError);
  Chain := Provider.GetCertificateChain;   // CSC našteje končni certifikat najprej
  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;

Merjenje ograda /Contents gre skozi EstimateSignatureSize, ki vrne EstimatedSignatureBytes, ko ga nastavite, sicer pa velikost modula RSA iz dolžine ključa poverilnice. Za ECDSA nastavite EstimatedSignatureBytes sami, sicer ocena poroča spsUnsupported. Različica podpisovanja s samodejno velikostjo ponovno podpiše, ko se pokaže, da je ogradek premajhen, in to stori le za ponudnike, ki oglašujejo spcSafeSignRetry — kar THPDFCSCSignatureProvider počne le, medtem ko je EnableIdempotency vklopljen. Za potke PAdES-B-T TimestampDigest zahteva žeton časovnega žiga od iste storitve skozi signatures/timestamp, omejen na MaxTimestampBytes (1 MB)

Kaj ponudnik CSC ne počne?

Ne podpisuje sporočil, le povzetke. Ed25519 in Ed448 v čistem načinu izročita ponudniku celotno sporočilo podpisanih lastnosti (sikMessage), preverjevalnik serij pa to zavrne kot deformirano, ker je signHash po definiciji na hashih. Enota ponudnika se prevede pod Free Pascal z navadnimi tipi funkcij namesto anonimnih metod, graditelji CMS, gnani s ponudnikom, pa danes pod FPC sprožijo izjemo, zato je vstavljanje podpisa CSC v PDF pot Delphi

Ne odloča tudi o politiki. CredentialInfo poroča stanje ključa, stanje certifikata, način avtorizacije, raven SCAL in omejitev multisign, ponudnik pa ne bo odklonil onemogočenega ključa ali poverilnice SCAL1 na svojo roko — preverite to, preden podpisovalcu pokažete poziv za OTP. In en primerek ponudnika podpiše eno serijo naenkrat: SignHashBatch je serializiran interno, tako da dve niti ne moreta tekmovati za en SAD, kar pomeni, da pretočnost pride od serij, ne od delitve ponudnika čez delovne niti. Ali je nastali podpis kvalificiran, je odvisno od storitve zaupanja in njene poverilnice, ne od knjižnice, ki je hash nesla tja

Ponudnik CSC, graditelja CMS in PAdES ter krajevni ponudnik in ponudnik PKCS#11 vsi prihajajo v komponento HotPDF Delphi PDF