O PDFiumPas divide a assinatura PAdES em duas chamadas, de modo que a chave privada nunca precisa estar no seu processo. PreparePadesRemoteSignature grava uma atualização incremental com um placeholder /Contents vazio, de largura fixa, e devolve um registro de requisição carregando o digest SHA-256 do documento, o ByteRange exato e uma impressão digital do arquivo preparado. CompletePadesRemoteSignature pega o CMS destacado que o seu serviço de assinatura retorna e o coloca naquele espaço reservado
Entre essas duas chamadas, minutos ou horas podem se passar, o processo pode reiniciar, e o trabalho pode se mover para outra máquina. Essa lacuna é o motivo inteiro pelo qual a API é moldada dessa forma
Por que uma chave remota não pode usar a chamada de assinatura comum?
Porque SignPadesBytes assume que a operação de assinatura acontece dentro da chamada. Ela constrói a atualização incremental, calcula o digest sobre o ByteRange, o assina, e grava o resultado, tudo antes de retornar. Isso está exatamente certo quando a chave vive no armazenamento de certificados do Windows ou em um arquivo PKCS#12 que você carregou
Isso é impossível quando a chave vive em um HSM de rede, em um dispositivo qualificado de criação de assinaturas operado por um prestador de serviços de confiança, ou em uma API de assinatura em nuvem que exige que o usuário confirme pelo celular. Nesses casos, a sequência não é uma chamada de função, é uma conversa: você envia um digest, outra coisa autentica um humano, e um CMS volta depois. Uma API síncrona não consegue expressar "depois" sem bloquear uma thread em uma operação que pode exigir um segundo fator
O protocolo em duas fases
A primeira fase prepara o documento. O PDFiumPas anexa o campo de assinatura e o dicionário de valor, reserva ContentsSize bytes de espaço codificado em hex dentro de /Contents, calcula o ByteRange ao redor dessa reserva, e produz um TPadesRemoteSigningRequest contendo FormatVersion, PreparedFingerprint, DocumentDigest, o ByteRange de quatro elementos, ContentsHexOffset e ContentsSize
O único valor de que o seu serviço de assinatura precisa é DocumentDigest: o SHA-256 que o CAdES SignedData retornado precisa carregar como seu message digest. Tudo o mais no registro existe para que a segunda fase possa provar que o arquivo que ela está completando é o arquivo a partir do qual esse digest foi calculado
uses
FPdfPades;
var
Options: TPadesRemoteSignOptions;
Request: TPadesRemoteSigningRequest;
Source, Prepared, Session: TFileStream;
begin
Options := TPadesRemoteSignOptions.Default;
Options.Reason := 'Approved by finance';
Options.Location := 'Lisbon';
Options.Name := 'A. Moreira';
Options.SigningTimeUtc := NowUtc;
Options.ContentsSize := 16384; // bytes hex reservados para o CMS
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
try
PreparePadesRemoteSignature(Source, Prepared, Options, Request);
finally
Prepared.Free;
Source.Free;
end;
// Persista a sessão para que uma execução posterior - ou outra máquina - possa concluí-la
Session := TFileStream.Create('contract.signreq', fmCreate);
try
SavePadesRemoteSigningRequest(Session, Request);
finally
Session.Free;
end;
SendDigestToSigningService(Request.DocumentDigest);
end;
O que o Complete recusa, e por que cada verificação existe?
A conclusão é onde um design de assinatura remota costuma dar errado, então a validação é deliberadamente implacável. CompletePadesRemoteSignature rejeita um PDF preparado cuja impressão digital não corresponde mais à requisição, um ByteRange que não corresponde às coordenadas de placeholder registradas, delimitadores /Contents modificados, um placeholder que não está mais vazio, um CMS maior do que a reserva, um CMS que não é exatamente um único valor DER, um formato de SignedData não suportado, um atributo signing-certificate-v2 ausente, e um CMS cujo message digest não é igual ao digest de documento preparado
Cada uma delas corresponde a uma falha real. As verificações de impressão digital e de ByteRange capturam o caso em que alguém regenerou o arquivo preparado entre as fases, o que produziria uma assinatura que valida contra bytes que ninguém possui. A verificação de placeholder vazio captura conclusão dupla, quando um segundo CMS é gravado sobre uma assinatura que já existe. A verificação de message digest captura o caso mais perigoso de todos: um CMS corretamente formado, mas assinado sobre um documento diferente, que é o que você obtém quando uma fila confunde duas sessões de assinatura concorrentes. Sem essa verificação, você produziria um arquivo que parece assinado e falha na validação em todo lugar, ou pior, que carrega a aprovação de outra pessoa
A exigência de signing-certificate-v2 é uma questão de conformidade com PAdES, não de integridade. A ETSI EN 319 142 exige que o certificado de assinatura seja vinculado aos atributos assinados, e um CMS sem esse atributo não é uma assinatura PAdES, mesmo que se verifique criptograficamente. Rejeitá-lo na conclusão significa que você descobre isso aqui, não em um relatório de validador vindo de um cliente, um tema explorado mais a fundo em por que validadores rejeitam assinaturas PAdES
var
Request: TPadesRemoteSigningRequest;
Session, Prepared, Dest: TFileStream;
CmsDer: TBytes;
begin
Session := TFileStream.Create('contract.signreq', fmOpenRead);
try
Request := LoadPadesRemoteSigningRequest(Session);
finally
Session.Free;
end;
CmsDer := FetchDetachedCmsFromService; // retornado pelo HSM ou TSP
Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
try
try
CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
except
on E: EPadesCrypto do
// Toda rejeição carrega um motivo específico; registre-o literalmente em log
FailSession(E.Message);
end;
finally
Dest.Free;
Prepared.Free;
end;
end;
Atravessando fronteiras de processo e de máquina
SavePadesRemoteSigningRequest e LoadPadesRemoteSigningRequest serializam a sessão por meio de um formato binário versionado e estável, o que é o que torna o design prático, e não apenas correto. Uma aplicação web pode preparar um documento em uma requisição, armazenar o PDF preparado e o blob de sessão, retornar um digest ao navegador para uma assinatura por smart card, e completar o arquivo em um handler de requisição completamente diferente
O campo FormatVersion é o que mantém isso seguro entre upgrades. Uma sessão gravada por um build mais antigo e carregada por um mais novo é reconhecida ou rejeitada explicitamente, em vez de ser lida incorretamente como um registro de outro formato. Se a sua fila consegue manter sessões por dias, trate a versão do formato como um fato operacional que vale a pena registrar em log, não como um detalhe de implementação
Dimensionando o placeholder
ContentsSize é o único parâmetro sobre o qual você precisa pensar, porque ele é fixado antes que o CMS exista. Ele conta a reserva codificada em hex, então um CMS DER de 6 KB precisa de pelo menos 12 KB de espaço, e a implementação limita a reserva a 64 MiB
Reserve pouco demais e a conclusão falha com um erro de CMS maior do que o tamanho reservado, depois que o seu serviço de assinatura já fez o trabalho dele, o que em um serviço de assinatura qualificada medido significa uma operação desperdiçada. Reserve demais e todo documento assinado carrega o padding para sempre. A abordagem sensata é medir: assine um documento com a sua cadeia de certificados real, veja o comprimento do DER, dobre-o para hex, depois adicione uma folga generosa para o token de timestamp se você pretende evoluir para uma assinatura de nível T. Cadeias com vários intermediários e uma resposta OCSP longa crescem mais rápido do que as pessoas esperam
O que vem depois da assinatura
Uma assinatura remota concluída é PAdES B-B. A validação de longo prazo precisa de um timestamp e do material de validação, que é uma atualização incremental separada que adiciona um DSS e seus dicionários VRI por assinatura, descrita em assinaturas de longo prazo com timestamps RFC 3161 e DSS. Essa etapa é local: ela adiciona certificados, respostas OCSP e CRLs, nenhum dos quais precisa da chave privada
Antes de colocar em produção, verifique o que você produziu com o mesmo caminho de código que uma parte confiante usaria, abordado em inspecionando assinaturas digitais e níveis PAdES. Assinatura e verificação são códigos diferentes, e um pipeline de assinatura remota é exatamente o lugar onde os dois podem se distanciar sem que ninguém perceba, até que um validador externo avise
O PDFiumPas é um componente Delphi e Lazarus construído em torno do motor PDFium, com uma pilha PAdES nativa em Pascal, de modo que assinatura, timestamp e validação funcionam sem ferramentas de linha de comando externas. A documentação completa da API e uma versão de teste estão na página do componente PDFium Delphi