O Componente PDFium assina um PDF com uma assinatura digital PAdES B-B por meio de seu método SignPades: ele carrega o documento, calcula o hash do intervalo de bytes assinado, constrói uma estrutura CAdES CMS e anexa a assinatura como uma atualização incremental. O backend de criptografia está restrito ao Windows, portanto, envolva cada chamada com PadesCryptoAvailable antes de assinar
A situação é familiar. Um contrato em PDF chega à sua mesa, a área jurídica deseja que ele seja assinado digitalmente antes de ser enviado, e você recorre à mesma compilação do PDFium que já usa para renderizar e inspecionar documentos, apenas para descobrir que o PDFium não consegue gravar uma assinatura de forma alguma. Sua API de assinatura é estritamente de leitura. O Componente PDFium resolve essa limitação ao gerenciar todo o fluxo de assinatura em Pascal, desde a função de hash até a injeção no nível de byte, e este artigo percorre esse fluxo de ponta a ponta
Por que o PDFium não consegue gravar uma assinatura digital?
O PDFium expõe assinaturas como objetos apenas de leitura e não oferece nada para criá-las. A família FPDFSignatureObj_* permite enumerar uma assinatura existente, ler seu /Contents e inspecionar seu /ByteRange, mas não há contrapartida que monte um dicionário de assinatura, reserve um espaço para /Contents ou grave um intervalo de bytes; o salvamento incremental existe (FPDF_SaveAsCopy com FPDF_INCREMENTAL), mas não possui gancho para assinatura. Qualquer componente que assine um PDF sobre o PDFium deve, portanto, gerar cada byte da assinatura por si mesmo, razão pela qual o Componente PDFium constrói a engrenagem a partir de três unidades escritas em Pascal puro. O FPC 3.2.2 suporta md5 e sha1, mas não oferece SHA-2 de forma alguma, e a API System.Hash do Delphi para SHA-256 não é compatível com o código-fonte do FPC. Por isso, a unit FPdfSha256 é uma implementação independente da especificação FIPS 180-4 que mantém todos os caminhos do CMS sob o tipo TSHA256Digest, sem ramificações de compilação. A unit FPdfAsn1 fornece o codificador e leitor DER que as estruturas CMS necessitam, e a unit FPdfCms monta a estrutura CAdES SignedData com base em ambas
Como assinar digitalmente um PDF no Delphi?
Carregue o documento e, em seguida, chame SignPades com a identificação digital (thumbprint) do certificado. O Componente PDFium resolve essa identificação contra o repositório de certificados "MY" do Usuário Atual, obtém o certificado correspondente e sua chave privada, e grava uma cópia assinada no caminho que você indicar
uses
PDFium, FPdfCrypto;
procedure SignContract(const AThumbprint: string);
var
Pdf: TPdf;
begin
if not PadesCryptoAvailable then
raise Exception.Create('A assinatura PAdES exige o backend CNG do Windows');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf'; // o documento a ser assinado
Pdf.Active := True;
// Segundo argumento: identificação digital SHA-1 de um certificado no repositório
// "MY" do Usuário Atual. Primeiro argumento: destino da cópia assinada.
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
raise Exception.Create('Falha na assinatura');
finally
Pdf.Free;
end;
end;
PadesCryptoAvailable é a verificação que você faz primeiro, sempre. No Windows ela retorna True e o backend crypt32/ncrypt fica ativo; em qualquer outra plataforma ela retorna False e uma chamada de assinatura geraria a exceção EPadesCrypto. Tratar essa verificação como obrigatória evita que uma compilação para Linux ou macOS falhe em tempo de execução em um caminho que não pode funcionar. A identificação digital é a hash SHA-1 do certificado, o mesmo valor que o gerenciador de certificados do Windows mostra em sua aba Detalhes, e ela identifica um signatário específico sem expor chaves privadas no seu código
O que vai no CMS: atributos assinados e RFC 5652
Uma assinatura de linha de base PAdES não é uma assinatura RSA bruta sobre o arquivo; é uma estrutura CAdES CMS SignedData contendo um conjunto obrigatório de atributos assinados, e a função FPdfCms.BuildSignedData emite exatamente esse conjunto: content-type, message-digest e signing-certificate-v2, o atributo ESS que vincula a assinatura ao certificado do signatário por meio de hash. Um detalhe aqui costuma inviabilizar quase todas as implementações manuais de CMS. A especificação RFC 5652 §5.4 exige que a hash dos atributos assinados seja calculada sobre a codificação DER SET OF, tag 0x31, enquanto os mesmos atributos viajam dentro do SignerInfo sob a tag IMPLICIT [0], tag 0xA0. O Componente PDFium codifica o conjunto de atributos uma vez, calcula a hash sobre a forma 0x31 e depois reescreve apenas o byte da tag inicial para 0xA0 para envio, assim, um único buffer serve a ambos os propósitos sem a necessidade de uma segunda passagem na árvore
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 := 'Aprovo este acordo';
Opts.Location := 'São Paulo, BR';
Opts.ContentsSize := 16384; // largura hexadecimal de /Contents
if not Pdf.SignPades('contract-signed.pdf', Opts) then
raise Exception.Create('Falha na assinatura');
finally
Pdf.Free;
end;
end;
A sobrecarga com opções adiciona os metadados do dicionário de assinatura definidos pela ISO 32000-1 §12.8.1: Reason, Location, ContactInfo e Name, todos opcionais e gravados no dicionário de valor da assinatura. Uma limitação é fácil de se deparar: se você definir o CommitmentTypeOid para adicionar um atributo assinado CAdES de indicação de tipo de compromisso, não defina também o Reason; a norma ETSI EN 319 142-1 §6.3 proíbe a presença de ambos, pois os dois expressam a mesma intenção por meios diferentes
Como o ByteRange e o espaço /Contents se encaixam?
Uma assinatura precisa cobrir todo o arquivo, exceto os bytes que guardam a própria assinatura, e o PAdES resolve essa circularidade com um espaço reservado de tamanho fixo que a função SignPadesBytes gerencia com precisão. Ela reserva uma string hexadecimal de /Contents com ContentsSize bytes (16384 by default, confortavelmente maior do que uma estrutura CMS SignedData típica), serializa a atualização incremental para localizar o offset exato do espaço reservado, e calcula o /ByteRange como dois intervalos que delimitam esse espaço: tudo antes do delimitador de abertura da string hexadecimal e tudo após seu delimitador de fechamento. O cálculo SHA-256 é executado apenas sobre esses dois intervalos. O CMS finalizado é codificado em hexadecimal no espaço reservado, preenchido com zeros até o tamanho fixo, e a atualização de referência cruzada é anexada. Como o tamanho é fixado antecipadamente, o preenchimento do espaço reservado não desloca bytes subsequentes, razão pela qual o intervalo de bytes continua válido; os bytes originais do documento são preservados verbatim, garantindo que uma assinatura anterior no mesmo arquivo continue intacta, exatamente como determina a assinatura incremental da ISO 32000-1 §12.8.1
O backend CNG do Windows e seus limites
O Componente PDFium assina apenas no Windows, e essa limitação é intencional. A unit 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, localiza o certificado por meio da hash, adquire o handle de sua chave privada via CryptAcquireCertificatePrivateKey e chama NCryptSignHash. RSA com PKCS#1 v1.5, RSA-PSS e ECDSA são todos suportados. O ECDSA necessita de um ajuste que os demais não exigem, porque a função NCryptSignHash retorna o par bruto r-e-s da especificação IEEE P1363, enquanto o CMS espera uma SEQUENCE DER ECDSA-Sig-Value, de modo que o backend a recodifica conforme a RFC 5480
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Output: TFileStream;
begin
if not PadesCryptoAvailable then
Exit; // sem backend 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 em um arquivo PFX só funciona após ser importado no repositório do Usuário Atual, momento em que sua identificação digital (thumbprint) passa a ser o valor que você passa para SignPades. Esta versão não possui caminhos para PKCS#11 ou HSM, nem backend de arquivos de chaves por software, portanto, quando PadesCryptoAvailable retorna False, simplesmente não é possível realizar assinaturas nessa máquina
Onde o PAdES B-B para
O PAdES B-B é a linha de base, o patamar mínimo dos quatro níveis PAdES: ele prova quem assinou e que os bytes não foram alterados desde então, e nada mais. Uma assinatura B-B não carrega carimbo do tempo confiável, de modo que não pode provar quando a assinatura ocorreu, e não incorpora dados de revogação, exigindo que o validador obtenha a cadeia de certificados e seu status por conta própria no futuro. Essas lacunas são precisamente o que os níveis superiores corrigem. Quando precisar de um horário de assinatura aceito por auditores, adicionar um carimbo do tempo RFC 3161 e DSS para validação de longo prazo eleva a assinatura para B-T e superiores; quando desejar ler uma assinatura finalizada de volta e confirmar o nível que ela atingiu, inspecionar uma assinatura de PDF e seu nível PAdES é a ferramenta complementar; e antes de assinar qualquer documento, auditar riscos de segurança em PDF diz a você o que você está prestes a assinar
Os métodos SignPades mostrados aqui acompanham o Componente PDFium para Delphi e C++Builder, junto com a inspeção de assinatura em modo de leitura fornecida nativamente pelo PDFium