O HotPDF assina documentos PDF com uma chave privada guardada num serviço remoto da Cloud Signature Consortium (CSC) através 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 token de acesso OAuth. A chave nunca sai do HSM do serviço
Essa é cada vez mais a única maneira de obter uma chave de assinatura qualificada. Os prestadores de serviços de confiança entregam um endpoint CSC e um cliente OAuth, não um ficheiro PFX nem um token USB, por isso não há nada para carregar numa loja de certificados local à maneira da assinatura pela loja de certificados do Windows via CNG e CAPI. A integração ingénua falha de maneiras previsíveis: uma chamada signHash excede o tempo e a repetição assina o mesmo contrato duas vezes, ou um lote de quarenta faturas dispara quarenta códigos de uso único porque cada hash foi autorizado em separado. A maior parte do que o provider faz é defender-se contra essas duas falhas
Porque é que o HotPDF deixa o HTTP para a sua aplicação?
Porque o transporte é exatamente o sítio onde cada deployment difere. Proxies, TLS pinning, certificados de cliente, cofres OAuth empresariais e política de logging vivem todos na camada HTTP, por isso o THPDFCSCSignatureProvider orquestra o estado do protocolo e chama uma função THPDFCSCTransport por cada pedido. O provider entrega-lhe um THPDFCSCTransportRequest com Method (sempre POST), o URL completo construído a partir de ServiceBaseURL mais o caminho do endpoint, um cabeçalho Authorization bearer pronto, ContentType, o Body JSON, um IdempotencyKey, o número Attempt e MaxResponseBytes. Preenche um THPDFCSCTransportResponse com StatusCode, Body e RetryAfterMS, e devolve 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 cabeçalho tal como o seu serviço 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); // problemas de socket ou DNS: repetível
end;
Response.StatusCode := HttpResp.StatusCode; // reportar 503 tal como vem, sem classificar
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 regra única que vale a pena decorar: devolva ctsSuccess sempre que um servidor realmente respondeu, mesmo com um 503. O provider classifica os códigos de estado ele próprio, e um transporte que transforme um 429 em ctsPermanentFailure desliga em silêncio a lógica de repetição descrita abaixo. O construtor é estrito na outra direção — levanta EHPDFCSCSignatureProviderError quando falta o transporte, o CredentialID está vazio, nem um AccessToken nem um callback de token são fornecidos, um budget está fora do intervalo, ou o ServiceBaseURL não é HTTPS. O http:// simples só é aceite com AllowInsecureHTTP, que pertence a um rig de testes e a mais nenhum sítio
O que é o SAD, e porque o HotPDF o deita fora depois de um uso?
O THPDFCSCSignatureProvider trata a Signature Activation Data (SAD) como de uso único: é limpa do estado do provider no momento em que o signatures/signHash é aceite, mesmo quando a assinatura em si chega mais tarde por polling assíncrono. A SAD é a prova do serviço de que o assinante aprovou aqueles hashes concretos, e uma SAD que persiste em memória é uma autorização à espera de ser gasta no documento errado
Com os valores por defeito de THPDFCSCOptions.Default — RequireSAD e AutoAuthorize ambos True — o provider carrega o credentials/info uma vez, pede ao seu THPDFCSCAuthenticationCallback os valores authData (um OTP, um PIN, o que quer que o bloco auth da credencial exija), e publica credentials/authorize. Um 200 traz a SAD diretamente; um 202 traz um handle que é polled através de credentials/authorizeCheck até MaxPollAttempts (60) vezes a PollIntervalMS (250 ms). O callback pode devolver 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 cair antes de o signHash ser aceite, uma SAD obtida automaticamente é guardada para que o mesmo lote possa ser repetido sem voltar a pedir ao assinante
var
Options: THPDFCSCOptions;
Provider: THPDFCSCSignatureProvider;
begin
Options := THPDFCSCOptions.Default; // RequireSAD, AutoAuthorize, modo assíncrono, 2 repetições
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
// o 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 // a sua UI
Exit(spsCancelled);
SetLength(Values, 1);
Values[0].ID := 'otp';
Values[0].Value := AnsiString(Otp);
Result := spsValid;
end);
Uma SAD que passe você próprio através de Options.SAD comporta-se de outra forma, e deliberadamente. O HotPDF não pode saber para que hashes foi emitida, por isso o provider usa uma SAD pré-definida só para um pedido de hash único. Numa bateria com AutoAuthorize desligado, o provider falha com «CSC SAD is not pinned to the requested hash batch» em vez de adivinhar
Como assina o SignHashBatch muitos documentos com uma autorização?
O SignHashBatch envia um credentials/authorize e um signatures/signHash para até MaxBatchSignatures (64) digests, e constrói ambos os corpos a partir do mesmo array, de modo que numSignatures, a ordem de hashes e o hashAlgorithmOID sejam idênticos nas duas chamadas. Essa correspondência é o que o modelo multisign do CSC exige. Fazer um ciclo no método de hash único Sign quarenta vezes dá quarenta autorizações; enviar um authorize e um signHash que discordam pode fazer o serviço gastar a SAD contra o lote errado
Antes de qualquer tráfego de rede, o provider valida o lote. Cada pedido tem de ser um digest (sikDigest) de 1 a 1.024 bytes com um OID de digest, e todos os pedidos têm de partilhar um OID de algoritmo de assinatura, um OID de digest e, para RSASSA-PSS, um comprimento de sal. Uma bateria multi-hash também carrega o credentials/info e devolve spsUnsupported quando o valor multisign da credencial é menor que o lote. A SAD é depois cravada numa impressão digital do lote — um SHA-256 sobre um rótulo de versão, a contagem e, por pedido, o OID de algoritmo, o OID de digest, o algoritmo, o comprimento de sal e os bytes do digest, cada um prefixado com o comprimento. Trocar dois hashes é um lote diferente que precisa de autorização fresca
var
Requests: THPDFCSCSignatureRequests;
Signatures: THPDFCSCSignatures;
Status: THPDFSignatureProviderStatus;
I: Integer;
begin
SetLength(Requests, Length(Digests)); // Digests: valores SHA-256 que calculou
for I := 0 to High(Digests) do
begin
Requests[I] := Default(THPDFSignatureProviderRequest);
Requests[I].Algorithm := hsaRSAPKCS1v15; // signAlgo derivado quando AlgorithmOID está 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 verificada contra o pedido
end;
Para RSASSA-PSS o provider também envia signAlgoParams, uma estrutura RSASSA-PSS-params DER em base64 com o algoritmo de hash, o MGF1 e o comprimento de sal. Construí-la significa codificar OIDs, e a versão 2.748.5 corrigiu um canto disso: o 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 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 do próprio provider em vez de um EConvertError
Porque é que um pedido repetido não produz uma segunda assinatura?
O THPDFCSCSignatureProvider faz cada chamada repetível transportar uma chave de idempotência determinística e põe em cache resultados concluídos, por isso uma repetição depois de uma resposta perdida devolve 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 incorpora a impressão digital do lote tanto para a autorização como para o signHash. Fazer hash em vez de truncar interessa: dois identificadores de operação longos que partilhem um prefixo colidiriam sob truncamento, enquanto uma chave de comprimento fixo endereçada pelo conteúdo permanece única e estável entre tentativas
A política de repetição no caminho de pedidos partilhado é estreita de propósito:
- HTTP 401 força exatamente uma renovação de token através do callback de token de acesso, e o pedido é repetido uma vez quando existe um callback de token de acesso atribuído; um segundo 401 é final
- Outras respostas 4xx e
ctsPermanentFailureterminam a chamada comspsProviderError, e oerror_descriptiondo serviço aterra noLastError - 408, 429, 5xx e
ctsTemporaryFailuresão repetidos atéRetryLimit(por defeito 2), esperandoRetry-AfterouRetryBaseDelayMS× 2attempt (base de 100 ms), com teto emMaxRetryAfterMS(5.000 ms) - As esperas correm em fatias de 25 ms que verificam
Cancel, por isso um utilizador que aborta não fica sentado num back-off de cinco segundos - O
signHashsó é repetido comEnableIdempotencyligado; desligue-o e um timeout depois da submissão é final, porque ninguém consegue dizer se a chave já foi usada
A assinatura assíncrona (operationMode «A», o valor por defeito) acrescenta uma guarda: o responseID é guardado antes de fazer polling ao signatures/signPolling, por isso uma chamada repetida com o mesmo identificador de operação retoma o polling em vez de resubmeter. Lotes concluídos ficam numa cache chaveada por identificador de operação, credencial e impressão digital, limitada por MaxOperationCacheEntries (128) e devolvida como cópias profundas. Essa cache vive na instância do provider e não sobrevive a um reinício. A chave de idempotência sobrevive, porque é derivada e não aleatória, por isso um processo reiniciado que reutilize o seu identificador de operação envia a mesma chave — se o serviço deduplica com base nela é uma promessa do serviço, e não do HotPDF
Como se põe uma assinatura CSC num PDF?
Passe o provider ao HPDFCMSSignPDFStreamWithProvider juntamente com o certificado de entidade final de GetCertificateChain; o HotPDF constrói o CMS SignedData e o provider assina o digest dos atributos assinados. O PDF de entrada precisa do placeholder /ByteRange e /Contents que o THPDFPage.AddSignedSignatureField escreve, exatamente como no fluxo de assinatura PAdES no HotPDF, e o modelo de provider é o mesmo coberto em os signature providers ligá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; // O CSC lista o certificado de entidade final 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 devolve EstimatedSignatureBytes quando o define e caso contrário o tamanho do módulo RSA a partir do comprimento da chave da credencial. Para ECDSA defina EstimatedSignatureBytes você próprio, ou a estimativa reporta spsUnsupported. A variante de assinatura com dimensionamento automático assina de novo quando um placeholder revela ser pequeno de mais, e só o faz para providers que anunciem spcSafeSignRetry — o que o THPDFCSCSignatureProvider faz só com EnableIdempotency ligado. Para fluxos PAdES-B-T, o TimestampDigest pede um token de carimbo temporal ao mesmo serviço através de signatures/timestamp, limitado a MaxTimestampBytes (1 MB)
O que é que o provider CSC não faz?
Não assina mensagens, só digests. Ed25519 e Ed448 em modo puro entregam ao provider a mensagem inteira de atributos assinados (sikMessage), e o validador de baterias rejeita-a como malformada, 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 conduzidos por provider levantam exceção em FPC hoje, por isso incorporar uma assinatura CSC num PDF é um caminho Delphi
Também não decide políticas. O CredentialInfo reporta o estado da chave, o estado do certificado, o modo de autorização, o nível SCAL e o limite multisign, mas o provider não vai recusar sozinho uma chave desativada nem uma credencial SCAL1 — verifique isso antes de mostrar ao assinante o pedido de OTP. E uma instância de provider assina um lote de cada vez: o SignHashBatch é serializado internamente para que duas threads não disputem uma SAD, o que significa que o throughput vem do batching, e não de partilhar um provider por threads de trabalho. Se a assinatura resultante é qualificada depende do serviço de confiança e da sua credencial, e não da biblioteca que transportou o hash até lá
O provider CSC, os builders CMS e PAdES e os providers locais e PKCS#11 saem todos no componente PDF HotPDF para Delphi