HotPDF signerer PDF-dokumenter med en privatnøkkel holdt av en ekstern Cloud Signature Consortium-tjeneste (CSC) gjennom THPDFCSCSignatureProvider, en signaturprovider som driver CSC-API-et — credential info, autorisasjon, signatures/signHash og polling — mens Delphi-applikasjonen din leverer HTTP-transporten og OAuth access token. Nøkkelen forlater aldri tjenestens HSM
Det er i økende grad den eneste måten å få en kvalifisert signeringsnøkkel i det hele tatt. Tillitstjenesteleverandører deler ut et CSC-endepunkt og en OAuth-klient, ikke en PFX-fil eller en USB-token, så det er ingenting å laste inn i et lokalt sertifikatlager slik Windows cert store-signering gjennom CNG og CAPI gjør. Den naive integrasjonen feiler på forutsigbare måter: et signHash-kall får tidsavbrudd og retryen signerer samme kontrakt to ganger, eller en bunke på førti fakturaer utløser førti engangskoder fordi hver hash ble autorisert separat. Det meste provideren gjør, er å forsvare seg mot de to feilene
Hvorfor lar HotPDF HTTP ligge til applikasjonen din?
Fordi transporten er nøyaktig der hver utrulling er forskjellig. Proxyer, TLS pinning, klientsertifikater, bedriftens OAuth-hvelv og loggepolicy bor alle i HTTP-laget, så THPDFCSCSignatureProvider orkestrerer protokolltilstanden og kaller en THPDFCSCTransport-funksjon for hver forespørsel. Provideren gir deg en THPDFCSCTransportRequest med Method (alltid POST), hele URL-en bygget fra ServiceBaseURL pluss endepunktstien, en klar Authorization-bearer-header, ContentType, JSON-Body-en, en IdempotencyKey, Attempt-tallet og MaxResponseBytes. Du fyller en THPDFCSCTransportResponse med StatusCode, Body og RetryAfterMS, og returnerer én av 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 slik tjenesten din 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-problem: kan retries
end;
Response.StatusCode := HttpResp.StatusCode; // rapporter 503 som den er, ikke klassifiser
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 regelen verdt å memorere: returner ctsSuccess når som helst en server faktisk svarte, selv med en 503. Provideren klassifiserer statuskoder selv, og en transport som gjør en 429 om til ctsPermanentFailure, deaktiverer stille retry-logikken beskrevet nedenfor. Konstruktøren er streng i den andre retningen — den reiser EHPDFCSCSignatureProviderError når transporten mangler, CredentialID er tom, verken en AccessToken eller en token-callback er levert, et budsjett er utenfor området, eller ServiceBaseURL ikke er HTTPS. Ren http:// godtas bare med AllowInsecureHTTP, som hører hjemme i en testrigg og ingen andre steder
Hva er SAD-en, og hvorfor kaster HotPDF den etter én bruk?
THPDFCSCSignatureProvider behandler Signature Activation Data (SAD) som engangs: den tømmes fra providertilstanden i det øyeblikket signatures/signHash aksepteres, selv når signaturen selv ankommer senere gjennom asynkron polling. SAD-en er tjenestens bevis på at signeren godkjente nettopp disse hashene, og en SAD som blir hengende i minnet, er en autorisasjon som venter på å bli brukt opp på feil dokument
Med standardverdiene fra THPDFCSCOptions.Default — RequireSAD og AutoAuthorize begge True — laster provideren credentials/info én gang, spør THPDFCSCAuthenticationCallback din om authData-verdiene (en OTP, en PIN, hva enn credentialens auth-blokk krever), og poster credentials/authorize. En 200 bærer SAD-en direkte; en 202 bærer et handle som polles gjennom credentials/authorizeCheck opptil MaxPollAttempts (60) ganger med PollIntervalMS (250 ms). Callbacken kan returnere høyst 32 verdier, hver med en ikke-tom ID på opptil 256 byte og en verdi på opptil 4 096 byte. Hvis nettverket faller bort før signHash aksepteres, beholdes en automatisk innhentet SAD slik at samme bunke kan retries uten å spørre signeren igjen
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, async-modus, 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 etter at tjenesten svarte 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 // ditt UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
En SAD du sender inn selv gjennom Options.SAD, oppfører seg annerledes, og bevisst så. HotPDF kan ikke vite hvilke hasher den ble utstedt for, så provideren bruker en forhåndsatt SAD bare for en enkelt-hash-forespørsel. For en bunke med AutoAuthorize slått av, feiler provideren med «CSC SAD is not pinned to the requested hash batch» i stedet for å gjette
Hvordan signerer SignHashBatch mange dokumenter med én autorisasjon?
SignHashBatch sender én credentials/authorize og én signatures/signHash for opptil MaxBatchSignatures (64) digester, og bygger begge bodyene fra den samme arrayen slik at numSignatures, rekkefølgen på hashes og hashAlgorithmOID er identiske i de to kallene. Det treffet er det CSC-multisign-modellen krever. Løkk den enkelts-hash Sign-metoden førti ganger, så får du førti autorisasjoner; send en authorize og en signHash som er uenige, så kan tjenesten bruke SAD-en mot feil bunke
Før noe nettverkstrafikk validerer provideren bunker. Hver forespørsel må være en digest (sikDigest) på 1 til 1 024 byte med en digest-OID, og alle forespørsler må dele én signaturalgoritme-OID, én digest-OID og, for RSASSA-PSS, én saltlengde. En multi-hash-bunke laster også credentials/info og returnerer spsUnsupported når credentialens multisign-verdi er mindre enn bunker. SAD-en festes deretter til et bunkefingeravtrykk — en SHA-256 over en versjonsetikett, antallet og, per forespørsel, algoritme-OID, digest-OID, algoritme, saltlengde og digest-byte, hver lengdeprefikset. Bytt to hasher, så er det en annen bunke som trenger en fersk autorisasjon
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256-verdiene du beregnet
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo utledes 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] tilhører Digests[I]; antallet ble sjekket mot forespørselen
end;
For RSASSA-PSS sender provideren også signAlgoParams, en base64 DER RSASSA-PSS-params-struktur med hashalgoritmen, MGF1 og saltlengden. Å bygge den betyr å enkode OID-er, og versjon 2.748.5 fikset en krok av det: X.690 §8.19.4 folder de to første buene inn i én verdi (40 × første + andre), og under 2-roten dytter en andre bue over 39 den verdien forbi 127, der den trenger base-128-flerbyteformen som tidligere bygg ikke anvendte. Ingen SHA-2-OID er berørt — 2.16 folder til 96 — men en misdannet OID reiser nå providerens egen feil i stedet for en EConvertError
Hvorfor gir en retried forespørsel ikke en ny signatur?
THPDFCSCSignatureProvider får hvert retrybare kall til å bære en deterministisk idempotensnøkkel og cachar ferdige resultater, så en retry etter et tapt svar returnerer de opprinnelige signaturene i stedet for å be HSM-en om nye. Nøkkelen er csc- fulgt av den heksadesimale SHA-256-en av operasjonsidentifikatoren og fasen, og fasen inneblander bunkefingeravtrykket for både autorisasjon og signHash. Å hashe i stedet for å trunkere betyr noe: to lange operasjons-ID-er som deler et prefiks, ville kollidere under trunkering, mens en fastlengde innholdsadressert nøkkel forblir unik og stabil på tvers av forsøk
Retry-policyen i den delte forespørselsstien er smal med hensikt:
- HTTP 401 tvinger frem nøyaktig én token-oppfriskning gjennom access token-callbacken, så gjentas forespørselen én gang når en access token-callback er tildelt; en andre 401 er endelig
- Andre 4xx-svar og
ctsPermanentFailureavslutter kallet medspsProviderError, og tjenestenserror_descriptionlander iLastError - 408, 429, 5xx og
ctsTemporaryFailureretries opptilRetryLimit(standard 2) ganger, med venting påRetry-AfterellerRetryBaseDelayMS× 2attempt (100 ms base), taksert tilMaxRetryAfterMS(5 000 ms) - Ventingene kjører i 25 ms-skiver som sjekker
Cancel, så en bruker som avbryter, ikke sitter gjennom en fem sekunders back-off signHashretries bare mensEnableIdempotencyer på; slå den av, og et tidsavbrudd etter innsending er endelig, for ingen kan si om nøkkelen allerede var brukt
Asynkron signering (operationMode «A», standarden) legger til enda en vakt: responseID-en lagres før polling av signatures/signPolling, så et gjentatt kall med samme operasjonsidentifikator gjenopptar polling i stedet for å sende på nytt. Ferdige bunker ligger i en cache nøklet etter operasjonsidentifikator, credential og fingeravtrykk, taksert av MaxOperationCacheEntries (128) og returnert som dype kopier. Den cachen lever i providerinstansen og overlever ikke en omstart. Idempotensnøkkelen gjør det, for den er utledet snarere enn tilfeldig, så en omstartet prosess som gjenbruker sin operasjonsidentifikator, sender samme nøkkel — hvorvidt tjenesten dedupliserer på den, er tjenestens løfte, ikke HotPDFs
Hvordan får du en CSC-signatur inn i en PDF?
Send provideren til HPDFCMSSignPDFStreamWithProvider sammen med endenhetssertifikatet fra GetCertificateChain; HotPDF bygger CMS SignedData, og provideren signerer digesten av de signerte attributtene. Input-PDF-en trenger /ByteRange- og /Contents-plassholderen som THPDFPage.AddSignedSignatureField skriver, nøyaktig som i PAdES-signeringsarbeidsflyten i HotPDF, og providermodellen er den samme som dekkes i HotPDF pluggbare signatureproviders for 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 endenhetssertifikatet 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;
Størrelsessetting av /Contents-plassholderen går gjennom EstimateSignatureSize, som returnerer EstimatedSignatureBytes når du setter den, og ellers RSA-modulstørrelsen fra credentialens nøkkellengde. For ECDSA setter du EstimatedSignatureBytes selv, ellers rapporterer estimeringen spsUnsupported. Varianten med automatisk størrelse signerer på nytt når en plassholder viser seg for liten, og den gjør det bare for providers som annonserer spcSafeSignRetry — noe THPDFCSCSignatureProvider gjør bare mens EnableIdempotency er på. For PAdES-B-T-arbeidsflyter ber TimestampDigest om et tidsstempel-token fra samme tjeneste gjennom signatures/timestamp, taksert til MaxTimestampBytes (1 MB)
Hva gjør ikke CSC-provideren?
Den signerer ikke meldinger, bare digester. Ed25519 og Ed448 i ren modus gir provideren hele den signerte attributtmeldingen (sikMessage), og bunker-validatoren avviser den som misdannet, for signHash er per definisjon hashbasert. Provider-uniten kompilerer under Free Pascal med rene funksjonstyper i stedet for anonyme metoder, men providerdrevne CMS-byggere reiser under FPC i dag, så å bygge inn en CSC-signatur i en PDF er en Delphi-sti
Den avgjør heller ikke policy. CredentialInfo rapporterer nøkkelstatus, sertifikatstatus, autorisasjonsmodus, SCAL-nivå og multisign-grense, men provideren nekter ikke av seg selv en deaktivert nøkkel eller en SCAL1-credential — sjekk de før du viser en signerer OTP-spørsmålet. Og én providerinstans signerer én bunke om gangen: SignHashBatch serialiseres internt slik at to tråder ikke kan kappløpe om én SAD, noe som betyr at gjennomstrømning kommer fra batching, ikke fra å dele en provider på tvers av arbeidertråder. Hvorvidt den resulterende signaturen er kvalifisert, avhenger av tillitstjenesten og dens credential, ikke av biblioteket som bar hashen dit
CSC-provideren, CMS- og PAdES-byggerne og de lokale og PKCS#11-providerne leveres alle i HotPDF Delphi PDF-komponenten