Artigo Técnico

Assinar PDFs com PAdES B-B no Delphi Utilizando o PDFium

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('Falha na assinatura');
  finally
    Pdf.Free;
  end;
end;

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 := 'Eu aprovo este acordo';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // comprimento hexadecimal de /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Falha na assinatura');
  finally
    Pdf.Free;
  end;
end;

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