Artigo Técnico

Assinaturas Digitais PAdES em Delphi: Assinatura e Validação com PDF Library for Delphi

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 (PDF Library for Delphi) 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 PDF Library for Delphi

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 PDF Library for Delphi 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

Escada dos níveis de base PAdES B-B, B-T, B-LT e B-LTA construída com a PDF Library for Delphi, mostrando como cada nível acrescenta carimbos de data/hora, evidência DSS ou um carimbo de data/hora do documento renovável sobre o núcleo ETSI.CAdES.detached
Cada nível baseline ETSI acrescenta mais uma garantia sobre o mesmo núcleo CAdES, desde atributos assinados até um selo de tempo de documento renovável

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 PDF Library for Delphi 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

Pipeline para acrescentar um carimbo de data/hora RFC 3161 a uma assinatura PAdES em Delphi, separando o hashing e a incorporação da PDF Library for Delphi do pedido TSA de quem chama e da recodificação CMS dentro do espaço /Contents reservado
A biblioteca faz o hash e reincorpora, enquanto o seu código busca o token e realiza a cirurgia CMS, e o resultado tem de caber dentro da reserva de 8192 bytes de /Contents
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);
    // as duas chamadas abaixo são código da aplicação: um HTTP POST à tua TSA,
    // e uma recodificação CMS que anexa o token como 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

Auditoria do layout de bytes de um PDF assinado em Delphi mostrando os intervalos cobertos pelo ByteRange, os bytes /Contents excluídos, as atualizações incrementais anexadas fora do intervalo e o veredicto de cobertura face ao tamanho do ficheiro
Dois segmentos cobertos com os bytes da própria assinatura excluídos contam a verdadeira história da cobertura, e as atualizações acrescentadas são classificadas segundo a política DocMDP em vez de serem temidas
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 significa efetivamente assinado
        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 PDF Library for Delphi 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