Uma assinatura PDF resume-se maioritariamente a contabilidade de bytes, e é aí que as coisas correm mal. A criptografia corre em código que foi auditado durante duas décadas, e essa parte quase nunca falha. O que falha em produção é mais humilde: um marcador de posição reservado que é demasiado pequeno para a assinatura real, um hash obtido sobre o trecho errado do ficheiro, ou um "guardar" após a assinatura que reescreveu silenciosamente os bytes que a assinatura já tinha congelado. Disponha os bytes corretamente e a marca de verificação verde tratará de si própria.
O HotPDF abrange a assinatura para Delphi e C++Builder em três níveis, e o utilizador escolhe entre eles respondendo a uma pergunta: onde reside a chave privada? Um ficheiro PFX no disco requer uma única chamada de função. Uma chave trancada num HSM ou num serviço de assinatura remota necessita da sequência reserva-hash-inserção, porque nenhuma biblioteca consegue aceder a um token e extrair a chave. Uma assinatura que tenha de satisfazer a regulamentação europeia necessita, além disso, das estruturas de referência PAdES. As secções seguintes acompanham essa progressão.
Como o /ByteRange fixa os bytes assinados
Uma assinatura tem de residir dentro do ficheiro que assina, e não pode assinar-se a si própria. O PDF contorna este paradoxo deixando um espaço vazio. Antes de assinar, o gravador reserva uma entrada /Contents de tamanho fixo cheia de zeros e regista uma matriz /ByteRange para os dois intervalos em cada um dos lados: tudo antes do espaço vazio, tudo depois. O assinante calcula o hash desses dois intervalos e escreve o blob CMS resultante no espaço vazio como hexadecimal. A armadilha está na palavra fixo. O utilizador compromete-se com o tamanho desse espaço vazio antes de saber o quão grande será a assinatura final, pelo que a reserva tem de ser uma estimativa por excesso confiável. Oito kilobytes contêm confortavelmente uma assinatura CMS destacada com uma cadeia curta de certificados.
O HotPDF divide os dois casos em duas chamadas, e confundi-los é um erro inicial comum. AddSignatureField insere um campo vazio e visível para uma pessoa assinar mais tarde num visualizador. AddSignedSignatureField cria o campo e reserva o espaço vazio em /Contents, que é o pretendido sempre que o código, em vez de um humano, irá concluir a assinatura. Entregar a um assinante externo um campo vazio faz com que este não tenha nada para preencher.
O caminho de chamada única: assinar a partir de um PFX
Quando o certificado e a sua chave privada residem num ficheiro PFX/PKCS#12 que o seu processo consegue ler, todo o pipeline reduz-se a uma função de classe:
if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
'company-cert.pfx', 'pfx-password') then
Writeln('Signed: invoice-signed.pdf')
else
raise Exception.Create('PFX signing failed');
Quando isto falha, o PDF raramente é o problema. O problema é o PFX. O HotPDF lê contentores protegidos com PBES2, o que significa derivação de chave PBKDF2 sobre AES-256-CBC. Um PFX exportado por um assistente de certificados antigo do Windows, ou pelo OpenSSL anterior à versão 3.0, é normalmente envolvido em RC2 ou 3DES herdados, e simplesmente não será analisado. A correção passa por exportar novamente o contentor uma vez com proteção moderna; o OpenSSL atual faz isso por defeito, e não se trata de uma alteração de código. Portanto, quando a assinatura falha instantaneamente num certificado que "funciona em qualquer outro lugar", examine a forma como o PFX foi criado antes de suspeitar do seu próprio código.
O caminho reserva-hash-inserção para HSMs e tokens
O caminho de chamada única pressupõe que o seu processo consegue ler a chave como um ficheiro. Cada vez mais, isso não é possível. A chave reside num HSM, num token USB ou por trás da API de um serviço de assinatura, e não existe forma de uma biblioteca aceder diretamente à mesma. O HotPDF lida com isso dividindo a assinatura em etapas ao nível dos bytes: escrever um documento de marcador de posição, solicitar à biblioteca os intervalos de hash, passar o input de hash para o que quer que contenha a chave e, em seguida, encaixar o CMS retornado de volta no espaço vazio.
var
Doc: THotPDF;
Fs: TFileStream;
PdfBytes, HashInput, SigHex: AnsiString;
R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
// 1. Write the document with a reserved /Contents hole
Doc := THotPDF.Create(nil);
try
Doc.FileName := 'placeholder.pdf';
Doc.BeginDoc;
Doc.CurrentPage.AddSignedSignatureField('Sig1',
Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
'Contract approval', 'Boston, MA', 'legal@example.com');
Doc.EndDoc;
finally
Doc.Free;
end;
// 2. Load the saved bytes; the returned offsets are 0-based
Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
try
SetLength(PdfBytes, Fs.Size);
Fs.ReadBuffer(PdfBytes[1], Fs.Size);
finally
Fs.Free;
end;
THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
CStart, CLen);
// 3. Hash both spans and sign externally (HSM, token, service)
HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
Copy(PdfBytes, R2Start + 1, R2Len);
SigHex := SignWithHsm(HashInput); // your integration: returns CMS as hex
// 4. Splice the signature into the reserved hole
THotPDF.InsertSignatureHex(PdfBytes, SigHex);
Fs := TFileStream.Create('signed.pdf', fmCreate);
try
Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
finally
Fs.Free;
end;
end;
Dois detalhes nesta sequência causam a maioria das falhas intermitentes. O primeiro é que o PreparePDFForSigning funciona sobre os bytes de um ficheiro concluído. O marcador de posição tem de ser escrito e guardado na totalidade antes que os offsets signifiquem algo; calculá-los em relação a um fluxo que ainda está a ser montado fará com que não se alinhem com os bytes que acabará por hash. O segundo é, novamente, o tamanho da reserva. Os 8192 bytes solicitados têm de conter o CMS final, e uma assinatura que transporte certificados intermédios, ou que seja decorada por um serviço com atributos assinados, pode exceder esse limite. O InsertSignatureHex não aumentará o espaço vazio para criar espaço. O sinal claro é um pipeline que funciona bem com um certificado e falha com o seguinte; a solução passa por regenerar o marcador de posição com uma reserva medida a partir de uma assinatura real produzida pelo assinante real, e não adivinhada.
Referências PAdES e os selos temporais que mantêm uma assinatura viva
Se estiver a assinar sob as regras europeias, a norma em vigor é a ETSI EN 319 142-1, que empilha quatro níveis de referência PAdES. O B-B é a assinatura simples. O B-T adiciona um selo temporal fidedigno que comprova quando foi realizada. O B-LT incorpora o material de validação, os certificados e os dados de revogação, dentro do documento para que este ainda possa ser verificado anos mais tarde. O B-LTA sobrepõe selos temporais periódicos do documento, de modo que a evidência sobreviva aos algoritmos sobre os quais foi construída. O HotPDF emite as estruturas do lado do documento para cada nível:
// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
'Contract approval', 'Boston, MA', 'legal@example.com');
// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);
A reserva de 16384 bytes no selo temporal é deliberada. Uma autoridade de selos temporais (TSA) devolve um token que arrasta a sua própria cadeia de certificados, pelo que necessita rotineiramente de mais espaço do que os 8 KB com que uma assinatura simples se contenta. Esses selos temporais de documentos são também o mecanismo por trás do B-LTA: voltar a colocar um selo temporal numa assinatura arquivada de poucos em poucos anos, com algoritmos que ainda estejam atuais, é o que mantém um documento assinado em 2026 verificável em 2040.
Uma nota sobre as strings de motivo, localização e contacto que ambas as chamadas de campo aceitam: são metadados de conveniência e nada mais. O HotPDF armazena-os como entradas de dicionário simples e desenha-os na aparência visual da assinatura, mas nenhum validador os verifica contra o que quer que seja. Preencha-os de forma consistente com os dados do seu fluxo de trabalho, uma vez que os auditores os leem, e nunca os confunda com provas. A alegação criptográfica real reside inteiramente no CMS e na sua cadeia de certificados, e um verificador ignora completamente o texto visível.
Após a assinatura, o ficheiro só pode crescer
No momento em que existe uma assinatura, os bytes dentro dos seus intervalos são congelados. A única forma legítima de alterar o ficheiro posteriormente é uma atualização incremental ISO 32000-1 §7.5.6, que anexa objetos novos e alterados após os bytes originais e encadeia uma nova secção de referência cruzada de volta aos mesmos. Dessa forma, a assinatura permanece válida para a sua revisão e um visualizador reporta o estado real: a revisão assinada está intacta, o documento foi expandido posteriormente. Se, em vez disso, voltar a serializar todo o ficheiro, reescreverá os intervalos assinados, o que destrói a assinatura mesmo quando nada de visível foi alterado. O mesmo mecanismo de revisão é também a forma como um documento transporta várias assinaturas: cada nova assinatura é colocada na sua própria atualização incremental, e os seus intervalos cobrem tudo o que estiver antes dela, incluindo as assinaturas anteriores. O mecanismo de apenas anexar (append-only), e quando é seguro compactá-los, são abordados no artigo sobre fluxos de objetos e atualizações incrementais.
Vale a pena ter em mente duas limitações enquanto desenha a sua solução. O modo de saída PDF/A do HotPDF rejeita campos de assinatura por completo, pelo que a conformidade de arquivo e uma assinatura incorporada têm de ser distribuídas como ficheiros separados. E a assinatura não garante qualquer segredo: prova quem produziu um documento e que este não foi alterado desde então, mas qualquer pessoa o pode ler. Ocultar o conteúdo é uma tarefa separada, tratada pela encriptação AES-256 e política de permissões.
Independentemente do que desenvolver, teste-o com algo diferente do código que escreveu o ficheiro. Abra o resultado no painel de assinaturas do Acrobat e confirme três coisas: a assinatura é válida, a identidade encadeia-se na raiz esperada e o painel não indica alterações desde a assinatura. Depois, altere um único byte dentro do intervalo assinado de uma cópia descartável e confirme que o painel agora classifica o documento como alterado. Um pipeline de assinatura no qual nunca tenha visto rejeitar um ficheiro adulterado é um pipeline cuja verificação não foi realmente testada.
Todos os três níveis de assinatura são disponibilizados com o HotPDF Component para Delphi e C++Builder; a página do produto liga à referência completa da API de assinaturas.