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
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ą
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
ctsPermanentFailurebaigia iškvietimą suspsProviderError, o paslaugoserror_descriptionnukeliauja įLastError - 408, 429, 5xx ir
ctsTemporaryFailurekartojami ikiRetryLimit(pagal numatymą 2), laukiantRetry-AfterarbaRetryBaseDelayMS× 2attempt (100 ms pagrindo), ribojama tiesMaxRetryAfterMS(5 000 ms) - Laukimai bėga 25 ms gabalais, tikrinančiais
Cancel, tad nutraukęs vartotojas nesėdi per penkių sekundžių atitolinimą signHashkartojamas tik kolEnableIdempotencyįjungta; išjungus, laiko limitas po pateikimo galutinis, nes niekas negali pasakyti, ar raktas jau panaudotas
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