Artigo Técnico

Listas confiáveis eIDAS e assinaturas PDF qualificadas

Decidir que uma assinatura PDF é qualificada sob eIDAS significa responder a uma pergunta que não tem nada a ver com criptografia: o certificado foi emitido por um serviço de confiança que um estado-membro listou como qualificado, no momento em que a assinatura foi feita. A resposta vive em uma lista confiável, um documento XML publicado por território, e todo o valor desse documento depende de sua autenticidade. Então o componente PDFium se recusa a olhar dentro de uma até que alguém a ateste. O TPdfEuropeanTrustedList.ParseAuthenticated entrega os bytes brutos completos a um IPdfTrustedListAuthenticator fornecido pelo chamador antes de interpretar um único serviço, e cria um snapshot só se esse autenticador passar explicitamente

Fluxo de autenticar antes de interpretar para uma lista confiável europeia no Delphi: XML bruto de uma busca fresca ou de um snapshot em cache passa pelo IPdfTrustedListAuthenticator antes de o TPdfEuropeanTrustedList interpretar qualquer coisa
Listas frescas e em cache passam pelo mesmo autenticador, e um snapshot só existe depois de ele passar

Essa ordenação é o design. Todo o resto deste recurso decorre dela, incluindo as partes que parecem inconvenientes

Interpretado não é confiável

Uma lista confiável que interpreta limpo diz que o XML está bem formado. Não diz nada sobre quem a escreveu. Como a lista é o que sustenta toda a sua decisão de status qualificado, aceitar uma porque interpretou tornaria a decisão sem sentido: um atacante que pode substituir a lista pode declarar sua própria autoridade certificadora qualificada

O mesmo raciocínio se aplica ao cache, e esta é a armadilha que vale nomear. O cache de snapshots guarda o XML original junto com um digest SHA-256, e seria fácil tratar um digest que bate no carregamento como prova de que a lista é genuína. Não é. Um digest calculado pelo mesmo processo que guardou o arquivo, sem chave envolvida, verifica apenas que os bytes não mudaram desde que você os escreveu; se a lista era fraudulenta quando foi colocada em cache, o digest confirma que é a mesma lista fraudulenta. Então carregar um snapshot em cache passa pelo mesmo autenticador que interpretar uma fresca. Integridade e autenticidade são propriedades diferentes e só uma delas precisa de uma chave

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // Sua política mora aqui: verifique a assinatura enveloped XMLDSIG
  // contra o certificado de assinatura de lista que você fixou fora de
  // banda, e descreva o que conferiu para o rastro de auditoria
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // O snapshot existe só porque o autenticador disse sim
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

O validador não é dono da política de rede

Um validador PAdES não tem nada a ver com decidir como alcançar a lista de listas de confiança, se passar por um proxy, com que frequência repetir, ou o que fazer quando um território está inalcançável. Essas são decisões de aplicativo e implantação, e em ambientes regulados são frequentemente auditadas. Então atualizações chegam por IPdfTrustedListSource, que recebe uma URI e um teto de bytes e retorna bytes

O que o componente aplica são os invariantes que fazem de uma atualização uma atualização em vez de uma substituição. O Update exige que o território não mude, que o número de sequência estritamente aumente e que o tempo de emissão não recue. Essas três checagens derrotam os ataques de downgrade mais óbvios: reproduzir uma lista mais antiga que ainda lista um serviço desde então retirado, ou trocar pela lista de outro território cujos serviços você nunca teve a intenção de confiar

Invariantes de atualização aplicados pelo componente de lista confiável do PDFium: território inalterado, número de sequência estritamente crescente e um tempo de emissão que nunca recua, que juntos bloqueiam ataques de downgrade
Três checagens monótonas separam uma atualização genuína de uma lista reproduzida ou substituída

Limites do parser, e nenhum DTD de forma alguma

O TPdfTrustedListOptions limita o tamanho do XML, a contagem de tokens, a profundidade de aninhamento, o número de serviços, o número de certificados e o tamanho de um certificado individual, com uma função de classe Default fornecendo valores utilizáveis. Listas confiáveis são documentos publicados de tamanho previsível, então limites são baratos de definir e não há lista legítima que precise excedê-los

