Techninis straipsnis

HotPDF CSC nuotolinis pasirašymas: debesijos PDF parašai

HotPDF pasirašo PDF dokumentus privatuoju raktu, kurį laiko nuotolinė Cloud Signature Consortium (CSC) paslauga, per THPDFCSCSignatureProvider – parašų tiekėją, kuris valdo CSC API (credential info, autorizacija, signatures/signHash ir apklausinėjimas), kol jūsų Delphi programa tiekia HTTP transportą ir OAuth prieigos tokeną. Raktas niekada nepalieka paslaugos HSM

Tai vis dažniau vienintelis būdas apskritai gauti kvalifikuotą pasirašymo raktą. Pasitikėjimo paslaugų tiekėjai išdalija CSC galinį tašką ir OAuth klientą, o ne PFX failą arba USB tokeną, tad nėra ko įkelti į vietinę sertifikatų saugyklą, kaip daro Windows cert store pasirašymas per CNG ir CAPI. Naivi integracija žlunga nuspėjamai: signHash iškvietimas išsenka, o pakartojimas pasirašo tą pačią sutartį dukart, arba keturiasdešimt sąskaitų paketas sužadina keturiasdešimt vienkartinių slaptažodžių, nes kiekvienas hašas buvo autorizuotas atskirai. Dauguma to, ką daro tiekėjas, yra gynyba nuo tų dviejų žlugimų

Kodėl HotPDF HTTP palieka jūsų programai?

Nes transportas yra būtent ta vieta, kur kiekviena aplinka skiriasi. Proxy, TLS pinning, kliento sertifikatai, korporatyvinės OAuth saugyklos ir žurnalo politika visos gyvena HTTP sluoksnyje, tad THPDFCSCSignatureProvider valdo protokolo būseną ir kiekvienai užklausai kviečia THPDFCSCTransport funkciją. Tiekėjas jums įteikia THPDFCSCTransportRequest su Method (visada POST), pilnu URL, sukurtu iš ServiceBaseURL plius galinio taško kelio, paruošta Authorization bearer antrašte, ContentType, JSON Body, IdempotencyKey, Attempt numeriu ir MaxResponseBytes. Jūs užpildote THPDFCSCTransportResponse su StatusCode, Body ir RetryAfterMS, ir grąžinate vieną iš ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ar ctsCancelled

HotPDF CSC transporto ribų diagrama: THPDFCSCSignatureProvider valdo protokolą ir jūsų kodui įteikia THPDFCSCTransportRequest su POST metodu, pilnu URL, paruošta Authorization bearer antrašte, JSON turiniu, IdempotencyKey ir bandymo numeriu, o jūs grąžinate StatusCode, Body, RetryAfterMS plius vieną iš keturių cts būsenos reikšmių, kol raktas niekada nepalieka HSM
Tiekėjas būsenos kodus klasifikuoja pats, tad transportas, kuris atsakiusią 503 paverčia nuolatine klaida, tyliai išjungia pakartojimo logiką, o proxy ir TLS politika lieka jums priklausančiame kode
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  // antraštės vardas, kaip dokumentavo jūsų paslauga
          Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
            string(Request.IdempotencyKey))];
        try
          HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
        except
          on ENetHTTPClientException do
            Exit(ctsTemporaryFailure);            // lizdo ar DNS bėda: pakartojama
        end;
        Response.StatusCode := HttpResp.StatusCode;   // praneškite 503 kaip yra, neklasifikuokite
        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;

Viena taisyklė, verta išmokti atmintinai: grąžinkite ctsSuccess kaskart, kai serveris iš tikrųjų atsakė, netgi su 503. Tiekėjas būsenos kodus klasifikuoja pats, o transportas, kuris 429 paverčia ctsPermanentFailure, tyliai išjungia žemiau aprašytą pakartojimo logiką. Konstruktorius griežtas priešinga kryptimi – jis kelia EHPDFCSCSignatureProviderError, kai transportas dingęs, CredentialID tuščias, nepateikta nei AccessToken, nei token callback, biudžetas už ribų arba ServiceBaseURL nėra HTTPS. Paprastas http:// priimamas tik su AllowInsecureHTTP, kuri priklauso testų stendui ir niekur kitur

Kas yra SAD ir kodėl HotPDF jos atsikrato po vieno naudojimo?

