HotPDF allekirjoittaa PDF-asiakirjoja yksityisellä avaimella, jota pitää hallussaan etäinen Cloud Signature Consortium (CSC) -palvelu, THPDFCSCSignatureProviderin kautta — allekirjoituspalveluntarjoaja, joka ajaa CSC API:a: credential infoa, valtuutusta, signatures/signHashia ja pollausta — sillä aikaa kun Delphi-sovelluksesi toimittaa HTTP-kuljetuksen ja OAuth-pääsytokenin. Avain ei koskaan poistu palvelun HSM:stä
Se on yhä useammin ainoa tapa saada lainkaan kvalifioitu allekirjoitusavain. Luottamuspalveluntarjoajat jakavat CSC-päätepisteen ja OAuth-asiakkaan, ei PFX-tiedostoa eikä USB-tokenia, joten paikalliseen sertifikaattisäilöön ei ole mitään ladattavaa siinä missä Windowsin sertifikaattisäilön allekirjoitus CNG:n ja CAPI:n kautta tekee. Naiivi integraatio kaatuu ennustettavilla tavoilla: signHash-kutsu aikakatkeaa ja uusinta allekirjoittaa saman sopimuksen kahdesti, tai erä neljääkymmentä laskua laukoo neljäkymmentä kertakäyttösalasanaa, koska jokainen tiiviste valtuutettiin erikseen. Suurin osa siitä, mitä palveluntarjoaja tekee, on puolustautumista näitä kahta epäonnistumista vastaan
Miksi HotPDF jättää HTTP:n sovelluksellesi?
Koska kuljetus on täsmälleen se kohta, jossa jokainen käyttöönotto eroaa. Välityspalvelimet, TLS-pinnitys, asiakassertifikaatit, yrityksen OAuth-holvit ja lokituspolitiikka elävät kaikki HTTP-kerroksessa, joten THPDFCSCSignatureProvider orkestroi protokollan tilan ja kutsuu THPDFCSCTransport-funktiota jokaista pyyntöä kohden. Palveluntarjoaja antaa sinulle THPDFCSCTransportRequestin, jossa on Method (aina POST), täysi URL, joka on rakennettu ServiceBaseURLista plus päätepistepolku, valmis Authorization-bearer-otsikko, ContentType, JSON-Body, IdempotencyKey, Attempt-numero ja MaxResponseBytes. Sinä täytät THPDFCSCTransportResponsein kentillä StatusCode, Body ja RetryAfterMS ja palautat yhden arvoista ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure tai 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 // otsikon nimi niin kuin palvelusi dokumentoi sen
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- tai DNS-häiriö: uusittava
end;
Response.StatusCode := HttpResp.StatusCode; // raportoi 503 sellaisenaan, älä luokittele
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;
Sääntö, joka kannattaa opetella ulkoa: palauta ctsSuccess aina, kun palvelin oikeasti vastasi, vaikka 503:lla. Palveluntarjoaja luokittelee tilakoodit itse, ja kuljetus, joka muuttaa 429:n ctsPermanentFailureiksi, poistaa alla kuvatun uusintalogiikan hiljaa käytöstä. Konstruktori on tiukka toiseen suuntaan — se nostaa EHPDFCSCSignatureProviderErrorin, kun kuljetus puuttuu, CredentialID on tyhjä, ei ole annettu AccessTokenia eikä token-takaisinkutsua, budjetti on alueen ulkopuolella tai ServiceBaseURL ei ole HTTPS. Pelkkä http:// hyväksytään vain AllowInsecureHTTPilla, joka kuuluu testiympäristöön eikä mihinkään muuhun
Mikä SAD on, ja miksi HotPDF heittää sen pois yhden käytön jälkeen?
THPDFCSCSignatureProvider käsittelee Signature Activation Dataa (SAD) kertakäyttöisenä: se pyyhitään palveluntarjoajan tilasta sillä hetkellä, kun signatures/signHash hyväksytään, silloinkin kun allekirjoitus itse saapuu myöhemmin asynkronisen pollauksen kautta. SAD on palvelun todiste siitä, että allekirjoittaja hyväksyi juuri nämä tiivisteet, ja muistissa roikkuva SAD on valtuutus, joka odottaa kuluttamista väärälle asiakirjalle
Oletuksilla THPDFCSCOptions.Defaultista — RequireSAD ja AutoAuthorize molemmat True — palveluntarjoaja lataa credentials/infoin kerran, kysyy THPDFCSCAuthenticationCallbackiltasi authData-arvot (OTP, PIN, mitä tahansa sertifikaatin auth-lohko vaatii) ja lähettää credentials/authorizein. 200 kantaa SAD:n suoraan; 202 kantaa kahvan, jota pollataan credentials/authorizeCheckin kautta enintään MaxPollAttempts (60) kertaa PollIntervalMSin (250 ms) välein. Takaisinkutsu saa palauttaa enintään 32 arvoa, joista kummallakin on ei-tyhjä enintään 256 tavun ID ja enintään 4 096 tavun arvo. Jos verkko katkeaa ennen kuin signHash hyväksytään, automaattisesti saatu SAD säilytetään, joten sama erä voidaan uusia kysymättä allekirjoittajalta uudelleen
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, asynkroninen tila, 2 uusintaa
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
// OAuth-asiakkaasi; ForceRefresh on True sen jälkeen kun palvelu vastasi 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 // käyttöliittymäsi
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Itse Options.SADin kautta välittämäsi SAD käyttäytyy toisin, ja tahallaan. HotPDF ei voi tietää, mille tiivisteille se myönnettiin, joten palveluntarjoaja käyttää esiasetettua SAD:ia vain yhden tiivisteen pyyntöön. Erälle, jolla AutoAuthorize on pois päältä, palveluntarjoaja epäonnistuu virheeseen ”CSC SAD is not pinned to the requested hash batch” arvaamisen sijaan
Miten SignHashBatch allekirjoittaa monta asiakirjaa yhdellä valtuutuksella?
SignHashBatch lähettää yhden credentials/authorizein ja yhden signatures/signHashin enintään MaxBatchSignatures (64) tiivisteelle ja rakentaa molemmat rungot samasta taulukosta niin, että numSignatures, hashesin järjestys ja hashAlgorithmOID ovat identtiset kummassakin kutsussa. Se täsmäys on se, mitä CSC:n multisign-malli vaatii. Silmukoi yhden tiivisteen Sign-metodia neljäkymmentä kertaa ja saat neljäkymmentä valtuutusta; lähetä authorize ja signHash, jotka eroavat toisistaan, ja palvelu voi kuluttaa SAD:n väärälle erälle
Ennen mitään verkkoliikennettä palveluntarjoaja valido erän. Jokaisen pyynnön on oltava tiiviste (sikDigest), jonka pituus on 1–1 024 tavua ja jolla on tiiviste-OID, ja kaikkien pyyntöjen on jaettava yksi allekirjoitusalgoritmin OID, yksi tiiviste-OID ja, RSASSA-PSS:lle, yksi suolan pituus. Monitiivisteinen erä lataa myös credentials/infoin ja palauttaa spsUnsupportedin, kun sertifikaatin multisign-arvo on pienempi kuin erä. SAD kiinnitetään sitten erän sormenjälkeen — SHA-256 versiolabelista, määrästä ja jokaisen pyynnön kohdalla algoritmin OIDista, tiiviste-OIDista, algoritmista, suolan pituudesta ja tiivistetavuista, kukin pituusetuliitteellä. Vaihda kaksi tiivistettä keskenään, niin kyseessä on eri erä, joka tarvitsee tuoreen valtuutuksen
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: laskemasi SHA-256-arvot
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo johdetaan kun AlgorithmOID on tyhjä
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] kuuluu Digests[I]:lle; määrä tarkistettiin pyyntöä vasten
end;
RSASSA-PSS:lle palveluntarjoaja lähettää myös signAlgoParamsin, base64-DER-RSASSA-PSS-params-rakenteen tiivistealgoritmilla, MGF1:llä ja suolan pituudella. Sen rakentaminen tarkoittaa OIDien koodaamista, ja versio 2.748.5 korjasi yhden sen kulman: X.690 §8.19.4 taittaa kaksi ensimmäistä kaarta yhdeksi arvoksi (40 × ensimmäinen + toinen), ja 2-juuren alla toinen kaari yli 39:n työntää kyseisen arvon yli 127:n, jolloin se tarvitsee kantaluku 128:n monitavuisen muodon, jota aiemmat koosteet eivät soveltaneet. Mikään SHA-2-OID ei ole vaikutusalueella — 2.16 taittuu arvoon 96 — mutta epämuodostunut OID nostaa nyt palveluntarjoajan oman virheen EConvertErrorin sijaan
Miksi uusittu pyyntö ei tuota toista allekirjoitusta?
THPDFCSCSignatureProvider panee jokaisen uusittavan kutsun kantamaan deterministisen idempotenssiavaimen ja välimuistittaa valmiit tulokset, joten uusinta kadonneen vastauksen jälkeen palauttaa alkuperäiset allekirjoitukset sen sijaan että pyytäisi HSM:ltä uusia. Avain on csc- ja sitä seuraa operaatiotunnisteen ja vaiheen heksadesimaalinen SHA-256, ja vaihe upottaa erän sormenjäljen sekä valtuutukseen että signHashiin. Tiivistäminen katkaisemisen sijaan on merkitsevää: kaksi pitkää operaatiotunnistetta, joilla on yhteinen etuliite, törmäisivät katkaisun alla, kun taas kiinteäpituinen sisältöosoitteinen avain pysyy ainutlaatuisena ja vakaana yritysten yli
Uusintapolitiikka jaetussa pyyntöpolussa on tarkoituksella kapea:
- HTTP 401 pakottaa täsmälleen yhden tokenin päivityksen pääsytokenin takaisinkutsun kautta, sitten pyyntö toistetaan kerran, kun pääsytokenin takaisinkutsu on sijoitettu; toinen 401 on lopullinen
- Muut 4xx-vastaukset ja
ctsPermanentFailurepäättävät kutsunspsProviderErroriin, ja palvelunerror_descriptionpäätyyLastErroriin - 408, 429, 5xx ja
ctsTemporaryFailureuusitaan enintäänRetryLimit(oletus 2) kertaa, odottaenRetry-Afteria taiRetryBaseDelayMS× 2attemptia (100 ms perusta), kattolukunaMaxRetryAfterMS(5 000 ms) - Odotukset ajavat 25 ms viipaleissa, jotka tarkistavat
Cancelin, joten keskeyttävä käyttäjä ei joudu istumaan viiden sekunnin viiveen läpi signHashia uusitaan vain, kunEnableIdempotencyon päällä; kytke se pois, ja aikakatkaisu lähetyksen jälkeen on lopullinen, koska kukaan ei voi tietää, käytettiinkö avainta jo
Asynkroninen allekirjoitus (operationMode ”A”, oletus) lisää yhden vartiota lisää: responseID tallennetaan ennen kuin signatures/signPollingia pollataan, joten toistettu kutsu samalla operaatiotunnisteella jatkaa pollausta uudelleenlähetyksen sijaan. Valmiit erät istuvat välimuistissa, jonka avaimina ovat operaatiotunniste, sertifikaatti ja sormenjälki, kattolukuna MaxOperationCacheEntries (128), ja ne palautetaan syväkopioina. Kyseinen välimuisti asuu palveluntarjoajainstanssissa eikä selviä uudelleenkäynnistyksestä. Idempotenssiavain selviää, koska se on johdettua eikä satunnaista, joten uudelleenkäynnistetty prosessi, joka käyttää operaatiotunnistettaan uudelleen, lähettää saman avaimen — deduplikoiko palvelu sen perusteella, on palvelun lupaus, ei HotPDF:n
Miten saat CSC-allekirjoituksen PDF:ään?
Välitä palveluntarjoaja HPDFCMSSignPDFStreamWithProviderille yhdessä loppukäyttäjän sertifikaatin kanssa funktiolta GetCertificateChain; HotPDF rakentaa CMS SignedDatan ja palveluntarjoaja allekirjoittaa allekirjoitettujen attribuuttien tiivisteen. Syöte-PDF tarvitsee /ByteRange- ja /Contents-paikanpitäjät, jotka THPDFPage.AddSignedSignatureField kirjoittaa, täsmälleen niin kuin artikkelissa PAdES-allekirjoituksen työnkulku HotPDF:ssä, ja palveluntarjoajamalli on sama, jota käsittelee artikkeli HotPDF:n liitettävät allekirjoituspalveluntarjoajat ML-DSA:lle ja EdDSA:lle
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC luettelee loppukäyttäjän sertifikaatin ensin
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-paikanpitäjän koon arviointi kulkee EstimateSignatureSizein kautta, joka palauttaa EstimatedSignatureBytesin, kun asetat sen, ja muuten RSA-moduluksen koon sertifikaatin avainpituudesta. ECDSA:lle aseta EstimatedSignatureBytes itse, tai arviointi raportoi spsUnsupportedin. Automaattikokoinen allekirjoitusvarianti allekirjoittaa uudelleen, kun paikanpitäjä osoittautuu liian pieneksi, ja se tekee niin vain palveluntarjoajille, jotka mainostavat spcSafeSignRetryia — mitä THPDFCSCSignatureProvider tekee vain, kun EnableIdempotency on päällä. PAdES-B-T-työnkuluille TimestampDigest pyytää aikaleimatokenin samalta palvelulta signatures/timestampin kautta, kattolukuna MaxTimestampBytes (1 MB)
Mitä CSC-palveluntarjoaja ei tee?
Se ei allekirjoita viestejä, vain tiivisteitä. Ed25519 ja Ed448 puhaassa tilassa antavat palveluntarjoajalle koko allekirjoitettujen attribuuttien viestin (sikMessage), ja erän validoija hylkää sen epämuodostuneena, koska signHash on määritelmällisesti tiivisteisiin pohjautuva. Palveluntarjoajayksikkö kääntyy Free Pascalin alla tavallisilla funktiotyypeillä anonyymien metodien sijaan, mutta palveluntarjoajavetoiset CMS-rakentajat nostavat poikkeuksen FPC:llä tänään, joten CSC-allekirjoituksen upottaminen PDF:ään on Delphi-polku
Se ei myöskään päätä politiikkaa. CredentialInfo raportoi avaimen tilan, sertifikaatin tilan, valtuutustilan, SCAL-tason ja multisign-rajan, mutta palveluntarjoaja ei kiellä käytöstä poistettua avainta tai SCAL1-sertifikaattia omatoimisesti — tarkista ne ennen kuin näytät allekirjoittajalle OTP-kehotteen. Ja yksi palveluntarjoajainstanssi allekirjoittaa yhden erän kerrallaan: SignHashBatch sarjoitetaan sisäisesti, joten kaksi säiettä ei voi kilpailla yhdestä SAD:sta, mikä tarkoittaa, että läpäisy tulee eräyttämisestä, ei palveluntarjoajan jakamisesta säietyöläisten kesken. Onko lopullinen allekirjoitus kvalifioitu, riippuu luottamuspalvelusta ja sen sertifikaatista, ei kirjastosta, joka kantoi tiivisteen sinne
CSC-palveluntarjoaja, CMS- ja PAdES-rakentajat sekä paikalliset ja PKCS#11-palveluntarjoajat toimitetaan kaikki HotPDF Delphi PDF -komponentissa