O HotPDF assina documentos PDF com uma chave privada guardada por um serviço remoto Cloud Signature Consortium (CSC) por meio do THPDFCSCSignatureProvider, um signature provider que conduz a API CSC — credential info, autorização, signatures/signHash e polling — enquanto a sua aplicação Delphi fornece o transporte HTTP e o access token OAuth. A chave nunca sai do HSM do serviço
Esse está cada vez mais sendo o único jeito de obter uma chave de assinatura qualified. Trust service providers entregam um endpoint CSC e um cliente OAuth, não um arquivo PFX nem um token USB, então não há nada para carregar num repositório de certificados local como faz a assinatura via cert store do Windows por CNG e CAPI. A integração ingênua falha de jeitos previsíveis: uma chamada signHash dá timeout e o retry assina o mesmo contrato duas vezes, ou um lote de quarenta notas dispara quarenta one-time passwords porque cada hash foi autorizado separadamente. Quase tudo que o provider faz é se defender dessas duas falhas
Por que o HotPDF deixa o HTTP para a sua aplicação?
Porque o transporte é exatamente onde cada deployment difere. Proxies, TLS pinning, certificados de cliente, vaults corporativos de OAuth e política de logging moram todos na camada HTTP, então o THPDFCSCSignatureProvider orquestra o estado do protocolo e chama uma função THPDFCSCTransport para cada request. O provider te entrega um THPDFCSCTransportRequest com Method (sempre POST), a URL completa construída de ServiceBaseURL mais o caminho do endpoint, um header bearer Authorization pronto, ContentType, o Body JSON, um IdempotencyKey, o número de Attempt e o MaxResponseBytes. Você preenche um THPDFCSCTransportResponse com StatusCode, Body e RetryAfterMS, e retorna um de ctsSuccess, ctsTemporaryFailure, ctsPermanentFailure ou 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 do header como o seu serviço documenta
Headers := Headers + [TNameValuePair.Create('Idempotency-Key',
string(Request.IdempotencyKey))];
try
HttpResp := Client.Post(Request.URL, Body, Reply, Headers);
except
on ENetHTTPClientException do
Exit(ctsTemporaryFailure); // problema de socket ou DNS: retryável
end;
Response.StatusCode := HttpResp.StatusCode; // reporta 503 como está, não classifique
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;
A única regra que vale memorizar: retorne ctsSuccess sempre que um servidor realmente respondeu, mesmo com um 503. O provider classifica os status codes por conta própria, e um transporte que transforme um 429 em ctsPermanentFailure desliga silenciosamente a lógica de retry descrita adiante. O construtor é estrito no outro sentido — ele levanta EHPDFCSCSignatureProviderError quando o transporte falta, o CredentialID está vazio, nem um AccessToken nem um callback de token foram fornecidos, um budget está fora da faixa, ou o ServiceBaseURL não é HTTPS. http:// puro só é aceito com AllowInsecureHTTP, que pertence a um rig de teste e a lugar nenhum além dele
O que é a SAD, e por que o HotPDF a descarta depois de um uso?
O THPDFCSCSignatureProvider trata a Signature Activation Data (SAD) como de uso único: ela é limpa do estado do provider no momento em que o signatures/signHash é aceito, mesmo quando a assinatura em si chega depois por polling assíncrono. A SAD é a prova do serviço de que o signatário aprovou esses hashes específicos, e uma SAD que fica sobrando na memória é uma autorização esperando para ser gasta no documento errado
Com os defaults de THPDFCSCOptions.Default — RequireSAD e AutoAuthorize ambos True — o provider carrega o credentials/info uma vez, pede ao seu THPDFCSCAuthenticationCallback os valores de authData (um OTP, um PIN, o que for que o bloco auth do credential exija), e posta o credentials/authorize. Um 200 traz a SAD direto; um 202 traz um handle que é polado via credentials/authorizeCheck até MaxPollAttempts (60) vezes a cada PollIntervalMS (250 ms). O callback pode retornar no máximo 32 valores, cada um com um ID não vazio de até 256 bytes e um valor de até 4.096 bytes. Se a rede cai antes de o signHash ser aceito, uma SAD obtida automaticamente é mantida para que o mesmo lote possa ser retentado sem perguntar ao signatário de novo
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, modo async, 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
// seu cliente OAuth; ForceRefresh é True depois de o serviço responder 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 // sua UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Uma SAD que você mesmo passa por Options.SAD se comporta diferente, e deliberadamente. O HotPDF não tem como saber para quais hashes ela foi emitida, então o provider usa uma SAD pré-definida só para um request de hash único. Para um lote com AutoAuthorize desligado, o provider falha com "CSC SAD is not pinned to the requested hash batch" em vez de adivinhar
Como o SignHashBatch assina muitos documentos com uma autorização?
O SignHashBatch envia um credentials/authorize e um signatures/signHash para até MaxBatchSignatures (64) digests, e monta os dois corpos a partir do mesmo array para que numSignatures, a ordem de hashes e o hashAlgorithmOID sejam idênticos nas duas chamadas. Esse match é o que o modelo multisign do CSC exige. Faça loop do método Sign de hash único quarenta vezes e você ganha quarenta autorizações; mande um authorize e um signHash que discordam e o serviço pode consumir a SAD contra o lote errado
Antes de qualquer tráfego de rede, o provider valida o lote. Cada request precisa ser um digest (sikDigest) de 1 a 1.024 bytes com um OID de digest, e todos os requests precisam compartilhar um mesmo OID de algoritmo de assinatura, um mesmo OID de digest e, para RSASSA-PSS, um mesmo salt length. Um lote multi-hash também carrega o credentials/info e retorna spsUnsupported quando o valor de multisign do credential é menor que o lote. A SAD então é pregada a uma fingerprint de lote — um SHA-256 sobre um rótulo de versão, a contagem e, por request, o OID do algoritmo, o OID do digest, o algoritmo, o salt length e os bytes do digest, cada um prefixado pelo comprimento. Troque dois hashes de lugar e é um lote diferente, que precisa de uma autorização nova
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: valores SHA-256 que você calculou
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo derivado quando AlgorithmOID é vazio
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] pertence a Digests[I]; a contagem foi conferida contra o request
end;
Para RSASSA-PSS o provider também envia signAlgoParams, uma estrutura DER RSASSA-PSS-params em base64 com o algoritmo de hash, MGF1 e salt length. Montá-la significa codificar OIDs, e a versão 2.748.5 consertou um canto disso: a X.690 §8.19.4 dobra os dois primeiros arcos num único valor (40 × primeiro + segundo), e sob a raiz 2 um segundo arco acima de 39 empurra esse valor para além de 127, onde ele precisa da forma multi-byte base-128 que builds anteriores não aplicavam. Nenhum OID SHA-2 é afetado — 2.16 dobra para 96 — mas um OID malformado agora levanta o erro próprio do provider em vez de um EConvertError
Por que um request retentado não produz uma segunda assinatura?
O THPDFCSCSignatureProvider faz toda chamada retentável carregar uma idempotency key determinística e cacheia resultados completados, então um retry depois de uma resposta perdida retorna as assinaturas originais em vez de pedir novas ao HSM. A chave é csc- seguido do SHA-256 hex do identificador de operação e da fase, e a fase embute a fingerprint do lote tanto para a autorização quanto para o signHash. Fazer hash em vez de truncar importa: dois IDs de operação longos que compartilham um prefixo colidiriam sob truncamento, enquanto uma chave de comprimento fixo endereçada por conteúdo permanece única e estável entre tentativas
A política de retry no caminho de request compartilhado é estreita de propósito:
- HTTP 401 força exatamente um refresh de token pelo callback de access token, então o request é repetido uma vez quando um callback de access token está atribuído; um segundo 401 é final
- Outras respostas 4xx e
ctsPermanentFailureencerram a chamada comspsProviderError, e oerror_descriptiondo serviço cai noLastError - 408, 429, 5xx e
ctsTemporaryFailuresão retentados atéRetryLimit(default 2), esperandoRetry-AfterouRetryBaseDelayMS× 2attempt (base de 100 ms), com teto emMaxRetryAfterMS(5.000 ms) - As esperas rodam em fatias de 25 ms que conferem o
Cancel, então um usuário que aborta não fica sentado num back-off de cinco segundos - O
signHashsó é retentado enquanto oEnableIdempotencyestá ligado; desligue-o e um timeout depois do envio é final, porque ninguém consegue dizer se a chave já foi usada
Assinatura assíncrona (operationMode "A", o default) acrescenta uma proteção a mais: o responseID é guardado antes de polar o signatures/signPolling, então uma chamada repetida com o mesmo identificador de operação retoma o polling em vez de reenviar. Lotes completados ficam num cache indexado por identificador de operação, credential e fingerprint, com teto de MaxOperationCacheEntries (128) e devolvidos como cópias profundas. Esse cache vive na instância do provider e não sobrevive a um restart. A idempotency key sobrevive, porque é derivada e não aleatória, então um processo reiniciado que reutiliza o identificador de operação dele envia a mesma chave — se o serviço deduplica por ela é promessa do serviço, não do HotPDF
Como colocar uma assinatura CSC num PDF?
Passe o provider ao HPDFCMSSignPDFStreamWithProvider junto com o certificado end-entity do GetCertificateChain; o HotPDF monta o CMS SignedData e o provider assina o digest dos signed attributes. O PDF de entrada precisa do placeholder /ByteRange e /Contents que o THPDFPage.AddSignedSignatureField escreve, exatamente como no fluxo de assinatura PAdES do HotPDF, e o modelo de provider é o mesmo coberto em providers de assinatura plugáveis do HotPDF para 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 lista o certificado end-entity primeiro
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;
Dimensionar o placeholder /Contents passa pelo EstimateSignatureSize, que retorna o EstimatedSignatureBytes quando você o define e, caso contrário, o tamanho do modulus RSA a partir do comprimento da chave do credential. Para ECDSA defina o EstimatedSignatureBytes você mesmo, ou a estimativa reporta spsUnsupported. A variante de assinatura com tamanho automático re-assina quando um placeholder acaba pequeno demais, e só faz isso para providers que anunciam spcSafeSignRetry — o que o THPDFCSCSignatureProvider faz apenas enquanto o EnableIdempotency está ligado. Para fluxos PAdES-B-T, o TimestampDigest pede um timestamp token ao mesmo serviço via signatures/timestamp, com teto de MaxTimestampBytes (1 MB)
O que o provider CSC não faz?
Ele não assina mensagens, só digests. Ed25519 e Ed448 em modo puro entregam ao provider a mensagem inteira de signed attributes (sikMessage), e o validador de lote rejeita isso como malformado, porque o signHash é por definição baseado em hash. A unit do provider compila em Free Pascal com tipos de função simples no lugar de anonymous methods, mas os builders de CMS dirigidos por provider levantam exceção sob FPC hoje, então embutir uma assinatura CSC num PDF é um caminho Delphi
Ele também não decide política. O CredentialInfo reporta o status da chave, o status do certificado, o modo de autorização, o nível SCAL e o limite de multisign, mas o provider não vai recusar sozinho uma chave desabilitada ou um credential SCAL1 — confira isso antes de mostrar ao signatário o prompt de OTP. E uma instância de provider assina um lote por vez: o SignHashBatch é serializado internamente para que duas threads não disputem uma mesma SAD, o que significa que throughput vem de batching, não de compartilhar um provider entre worker threads. Se a assinatura resultante é qualified depende do trust service e do credential dele, não da biblioteca que levou o hash até lá
O provider CSC, os builders CMS e PAdES e os providers locais e PKCS#11 entram todos no componente HotPDF PDF para Delphi