Artigo Técnico

Assinatura PAdES Remota PDFium VCL: HSM e Chaves Cloud

O PDFiumPas divide a assinatura PAdES em duas chamadas para que a chave privada nunca tenha de estar no seu processo. PreparePadesRemoteSignature escreve uma atualização incremental com um marcador /Contents vazio de largura fixa e devolve um registo de pedido que transporta o resumo SHA-256 do documento, o ByteRange exato e uma impressão digital do ficheiro preparado. CompletePadesRemoteSignature recebe o CMS destacado que o seu serviço de assinatura devolve e insere-o nesse espaço reservado

Entre essas duas chamadas podem passar minutos ou horas, o processo pode reiniciar, e o trabalho pode mudar para outra máquina. Esse intervalo é toda a razão pela qual a API tem esta forma

Porque não pode uma chave remota usar a chamada de assinatura normal?

Porque SignPadesBytes assume que a operação de assinatura ocorre dentro da chamada. Constrói a atualização incremental, calcula o resumo sobre o ByteRange, assina-o e escreve o resultado, tudo antes de regressar. Isso está exatamente correto quando a chave reside na loja de certificados do Windows ou num ficheiro PKCS#12 que carregou

É impossível quando a chave reside num HSM de rede, num dispositivo qualificado de criação de assinaturas operado por um prestador de serviços de confiança, ou numa API de assinatura cloud que exige que o utilizador confirme num telemóvel. Nesses casos a sequência não é uma chamada de função, é uma conversa: envia um resumo, algo mais autentica um ser humano, e um CMS chega mais tarde. Uma API síncrona não consegue exprimir "mais tarde" sem bloquear uma thread numa operação que pode exigir um segundo fator

O protocolo em duas fases

A primeira fase prepara o documento. O PDFiumPas anexa o campo e o dicionário de valor da assinatura, reserva ContentsSize bytes de espaço codificado em hexadecimal em /Contents, calcula o ByteRange em torno dessa reserva, e produz um TPadesRemoteSigningRequest contendo FormatVersion, PreparedFingerprint, DocumentDigest, o ByteRange de quatro elementos, ContentsHexOffset e ContentsSize

O único valor de que o seu serviço de assinatura precisa é DocumentDigest: o SHA-256 que o SignedData CAdES devolvido tem de transportar como resumo de mensagem. Tudo o resto no registo existe para que a segunda fase possa provar que o ficheiro que está a completar é o ficheiro a partir do qual esse resumo foi calculado

Fluxo PAdES remoto PDFium em duas fases em Delphi, em que o PreparePadesRemoteSignature reserva o slot CMS e entrega o digest do documento a um HSM ou serviço na nuvem, e o CompletePadesRemoteSignature incorpora o CMS devolvido em qualquer processo ou máquina mais tarde
A fase um reserva a ranhura do CMS e entrega o digest do documento; a fase dois incorpora o CMS que regressa do HSM ou da chave na nuvem
uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // bytes hex reservados para o CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Persistir a sessão para que uma execução posterior - ou outra máquina - a possa concluir
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

O que recusa o Complete, e porque existe cada verificação?

A conclusão é onde um desenho de assinatura remota costuma correr mal, pelo que a validação é deliberadamente rigorosa. CompletePadesRemoteSignature rejeita um PDF preparado cuja impressão digital já não corresponda ao pedido, um ByteRange que não corresponda às coordenadas do marcador registado, delimitadores /Contents modificados, um marcador que já não está vazio, um CMS maior do que a reserva, um CMS que não seja exatamente um único valor DER, uma forma de SignedData não suportada, um atributo signing-certificate-v2 em falta, e um CMS cujo resumo de mensagem não seja igual ao resumo do documento preparado

Cada uma dessas verificações corresponde a uma falha real. As verificações de impressão digital e de ByteRange detetam o caso em que alguém regenerou o ficheiro preparado entre as duas fases, o que produziria uma assinatura que valida contra bytes que já não existem. A verificação do marcador vazio deteta a dupla conclusão, em que um segundo CMS é escrito por cima de uma assinatura já existente. A verificação do resumo de mensagem deteta o caso mais perigoso de todos: um CMS corretamente formado mas assinado sobre um documento diferente, o que acontece quando uma fila mistura duas sessões de assinatura concorrentes. Sem ela, produziria um ficheiro que parece assinado e falha a validação em todo o lado, ou pior, que transporta a aprovação de outra pessoa