THPDFCSCSignatureProvider Signature Activation Data (SAD) traktuoja kaip vienkartinę: ji išvaloma iš tiekėjo būsenos tą akimirką, kai signatures/signHash priimamas, netgi kai pats parašas atkeliauja vėliau per asinchroninį apklausinėjimą. SAD yra paslaugos įrodymas, kad pasirašytojas patvirtino būtent tuos hašus, ir SAD, gulinčios atmintyje, yra autorizacija, laukianti, kada bus išleista netinkamam dokumentui

Su numatytosiomis iš THPDFCSCOptions.Default – RequireSAD ir AutoAuthorize abi True – tiekėjas vienąkart užkrauna credentials/info, paprašo jūsų THPDFCSCAuthenticationCallback authData reikšmių (OTP, PIN, ką ten reikalauja kredencialo auth blokas) ir nusiunčia credentials/authorize. 200 neša SAD tiesiai; 202 neša rankeną, kuri apklausinėjama per credentials/authorizeCheck iki MaxPollAttempts (60) kartų kas PollIntervalMS (250 ms). Callback gali grąžinti ne daugiau kaip 32 reikšmes, kiekvieną su netuščiu ID iki 256 baitų ir reikšme iki 4 096 baitų. Jei tinklas nukrenta, dar nepriėmus signHash, automatiškai gauta SAD išlaikoma, kad tas pats paketas galėtų būti pakartotas nekviečiant pasirašytojo antrą kartą

HotPDF SAD gyvavimo ciklo diagrama: su RequireSAD ir AutoAuthorize tiekėjas vienąkart užkrauna credentials/info, paprašo autentifikacijos callback OTP ar PIN reikšmių, nusiunčia credentials/authorize, apklausinėja credentials/authorizeCheck iki 60 kartų kas 250 ms, kai atsakymas 202, ir išvalo Signature Activation Data tą akimirką, kai signatures/signHash priimamas, išlaikydamas gautą SAD, jei tinklas nukrito prieš priėmimą
SAD, gulinčios atmintyje, yra autorizacija, laukianti, kada bus išleista netinkamam dokumentui, o per parinktis perduota užkalėta SAD naudojama tik vieno hašo užklausai
var
  Options: THPDFCSCOptions;
  Provider: THPDFCSCSignatureProvider;
begin
  Options := THPDFCSCOptions.Default;   // RequireSAD, AutoAuthorize, async režimas, 2 pakartojimai
  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
      // jūsų OAuth klientas; ForceRefresh yra True, paslaugai atsakius 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   // jūsų UI
        Exit(spsCancelled);
      SetLength(Values, 1);
      Values[0].ID := 'otp';
      Values[0].Value := AnsiString(Otp);
      Result := spsValid;
    end);

SAD, kurią paduodate pats per Options.SAD, elgiasi kitaip, ir sąmoningai. HotPDF negali žinoti, kam ji buvo išduota, tad tiekėjas užkalėtą SAD naudoja tik vieno hašo užklausai. Paketui su išjungtu AutoAuthorize tiekėjas žlunga su „CSC SAD is not pinned to the requested hash batch“, vietoj to, kad spėliotų

Kaip SignHashBatch pasirašo daug dokumentų viena autorizacija?

SignHashBatch nusiunčia vieną credentials/authorize ir vieną signatures/signHash iki MaxBatchSignatures (64) santraukų, ir abu turinius stato iš to paties masyvo, kad numSignatures, hashes tvarka ir hashAlgorithmOID abiejuose iškvietimuose būtų identiški. Tas sutapimas yra tai, ko reikalauja CSC multisign modelis. Sukite vieno hašo Sign metodą keturiasdešimt kartų ir gausite keturiasdešimt autorizacijų; nusiųskite authorize ir signHash, kurie nesutampa, ir paslauga gali išleisti SAD prieš netinkamą paketą

Dar prieš bet kokį tinklo srautą tiekėjas patvirtina paketą. Kiekviena užklausa turi būti santrauka (sikDigest) nuo 1 iki 1 024 baitų su santraukos OID, ir visos užklausos turi dalintis vienu parašo algoritmo OID, vienu santraukos OID ir, RSASSA-PSS atveju, vienu salt ilgiu. Kelių hašų paketas taip pat užkrauna credentials/info ir grąžina spsUnsupported, kai kredencialo multisign reikšmė mažesnė už paketą. Tada SAD užkalėjama prieš paketo pirštų atspaudą – SHA-256 per versijos žymę, skaičių ir, kiekvienai užklausai, algoritmo OID, santraukos OID, algoritmą, salt ilgį ir santraukos baitus, kiekvieną su ilgio priešdėliu. Apkeiskite du hašus vietomis, ir tai kitas paketas, reikalaujantis šviežios autorizacijos

