💡 Exemplo completo funcionando disponível no GitHub: vault-transit-pdf-signing-python

Por que a assinatura automatizada falha em uma revisão de custódia de chaves

Equipes de conformidade e operações financeiras assinam documentos por milhares: contratos de empréstimo, extratos, pacotes de políticas, cartas de auditoria. Quase nenhum disso é assinado por uma pessoa clicando em um botão. Um serviço faz isso, em um cronograma, e o certificado anexado a esses documentos traz o nome da organização. Isso torna a chave de assinatura um dos itens mais sensíveis que a empresa possui, e geralmente ela fica em um arquivo PFX ao lado da aplicação que a utiliza.

A assinatura externa é um recurso do GroupDocs.Signature para Python via .NET que passa o resumo do documento para o código que você fornece, em vez de assinar com uma chave privada que a aplicação mantém. Ela existe porque a configuração com PFX não resiste ao escrutínio. Um arquivo é uma cópia, e as cópias se espalham: para a imagem do contêiner, o pipeline de implantação, o backup, o laptop do desenvolvedor que depurou um problema de produção na primavera passada. Qualquer pessoa que possua uma dessas cópias e a senha pode assinar em seu nome até que alguém perceba e revogue o certificado.

O que essas equipes precisam não é um formato de arquivo diferente. Elas precisam que a chave exista em exatamente um lugar, com um registro de auditoria de cada vez que for usada, e um serviço de assinatura que possa solicitar uma assinatura sem jamais ser confiado com a chave. É para isso que um gerenciador de chaves como o HashiCorp Vault serve, e só ajuda se a biblioteca de documentos puder ser instruída a delegar a criptografia.

Por que um arquivo de certificado protegido por senha não é suficiente

O instinto é bloquear o PFX: restringir permissões de arquivo, armazenar a senha em um gerenciador de segredos, criptografar o volume. Cada passo ajuda e nenhum deles altera o fato central. No momento da assinatura, a chave privada é carregada no processo, de modo que um dump de memória, uma fuga de contêiner ou uma declaração de log descuidada ainda podem expô‑la. O controle que você deseja é uma chave que não possa ser lida de forma alguma, não uma chave que seja mais difícil de ler.

O mecanismo Transit do Vault fornece isso. Ele realiza criptografia como serviço: você envia um valor, ele assina ou criptografa com uma chave que possui e devolve o resultado. Chaves criadas dentro dele podem ser marcadas como não exportáveis, e ele nunca armazena o que recebe. O GroupDocs.Signature cobre a outra metade, porque DigitalSignOptions.custom_sign_hash permite conectar seu próprio assinante ao pipeline de assinatura de PDF.

A Configuração: GroupDocs.Signature para Python com Vault Transit

O GroupDocs.Signature faz tudo até a criptografia. Ele abre o PDF, decide qual intervalo de bytes a assinatura cobre, calcula o hash desse intervalo e chama seu assinante com o digest. Seu assinante entrega o digest ao Vault e devolve os bytes produzidos pelo Vault. A biblioteca os incorpora junto com o certificado e grava um PDF assinado normal.

Você precisa de Python 3.11 ou 3.13, dos pacotes groupdocs-signature-net e asn1crypto, e do Docker para um contêiner Vault em modo de desenvolvimento. Uma licença GroupDocs é opcional; sem ela, o modo de avaliação aceita documentos de até 2 páginas. O repositório de exemplo roda de ponta a ponta em dois comandos, e o mesmo código aponta para um Vault de produção alterando duas variáveis de ambiente.

Construindo Passo a Passo

Etapa 1 - Ler a chave de assinatura do Vault

A aplicação solicita ao Vault a chave que tem permissão para usar e recebe de volta um número de versão e uma chave pública. A metade privada nunca faz parte da resposta.

key = vault.read_key(KEY_NAME)
if key is None:
    # First run: enable Transit and create an RSA-2048 key.
    vault.enable_transit()
    vault.create_key(KEY_NAME, "rsa-2048")
    key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")

Ler a versão uma única vez é o objetivo desta etapa. Um certificado contém uma única chave pública, portanto o certificado e cada assinatura devem provir da mesma versão.

Etapa 2 - Construir um certificado para uma chave que ninguém pode exportar

Uma assinatura de PDF carrega um certificado X.509 para que o verificador encontre a chave pública. Com uma chave não exportável, o certificado é montado ao redor da metade pública e assinado pelo Vault.

tbs_certificate = x509.TbsCertificate({...})   # built around the Vault public key

# Hash locally, then have Vault sign the certificate's own digest.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")

