Programação Por Equipe DevBras

Capa do artigo: Assinar PDF no servidor sem deixar a chave privada à solta

Assinar PDF no servidor sem deixar a chave privada à solta

Decida onde a chave privada mora, assine o PDF em .NET sem exportar a chave, limite quem dispara a assinatura e teste se a alteração é detectada.

O sistema de contratos da imobiliária gera o PDF, assina com o certificado da empresa e manda para o cliente. Funciona há um ano. Um dia alguém percebe que o arquivo empresa.pfx está na pasta wwwroot/certs, com a senha no appsettings.json logo ao lado, ambos no repositório. Quem clonou o projeto pode assinar qualquer documento em nome da empresa. A resposta curta: a assinatura digital vale exatamente o quanto vale o controle sobre a chave privada. A chave fica num lugar de onde não pode ser copiada (repositório de certificados com chave não exportável, cofre de chaves ou token físico), o código pede "assine estes bytes" em vez de carregar a chave, e só um processo com permissão explícita consegue pedir.

Este guia trata de onde guardar a chave e como assinar sem expô-la. O que a assinatura prova está em assinatura digital não é imagem da rubrica, e como o destinatário confere está em conferir assinatura de PDF.

Por que a chave é o ponto fraco

Na assinatura, o programa calcula um resumo (hash) do conteúdo do PDF e cifra esse resumo com a chave privada. Quem recebe usa a chave pública, que viaja dentro do certificado embutido no arquivo, para conferir. Se uma letra do documento muda, o hash muda e a conferência falha.

Nada nessa matemática sabe quem apertou o botão. Se a chave privada estiver num arquivo copiável, qualquer pessoa com o arquivo e a senha produz assinaturas idênticas às legítimas. Não há como distinguir depois. Por isso o vazamento de um .pfx é pior do que o vazamento de uma senha de usuário: a senha você troca e invalida as sessões; a chave vazada exige revogar o certificado, e os documentos assinados com ela no período ficam sob suspeita.

Onde a chave pode morar

Do mais frágil ao mais robusto:

  • Arquivo .pfx no disco do servidor. Aceitável só fora do repositório, fora da pasta pública, com permissão de leitura apenas para o usuário do serviço e senha vinda de um cofre. Ainda assim é copiável por quem invadir o servidor.
  • Repositório de certificados do sistema com chave não exportável. No Windows, você importa o .pfx marcando a chave como não exportável e apaga o arquivo. O processo usa a chave, mas não consegue extraí-la.
  • Cofre de chaves na nuvem ou HSM. A chave nunca sai do hardware. O código manda o hash e recebe a assinatura. Cada operação fica registrada.
  • Token ou cartão (como certificados A3). A chave está num dispositivo físico com PIN. Bom para assinatura feita por uma pessoa na própria máquina; ruim para assinatura automática no servidor.

Para a imobiliária, que assina dezenas de contratos por dia de forma automática, o meio-termo realista é o repositório do Windows com chave não exportável, num serviço separado do site.

Como assinar sem carregar a chave no código

  1. Tire o .pfx do repositório e de qualquer pasta servida pela web. Se ele já esteve no Git, trate como vazado: peça outro certificado e revogue o antigo, como explica segredo no Git se troca antes de limpar.
  2. Importe o novo certificado no repositório da máquina do serviço de assinatura, com a chave não exportável, e dê acesso à chave só à conta desse serviço.
  3. Separe o serviço de assinatura do site. O site pede "assine o contrato 4812"; o serviço confere se o contrato existe e está aprovado, assina e registra quem pediu.
  4. No código, localize o certificado pela impressão digital (thumbprint) e use a chave por referência.
  5. Adicione carimbo de tempo de uma autoridade de carimbo, para a data da assinatura ser verificável depois que o certificado vencer.

A parte que encontra o certificado e assina bytes, em .NET, fica assim:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

