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