Separamente e incondicionalmente, o parser rejeita declarações DTD e entity. Isso fecha tanto a negação de serviço por expansão de entity quanto a rota de divulgação por entity externa em uma única recusa, e não custa nada porque listas confiáveis não usam entities. Qualquer parser XML alcançável de entrada não confiável deveria ser configurado assim; a diferença aqui é que a recusa não é configurável, então não pode ser desligada por uma mudança de opção bem-intencionada

O status qualificado é registrado ao lado da confiança de cadeia, não mesclado nela

O lado da avaliação é deliberadamente separado. O TPadesTrustValidationOptions.QualifiedTrustEvaluator recebe um IPdfQualifiedTrustEvaluator, que o snapshot de lista confiável implementa. Durante a validação o avaliador recebe o certificado folha, a cadeia e um tempo de validação, casa certificados de serviço por comparação DER exata contra o signatário e a cadeia, combina o status do serviço, o identificador de tipo de serviço e as URIs de qualificador naquele momento, e retorna um record de avaliação

O resultado pousa em dois lugares em cada assinatura: QualifiedTrustStatus como um status grosso, e QualifiedTrust como a avaliação completa com território, nome do provedor, nome do serviço, identificador de tipo, status e tempo inicial do status. O que não faz é mudar o CertificateTrustStatus. Confiança de cadeia do sistema e status qualificado respondem a perguntas diferentes, e um relatório que os colapsa não distingue "confiável mas não qualificado" de "qualificado mas a cadeia não valida", ambos reais e ambos precisando de tratamento diferente

Avaliação de confiança qualificada PAdES no Delphi: o IPdfQualifiedTrustEvaluator casa certificados de serviço por comparação DER exata e preenche QualifiedTrustStatus e QualifiedTrust enquanto CertificateTrustStatus permanece intocado
Confiança de cadeia e status qualificado eIDAS são registrados lado a lado para que ambos os findings fiquem visíveis
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // o snapshot autenticado
  Options.QualifiedValidationTime := SigningTime; // não Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // Nenhum serviço correspondente, ou o snapshot não pode responder por este tempo
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

Por que o tempo de validação não é agora

Porque a qualificação é uma propriedade de um momento. Um serviço de confiança pode receber o status qualificado, depois tê-lo retirado e depois ainda ser reintegrado, e cada uma dessas transições carrega um tempo inicial na lista. Uma assinatura feita enquanto o serviço estava qualificado permanece qualificada depois; uma assinatura feita antes da concessão não se torna qualificada retroativamente. Avaliar contra o tempo atual portanto dá a resposta errada nas duas direções

A lista carrega o necessário para isso: cada record de serviço tem um tempo inicial de status e um flag distinguindo entradas históricas das atuais, e o avaliador os combina contra o tempo que você fornece. Na prática esse tempo vem de um timestamp confiável na assinatura em vez do tempo de assinatura alegado no CMS, que é por que o material de validação de longo prazo importa mesmo para uma pergunta que parece uma consulta de política; o lado de timestamp e DSS é coberto em o artigo de assinaturas de longo prazo

O que você ainda precisa construir

Três coisas, e nenhuma delas pertence a uma biblioteca PDF. O autenticador, significando verificação XMLDSIG real contra um certificado de assinatura de lista que você obteve por um canal em que confia. A política de busca, significando como e com que frequência você atualiza, e o que seu aplicativo faz quando uma atualização falha. E o escopo territorial, significando quais listas você carrega de fato, o que é uma decisão de negócio sobre em quais estados-membros suas contrapartes assinam

O que você obtém do componente é a parte que é fácil de errar sutilmente: a ordenação autenticar-antes-de-interpretar, parse de XML limitado e sem entities, invariantes de atualização monótonos, casamento de serviço por DER exato, avaliação de status histórico e um resultado que permanece separado da confiança de cadeia comum. Se seu problema imediato é mais básico, que um validador rejeita uma assinatura que você acredita estar boa, as causas usuais estão catalogadas em por que validadores rejeitam assinaturas PAdES, e a superfície de inspeção de assinaturas é descrita em inspecionar assinaturas e níveis PAdES. As capacidades do componente estão listadas na página de produto do PDFium Delphi component