Artículo técnico

Listas de confianza eIDAS y firmas PDF cualificadas

Decidir que una firma PDF es cualificada bajo eIDAS significa responder a una pregunta que no tiene nada que ver con la criptografía: si el certificado lo emitió un servicio de confianza que un estado miembro listó como cualificado en el momento en que se hizo la firma. La respuesta vive en una lista de confianza, un documento XML publicado por territorio, y todo el valor de ese documento depende de su autenticidad. Así que el componente PDFium se niega a mirar dentro de una hasta que alguien la avale. TPdfEuropeanTrustedList.ParseAuthenticated entrega los bytes crudos completos a un IPdfTrustedListAuthenticator aportado por quien llama antes de analizar un solo servicio, y solo crea una instantánea si ese autenticador aprueba explícitamente

Flujo de autenticar-antes-de-analizar para una lista de confianza europea en Delphi: el XML crudo de una obtención fresca o de una instantánea en caché pasa por IPdfTrustedListAuthenticator antes de que TPdfEuropeanTrustedList analice nada
Las listas frescas y en caché se enfrentan al mismo autenticador, y una instantánea solo existe después de que apruebe

Ese orden es el diseño. Todo lo demás de esta función se deriva de él, incluidas las partes que resultan incómodas

Analizar no es confiar

Una lista de confianza que analiza limpiamente os dice que el XML está bien formado. No os dice nada sobre quién la escribió. Como la lista es lo que sostiene toda vuestra decisión de estado cualificado, aceptar una porque analizó bien volvería la decisión sin sentido: un atacante que pueda sustituir la lista puede declarar cualificada a su propia autoridad certificadora

El mismo razonamiento aplica a la caché, y esta es la trampa que merece nombrarse. La caché de instantáneas guarda el XML original junto con un digest SHA-256, y sería fácil tratar un digest coincidente al cargar como prueba de que la lista es genuina. No lo es. Un digest calculado por el mismo proceso que guardó el fichero, sin clave de por medio, solo verifica que los bytes no han cambiado desde que los escribisteis; si la lista era fraudulenta cuando se guardó en caché, el digest confirma que es la misma lista fraudulenta. Así que cargar una instantánea en caché pasa por el mismo autenticador que analizar una fresca. Integridad y autenticidad son propiedades distintas y solo una de ellas necesita clave

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
  // Vuestra política vive aquí: verificar la firma enveloped XMLDSIG
  // contra el certificado de firma de listas que fijasteis fuera de banda,
  // y describir qué comprobasteis para la pista de auditoría
  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);
  // La instantánea existe solo porque el autenticador dijo que sí
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

El validador no es dueño de la política de red

Un validador PAdES no tiene por qué decidir cómo alcanzar la lista de listas de confianza, si pasar por un proxy, con qué frecuencia reintentar o qué hacer cuando un territorio es inalcanzable. Esas son decisiones de aplicación y de despliegue, y en entornos regulados se auditan con frecuencia. Así que las actualizaciones llegan a través de IPdfTrustedListSource, al que se le entrega una URI y un tope de bytes y devuelve bytes

Lo que el componente sí aplica son los invariantes que hacen de una actualización una actualización y no una sustitución. Update exige que el territorio no cambie, que el número de secuencia aumente estrictamente y que la hora de emisión no retroceda. Esas tres comprobaciones derrotan los ataques de downgrade más obvios: reproducir una lista más antigua que aún liste un servicio retirado desde entonces, o colar la lista de otro territorio cuyos servicios nunca tuvisteis intención de confiar

Invariantes de actualización aplicados por el componente de listas de confianza de PDFium: territorio sin cambios, número de secuencia estrictamente creciente y una hora de emisión que nunca retrocede, que en conjunto bloquean los ataques de downgrade
Tres comprobaciones monótonas separan una actualización genuina de una lista reproducida o sustituida

Límites del parser, y nada de DTD

TPdfTrustedListOptions acota el tamaño del XML, el recuento de tokens, la profundidad de anidamiento, el número de servicios, el número de certificados y el tamaño de un certificado individual, con una función de clase Default que aporta valores utilizables. Las listas de confianza son documentos publicados de tamaño previsible, así que los límites salen baratos y no hay lista legítima que necesite superarlos

