O PDFium Component assina um PDF com uma assinatura digital PAdES B-B através do seu método SignPades: carrega o documento, calcula a hash do intervalo de bytes assinado, constrói uma estrutura CAdES CMS após o cálculo, e anexa a assinatura como uma atualização incremental. O backend criptográfico é exclusivo do Windows, pelo que deve proteger cada chamada com PadesCryptoAvailable antes de assinar
A situação é familiar. Um contrato em PDF chega à sua secretária, o departamento jurídico quer que seja assinado digitalmente antes de ser enviado, e recorre à mesma compilação do PDFium que já utiliza para renderizar e inspecionar documentos, apenas para descobrir que o PDFium não consegue gravar uma assinatura de todo. A sua API de assinatura é estritamente de apenas leitura. O PDFium Component preenche essa lacuna gerindo todo o pipeline de assinatura em Pascal, desde a função de hash até à injeção ao nível de bytes, e este artigo descreve esse pipeline de ponta a ponta
Por que o PDFium não consegue gravar uma assinatura digital?
O PDFium expõe as assinaturas como objetos de apenas leitura e não oferece recursos para criar uma. A família FPDFSignatureObj_* permite-lhe enumerar uma assinatura existente, ler a sua /Contents e inspecionar o seu /ByteRange, mas não existe contrapartida que crie um dicionário de assinaturas, reserve um espaço para /Contents ou grave um intervalo de bytes; a gravação incremental existe (FPDF_SaveAsCopy com FPDF_INCREMENTAL), mas não dispõe de suporte (hook) de assinatura. Qualquer componente que assine um PDF com base no PDFium deve, por isso, gerar cada byte da assinatura, razão pela qual o PDFium Component constrói esse mecanismo a partir de três units em Pascal puro. O FPC 3.2.2 inclui suporte a md5 e sha1, mas não disponibiliza qualquer SHA-2, e a API SHA-256 de System.Hash do Delphi não é compatível a nível de código-fonte com o FPC, pelo que a FPdfSha256 é uma implementação FIPS 180-4 autónoma que mantém todos os caminhos de código CMS sob um tipo TSHA256Digest sem diretivas de compilação condicionais. A FPdfAsn1 fornece o codificador e o leitor DER de que as estruturas CMS necessitam, e a FPdfCms monta a CAdES SignedData com base em ambos
Como assinar digitalmente um PDF no Delphi?
Carregue o documento e, em seguida, chame o método SignPades com a impressão digital (thumbprint) de um certificado. O PDFium Component resolve essa impressão digital contra o repositório de certificados "MY" do Utilizador Atual, obtém o certificado correspondente e a sua chave privada, e grava uma cópia assinada no caminho que indicar
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('PAdES signing requires the Windows CNG backend');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // o documento a assinar
Pdf.Active := True;
// Segundo argumento: impressão digital (thumbprint) SHA-1 de um certificado no repositório
// "MY" do Utilizador Atual. Primeiro argumento: destino da cópia assinada.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
O PadesCryptoAvailable é a verificação a fazer primeiro, sempre. Em Windows devolve True e o backend crypt32/ncrypt está ativo; em qualquer outra plataforma devolve False e uma chamada de assinatura levantaria EPadesCrypto. Tratar esta salvaguarda como obrigatória evita que uma build de Linux ou macOS falhe em tempo de execução num caminho que ali não pode funcionar. A própria impressão digital é o hash SHA-1 do certificado, o mesmo valor que o gestor de certificados do Windows mostra no separador Detalhes, e identifica um signatário concreto sem alguma vez colocar material de chave no seu código-fonte
O que integra o CMS: atributos assinados e o RFC 5652
Uma assinatura de referência PAdES não é uma assinatura RSA simples sobre o ficheiro; trata-se de uma estrutura CAdES CMS SignedData que transporta um conjunto obrigatório de atributos assinados, e o método FPdfCms.BuildSignedData emite precisamente esse conjunto: content-type, message-digest e signing-certificate-v2, o atributo ESS que vincula a assinatura ao certificado do signatário por hash. Um detalhe específico inviabiliza quase todas as implementações manuais de CMS. A norma RFC 5652 §5.4 exige que o resumo dos atributos assinados seja calculado sobre a codificação DER SET OF, tag 0x31, ao passo que os mesmos atributos viajam dentro de SignerInfo sob a tag IMPLICIT [0], 0xA0. O PDFium Component codifica o conjunto de atributos uma vez, calcula o resumo do formato 0x31, e depois reescreve apenas o byte da tag inicial para 0xA0 para envio, permitindo que um único buffer sirva ambas as funções sem necessidade de uma segunda passagem sobre a estrutura
var
Pdf: TPdf;
Opts: TPadesSignOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := 'a1b2c3d4e5f6...'; // signatário no repositório MY
Opts.Reason := 'I approve this agreement';
Opts.Location := 'Berlin, DE';
Opts.ContentsSize := 16384; // comprimento hexadecimal de /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Signing failed');
finally
Pdf.Free;
end;
end;
A sobrecarga com opções acrescenta os metadados do dicionário de assinatura definidos na ISO 32000-1 §12.8.1: Reason, Location, ContactInfo e Name, todos opcionais e todos escritos no dicionário de valor da assinatura. Há uma restrição em que é fácil tropeçar. Se definir CommitmentTypeOid para acrescentar um atributo assinado CAdES de indicação de tipo de compromisso, não defina também Reason; a ETSI EN 319 142-1 §6.3 proíbe transportar os dois, porque ambos exprimem a mesma intenção por meios diferentes
Como a ByteRange e o espaço /Contents se articulam?
Uma assinatura tem de cobrir todo o ficheiro, exceto os bytes que guardam a própria assinatura, e o PAdES resolve essa circularidade com um marcador de posição de largura fixa que a rotina SignPadesBytes gere com precisão. Reserva uma string hexadecimal para /Contents com o comprimento de ContentsSize bytes (16384 por padrão, confortavelmente superior a uma CMS SignedData típica), serializa a atualização incremental para localizar o offset exato do espaço, calcula depois a /ByteRange como dois intervalos que delimitam o espaço (tudo o que está antes do delimitador de abertura da string hexadecimal e tudo o que está após o delimitador de fecho). O SHA-256 processa apenas esses dois intervalos. O CMS finalizado é codificado em hexadecimal no espaço reservado, preenchido com zeros até à largura fixa, e a atualização de referências cruzadas é anexada. Como o comprimento é fixado à partida, o preenchimento do espaço não desloca qualquer byte a jusante, permitindo que o intervalo de bytes se mantenha válido; os bytes originais do documento são preservados na íntegra, pelo que uma assinatura anterior no mesmo ficheiro sobrevive intacta, exatamente como exige a gravação incremental da norma ISO 32000-1 §12.8.1
O backend Windows CNG e as suas limitações
O PDFium Component realiza assinaturas apenas em Windows, e esse limite é intencional. A FPdfCryptoWin vincula dinamicamente a crypt32.dll e a ncrypt.dll, sem adicionar dependências de DLL em tempo de compilação, e a cadeia de assinatura segue o padrão CNG: abre o repositório MY, encontra o certificado por hash, obtém o handle da sua chave privada através de CryptAcquireCertificatePrivateKey e chama a NCryptSignHash. São suportados RSA com PKCS#1 v1.5, RSA-PSS e ECDSA. O ECDSA necessita de uma correção de que os restantes não carecem, visto que a NCryptSignHash devolve o par em bruto IEEE P1363 de r e s, enquanto o CMS espera uma SEQUENCE DER ECDSA-Sig-Value, pelo que o backend volta a codificá-lo de acordo com o RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // sem motor de assinatura nesta plataforma
Opts := TPadesSignOptions.Default;
Opts.CertificateThumbprint := ReadThumbprintFromConfig;
Pdf := TPdf.Create(nil);
Output := TFileStream.Create('contract-signed.pdf', fmCreate);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.SignPadesToStream(Output, Opts);
finally
Output.Free;
Pdf.Free;
end;
end;
A consequência prática é que a chave privada deve residir no repositório de certificados do Windows. Um certificado contido num ficheiro PFX funciona apenas depois de o importar para o repositório do Utilizador Atual, ponto em que a sua impressão digital (thumbprint) passa a ser o valor que transmite à SignPades. Esta versão não disponibiliza caminhos para PKCS#11 ou HSM, nem backend para ficheiros de chaves de software, pelo que, quando a PadesCryptoAvailable devolve False, a assinatura não é possível nessa máquina
Onde termina o PAdES B-B
O PAdES B-B é o nível básico, o patamar inicial dos quatro níveis PAdES: prova quem assinou e que os bytes não foram alterados desde então, e nada mais do que isso. Uma assinatura B-B não transporta um carimbo de data/hora (timestamp) fidedigno, pelo que não pode comprovar quando ocorreu a assinatura, e não incorpora dados de revogação, pelo que um validador necessitará futuramente de obter a cadeia de certificados e o seu estado por si próprio. Essas lacunas são precisamente as que os níveis superiores resolvem. Quando necessitar de uma hora de assinatura que um auditor aceite, adicionar um carimbo de data/hora RFC 3161 e DSS para validação de longo prazo eleva a assinatura para o nível B-T ou superior; quando desejar ler uma assinatura finalizada e confirmar o nível atingido, inspecionar uma assinatura PDF e o seu nível PAdES é a ferramenta indicada; e antes de assinar qualquer documento, auditar um PDF em busca de riscos de segurança indica-lhe exatamente no que está prestes a colocar o seu nome
Os métodos SignPades apresentados aqui são fornecidos com o PDFium Component para Delphi e C++Builder, juntamente com a funcionalidade de inspeção de assinaturas de apenas leitura disponibilizada pelo PDFium por padrão