certificate = x509.Certificate({
    "tbs_certificate": tbs_certificate,
    "signature_algorithm": algorithm,
    "signature_value": signature,
})

O certificado deliberadamente começa um dia antes de agora. Um leitor de PDF compara o horário da assinatura com essa data de início, e em uma máquina duas horas a leste de UTC um certificado emitido segundos antes da assinatura foi relatado pelo Adobe Acrobat como expirado ou ainda não válido.

Etapa 3 - Assinar o PDF através do assinante externo

certificate_object = DigitalVerifyOptions(
    certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()

options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"

with gs.Signature(str(input_pdf)) as signature:
    signature.sign(str(output_pdf), options)

Duas linhas são específicas ao Python. A ligação não possui tipo X.509, portanto o objeto de certificado vem de DigitalVerifyOptions; passar um caminho para DigitalSignOptions em vez disso gera “Specified certificate doesn’t contain private key”. E sign_time é consciente de fuso horário de propósito, porque um valor com deslocamento é escrito no PDF junto a ele.

Dentro do assinante, o digest vai ao Vault com prehashed = true e pkcs1v15. Esses dois campos decidem se a assinatura é válida, e o primeiro falha silenciosamente quando está ausente.

Etapa 4 - Verificar antes que algo saia do serviço

options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
    is_valid = signature.verify(options).is_valid

Assinar e verificar na mesma execução transforma o modo de falha silenciosa em uma falha imediata. Para um pipeline de documentos isso é o portão: um arquivo que não verifica nunca chega ao arquivo, à contraparte ou ao portal do cliente.

Impacto nos Negócios: O que Muda para a Auditoria

Preocupação PFX no host da aplicação Assinatura via Vault Transit
Cópias da chave privada uma por host, imagem, backup e laptop uma, dentro do Vault
Permissões que o aplicativo de assinatura precisa acesso total ao arquivo da chave ler a chave, chamar transit/sign
Evidência de que um documento foi assinado logs da aplicação, se alguém os adicionou uma entrada de auditoria do Vault por assinatura
Resposta a um vazamento suspeito revogar, reemitir, reimplantar em todos os lugares rotacionar a chave no Vault, reemitir uma vez
Quem pode assinar em seu nome qualquer pessoa com o arquivo e a senha qualquer pessoa com um token Vault válido

A última linha é a honesta. Esse design move o segredo em vez de removê‑lo: um token Vault roubado ainda pode solicitar assinaturas. A diferença é que um token tem vida curta, pode ser revogado em um único lugar, está limitado a duas operações e cada uso é registrado. Isso é materialmente mais fácil de defender em uma revisão do que um arquivo cujas cópias ninguém consegue enumerar.

Fluxo Real: um Contrato de Empréstimo Assinado por um Serviço

Uma plataforma de empréstimos gera um PDF de contrato quando uma solicitação é aprovada, assina‑o e o deposita no cofre de documentos do cliente. Antes dessa mudança, o serviço de assinatura mantinha o certificado da empresa e o manual de implantação tinha um passo para copiar o PFX para novos hosts. Depois da mudança, o serviço mantém um token AppRole com duas permissões, e a chave privada do certificado nunca existiu em nenhum outro lugar além do Vault.

Quando os auditores perguntam quantos documentos foram assinados no último trimestre e por quem, a resposta vem do dispositivo de auditoria do Vault em vez de reconstruir os logs da aplicação. Quando a equipe rotaciona a chave, ela a rotaciona em um único lugar e emite um novo certificado para a nova versão.

O que devo acertar primeiro?

O manuseio do digest. Um nome de algoritmo errado falha de forma barulhenta, com o Vault respondendo 500 crypto/rsa: input must be hashed message, e custa um minuto. A falta da flag prehashed não produz erro algum: o Vault hash novamente o digest, a assinatura cobre o valor errado e o PDF parece concluído. Verifique cada arquivo que você assina e faça com que essa falha deixe de ser invisível.

Outros Cenários Onde Esse Design se Encaixa

O hook não tem ideia do que está por trás dele, portanto os mesmos quatro passos funcionam contra um módulo de segurança de hardware, um KMS na nuvem ou um cartão inteligente. Eu portei essa implementação do seu irmão .NET esperando que o assinante precisasse de retrabalho, e a única parte que realmente mudou foi a entrega do certificado; o corpo da requisição ao Vault e o fixo da versão eram idênticos. Equipes de saúde, governo e seguros recorrem a isso pelo mesmo motivo que as finanças: a chave precisa estar em um lugar que possa provar para que foi usada.

Começando

Recursos