public static class Assinador
{
    public static X509Certificate2 Certificado(string thumbprint)
    {
        using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        store.Open(OpenFlags.ReadOnly);
        var achados = store.Certificates.Find(
            X509FindType.FindByThumbprint, thumbprint, validOnly: true);
        if (achados.Count != 1)
            throw new InvalidOperationException("Certificado de assinatura não encontrado.");
        return achados[0];
    }

    public static byte[] AssinarBytes(X509Certificate2 cert, byte[] dados)
    {
        using RSA rsa = cert.GetRSAPrivateKey()
            ?? throw new InvalidOperationException("Certificado sem chave RSA.");
        return rsa.SignData(dados, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    }
}

GetRSAPrivateKey() devolve um objeto que usa a chave guardada pelo sistema. Com a chave não exportável, uma tentativa de exportá-la falha, mas a assinatura funciona. A thumbprint vem da configuração, que não é segredo: saber qual certificado usar não permite usá-lo.

A biblioteca de PDF (iText, por exemplo, como no artigo original) cuida de reservar o espaço da assinatura no arquivo, calcular o hash dos trechos certos e montar a estrutura CMS. No iText, você entrega ao assinador uma implementação de IExternalSignature cujo método de assinatura chama algo como AssinarBytes. Os nomes exatos dos outros métodos dessa interface mudaram entre as versões 7 e 8 da biblioteca; siga a documentação da versão que você instalou. O ponto que não muda: a biblioteca recebe uma função que assina, não a chave. Trocar o repositório do Windows por um cofre na nuvem depois é trocar só essa função.

Como testar se a assinatura protege

Assine um contrato de teste e abra no Adobe Acrobat Reader, que mostra o painel de assinaturas. Confira três coisas: a assinatura é válida, o nome do certificado é o da empresa e a data aparece com carimbo de tempo. Depois abra o PDF num editor de texto ou hexadecimal, troque um dígito do valor do aluguel dentro de um trecho de texto e salve. Ao reabrir, o leitor tem de acusar que o documento foi alterado. Se não acusa, você não assinou o conteúdo; só anexou um bloco ao arquivo.

Erros comuns

  • Certificado e senha no repositório. Qualquer clone vira um carimbo da empresa. Apagar o commit não resolve, porque as cópias existem.
  • .pfx dentro de wwwroot ou pasta pública. Um caminho adivinhado baixa a chave. Servidor web serve arquivo; ele não sabe o que é sensível.
  • Endpoint "assinar" aberto a qualquer usuário logado. A chave está protegida, mas quem tem login assina o que quiser. Proteger a chave sem proteger quem dispara não adianta.
  • Sem log de assinatura. Quando aparece um documento estranho, não há como saber quem pediu, quando e qual arquivo.
  • Sem carimbo de tempo. Quando o certificado vence, fica difícil provar que a assinatura foi feita enquanto ele era válido.
  • Testar só se "aparece assinado". Sem o teste de alteração, um erro na montagem passa despercebido até alguém contestar o documento.

Checklist

  • Nenhum .pfx, .p12 ou .key no repositório nem em pasta pública (procure no histórico também).
  • Chave importada como não exportável, ou mantida em cofre ou HSM.
  • Acesso à chave só para a conta do serviço de assinatura.
  • Serviço de assinatura separado do site, conferindo se o documento pode ser assinado.
  • Log com quem pediu, qual documento e o hash do resultado.
  • Carimbo de tempo em toda assinatura.
  • Teste de alteração feito e acusado pelo leitor.
  • Data de vencimento do certificado num calendário com responsável.

Isto é orientação técnica; validade jurídica de assinatura depende do tipo de certificado e do uso, e não substitui a orientação de um profissional habilitado. A pergunta para responder agora, olhando o servidor: se alguém copiar a pasta do sistema inteira para um pendrive, ele leva junto a capacidade de assinar como a empresa?

Avalie este artigo

Referência: https://viewbag.wordpress.com/2019/12/24/pdf-digital-signatures-itext7-bouncy-castle-net-core/