Validar uma assinatura PAdES implica verificar três aspetos independentes, sendo que uma marca de confirmação verde num visualizador apenas indica o sucesso do terceiro. Primeiro, a matriz /ByteRange deve cobrir os bytes corretos: os intervalos especificados têm de reconstruir exatamente os dados de entrada sobre os quais o resumo (digest) CMS foi calculado, sem deixar quaisquer bytes assinados fora deles. Segundo, o certificado contido no CMS deve encadear-se até uma raiz fidedigna e conter o atributo assinado do certificado de assinatura exigido pela especificação PAdES. Terceiro, se o perfil requerer um carimbo de data/hora, um token RFC 3161 deve vincular o valor da assinatura a um momento no tempo anterior à expiração do certificado. O Acrobat junta estes três aspetos num único ícone; um validador de conformidade mantém-nos separados, e o mesmo deve fazer o código que gera estes ficheiros. A losLab PDF Library (PDFlibPas) disponibiliza o processo de assinatura, a reinserção do carimbo de data/hora e as chamadas de auditoria para inspecionar um ByteRange antes de lhe conceder confiança.
Há uma distinção que baralha quase todas as primeiras implementações de PAdES, pelo que convém esclarecê-la antes de qualquer código. Uma assinatura escrita com /SubFilter /adbe.pkcs7.detached é uma assinatura ISO 32000-1 §12.8 perfeitamente válida que o Acrobat considerará correta. Contudo, não é uma assinatura PAdES, porque a norma ETSI EN 319 142-1 exige ETSI.CAdES.detached em todos os níveis base (baseline). Um validador de conformidade eIDAS rejeita a primeira e aceita a segunda, mesmo que a criptografia subjacente seja idêntica. O perfil é uma declaração que o documento faz sobre si mesmo, e obter a declaração correta exige apenas uma chamada no PDFlibPas.
O que transforma uma assinatura PDF numa assinatura PAdES
A norma ETSI EN 319 142-1 define quatro níveis base (baseline) estruturados sobre o formato CMS. O nível PAdES-B-B é o ponto de entrada: uma assinatura CAdES num campo de assinatura PDF com o SubFilter ETSI.CAdES.detached e um atributo de assinatura do certificado assinado. O PAdES-B-T adiciona um carimbo de data/hora RFC 3161 sobre o valor da assinatura, comprovando que a mesma existia antes de um momento temporal que ninguém pode manipular. O PAdES-B-LT incorpora os certificados, as CRLs e as respostas OCSP necessárias para a validação num Armazenamento de Segurança do Documento (Document Security Store ou DSS), garantindo que o ficheiro se mantém verificável mesmo após a autoridade de certificação (CA) desativar a sua infraestrutura. Por fim, o PAdES-B-LTA encerra a pilha com um carimbo de data/hora do documento que protege novamente as provas acumuladas à medida que os algoritmos criptográficos enfraquecem.
O PDFlibPas mapeia estes conceitos na sua API de processo de assinatura. O marcador do perfil é definido por SetSignProcessCustomSubFilter. Se a sua política exigir uma indicação do tipo de compromisso (prova de origem, prova de aprovação ou um dos outros identificadores ETSI numerados de 1 a 6), esta é configurada por SetSignProcessCommitmentType. Uma política de assinatura explícita é anexada com SetSignProcessSignaturePolicy, que recebe o OID da política e o seu resumo. Um dos padrões merece particular atenção: quando o algoritmo de resumo é deixado em automático, a biblioteca escolhe SHA-256 para assinaturas ETSI e adbe.pkcs7.detached, recorrendo ao SHA-1 apenas no caminho legado de adbe.pkcs7.sha1. Recomenda-se defini-lo explicitamente. Os auditores questionam qual o hash utilizado, e um valor explícito no código é mais fácil de justificar do que um padrão que obrigue a ler o manual para explicar.
Produzir a assinatura base
A API plana conduz a assinatura como uma máquina de estados de execução única: abre-se um processo no ficheiro de origem, configura-se, conclui-se para um ficheiro de saída e lê-se o código de resultado. A sequência descrita abaixo produz uma assinatura PAdES-B-B com SHA-256. A linha mais importante nada tem a ver com a assinatura propriamente dita: trata-se da reserva intencionalmente sobredimensionada do marcador /Contents, dado ser o único elemento que não poderá alterar mais tarde se for necessário adicionar um carimbo de data/hora a esta assinatura.
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // room for a timestamp later
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
O método NewSignProcessFromFile devolve 0 quando a origem não pode ser aberta. Em seguida, GetSignProcessResult distingue as tipologias de falha que ocorrem em produção: 4 indica palavra-passe do PDF incorreta, 7 palavra-passe do PFX incorreta, 9 um ficheiro de certificado sem chave privada, 10 um caminho de saída sem permissões de escrita e 11 uma falha durante a aplicação dos bytes de assinatura. Registar o código numérico ao lado do nome do ficheiro de entrada transforma um pedido de suporte genérico num diagnóstico rápido.
Adicionar o carimbo de data/hora RFC 3161 que a biblioteca não irá obter por si
O PDFlibPas não inclui um cliente TSA (Timestamp Authority), o que constitui uma fronteira de design intencional e não uma lacuna. A biblioteca calcula o hash que a autoridade de carimbos de data/hora deve assinar e volta a incorporar o CMS expandido posteriormente; a comunicação HTTP e a alteração do CMS cabem ao chamador. Existe uma razão técnica forte para esta separação. O controlo CryptoAPI do Windows que teoricamente adiciona atributos não assinados, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, falha com o erro CRYPT_E_INVALID_INDEX na estrutura SignedData destacada utilizada pelo PAdES. Por conseguinte, o CMS melhorado tem de provir de um codificador CMS sob o seu próprio controlo. Nenhuma biblioteca consegue integrar o token de forma transparente com uma única chamada de sistema, e any que a afirme estará a realizar a manipulação em locais invisíveis.
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// both calls below are application code: an HTTP POST to your TSA,
// and a CMS re-encode that attaches the token as an unsigned attribute
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
Preste atenção aos códigos de resultado nesta fase: 12 indica que o campo de assinatura designado não existe, 11 que o CMS existente não pôde ser analisado e 13 que o CMS aumentado já não cabe no espaço reservado /Contents. O código 13 é o mais problemático, dado que a única solução passa pela nova assinatura do documento: um token de carimbo de data/hora típico com a respetiva cadeia de certificados ocupa entre 4 e 6 KB, e a reserva de 8192 bytes efetuada na etapa B-B serve justamente para que esta fase tenha espaço suficiente.
A validação começa no ByteRange e não na cadeia de certificados
Uma marca de confirmação verde num visualizador representa uma decisão de confiança baseada no repositório de certificados da própria máquina e não um veredicto estrutural do ficheiro. A validação programática deve começar num nível inferior, com uma questão que as atualizações incrementais tornam complexa: que bytes são efetivamente cobertos por cada assinatura? Cada melhoria discutida neste artigo, quer se trate de uma segunda assinatura, de um dicionário DSS ou de um carimbo de data/hora do documento, é introduzida através de uma atualização incremental, e cada atualização acrescenta bytes fora do /ByteRange da assinatura anterior. Esses bytes adicionados são legítimos. Contudo, um validador deve classificá-los segundo a política de modificação do documento, e o nível DocMDP por campo onde essa política reside pode ser lido com GetSignatureDocMDPLevelByName.
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // before Open: SignDoc holds a share lock
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 means actually signed
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
Existem duas armadilhas neste caminho de auditoria. O método TPDFlibSignDoc.Open bloqueia o ficheiro com um bloqueio de partilha exclusivo, pelo que um validador que também pretenda calcular o hash dos bytes brutos do ficheiro para a verificação do CMS tem de ler o ficheiro para a memória antes de o abrir para auditoria. Caso inverta esta ordem, a leitura falhará devido a um bloqueio estabelecido pelo próprio código. A segunda armadilha é silenciosa: a função equivalente da API plana, GetSignProcessByteRange, devolve o tipo Integer enquanto os desvios subjacentes são Int64, de modo que além dos 2 GB a chamada plana trunca os valores sem aviso, motivo pelo qual este exemplo obtém os desvios através da classe de auditoria. Importa também assinalar uma ausência: a camada plana não possui um invólucro para VerifySignature. Os veredictos criptográficos provêm da classe TPDFlibSignatureVerifier, que devolve vsValid, vsInvalid ou vsUnknown, ou de um validador externo em que a sua política de conformidade já confie.
Validação de longo prazo: DSS, VRI e o carimbo de data/hora do documento
O PAdES-B-LT existe porque a infraestrutura de revogação é finita. A norma ETSI EN 319 142-1 §5.4.2.2 especifica o Document Security Store (DSS): um dicionário ao nível do documento que contém certificados, CRLs e respostas OCSP, opcionalmente indexados por assinatura através de entradas VRI identificadas pelo hash do campo /Contents de cada assinatura. O fluxo do PDFlibPas reflete o design do carimbo de data/hora. O método NewPAdESDSSProcessFromFile inicia o processo; AddPAdESDSSCertificate, AddPAdESDSSCRL e AddPAdESDSSOCSP aceitam os dados DER em bruto; AddPAdESDSSVRI associa o material selecionado a uma assinatura específica; EndPAdESDSSProcessToFile grava todos os elementos como uma atualização incremental. A parte complexa permanece do lado do programador: a obtenção dos dados de revogação e a decisão sobre se estes são suficientemente recentes para justificar a sua incorporação é responsabilidade do chamador. A biblioteca garante a conformidade estrutural dos dicionários, mas não pode atestar a veracidade das respostas do servidor OCSP.
O nível de arquivo, B-LTA, adiciona um carimbo de data/hora do documento: um campo de assinatura independente cujo tipo é DocTimeStamp em vez de Sig, gerado através de SetSignProcessDocTimeStamp com um comprimento de assinatura reservado. Este carimbo de data/hora não substitui o carimbo da assinatura obtido no passo B-T. O carimbo da assinatura comprova o momento em que uma assinatura específica foi efetuada, enquanto o carimbo do documento protege a totalidade do ficheiro, incluindo as provas de DSS, e constitui o elemento que um arquivo de longo prazo renova periodicamente à medida que os algoritmos se tornam vulneráveis. Um perfil de arquivo maduro inclui ambos os elementos. Para leitores de PDF que precedem estas estruturas, o método TPDFlibSignDoc.EnsurePAdESExtensions regista a extensão de programador ESIC no catálogo do documento, assinalando que o ficheiro recorre a funcionalidades definidas pela ETSI.
Convém desmistificar uma reação comum, pois assemelha-se a uma falha mas não o é. Um visualizador reporta frequentemente "validade desconhecida" num ficheiro cuja estrutura PAdES está perfeitamente correta. A confiança e a conformidade estrutural são eixos independentes. O visualizador simplesmente não consegue associar o signatário a uma raiz fidedigna configurada na máquina em questão, o que é comum com CAs privadas e certificados de teste, embora a auditoria do ByteRange e a verificação do CMS tenham sucesso. A solução consiste em distribuir o certificado de raiz adequadamente ou realizar a validação face às listas de confiança da União Europeia caso o objetivo seja obter o estatuto qualificado sob o regulamento eIDAS, em vez de alterar o código de assinatura.
Para a perspetiva do lado da auditoria, nomeadamente a enumeração de campos de assinatura num conjunto documental, exportação de esquemas ByteRange e leitura de níveis DocMDP em lote, consulte o artigo complementar sobre o painel de conformidade e assinatura. Os documentos assinados que necessitem de satisfazer regras de arquivo são integrados no fluxo de trabalho descrito no guia de verificação prévia PDF/A e PDF/UA em Delphi. A documentação completa da API e as transferências de avaliação encontram-se na página do produto losLab PDF Library para Delphi.