A exigência do signing-certificate-v2 é uma questão de conformidade PAdES e não de integridade. A ETSI EN 319 142 exige que o certificado de assinatura esteja vinculado nos atributos assinados, e um CMS sem esse atributo não é uma assinatura PAdES mesmo que se verifique criptograficamente. Rejeitá-lo na conclusão significa descobrir isto aqui, e não num relatório de validador vindo de um cliente, um tema aprofundado em porque os validadores rejeitam assinaturas PAdES

Diagrama do portão de conclusão da assinatura PAdES remota PDFium em Delphi, em que o CompletePadesRemoteSignature verifica a integridade do ficheiro, a forma do CMS e a conformidade PAdES, gravando o PDF assinado apenas quando todas as verificações passam
CompletePadesRemoteSignature valida integridade, forma do CMS e conformidade PAdES antes de qualquer byte ser escrito
var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // devolvido pelo HSM ou pelo TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Cada rejeição transporta um motivo específico; registe-o literalmente
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Atravessar fronteiras de processo e de máquina

SavePadesRemoteSigningRequest e LoadPadesRemoteSigningRequest serializam a sessão através de um formato binário estável e versionado, o que torna o desenho prático e não apenas correto. Uma aplicação web pode preparar um documento num pedido, guardar o PDF preparado e o blob de sessão, devolver um resumo ao navegador para uma assinatura por cartão inteligente, e concluir o ficheiro num manipulador de pedido completamente diferente

O campo FormatVersion é o que mantém isso seguro entre atualizações. Uma sessão escrita por uma compilação mais antiga e carregada por uma mais recente é reconhecida ou rejeitada explicitamente, em vez de ser lida incorretamente como um registo com forma diferente. Se a sua fila conseguir manter sessões durante dias, trate a versão de formato como um facto operacional que vale a pena registar, não como um pormenor de implementação

Dimensionar o marcador

ContentsSize é o único parâmetro sobre o qual tem de pensar, porque é fixado antes de o CMS existir. Conta a reserva codificada em hexadecimal, pelo que um CMS DER de 6 KB precisa de pelo menos 12 KB de espaço, e a implementação limita a reserva a 64 MiB

Reservar demasiado pouco faz a conclusão falhar com um erro de CMS demasiado grande depois de o seu serviço de assinatura já ter feito o seu trabalho, o que num serviço de assinatura qualificada medido representa uma operação desperdiçada. Reservar demasiado faz com que cada documento assinado transporte o preenchimento para sempre. A abordagem sensata é medir: assine um documento com a sua cadeia de certificados real, veja o comprimento do DER, duplique-o para hexadecimal, e acrescente uma margem generosa para o token de carimbo temporal caso pretenda avançar para uma assinatura de nível T. Cadeias com vários intermediários e uma resposta OCSP longa crescem mais depressa do que se espera

O que vem depois da assinatura

Uma assinatura remota concluída é PAdES B-B. A validação a longo prazo precisa de um carimbo temporal e do material de validação, o que é uma atualização incremental separada que acrescenta um DSS e os seus dicionários VRI por assinatura, descrita em assinaturas de longo prazo com carimbos temporais RFC 3161 e DSS. Esse passo é local: acrescenta certificados, respostas OCSP e CRLs, nenhum dos quais precisa da chave privada

Antes de distribuir, verifique o que produziu com o mesmo caminho de código que uma parte confiante utilizaria, abordado em inspecionar assinaturas digitais PDF e níveis PAdES. Assinar e verificar são códigos diferentes, e um pipeline de assinatura remota é exatamente o local onde os dois se podem afastar sem que ninguém dê por isso até um validador externo o indicar

O PDFiumPas é um componente para Delphi e Lazarus construído em torno do motor PDFium, com uma pilha PAdES nativa em Pascal, pelo que a assinatura, o carimbo temporal e a validação funcionam sem ferramentas de linha de comandos externas. A documentação completa da API e uma versão de avaliação estão disponíveis na página do componente PDFium para Delphi