HotPDF signerer PDF-dokumenter med en privat nøgle hos en ekstern Cloud Signature Consortium-tjeneste (CSC) gennem THPDFCSCSignatureProvider, en signature provider, der driver CSC-API'en — credential info, autorisation, signatures/signHash og polling — mens din Delphi-applikation leverer HTTP-transporten og OAuth access token. Nøglen forlader aldrig tjenestens HSM
Det er i stigende grad den eneste måde at få en kvalificeret signeringsnøgle overhovedet. Trust service providers udleverer et CSC-endpoint og en OAuth-klient, ikke en PFX-fil eller en USB-token, så der er intet at loade ind i et lokalt certifikatlager, sådan som Windows cert store-signering gennem CNG og CAPI gør. Den naive integration fejler på forudsigelige måder: et signHash-kald timer ud, og retry'en signerer samme kontrakt to gange, eller en batch på fyrre fakturaer udløser fyrre engangskoder, fordi hver hash blev autoriseret separat. Det meste af, hvad provideren gør, er at forsvare sig mod de to fejl
Hvorfor lader HotPDF HTTP være din applikations sag?
Fordi transporten er præcis dér, hvor hver deployment adskiller sig. Proxies, TLS pinning, client certificates, virksomhedens OAuth-vaults og logningspolicy bor alle i HTTP-laget, så THPDFCSCSignatureProvider orkestrerer protokoltilstanden og kalder en THPDFCSCTransport-funktion for hver request. Provideren giver dig en THPDFCSCTransportRequest med Method (altid POST), den fulde URL bygget af ServiceBaseURL plus endpoint-stien, en klar Authorization bearer-header, ContentType, JSON-Body, en IdempotencyKey, Attempt-nummeret og MaxResponseBytes. Du fylder en THPDFCSCTransportResponse med StatusCode, Body og RetryAfterMS og returnerer én af ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure eller 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 // headernavn som din tjeneste dokumenterer det
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- eller DNS-problemer: kan retries
end;
Response.StatusCode := HttpResp.StatusCode; // rapportér 503 som den er, klassificér ikke
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;
Den ene regel, der er værd at huske: returnér ctsSuccess, når som helst en server faktisk svarede, også med en 503. Provideren klassificerer selv statuskoder, og en transport, der gør en 429 til ctsPermanentFailure, deaktiverer i stilhed den retry-logik, der beskrives nedenfor. Constructoren er streng i den anden retning — den rejser EHPDFCSCSignatureProviderError, når transporten mangler, CredentialID er tom, hverken en AccessToken eller en token-callback er givet, en budget er ude af interval, eller ServiceBaseURL ikke er HTTPS. Bar http:// accepteres kun med AllowInsecureHTTP, som hører hjemme i et test-setup og ingen andre steder
Hvad er SAD, og hvorfor smider HotPDF den væk efter én brug?
THPDFCSCSignatureProvider behandler Signature Activation Data (SAD) som single-use: den ryddes fra provider-tilstanden i det øjeblik signatures/signHash accepteres, selv når signaturen selv ankommer senere gennem asynkron polling. SAD'en er tjenestens bevis på, at underskriveren godkendte netop disse hashes, og en SAD, der ligger og driver i hukommelsen, er en autorisation, der venter på at blive brugt på det forkerte dokument
Med defaults fra THPDFCSCOptions.Default — RequireSAD og AutoAuthorize begge True — loader provideren credentials/info én gang, beder din THPDFCSCAuthenticationCallback om authData-værdierne (en OTP, en PIN, hvad end credentialens auth-blok kræver) og poster credentials/authorize. En 200 bærer SAD'en direkte; en 202 bærer et handle, der polles gennem credentials/authorizeCheck op til MaxPollAttempts (60) gange med PollIntervalMS (250 ms). Callbacken må returnere højst 32 værdier, hver med en ikke-tom ID på op til 256 bytes og en værdi på op til 4.096 bytes. Dropper netværket, før signHash accepteres, gemmes en automatisk opnået SAD, så samme batch kan retries uden at spørge underskriveren igen
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async mode, 2 retries
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
// din OAuth-klient; ForceRefresh er True, efter tjenesten svarede 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 // din UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
En SAD, du giver selv gennem Options.SAD, opfører sig anderledes, og med vilje. HotPDF kan ikke vide, hvilke hashes den blev udstedt til, så provideren bruger en forudindstillet SAD kun til en single-hash-request. For en batch med AutoAuthorize slået fra fejler provideren med "CSC SAD is not pinned to the requested hash batch" i stedet for at gætte
Hvordan signerer SignHashBatch mange dokumenter med én autorisation?
SignHashBatch sender én credentials/authorize og én signatures/signHash for op til MaxBatchSignatures (64) digests og bygger begge bodier ud fra samme array, så numSignatures, rækkefølgen af hashes og hashAlgorithmOID er identiske i de to kald. Det match er, hvad CSC multisign-modellen kræver. Loop single-hash-Sign-metoden fyrre gange, og du får fyrre autorisationer; send en authorize og en signHash, der er uenige, og tjenesten kan bruge SAD'en på den forkerte batch
Før al netværkstrafik validerer provideren batchen. Hver request skal være en digest (sikDigest) på 1 til 1.024 bytes med en digest-OID, og alle requests skal dele én signaturalgoritme-OID, én digest-OID og, for RSASSA-PSS, én salt-længde. En multi-hash-batch loader også credentials/info og returnerer spsUnsupported, når credentialens multisign-værdi er mindre end batchen. SAD'en fastgøres derefter til et batch-fingeraftryk — en SHA-256 over et versionslabel, antallet og pr. request algoritme-OID, digest-OID, algoritme, salt-længde og digest-bytes, hver med længdepræfiks. Bytter du to hashes, er det en anden batch, der kræver en frisk autorisation
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256-værdier, du har beregnet
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo afledt, når AlgorithmOID er tom
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] hører til Digests[I]; antallet blev tjekket mod requesten
end;
Til RSASSA-PSS sender provideren også signAlgoParams, en base64 DER RSASSA-PSS-params-struktur med hash-algoritmen, MGF1 og salt-længden. At bygge den betyder at encode OID'er, og version 2.748.5 rettede et hjørne af det: X.690 §8.19.4 folder de første to arcs sammen til én værdi (40 × første + anden), og under 2-roden skubber en anden arc over 39 den værdi forbi 127, hvor den behøver base-128 multi-byte-formen, som tidligere builds ikke anvendte. Ingen SHA-2-OID er påvirket — 2.16 folder til 96 — men en misdannet OID rejser nu providerens egen fejl i stedet for en EConvertError
Hvorfor producerer en gentaget request ikke en ekstra signatur?
THPDFCSCSignatureProvider giver hvert retrybart kald en deterministisk idempotency-nøgle og cacher fuldførte resultater, så en retry efter et tabt svar returnerer de originale signaturer i stedet for at bede HSM'en om nye. Nøglen er csc- efterfulgt af hex SHA-256 af operations-identifikatoren og fasen, og fasen indlejrer batch-fingeraftrykket for både autorisation og signHash. At hashe i stedet for at truncate betyder noget: to lange operations-ID'er, der deler et præfiks, ville kollidere under truncation, mens en fastlængde content-adresseret nøgle forbliver unik og stabil på tværs af forsøg
Retry-policen i den delte request-sti er bevidst snæver:
- HTTP 401 gennemtvinger præcis én token refresh gennem access-token-callbacken, og derefter gentages requesten én gang, når en access-token-callback er tildelt; en anden 401 er endelig
- Andre 4xx-svar og
ctsPermanentFailureafslutter kaldet medspsProviderError, og tjenestenserror_descriptionlander iLastError - 408, 429, 5xx og
ctsTemporaryFailureretries op tilRetryLimit(default 2) med ventetid påRetry-AfterellerRetryBaseDelayMS× 2attempt (100 ms base), begrænset tilMaxRetryAfterMS(5.000 ms) - Ventetider kører i 25 ms-skiver, der tjekker
Cancel, så en bruger, der afbryder, ikke skal sidde ud en fem sekunders back-off signHashretries kun, mensEnableIdempotencyer slået til; slå den fra, og en timeout efter afsendelse er endelig, for ingen kan se, om nøglen allerede var brugt
Asynkron signering (operationMode "A", default) tilføjer endnu en vagt: responseID gemmes, før der polles signatures/signPolling, så et gentaget kald med samme operations-identifikator genoptager pollingen i stedet for at indsende igen. Fuldførte batches ligger i en cache nøglet efter operations-identifikator, credential og fingeraftryk, begrænset af MaxOperationCacheEntries (128) og returneret som deep copies. Cachen lever i provider-instansen og overlever ikke en genstart. Idempotency-nøglen gør, for den er afledt og ikke tilfældig, så en genstartet proces, der genbruger sin operations-identifikator, sender samme nøgle — hvorvidt tjenesten deduplikerer på den, er tjenestens løfte, ikke HotPDFs
Hvordan får du en CSC-signatur ind i en PDF?
Giv provideren til HPDFCMSSignPDFStreamWithProvider sammen med end-entity-certifikatet fra GetCertificateChain; HotPDF bygger CMS SignedData, og provideren signerer digesten af de signed attributes. Input-PDF'en behøver /ByteRange- og /Contents-pladsholderen, som THPDFPage.AddSignedSignatureField skriver, præcis som i PAdES-signeringsworkflowet i HotPDF, og provider-modellen er den samme, der dækkes i HotPDF pluggable signature providers til ML-DSA og EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC lister end-entity-certifikatet først
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;
Dimensionering af /Contents-pladsholderen går gennem EstimateSignatureSize, som returnerer EstimatedSignatureBytes, når du sætter den, og ellers RSA modulus-størrelsen fra credentialens nøglelængde. Til ECDSA sæt selv EstimatedSignatureBytes, eller estimeringen rapporterer spsUnsupported. Auto-size signeringsvarianten signerer igen, når en pladsholder viser sig at være for lille, og den gør det kun for providers, der annoncerer spcSafeSignRetry — hvilket THPDFCSCSignatureProvider kun gør, mens EnableIdempotency er slået til. Til PAdES-B-T-workflows anmoder TimestampDigest om et timestamp-token fra samme tjeneste gennem signatures/timestamp, begrænset til MaxTimestampBytes (1 MB)
Hvad gør CSC-provideren ikke?
Den signerer ikke beskeder, kun digests. Ed25519 og Ed448 i pure mode giver provideren hele signed-attributes-beskeden (sikMessage), og batch-validatoren afviser det som misdannet, for signHash er per definition hash-baseret. Provider-uniten kompilerer under Free Pascal med almindelige funktionstyper i stedet for anonymous methods, men CMS-builderne drevet af provideren rejser under FPC i dag, så at embedde en CSC-signatur i en PDF er en Delphi-sti
Den beslutter heller ikke policy. CredentialInfo rapporterer nøglestatus, certifikatstatus, autorisationsmode, SCAL-niveau og multisign-grænse, men provideren vil ikke nægte en deaktiveret nøgle eller en SCAL1-credential af sig selv — tjek det, før du viser en underskriver OTP-prompten. Og én provider-instans signerer én batch ad gangen: SignHashBatch serialiseres internt, så to tråde ikke kan løbe om én SAD, hvilket betyder, at throughput kommer fra batchning, ikke fra at dele en provider på tværs af worker-tråde. Hvorvidt den resulterende signatur er kvalificeret afhænger af trust-tjenesten og dens credential, ikke af biblioteket, der bar hashen derud
CSC-provideren, CMS- og PAdES-builderne og de lokale og PKCS#11-providers følger alle med i HotPDF Delphi PDF-komponenten