var
  Requests: THPDFCSCSignatureRequests;
  Signatures: THPDFCSCSignatures;
  Status: THPDFSignatureProviderStatus;
  I: Integer;
begin
  SetLength(Requests, Length(Digests));      // Digests: jūsų apskaičiuotos SHA-256 reikšmės
  for I := 0 to High(Digests) do
  begin
    Requests[I] := Default(THPDFSignatureProviderRequest);
    Requests[I].Algorithm := hsaRSAPKCS1v15;           // signAlgo išvedama, kai AlgorithmOID tuščias
    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] priklauso Digests[I]; skaičius buvo patikrintas prieš užklausą
end;

RSASSA-PSS atveju tiekėjas taip pat nusiunčia signAlgoParams – base64 DER RSASSA-PSS-params struktūrą su hašo algoritmu, MGF1 ir salt ilgiu. Jos statymas reiškia OID kodavimą, ir 2.748.5 versija pataisė vieną to kampo: X.690 §8.19.4 pirmuosius du lankus sulanksto į vieną reikšmę (40 × pirmasis + antrasis), o po 2 šaknimi antrasis lankas virš 39 tą reikšmę nustumia už 127, kur jos reikia bazės 128 daugiabaite forma, kurios ankstesni variantai netaikė. Joks SHA-2 OID nepaveiktas – 2.16 sulankstosi į 96 – bet netinkamai suformuotas OID dabar kelia paties tiekėjo klaidą, o ne EConvertError

Kodėl pakartota užklausa negamina antro parašo?

THPDFCSCSignatureProvider priverčia kiekvieną pakartotiną iškvietimą nešti deterministinį idempotency raktą ir talpina į podėlį baigtus rezultatus, tad pakartojimas po pradingusio atsakymo grąžina originalius parašus, vietoj to, kad prašytų HSM naujų. Raktas yra csc-, po kurio seka operacijos identifikatoriaus ir fazės hex SHA-256, o fazė įpina paketo pirštų atspaudą ir autorizacijai, ir signHash. Hašavimas, o ne trumpinimas, svarbus: du ilgi operacijų ID, dalijantys priešdėlį, po trumpinimo susidurtų, o fiksuoto ilgio pagal turinį adresuotas raktas lieka unikalus ir stabilus tarp bandymų

Pakartojimo politika bendrajame užklausų kelyje sąmoningai siaura:

  • HTTP 401 priverčia lygiai vieną token atnaujinimą pro access-token callback, tada užklausa kartojama vienąkart, kai access-token callback priskirtas; antras 401 galutinis
  • Kiti 4xx atsakymai ir ctsPermanentFailure baigia iškvietimą su spsProviderError, o paslaugos error_description nukeliauja į LastError
  • 408, 429, 5xx ir ctsTemporaryFailure kartojami iki RetryLimit (pagal numatymą 2), laukiant Retry-After arba RetryBaseDelayMS × 2attempt (100 ms pagrindo), ribojama ties MaxRetryAfterMS (5 000 ms)
  • Laukimai bėga 25 ms gabalais, tikrinančiais Cancel, tad nutraukęs vartotojas nesėdi per penkių sekundžių atitolinimą
  • signHash kartojamas tik kol EnableIdempotency įjungta; išjungus, laiko limitas po pateikimo galutinis, nes niekas negali pasakyti, ar raktas jau panaudotas
HotPDF pakartojimo politikos diagrama: kiekvienas pakartotinas iškvietimas neša deterministinį csc- idempotency raktą, hašuotą iš operacijos identifikatoriaus ir fazės, HTTP 401 priverčia lygiai vieną token atnaujinimą, kiti 4xx atsakymai baigiasi spsProviderError, o 408, 429, 5xx ar laikinas transporto gedimas kartojami iki RetryLimit 2, laukiant Retry-After ar eksponentinio atitolinimo, ribojamo ties 5 000 ms
Baigti paketai talpinami į podėlį pagal operacijos identifikatorių, kredencialą ir pirštų atspaudą, o asinchroniniu režimu saugomas responseID leidžia pakartotinam iškvietimui pratęsti apklausinėjimą, vietoj to, kad persiųstų hašą