Aparte e incondicionalmente, el parser rechaza las declaraciones de DTD y de entidades. Eso cierra tanto la denegación de servicio por expansión de entidades como la ruta de divulgación por entidades externas de un solo golpe, y no cuesta nada porque las listas de confianza no usan entidades. Cualquier parser XML alcanzable desde entrada no confiable debería configurarse así; la diferencia aquí es que el rechazo no es configurable, así que no puede apagarse con un cambio de opción bienintencionado

El estado cualificado se registra junto a la confianza de cadena, no fusionado con ella

El lado de la evaluación es deliberadamente separado. TPadesTrustValidationOptions.QualifiedTrustEvaluator toma un IPdfQualifiedTrustEvaluator, que la instantánea de la lista de confianza implementa. Durante la validación el evaluador recibe el certificado hoja, la cadena y una hora de validación, empareja certificados de servicio por comparación DER exacta contra el firmante y la cadena, combina el estado del servicio, el identificador de tipo de servicio y las URIs de calificador en ese punto del tiempo, y devuelve un record de evaluación

El resultado aterriza en dos sitios de cada firma: QualifiedTrustStatus como estado grueso, y QualifiedTrust como la evaluación completa con territorio, nombre del proveedor, nombre del servicio, identificador de tipo, estado y hora de inicio del estado. Lo que no hace es cambiar CertificateTrustStatus. La confianza de cadena del sistema y el estado cualificado responden a preguntas distintas, y un informe que las colapse no puede distinguir «de confianza pero no cualificada» de «cualificada pero la cadena no valida», ambas reales y ambas necesitantes de manejo distinto

Evaluación de confianza cualificada PAdES en Delphi: el IPdfQualifiedTrustEvaluator empareja certificados de servicio por comparación DER exacta y rellena QualifiedTrustStatus y QualifiedTrust mientras CertificateTrustStatus queda intacto
La confianza de cadena y el estado cualificado eIDAS se registran lado a lado para que ambos hallazgos sigan visibles
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // la instantánea autenticada
  Options.QualifiedValidationTime := SigningTime; // no 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
      // Sin servicio coincidente, o la instantánea no puede responder por esta hora
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

Por qué la hora de validación no es ahora

Porque la cualificación es una propiedad de un momento. A un servicio de confianza se le puede conceder el estado cualificado, retirárselo más tarde y rehabilitarlo más tarde todavía, y cada una de esas transiciones lleva una hora de inicio en la lista. Una firma hecha mientras el servicio estaba cualificado sigue siendo cualificada después; una firma hecha antes de la concesión no se vuelve cualificada retroactivamente. Evaluar contra la hora actual da, por tanto, la respuesta equivocada en ambas direcciones

La lista lleva lo necesario para esto: cada record de servicio tiene una hora de inicio de estado y una bandera que distingue las entradas históricas de las actuales, y el evaluador las combina contra la hora que aportéis. En la práctica esa hora viene de un sello de tiempo de confianza sobre la firma y no de la hora de firma que afirma el CMS, que es por lo que el material de validación a largo plazo importa incluso para una pregunta que parece una consulta de política; el lado de sello de tiempo y DSS se cubre en el artículo de firmas a largo plazo

Lo que aún tenéis que construir

Tres cosas, y ninguna pertenece a una biblioteca PDF. El autenticador, o sea, verificación XMLDSIG real contra un certificado de firma de listas que obtuvisteis por un canal de confianza. La política de obtención, o sea, cómo y con qué frecuencia refrescáis, y qué hace vuestra aplicación cuando un refresco falla. Y el alcance territorial, o sea, qué listas lleváis siquiera, que es una decisión de negocio sobre en qué estados miembros firman vuestras contrapartes

Lo que obtenéis del componente es la parte que es fácil hacer mal de forma sutil: el orden autenticar-antes-de-analizar, el parseo XML acotado y sin entidades, los invariantes de actualización monótonos, el emparejamiento de servicios por DER exacto, la evaluación de estado histórico y un resultado que se mantiene separado de la confianza de cadena ordinaria. Si vuestro problema inmediato es más básico, que un validador rechaza una firma que creéis correcta, las causas habituales están catalogadas en por qué los validadores rechazan firmas PAdES, y la superficie de inspección de firmas se describe en inspección de firmas y niveles PAdES. Las capacidades del componente están en la página de producto de PDFium Delphi component