A HotPDF PDF dokumentumokat ír alá olyan privát kulccsal, amit egy távoli Cloud Signature Consortium (CSC) szolgálat tart a kezében, a THPDFCSCSignatureProvider-en keresztül, egy signature provider, ami a CSC API-t vezeti — credential info, hitelesítés, signatures/signHash és polling —, miközben a Delphi alkalmazásod szállítja a HTTP transportot és az OAuth access tokent. A kulcs soha nem hagyja el a szolgálat HSM-jét
Ez egyre inkább az egyetlen módja annak, hogy egyáltalán qualified aláírókulcshoz juss. A trust szolgáltatók CSC végpontot és OAuth klienst osztogatnak, nem PFX fájlt vagy USB tokent, így nincs mit betölteni egy helyi tanúsítványtárba úgy, mint ahogy a Windows cert store aláírás a CNG-n és CAPI-n keresztül teszi. A naiv integráció kiszámítható módon bukik meg: egy signHash hívás kifut az időből, és az újrapróbálás kétszer írja alá ugyanazt a szerződést, vagy negyven számlás tétel negyven egyszer használatos jelszót vált ki, mert minden hash külön lett hitelesítve. A provider tevékenységének nagy része annak a két hibának a kivédése
Miért hagyja a HotPDF a HTTP-t az alkalmazásodra?
Mert a transport pontosan az a hely, ahol minden telepítés eltér. A proxyk, a TLS pinning, a kliens tanúsítványok, a céges OAuth széfbe zárt kulcsok és a naplózási szabályzat mind a HTTP rétegben laknak, ezért a THPDFCSCSignatureProvider a protokollállapotot irányítja, és minden kérésre meghív egy THPDFCSCTransport függvényt. A provider átad neked egy THPDFCSCTransportRequest-et Method-dal (mindig POST), a ServiceBaseURL-ből és a végponti útvonalból összerakott teljes URL-lel, kész Authorization bearer fejléccel, ContentType-tal, a JSON Body-val, IdempotencyKey-vel, az Attempt számmal és MaxResponseBytes-szal. Te egy THPDFCSCTransportResponse-t töltesz meg StatusCode-val, Body-val és RetryAfterMS-szel, és visszaadsz egyet a ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure vagy ctsCancelled közül
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 // fejlécnév, ahogy a te szolgálatod dokumentálja
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // socket- vagy DNS-gond: újrapróbálható
end;
Response.StatusCode := HttpResp.StatusCode; // jelentsd a 503-at úgy, ahogy van, ne osztályozd
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;
Az egyetlen szabály, amit érdemes megjegyezni: add vissza a ctsSuccess-ot, valahányszor a szerver ténylegesen válaszolt, még 503-mal is. A provider maga osztályozza a státuszkódokat, és egy transport, ami egy 429-et ctsPermanentFailure-ré tesz, csendben letiltja az alább leírt újrapróbálási logikát. A konstruktor a másik irányban szigorú — EHPDFCSCSignatureProviderError-t dob, ha a transport hiányzik, a CredentialID üres, se AccessToken se token callback nincs megadva, egy budget a tartományon kívül esik, vagy a ServiceBaseURL nem HTTPS. A sima http:// csak AllowInsecureHTTP-val fogadható el, ami egy tesztkörnyezetbe való, és sehova máshova
Mi az a SAD, és miért dobja ki a HotPDF egyetlen használat után?
A THPDFCSCSignatureProvider a Signature Activation Data-t (SAD) egyszer használatosként kezeli: törlődik a provider állapotából abban a pillanatban, amikor a signatures/signHash elfogadásra kerül, akkor is, ha maga az aláírás később, aszinkron pollingon át érkezik. A SAD a szolgálat bizonyítéka arra, hogy az aláíró jóváhagyta pontosan ezeket a hasheket, és egy memóriában lógó SAD egy jogosultság, ami arra vár, hogy a rossz dokumentumra költsék el
A THPDFCSCOptions.Default alapértékeivel — RequireSAD és AutoAuthorize egyaránt True — a provider egyszer betölti a credentials/info-t, a THPDFCSCAuthenticationCallback-től kéri az authData értékeket (egy OTP, egy PIN, bármi, amit a credential auth blokkja kíván), és elküldi a credentials/authorize-t. A 200 közvetlenül hordozza a SAD-ot; a 202 egy handlet hordoz, amit a credentials/authorizeCheck-en át polloznak legfeljebb MaxPollAttempts (60) alkalommal PollIntervalMS (250 ms) időközzel. A callback legfeljebb 32 értéket adhat vissza, mindegyik legfeljebb 256 bájtos nem üres ID-vel és legfeljebb 4 096 bájtos értékkel. Ha a hálózat megszakad, mielőtt a signHash elfogadásra kerülne, egy automatikusan beszerzett SAD megmarad, hogy ugyanaz a tétel újrapróbálható legyen az aláíró újbóli megkérdezése nélkül
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, aszinkron mód, 2 újrapróbálás
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
// a te OAuth kliensed; a ForceRefresh True a szolgálat 401-es válasza után
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 // a te UI-d
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Egy SAD, amit magad adsz át az Options.SAD-on, másképp viselkedik, és szándékosan. A HotPDF nem tudhatja, mely hashekre bocsátották ki, ezért a provider az előre beállított SAD-ot csak egyetlen hashes kérésre használja. Egy tételnél, amelynél az AutoAuthorize kikapcsolt, a provider „CSC SAD is not pinned to the requested hash batch" üzenettel bukik meg a találgatás helyett
Hogyan ír alá a SignHashBatch sok dokumentumot egy hitelesítéssel?
A SignHashBatch egy credentials/authorize-t és egy signatures/signHash-t küld legfeljebb MaxBatchSignatures (64) digestre, és mindkét testet ugyanabból a tömbből építi, hogy a numSignatures, a hashes sorrendje és a hashAlgorithmOID azonos legyen a két hívásban. Ez az egyezés az, amit a CSC multisign modell megkövetel. Negyvenszer ciklusozd a single-hash Sign metódust, és negyven hitelesítést kapsz; küldj egy egymással ellentmondó authorize-t és signHash-t, és a szolgálat a rossz tételre égetheti el a SAD-ot
Mielőtt bármilyen hálózati forgalom elindulna, a provider validálja a tételt. Minden kérésnek 1-től 1 024 bájtig terjedő digestnek (sikDigest) kell lennie digest OID-del, és minden kérésnek egyetlen aláírási algoritmus OID-et, egyetlen digest OID-et, RSASSA-PSS esetén pedig egyetlen sóhosszt kell megosztania. Egy multi-hash tétel betölti a credentials/info-t is, és spsUnsupported-ot ad vissza, amikor a credential multisign értéke kisebb, mint a tétel. A SAD ezután egy tételujjlenyomathoz rögzül — egy SHA-256 egy verziócímkén, a számon és, kérésenként, az algoritmus OID-en, a digest OID-en, az algoritmuson, a sóhosszon és a digest bájtokon, mindegyik hosszelőtaggal. Cserélj fel két hash-t, és máris egy másik tétel, ami friss hitelesítést igényel
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: a te számolt SHA-256 értékeid
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // a signAlgo származtatódik, ha az AlgorithmOID üres
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]);
// A Signatures[I] a Digests[I]-hez tartozik; a számot a kérés ellenőrizte
end;
RSASSA-PSS-hez a provider signAlgoParams-t is küld, egy base64 DER RSASSA-PSS-params struktúrát a hash algoritmussal, az MGF1-gyel és a sóhosszal. Felépíteni azt OID-ek kódolását jelenti, és a 2.748.5-ös verzió ennek egy zugát javította: az X.690 §8.19.4-e az első két ívet egyetlen értékbe hajtja (40 × első + második), és a 2 gyökér alatt egy 39 fölötti második ív azt az értéket 127 fölé nyomja, ahol a base-128 több bájtos formára van szükség, amit a korábbi buildek nem alkalmaztak. Egyetlen SHA-2 OID-et sem érint — a 2.16 96-ra hajlik — de egy rosszul formált OID mostantól a provider saját hibáját dobja egy EConvertError helyett
Miért nem gyárt második aláírást egy újrapróbált kérés?
A THPDFCSCSignatureProvider minden újrapróbálható hívást determinisztikus idempotencia kulccsal lát el, és cache-eli a befejezett eredményeket, így egy elveszett válasz utáni újrapróbálás az eredeti aláírásokat adja vissza ahelyett, hogy újakat kérne a HSM-től. A kulcs a csc- prefix, majd az operációazonosító és a fázis hex SHA-256-a, és a fázis mind a hitelesítéshez, mind a signHash-hoz beágyazza a tételujjlenyomatot. A hashelés számít a csonkolás helyett: két hosszú operációazonosító, amik megosztanak egy prefixet, csonkolás alatt ütköznének, míg egy fix hosszú tartalomalapú kulcs egyedi és stabil marad a próbálkozások között
Az újrapróbálási szabályzat a megosztott kérésútvonalban szándékosan szűk:
- A HTTP 401 pontosan egy tokenfrissítést kényszerít az access-token callbacken át, majd a kérés egyszer megismétlődik, ha van hozzárendelve access-token callback; a második 401 végleges
- A többi 4xx válasz és a
ctsPermanentFailurespsProviderError-ral fejezi be a hívást, és a szolgálaterror_description-ja aLastError-be kerül - A 408, a 429, az 5xx és a
ctsTemporaryFailurelegfeljebbRetryLimit(alapból 2) alkalommal próbálódik újra,Retry-After-ra vagyRetryBaseDelayMS× 2attempt (100 ms alap) értékre várva,MaxRetryAfterMS-re (5 000 ms) korlátozva - A várakozások 25 ms-os szeletekben futnak, amik ellenőrzik a
Cancel-t, így egy megszakító felhasználónak nem kell végigülnie egy öt másodperces back-offot - A
signHashcsak addig próbálódik újra, amíg azEnableIdempotencybe van kapcsolva; kapcsold ki, és egy beküldés utáni timeout végleges, mert senki nem tudja megmondani, használta-e már valaki a kulcsot
Az aszinkron aláírás (operationMode „A", az alapértelmezett) még egy őrt tesz hozzá: a responseID tárolódik, mielőtt a signatures/signPolling-t pollolnák, így egy ugyanazzal az operációazonosítóval érkező ismételt hívás folytatja a pollingot az újbóli beküldés helyett. A befejezett tételek egy, operációazonosító, credential és ujjlenyomat szerint kulcsolt cache-ben ülnek, MaxOperationCacheEntries-re (128) korlátozva, mély másolatként visszaadva. Az a cache a provider instanciájában lakik, és nem éli túl az újraindítást. Az idempotencia kulcs igen, mert származtatott, nem véletlen, így egy újraindított process, ami újrahasznosítja az operációazonosítóját, ugyanazt a kulcsot küldi — hogy a szolgálat deduplikál-e rá, az a szolgálat ígérete, nem a HotPDF-é
Hogyan kerül CSC aláírás egy PDF-be?
Add át a providert a HPDFCMSSignPDFStreamWithProvider-nek a GetCertificateChain-ből származó végentitás-tanúsítvánnyal együtt; a HotPDF felépíti a CMS SignedData-t, a provider pedig aláírja az aláírt attribútumok digestjét. A bemeneti PDF-nek szüksége van a /ByteRange-re és a /Contents helyőrzőre, amit a THPDFPage.AddSignedSignatureField ír, pontosanúgy, mint a PAdES aláírási munkafolyamat a HotPDF-ben, a provider modell pedig ugyanaz, amit a HotPDF cserélhető signature providerek ML-DSA-hoz és EdDSA-hoz tárgyal
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // a CSC a végentitás tanúsítványt sorolja először
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;
A /Contents helyőrző méretezése az EstimateSignatureSize-on át megy, ami a beállításod esetén az EstimatedSignatureBytes-t adja vissza, egyébként az RSA modulus méretét a credential kulcshosszából. ECDSA-nál állítsd be magad az EstimatedSignatureBytes-t, vagy a becslés spsUnsupported-ot jelent. Az automatikus méretű aláíró variáns újraaláír, amikor egy helyőrző kicsinek bizonyul, és csak olyan providereknek teszi ezt, amik hirdetik a spcSafeSignRetry-t — amit a THPDFCSCSignatureProvider csak akkor tesz, amíg az EnableIdempotency be van kapcsolva. PAdES-B-T munkafolyamatokhoz a TimestampDigest időbélyeg tokent kér ugyanattól a szolgálattól a signatures/timestamp-en át, MaxTimestampBytes-re (1 MB) korlátozva
Mit nem tesz a CSC provider?
Nem ír alá üzeneteket, csak digesteket. Az Ed25519 és az Ed448 tiszta módban a teljes aláírt-attribútum üzenetet adja a provider kezébe (sikMessage), és a tételvalidátor rosszul formáltként utasítja el, mert a signHash definíció szerint hash-alapú. A provider unit Free Pascal alatt is fordul sima függvénytípusokkal az anonim metódusok helyett, de a provider-vezérelt CMS építők ma FPC alatt kivételt dobnak, így egy CSC aláírás PDF-be ágyazása Delphi-útvonal
Szabályzatot sem dönt. A CredentialInfo jelenti a kulcs állapotát, a tanúsítvány állapotát, a hitelesítési módot, a SCAL szintet és a multisign limitet, de a provider önmagától nem utasít el letiltott kulcsot vagy SCAL1 credentiált — ellenőrizd ezeket, mielőtt egy aláírónak megmutatnád az OTP promptot. És egy provider instancia egyszer egy tételt ír alá: a SignHashBatch belül szerializált, így két szál nem versenyezhet egy SAD-ért, ami azt jelenti, hogy az áteresztőképesség a tételesítésből jön, nem abból, hogy providert osztanak meg worker szálak között. Hogy az eredményaláírás qualified-e, az a trust szolgálaton és a credentialján múlik, nem a libraryn, ami a hash-t oda szállította
A CSC provider, a CMS és PAdES építők, valamint a lokális és PKCS#11 providerek mind a HotPDF Delphi PDF komponensben érkeznek