HotPDF ondertekent PDF-documenten met een privésleutel die een externe Cloud Signature Consortium (CSC)-service beheert via THPDFCSCSignatureProvider, een signature provider die de CSC-API aanstuurt — credential info, autorisatie, signatures/signHash en polling — terwijl uw Delphi-applicatie het HTTP-transport en het OAuth-access token levert. De sleutel verlaat de HSM van de service nooit
Dat wordt steeds vaker de enige manier om überhaupt een qualified signing key te bemachtigen. Trust service providers geven een CSC-endpoint en een OAuth-client uit, geen PFX-bestand of USB-token, dus er is niets om in een lokale certificaatopslag te laden zoals ondertekenen via de Windows-certificaatopslag met CNG en CAPI doet. De naïeve integratie faalt op voorspelbare manieren: een signHash-aanroep timed out en de retry ondertekent hetzelfde contract twee keer, of een batch van veertig facturen lost veertig eenmalige wachtwoorden af omdat elke hash apart werd geautoriseerd. Het grootste deel van wat de provider doet is zich wapenen tegen die twee faalvormen
Waarom laat HotPDF HTTP over aan uw applicatie?
Omdat het transport precies het punt is waar elke implementatie verschilt. Proxies, TLS pinning, clientcertificaten, corporate OAuth-kluizen en loggingpolicy wonen allemaal in de HTTP-laag, dus THPDFCSCSignatureProvider orkestreert de protocoltoestand en roept per request een THPDFCSCTransport-functie aan. De provider geeft u een THPDFCSCTransportRequest mee met Method (altijd POST), de volledige URL opgebouwd uit ServiceBaseURL plus het endpoint-pad, een kant-en-klare Authorization-bearerheader, ContentType, de JSON-Body, een IdempotencyKey, het Attempt-nummer en MaxResponseBytes. U vult een THPDFCSCTransportResponse met StatusCode, Body en RetryAfterMS, en geeft één van ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure of ctsCancelled terug
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 // headernaam zoals uw service die documenteert
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- of DNS-probleem: retryable
end;
Response.StatusCode := HttpResp.StatusCode; // 503 doorgeven zoals hij is, niet classificeren
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;
De enige regel die u uit uw hoofd moet leren: geef ctsSuccess terug zodra een server daadwerkelijk heeft geantwoord, ook met een 503. De provider classificeert statuscodes zelf, en een transport dat een 429 tot ctsPermanentFailure verklaart schakelt de hieronder beschreven retry-logica geruisloos uit. De constructor is streng in de andere richting — hij gooit een EHPDFCSCSignatureProviderError wanneer het transport ontbreekt, CredentialID leeg is, noch een AccessToken noch een token-callback wordt meegegeven, een budget buiten bereik valt, of ServiceBaseURL geen HTTPS is. Kale http:// wordt alleen geaccepteerd met AllowInsecureHTTP, wat in een testopstelling thuishoort en nergens anders
Wat is de SAD, en waarom gooit HotPDF haar na één gebruik weg?
THPDFCSCSignatureProvider behandelt de Signature Activation Data (SAD) als single-use: ze wordt gewist uit de providertoestand op het moment dat signatures/signHash wordt geaccepteerd, zelfs wanneer de handtekening zelf later binnenkomt via asynchrone polling. De SAD is het bewijs van de service dat de ondertekenaar precies deze hashes heeft goedgekeurd, en een SAD die in het geheugen blijft hangen is een autorisatie die wacht om uitgegeven te worden aan het verkeerde document
Met de defaults uit THPDFCSCOptions.Default — RequireSAD en AutoAuthorize allebei True — laadt de provider één keer credentials/info, vraagt uw THPDFCSCAuthenticationCallback om de authData-waarden (een OTP, een PIN, wat de auth-blok van de credential ook eist), en post credentials/authorize. Een 200 brengt de SAD rechtstreeks mee; een 202 brengt een handle mee die via credentials/authorizeCheck tot MaxPollAttempts (60) keer wordt gepolld met tussenpozen van PollIntervalMS (250 ms). De callback mag hoogstens 32 waarden teruggeven, elk met een niet-lege ID van hoogstens 256 bytes en een waarde van hoogstens 4.096 bytes. Zakt het netwerk weg voordat signHash is geaccepteerd, dan blijft een automatisch verkregen SAD bewaard zodat dezelfde batch kan worden herhaald zonder de ondertekenaar opnieuw te vragen
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
// uw OAuth-client; ForceRefresh is True nadat de service 401 antwoordde
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 // uw UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Een SAD die u zelf via Options.SAD meegeeft gedraagt zich anders, en bewust zo. HotPDF kan niet weten waarvoor hij is uitgegeven, dus de provider gebruikt een voorgezette SAD alleen voor een single-hash-request. Bij een batch met AutoAuthorize uit faalt de provider met "CSC SAD is not pinned to the requested hash batch" in plaats van te gokken
Hoe ondertekent SignHashBatch veel documenten met één autorisatie?
SignHashBatch stuurt één credentials/authorize en één signatures/signHash voor maximaal MaxBatchSignatures (64) digests, en bouwt beide bodies uit dezelfde array zodat numSignatures, de volgorde van hashes en hashAlgorithmOID in de twee aanroepen identiek zijn. Die match is wat het CSC-multisignmodel vereist. Loop de single-hash-Sign-methode veertig keer af en u krijgt veertig autorisaties; stuur een authorize en een signHash die niet bij elkaar passen en de service kan de SAD verbruiken tegen de verkeerde batch
Vóór elke netwerkactiviteit valideert de provider de batch. Elke request moet een digest (sikDigest) zijn van 1 tot 1.024 bytes met een digest-OID, en alle requests moeten één signature-algoritme-OID, één digest-OID en, voor RSASSA-PSS, één saltlengte delen. Een multi-hash-batch laadt bovendien credentials/info en geeft spsUnsupported terug wanneer de multisign-waarde van de credential kleiner is dan de batch. De SAD wordt daarna vastgepind aan een batchvingerafdruk — een SHA-256 over een versielabel, het aantal en per request de algoritme-OID, digest-OID, het algoritme, de saltlengte en de digestbytes, elk met lengtevoorvoegsel. Wissel twee hashes om en het is een andere batch die een verse autorisatie nodig heeft
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: SHA-256-waarden die u zelf berekende
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo wordt afgeleid wanneer AlgorithmOID leeg is
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] hoort bij Digests[I]; het aantal is tegen het request gecontroleerd
end;
Voor RSASSA-PSS stuurt de provider ook signAlgoParams mee, een base64 DER-RSASSA-PSS-params-structuur met hash-algoritme, MGF1 en saltlengte. Die bouwen betekent OIDs encoderen, en v2.748.5 fixte daarvan een hoekje: X.690 §8.19.4 vouwt de eerste twee arcen samen tot één waarde (40 × eerste + tweede), en onder de 2-wortel duwt een tweede boog boven 39 die waarde voorbij 127, waar hij de base-128-multibyte-vorm nodig heeft die eerdere builds niet toepasten. Geen enkele SHA-2-OID wordt geraakt — 2.16 vouwt naar 96 — maar een misvormde OID gooit nu de eigen fout van de provider in plaats van een EConvertError
Waarom levert een herhaald request geen tweede handtekening op?
THPDFCSCSignatureProvider laat elke retryable aanroep een deterministische idempotency key meedragen en cachet voltooide resultaten, dus een retry na een verloren antwoord geeft de originele handtekeningen terug in plaats van de HSM om nieuwe te vragen. De key is csc- gevolgd door de hexadecimale SHA-256 van de operation identifier en de fase, en de fase embedt de batchvingerafdruk voor zowel autorisatie als signHash. Hashen in plaats van afkappen telt: twee lange operation-ID's die een prefix delen zouden onder afkapping colliden, terwijl een key met vaste lengte en op inhoud gebaseerd uniek en stabiel blijft over pogingen heen
Het retrybeleid in het gedeelde request-pad is met opzet smal:
- HTTP 401 dwingt precies één token-verversing af via de access-token-callback, daarna wordt het request één keer herhaald wanneer er een access-token-callback is toegewezen; een tweede 401 is definitief
- Andere 4xx-antwoorden en
ctsPermanentFailurebeëindigen de aanroep metspsProviderError, en deerror_descriptionvan de service belandt inLastError - 408, 429, 5xx en
ctsTemporaryFailureworden herhaald totRetryLimit(default 2), met wachten opRetry-AfterofRetryBaseDelayMS× 2attempt (100 ms basis), begrensd opMaxRetryAfterMS(5.000 ms) - Wachttijden lopen in slices van 25 ms die
Cancelcontroleren, dus een gebruiker die afbreekt hoeft geen back-off van vijf seconden uit te zitten signHashwordt alleen herhaald zolangEnableIdempotencyaanstaat; zet hem uit en een timeout na inzending is definitief, want niemand kan zeggen of de key al dan niet is gebruikt
Asynchroon ondertekenen (operationMode "A", de default) voegt nog één wachtpost toe: de responseID wordt bewaard vóór het pollen van signatures/signPolling, dus een herhaalde aanroep met dezelfde operation identifier hervat de polling in plaats van opnieuw in te zenden. Voltooide batches zitten in een cache met operation identifier, credential en vingerafdruk als sleutel, gecapteerd door MaxOperationCacheEntries (128) en teruggegeven als deep copies. Die cache woont in de providerinstantie en overleeft geen herstart. De idempotency key wel, want hij wordt afgeleid in plaats van random gekozen, dus een herstart proces dat zijn operation identifier hergebruikt stuurt dezelfde key — of de service daarop dedupliceert is een belofte van de service, niet van HotPDF
Hoe krijgt een CSC-handtekening een plek in een PDF?
Geef de provider door aan HPDFCMSSignPDFStreamWithProvider samen met het end-entity-certificaat uit GetCertificateChain; HotPDF bouwt de CMS SignedData en de provider ondertekent de digest van de signed attributes. De invoer-PDF heeft de /ByteRange- en /Contents-placeholder nodig die THPDFPage.AddSignedSignatureField wegschrijft, precies zoals in de PAdES-ondertekenworkflow in HotPDF, en het providermodel is hetzelfde als dat in HotPDF pluggable signature providers voor ML-DSA en EdDSA wordt behandeld
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC zet het end-entity-certificaat vooraan
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;
Het afmeten van de /Contents-placeholder loopt via EstimateSignatureSize, die EstimatedSignatureBytes teruggeeft wanneer u die zet en anders de RSA-modulusgrootte uit de sleutellengte van de credential. Voor ECDSA zet u EstimatedSignatureBytes zelf, anders meldt de schatting spsUnsupported. De auto-size-ondertekenvariant tekent opnieuw wanneer een placeholder te klein blijkt, en doet dat alleen voor providers die spcSafeSignRetry adverteren — wat THPDFCSCSignatureProvider alleen doet zolang EnableIdempotency aanstaat. Voor PAdES-B-T-workflows vraagt TimestampDigest een timestamp-token bij dezelfde service via signatures/timestamp, begrensd op MaxTimestampBytes (1 MB)
Wat doet de CSC-provider niet?
Hij ondertekent geen berichten, alleen digests. Ed25519 en Ed448 in pure mode geven de provider het hele signed-attributes-bericht (sikMessage) mee, en de batchvalidator verwerpt dat als misvormd, want signHash is per definitie hash-gebaseerd. De provider-unit compileert onder Free Pascal met gewone functietypes in plaats van anonieme methodes, maar de provider-gestuurde CMS-builders gooien vandaag onder FPC, dus een CSC-handtekening in een PDF embedden is een Delphi-pad
Hij beslist ook geen policy. CredentialInfo meldt de keystatus, de certificaatstatus, de autorisatiemodus, het SCAL-niveau en de multisign-limiet, maar de provider weigert niet uit eigen beweging een uitgeschakelde key of een SCAL1-credential — controleer dat voordat u een ondertekenaar de OTP-prompt toont. En één providerinstantie ondertekent één batch tegelijk: SignHashBatch wordt intern geserialiseerd zodat twee threads niet om één SAD kunnen strijden, wat betekent dat doorvoer uit batchen komt, niet uit het delen van een provider over workerthreads. Of de resulterende handtekening qualified is hangt af van de trust service en zijn credential, niet van de library die de hash erheen heeft gedragen
De CSC-provider, de CMS- en PAdES-builders en de lokale en PKCS#11-providers zitten allemaal in de HotPDF Delphi PDF component