HotPDF firma documenti PDF con una chiave privata custodita da un servizio remoto del Cloud Signature Consortium (CSC) attraverso THPDFCSCSignatureProvider, un signature provider che pilota la API CSC — credential info, autorizzazione, signatures/signHash e polling — mentre la tua applicazione Delphi fornisce il trasporto HTTP e l'access token OAuth. La chiave non lascia mai l'HSM del servizio
Questa è sempre più l'unica strada per avere una chiave di firma qualificata, punto. I trust service provider distribuiscono un endpoint CSC e un client OAuth, non un file PFX né un token USB, quindi non c'è nulla da caricare in un cert store locale come fa la firma con il cert store di Windows via CNG e CAPI. L'integrazione ingenua fallisce in modi prevedibili: una chiamata signHash va in timeout e il retry firma lo stesso contratto due volte, oppure un lotto di quaranta fatture innescano quaranta one-time password perché ogni hash è stato autorizzato separatamente. Gran parte di ciò che il provider fa è difendersi da quei due fallimenti
Perché HotPDF lascia HTTP alla tua applicazione?
Perché il trasporto è esattamente il punto in cui ogni deployment differisce. Proxy, TLS pinning, certificati client, vault OAuth aziendali e policy di logging vivono tutti nel layer HTTP, quindi THPDFCSCSignatureProvider orchestra lo stato del protocollo e chiama una funzione THPDFCSCTransport per ogni richiesta. Il provider ti consegna una THPDFCSCTransportRequest con Method (sempre POST), il URL completo costruito da ServiceBaseURL più il percorso dell'endpoint, un header Authorization bearer pronto, ContentType, il Body JSON, un IdempotencyKey, il numero di Attempt e MaxResponseBytes. Tu riempi una THPDFCSCTransportResponse con StatusCode, Body e RetryAfterMS, e restituisci uno tra ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure o 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 // nome dell'header come lo documenta il tuo servizio
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // guai di socket o DNS: ritentabile
end;
Response.StatusCode := HttpResp.StatusCode; // riporta 503 così com'è, non classificare
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;
L'unica regola da memorizzare: restituisci ctsSuccess ogni volta che il server ha davvero risposto, anche con un 503. Il provider classifica da solo i codici di stato, e un trasporto che trasforma un 429 in ctsPermanentFailure disattiva in silenzio la logica di retry descritta qui sotto. Il costruttore è severo nell'altra direzione — alza EHPDFCSCSignatureProviderError quando manca il trasporto, CredentialID è vuoto, non è fornito né un AccessToken né una callback di token, un budget è fuori range, o ServiceBaseURL non è HTTPS. Il semplice http:// è accettato solo con AllowInsecureHTTP, che sta in un rig di test e da nessuna altra parte
Che cos'è il SAD e perché HotPDF lo butta dopo un uso?
THPDFCSCSignatureProvider tratta la Signature Activation Data (SAD) come monouso: viene cancellata dallo stato del provider nell'istante in cui signatures/signHash è accettata, anche quando la firma in sé arriva dopo attraverso il polling asincrono. Il SAD è la prova del servizio che il firmatario ha approvato proprio questi hash, e un SAD che resta in memoria è un'autorizzazione in attesa di essere spesa sul documento sbagliato
Con i default di THPDFCSCOptions.Default — RequireSAD e AutoAuthorize entrambi True — il provider carica credentials/info una volta, chiede alla tua THPDFCSCAuthenticationCallback i valori authData (un OTP, un PIN, qualunque cosa esiga il blocco auth della credenziale), e invia credentials/authorize. Un 200 porta il SAD direttamente; un 202 porta un handle che viene pollato attraverso credentials/authorizeCheck fino a MaxPollAttempts (60) volte a PollIntervalMS (250 ms). La callback può restituire al massimo 32 valori, ciascuno con un ID non vuoto fino a 256 byte e un valore fino a 4.096 byte. Se la rete cade prima che signHash sia accettato, un SAD ottenuto automaticamente viene conservato così lo stesso batch può essere ritentato senza richiedere di nuovo al firmatario
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, modalità async, 2 retry
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
// il tuo client OAuth; ForceRefresh è True dopo che il servizio ha risposto 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 // la tua UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Un SAD che passi tu stesso attraverso Options.SAD si comporta diversamente, e deliberatamente. HotPDF non può sapere per quali hash è stato emesso, quindi il provider usa un SAD preimpostato solo per una richiesta a hash singolo. Per un batch con AutoAuthorize spento, il provider fallisce con "CSC SAD is not pinned to the requested hash batch" anziché tirare a indovinare
Come firma SignHashBatch molti documenti con una sola autorizzazione?
SignHashBatch invia un solo credentials/authorize e un solo signatures/signHash per fino a MaxBatchSignatures (64) digest, e costruisce entrambi i body dallo stesso array così che numSignatures, l'ordine degli hashes e hashAlgorithmOID siano identici nelle due chiamate. Quella corrispondenza è ciò che il modello multisign CSC esige. Fare il loop quaranta volte sul metodo Sign a hash singolo ti dà quaranta autorizzazioni; inviare un authorize e un signHash che discordano e il servizio può consumare il SAD contro il batch sbagliato
Prima di qualunque traffico di rete, il provider valida il batch. Ogni richiesta deve essere un digest (sikDigest) da 1 a 1.024 byte con un OID di digest, e tutte le richieste devono condividere un OID di algoritmo di firma, un OID di digest e, per RSASSA-PSS, una lunghezza di salt. Un batch multi-hash carica anche credentials/info e restituisce spsUnsupported quando il valore multisign della credenziale è più piccolo del batch. Il SAD viene poi fissato a un fingerprint del batch — uno SHA-256 su un'etichetta di versione, il conteggio e, per richiesta, l'OID dell'algoritmo, l'OID del digest, l'algoritmo, la lunghezza del salt e i byte del digest, ciascuno con prefisso di lunghezza. Scambia due hash ed è un batch diverso che richiede un'autorizzazione fresca
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: valori SHA-256 che hai calcolato
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo derivato quando AlgorithmOID è vuoto
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] appartiene a Digests[I]; il conteggio è stato verificato contro la richiesta
end;
Per RSASSA-PSS il provider invia anche signAlgoParams, una struttura RSASSA-PSS-params DER in base64 con l'algoritmo di hash, MGF1 e la lunghezza del salt. Costruirla significa codificare OID, e la versione 2.748.5 ha sistemato un angolo di quello: X.690 §8.19.4 ripiega i primi due archi in un unico valore (40 × primo + secondo), e sotto la radice 2 un secondo arco sopra 39 spinge quel valore oltre 127, dove serve la forma multi-byte base-128 che le build precedenti non applicavano. Nessun OID SHA-2 ne è toccato — 2.16 ripiega a 96 — ma un OID malformato ora alza l'errore proprio del provider anziché un EConvertError
Perché una richiesta ritentata non produce una seconda firma?
THPDFCSCSignatureProvider fa sì che ogni chiamata ritentabile porti una idempotency key deterministica e metta in cache i risultati completati, così un retry dopo una risposta perduta restituisce le firme originali invece di chiedere all'HSM firme nuove. La chiave è csc- seguita dallo SHA-256 esadecimale dell'identificatore di operazione e della fase, e la fase incorpora il fingerprint del batch sia per l'autorizzazione sia per signHash. Fare l'hash invece di troncare conta: due identificatori di operazione lunghi che condividono un prefisso colliderebbero con il troncamento, mentre una chiave a lunghezza fissa indirizzata per contenuto resta unica e stabile tra i tentativi
La policy di retry nel percorso di richiesta condiviso è stretta di proposito:
- Un HTTP 401 forza esattamente un refresh del token attraverso la callback dell'access token, poi la richiesta viene ripetuta una volta se una callback di access token è assegnata; un secondo 401 è definitivo
- Le altre risposte 4xx e
ctsPermanentFailurechiudono la chiamata conspsProviderError, e l'error_descriptiondel servizio finisce inLastError - 408, 429, 5xx e
ctsTemporaryFailurevengono ritentati fino aRetryLimit(default 2), aspettandoRetry-AfteroRetryBaseDelayMS× 2attempt (base 100 ms), con tetto aMaxRetryAfterMS(5.000 ms) - Le attese girano a fette da 25 ms che controllano
Cancel, così un utente che interrompe non resta seduto cinque secondi di back-off signHashviene ritentato solo conEnableIdempotencyacceso; spegnilo e un timeout dopo l'invio è definitivo, perché nessuno può dire se la chiave sia già stata usata
La firma asincrona (operationMode "A", il default) aggiunge una guardia in più: il responseID viene memorizzato prima di pollare signatures/signPolling, così una chiamata ripetuta con lo stesso identificatore di operazione riprende il polling invece di risottomettere. I batch completati stanno in una cache con chiave identificatore di operazione, credenziale e fingerprint, limitata da MaxOperationCacheEntries (128) e restituita come copie profonde. Quella cache vive nell'istanza del provider e non sopravvive a un riavvio. La idempotency key sì, perché è derivata anziché casuale, quindi un processo riavviato che riusa il suo identificatore di operazione invia la stessa chiave — che il servizio deduplichi su di essa è una promessa del servizio, non di HotPDF
Come si mette una firma CSC dentro un PDF?
Passa il provider a HPDFCMSSignPDFStreamWithProvider insieme al certificato end-entity da GetCertificateChain; HotPDF costruisce il CMS SignedData e il provider firma il digest degli signed attributes. Il PDF di input ha bisogno del segnaposto /ByteRange e /Contents che THPDFPage.AddSignedSignatureField scrive, esattamente come nel flusso di firma PAdES in HotPDF, e il modello del provider è lo stesso coperto in provider di firma pluggabili HotPDF per ML-DSA e EdDSA
var
Chain: THPDFCSCCertificateChain;
SignOpts: THPDFCMSSignOptions;
Src, Dst: TFileStream;
begin
if Provider.RefreshCredentialInfo <> spsValid then
raise Exception.Create(Provider.LastError);
Chain := Provider.GetCertificateChain; // CSC elenca il certificato end-entity per primo
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;
Dimensionare il segnaposto /Contents passa per EstimateSignatureSize, che restituisce EstimatedSignatureBytes quando lo imposti e altrimenti la dimensione del modulo RSA dalla lunghezza della chiave della credenziale. Per ECDSA imposta tu EstimatedSignatureBytes, o la stima riporta spsUnsupported. La variante di firma con dimensione automatica rifirma quando un segnaposto risulta troppo piccolo, e lo fa solo per provider che dichiarano spcSafeSignRetry — cosa che THPDFCSCSignatureProvider fa solo con EnableIdempotency acceso. Per i flussi PAdES-B-T, TimestampDigest richiede un timestamp token allo stesso servizio attraverso signatures/timestamp, con tetto a MaxTimestampBytes (1 MB)
Che cosa non fa il provider CSC?
Non firma messaggi, solo digest. Ed25519 e Ed448 in modalità pura consegnano al provider l'intero messaggio degli signed attributes (sikMessage), e il validatore di batch lo rifiuta come malformato, perché signHash è per definizione basato sull'hash. La unit del provider compila sotto Free Pascal con tipi funzione semplici al posto dei metodi anonimi, ma i builder CMS pilotati dal provider oggi alzano eccezioni sotto FPC, quindi includere una firma CSC in un PDF è un percorso Delphi
Non decide nemmeno la policy. CredentialInfo riporta lo stato della chiave, lo stato del certificato, la modalità di autorizzazione, il livello SCAL e il limite multisign, ma il provider non rifiuterà da solo una chiave disabilitata o una credenziale SCAL1 — controllali prima di mostrare al firmatario la richiesta di OTP. E un'istanza di provider firma un batch alla volta: SignHashBatch è serializzato internamente così due thread non possono contendersi lo stesso SAD, il che significa che il throughput viene dal batching, non dal condividere un provider tra thread di lavoro. Se la firma risultante è qualificata dipende dal trust service e dalla sua credenziale, non dalla libreria che ci ha portato l'hash
Il provider CSC, i builder CMS e PAdES e i provider locali e PKCS#11 sono tutti nella PDF component HotPDF Delphi