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
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
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