HotPDF signerar PDF-dokument med en privat nyckel som hålls av en fjärransluten Cloud Signature Consortium-tjänst (CSC) genom THPDFCSCSignatureProvider, en signaturprovider som driver CSC-API:t — credential info, auktorisering, signatures/signHash och polling — medan din Delphi-applikation levererar HTTP-transporten och OAuth-åtkomsttoken. Nyckeln lämnar aldrig tjänstens HSM
Det är i ökande grad den enda vägen att överhuvudtaget få en kvalificerad signeringsnyckel. Trust service providers delar ut en CSC-endpoint och en OAuth-klient, inte en PFX-fil eller en USB-token, så det finns ingenting att läsa in i ett lokalt certifikatarkiv på det sätt Windows cert store-signering genom CNG och CAPI gör. Den naiva integrationen fallerar på förutsägbara sätt: ett signHash-anrop får timeout och omförsöket signerar samma kontrakt två gånger, eller så utlöser en sats på fyrtio fakturor fyrtio engångslösenord för att varje hash auktoriserades separat. Det mesta providern gör är att försvara sig mot de två felen
Varför lämnar HotPDF HTTP åt din applikation?
För att transporten är exakt där varje driftsättning skiljer sig. Proxies, TLS pinning, klientcertifikat, företags OAuth-valv och loggningspolicy bor alla i HTTP-lagret, så THPDFCSCSignatureProvider orkestrerar protokolltillståndet och anropar en THPDFCSCTransport-funktion för varje request. Providern räcker dig en THPDFCSCTransportRequest med Method (alltid POST), den fullständiga URL:en byggd av ServiceBaseURL plus endpoint-sökvägen, en färdig Authorization-bearer-header, ContentType, JSON-Body, en IdempotencyKey, Attempt-numret och MaxResponseBytes. Du fyller en THPDFCSCTransportResponse med StatusCode, Body och RetryAfterMS och returnerar en 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 // headernamn som din tjänst dokumenterar 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-trubbel: går att göra om
end;
Response.StatusCode := HttpResp.StatusCode; // rapportera 503 som den är, klassificera inte
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 enda regel värd att memorera: returnera ctsSuccess närhelst en server faktiskt svarade, även med en 503. Providern klassificerar statuskoder själv, och en transport som gör en 429 till ctsPermanentFailure tyst avaktiverar retry-logiken som beskrivs nedan. Konstruktorn är strikt åt andra hållet — den kastar EHPDFCSCSignatureProviderError när transporten saknas, CredentialID är tom, varken en AccessToken eller en token-callback har lämnats, en budget är utanför intervallet, eller ServiceBaseURL inte är HTTPS. Ren http:// accepteras bara med AllowInsecureHTTP, som hör hemma i en testrigg och ingenstans annars
Vad är SAD, och varför kastar HotPDF bort den efter ett användande?
THPDFCSCSignatureProvider behandlar Signature Activation Data (SAD) som single-use: den rensas ur providerns tillstånd i samma stund som signatures/signHash accepteras, även när signaturen själv anländer senare genom asynkron polling. SAD:en är tjänstens bevis för att signeraren godkänt just dessa hashar, och en SAD som ligger kvar i minnet är en auktorisering som väntar på att spenderas på fel dokument
Med defaultvärdena från THPDFCSCOptions.Default — RequireSAD och AutoAuthorize båda True — läser providern in credentials/info en gång, ber din THPDFCSCAuthenticationCallback om authData-värdena (en OTP, en PIN, vad än credentialens auth-block kräver) och postar credentials/authorize. En 200 bär SAD:en direkt; en 202 bär ett handtag som pollas genom credentials/authorizeCheck upp till MaxPollAttempts (60) gånger vid PollIntervalMS (250 ms). Callbacken får returnera högst 32 värden, vart och ett med ett icke-tomt ID på upp till 256 byte och ett värde på upp till 4 096 byte. Om nätverket bryts innan signHash accepteras behålls en automatiskt erhållen SAD så att samma sats kan göras om utan att fråga signeraren igen
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, asynkronläge, 2 omförsök
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 är True efter att tjänsten svarat 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 själv skickar in genom Options.SAD beter sig annorlunda, och med flit. HotPDF kan inte veta vilka hashar den utfärdades för, så providern använder en förinställd SAD bara för ett enkelhash-request. För en sats med AutoAuthorize avstängt fallerar providern med "CSC SAD is not pinned to the requested hash batch" i stället för att gissa
Hur signerar SignHashBatch många dokument med en auktorisering?
SignHashBatch skickar en credentials/authorize och en signatures/signHash för upp till MaxBatchSignatures (64) digestar, och bygger båda body:erna ur samma array så att numSignatures, ordningen på hashes och hashAlgorithmOID är identiska i de två anropen. Den matchen är vad CSC-multisign-modellen kräver. Loopa enkelhash-Sign-metoden fyrtio gånger och du får fyrtio auktoriseringar; skicka en authorize och en signHash som inte överensstämmer och tjänsten kan konsumera SAD:en mot fel sats
Innan någon nätverkstrafik validerar providern satsen. Varje request måste vara en digest (sikDigest) på 1 till 1 024 byte med en digest-OID, och alla requests måste dela en signaturalgoritm-OID, en digest-OID och, för RSASSA-PSS, en saltlängd. En flerhashsats läser också in credentials/info och returnerar spsUnsupported när credentialens multisign-värde är mindre än satsen. SAD:en fästs sedan vid ett satsfingeravtryck — en SHA-256 över en versionsetikett, antalet och, per request, algoritm-OID, digest-OID, algoritm, saltlängd och digestbyte, vart och ett längdprefixat. Byt plats på två hashar och det är en annan sats som behöver en färsk auktorisering
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256-värden du beräknat
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo härleds när AlgorithmOID är tomt
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ör till Digests[I]; antalet kontrollerades mot requesten
end;
För RSASSA-PSS skickar providern också signAlgoParams, en base64 DER RSASSA-PSS-params-struktur med hashalgoritmen, MGF1 och saltlängd. Att bygga den innebär att koda OID:er, och version 2.748.5 fixade ett hörn av det: X.690 §8.19.4 viker ihop de två första bågarna till ett värde (40 × första + andra), och under 2-roten trycker en andra båge över 39 det värdet förbi 127, där det behöver bas-128-flerbyteformen som tidigare byggen inte tillämpade. Ingen SHA-2-OID påverkas — 2.16 viker ihop till 96 — men en felformaterad OID kastar nu providerns eget fel i stället för ett EConvertError
Varför ger en omkörd request inte en andra signatur?
THPDFCSCSignatureProvider låter varje försökbart anrop bära en deterministisk idempotensnyckel och cachar färdiga resultat, så ett omförsök efter ett förlorat svar returnerar originalsignaturerna i stället för att be HSM:en om nya. Nyckeln är csc- följt av hex-SHA-256 av operationidentifieraren och fasen, och fasen bäddar in satsfingeravtrycket för både auktorisering och signHash. Att hasha i stället för att trunkera spelar roll: två långa operation-ID:n som delar ett prefix skulle kollidera under trunkering, medan en fast längd innehållsadresserad nyckel förblir unik och stabil över försök
Retry-policyn i den delade requestvägen är smal med flit:
- HTTP 401 tvingar fram exakt en tokenuppdatering genom access-token-callbacken, sedan upprepas requesten en gång när en access-token-callback är tilldelad; en andra 401 är slutgiltig
- Andra 4xx-svar och
ctsPermanentFailureavslutar anropet medspsProviderError, och tjänstenserror_descriptionlandar iLastError - 408, 429, 5xx och
ctsTemporaryFailuregörs om upp tillRetryLimit(standard 2), med väntan påRetry-AfterellerRetryBaseDelayMS× 2försök (100 ms bas), takade vidMaxRetryAfterMS(5 000 ms) - Väntan löper i 25 ms-skivor som kontrollerar
Cancel, så en användare som avbryter sitter inte igenom en femsekunders back-off signHashgörs om bara medanEnableIdempotencyär på; stäng av den och en timeout efter inskickning är slutgiltig, för att ingen kan säga om nyckeln redan använts
Asynkron signering (operationMode "A", standarden) lägger till en bevakning till: responseID lagras innan polling av signatures/signPolling, så ett upprepat anrop med samma operationidentifierare återupptar pollingen i stället för att skicka in igen. Färdiga satser ligger i en cache nycklad av operationidentifierare, credential och fingeravtryck, takad av MaxOperationCacheEntries (128) och returnerade som djupa kopior. Den cachen lever i providerinstansen och överlever inte en omstart. Idempotensnyckeln gör det, för att den härleds snarare än slumpas, så en omstartad process som återanvänder sin operationidentifierare skickar samma nyckel — huruvida tjänsten deduplicerar på den är tjänstens löfte, inte HotPDF:s
Hur får du in en CSC-signatur i en PDF?
Skicka providern till HPDFCMSSignPDFStreamWithProvider tillsammans med slutentitetscertifikatet från GetCertificateChain; HotPDF bygger CMS SignedData och providern signerar digesten av de signerade attributen. Indata-PDF:en behöver platshållarna /ByteRange och /Contents som THPDFPage.AddSignedSignatureField skriver, exakt som i PAdES-signeringsarbetsflödet i HotPDF, och providermodellen är samma som täcks i HotPDF pluggbara signaturproviders för ML-DSA och EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC listar slutentitetscertifikatet 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;
Storlekssättning av /Contents-platshållaren går genom EstimateSignatureSize, som returnerar EstimatedSignatureBytes när du satt den och i övrigt RSA-modulstorleken ur credentialens nyckellängd. För ECDSA sätter du EstimatedSignatureBytes själv, annars rapporterar uppskattningen spsUnsupported. Auto-size-signeringsvarianten signerar om när en platshållare visar sig för liten, och den gör det bara för providers som utlovar spcSafeSignRetry — vilket THPDFCSCSignatureProvider gör bara medan EnableIdempotency är på. För PAdES-B-T-arbetsflöden begär TimestampDigest en tidsstämpeltoken från samma tjänst genom signatures/timestamp, takad vid MaxTimestampBytes (1 MB)
Vad gör CSC-providern inte?
Den signerar inte meddelanden, bara digestar. Ed25519 och Ed448 i pure mode räcker providern hela det signerade attributmeddelandet (sikMessage), och satsvalideraren avvisar det som felformaterat, för att signHash per definition är hashbaserad. Provideruniten kompilerar under Free Pascal med vanliga funktionstyper i stället för anonyma metoder, men providerdrivna CMS-byggare kastar under FPC i dag, så att bädda in en CSC-signatur i en PDF är en Delphi-väg
Den avgör inte heller policy. CredentialInfo rapporterar nyckelstatus, certifikatstatus, auktoriseringsläge, SCAL-nivå och multisign-gräns, men providern vägrar inte av sig själv en avstängd nyckel eller en SCAL1-credential — kontrollera det innan du visar en signerare OTP-frågan. Och en providerinstans signerar en sats i taget: SignHashBatch serialiseras internt så att två trådar inte kan tävla om en SAD, vilket betyder att genomströmningen kommer från batchning, inte från att dela en provider över arbetstrådar. Huruvida den resulterande signaturen är kvalificerad beror på trust service och dess credential, inte på biblioteket som bar hashen dit
CSC-providern, CMS- och PAdES-byggarna och de lokala och PKCS#11-baserade providers kommer alla i HotPDF Delphi PDF-komponenten