Asinchroninis pasirašymas (operationMode „A“, numatytasis) prideda dar vieną apsaugą: responseID saugomas dar prieš apklausinėjant signatures/signPolling, tad pakartotinas iškvietimas su tą pačiu operacijos identifikatoriumi pratęsia apklausinėjimą, vietoj to, kad persiųstų. Baigti paketai gulės podėlyje, rakta pagal operacijos identifikatorių, kredencialą ir pirštų atspaudą, ribojami MaxOperationCacheEntries (128) ir grąžinami kaip giliosios kopijos. Tas podėlis gyvena tiekėjo egzemplioriuje ir neišgyvena perkrovimo. Idempotency raktas – išgyvena, nes jis išvedamas, o ne atsitiktinis, tad perkrautas procesas, pakartotinai naudojantis savo operacijos identifikatorių, siunčia tą patį raktą – ar paslauga pagal jį dedubliuoja, yra paslaugos pažadas, o ne HotPDF

Kaip CSC parašą įdėti į PDF?

Perduokite tiekėją HPDFCMSSignPDFStreamWithProvider kartu su paskutiniu subjekto sertifikatu iš GetCertificateChain; HotPDF sukonstruoja CMS SignedData, o tiekėjas pasirašo pasirašytų atributų santrauką. Įvedimo PDF reikia /ByteRange ir /Contents vietos ženklo, kuriuos rašo THPDFPage.AddSignedSignatureField – tiksliai kaip PAdES pasirašymo darbo eigoje HotPDF, o tiekėjo modelis toks pats, kokį aprėpia HotPDF keičiami parašų tiekėjai ML-DSA ir EdDSA

var
  Chain: THPDFCSCCertificateChain;
  SignOpts: THPDFCMSSignOptions;
  Src, Dst: TFileStream;
begin
  if Provider.RefreshCredentialInfo <> spsValid then
    raise Exception.Create(Provider.LastError);
  Chain := Provider.GetCertificateChain;   // CSC paskutinį subjekto sertifikatą išvardija pirmą
  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;

/Contents vietos ženklo dydžio parinkimas eina per EstimateSignatureSize, kuri grąžina EstimatedSignatureBytes, kai jūs jį nustatote, ir kitu atveju RSA modulio dydį iš kredencialo rakto ilgio. ECDSA atveju EstimatedSignatureBytes nustatykite patys, kitaip įvertinimas praneša spsUnsupported. Auto-dydžio pasirašymo varianta persirašo, kai vietos ženklas pasirodo per mažas, ir tai daro tik tiems tiekėjams, kurie reklamuoja spcSafeSignRetry – ką THPDFCSCSignatureProvider daro tik kol EnableIdempotency įjungta. PAdES-B-T darbo eigoms TimestampDigest prašo laiko žymos tokeno iš tos pačios paslaugos per signatures/timestamp, ribojamą MaxTimestampBytes (1 MB)

Ko CSC tiekėjas nedaro?

Jis nepasirašo pranešimų, tik santraukas. Ed25519 ir Ed448 grynuoju režimu tiekėjui įteikia visą pasirašytų atributų pranešimą (sikMessage), o paketų tikrintuvas tą atmeta kaip netinkamai suformuotą, nes signHash pagal apibrėžimą grindžiamas hašu. Tiekėjo unitas kompiliuojasi Free Pascal aplinkoje su paprastaisiais funkcijų tipais vietoj anoniminių metodų, bet tiekėjo varomi CMS konstruktoriai šiandien žlunga FPC aplinkoje, tad CSC parašo įtaikymas PDF yra Delphi kelias

Jis taip pat nesprendžia politikos. CredentialInfo praneša rakto būseną, sertifikato būseną, autorizacijos režimą, SCAL lygį ir multisign limitą, bet tiekėjas savarankiškai neatsisakys išjungto rakto ar SCAL1 kredencialo – patikrinkite tai dar prieš rodant pasirašytojui OTP raginimą. Ir vienas tiekėjo egzempliorius vienu metu pasirašo vieną paketą: SignHashBatch viduje serializuojamas, tad dvi gijos negali varžytis dėl vienos SAD, o tai reiškia, kad pralaidumas ateina iš paketavimo, o ne iš tiekėjo dalijimosi tarp darbo gijų. Ar gautasis parašas kvalifikuotas, priklauso nuo pasitikėjimo paslaugos ir jos kredencialo, o ne nuo bibliotekos, kuri hašą ten nugabeno

CSC tiekėjas, CMS ir PAdES konstruktoriai bei vietiniai ir PKCS#11 tiekėjai visi atkeliauja su HotPDF Delphi PDF komponentu