Artículo técnico

Evidencia LTV PAdES y seed values en HotPDF

Un PDF que acabáis de firmar es una firma B-B y nada más. Prueba quién firmó y que los bytes no se han movido, pero no lleva prueba de que el certificado del firmante era válido en el momento de la firma, así que un validador dentro de años tendría que ir a buscar datos de revocación que quizá ya no existan. Cerrar ese hueco significa escribir respuestas OCSP y CRL en el Document Security Store a nivel de documento, y en HotPDF eso es una llamada: PopulatePAdESLTVEvidence recorre cada firma cargada, deriva las peticiones de revocación del conjunto de certificados, las ejecuta a través de un transporte que vosotros aportáis y escribe el material obtenido más la cadena CMS en el DSS. Devuelve el número de firmas cuya evidencia aterrizó, o menos uno cuando el documento no tiene campo de firma alguno

La decisión de diseño que conviene entender antes de usarla es que la biblioteca nunca abre un socket. Cada byte que llega de la red llega a través de un callback que vosotros escribisteis. Eso no es cautela por la cautela; es la única forma de que esta función pueda trabajar dentro de los entornos que de verdad exigen validación a largo plazo

Por qué la biblioteca se niega a hacer su propio HTTP?

Porque los sitios que exigen firmas B-LT son los sitios donde a una biblioteca no se le puede confiar la red. Los servicios de firma corren tras proxies con autenticación y raíces corporativas. Los niveles de firma aislados de la red no tienen ruta hacia un responder y deben alimentarse con evidencia en caché. Los regímenes de auditoría exigen que cada petición saliente quede registrada por la aplicación, no enterrada en una dependencia. Y las suites de prueba necesitan respuestas deterministas, algo imposible si la biblioteca marca por su cuenta

El transporte es una referencia a función simple con una forma fija, así que la política sigue siendo vuestra. HotPDF os entrega un record de petición que describe exactamente qué obtener, incluidos el tipo de contenido y un tope de tamaño de respuesta, y vosotros devolvéis los bytes más un estado

Flujo de PopulatePAdESLTVEvidence de HotPDF: el transporte FetchEvidence aportado por quien llama, los campos del record de petición y los estados por firma
Cada byte de red pasa por vuestro callback FetchEvidence, y cada firma recibe su propio estado, así que un timeout nunca aborta la pasada
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind dice si esto es un POST OCSP o un GET CRL;
    // Request.ContentType y Request.Body ya están preparados,
    // y Request.MaxResponseBytes es el tope que debéis respetar
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry deja que la política de reintentos espere; usad
      // setsPermanentFailure para un 404 o una URL mala
      Result := setsRetry;
    end;
  end;
end;

// Actualización B-B a B-LT en una llamada para cada firma del fichero cargado
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // Guardado de solo adición: los bytes que cubren las firmas
      // existentes se conservan verbatim
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

Los fallos son por firma, no por documento. Un responder que da timeout para un firmante se salta el material de ese firmante y deja intacto el resto de la pasada, que es el comportamiento que se quiere en un lote: la evidencia parcial supera a una ejecución abortada, y el valor de retorno os dice cuántas firmas mejoraron realmente

La cadena que el CMS olvidó incluir

La comprobación de revocación necesita el certificado del emisor, y un número sorprendente de stacks de firma omiten los intermediarios del contenedor CMS. La ruta de recuperación es la extensión Authority Information Access, método de acceso 1.3.6.1.5.5.7.48.2, que anuncia una URL donde puede descargarse el certificado del emisor. HPDFFetchAIAIntermediates recorre esas URLs a través del mismo transporte, extrae el DER de cada respuesta y devuelve solo los certificados que el CMS aún no llevaba, indexados por hash DER para que duplicados y bucles no puedan dar vueltas

Dos detalles deciden si esto funciona contra autoridades de certificación reales. El primero es la codificación: los endpoints de las CA sirven el certificado como DER desnudo aproximadamente tan a menudo como blindado en PEM, y no hay un tipo de contenido fiable para distinguirlos. La sonda robusta es textual y luego estructural. Buscad el marcador -----BEGIN CERTIFICATE-----, quitad el blindaje y decodificad base64 si está presente, y en ambas rutas confirmad que el primer byte del resultado es $30, la etiqueta DER de una SEQUENCE. El segundo es la profundidad: un intermediario obtenido puede anunciar a su vez una URL AIA para su propio emisor, así que el recorrido añade candidatos nuevos a la cola y completa cadenas a las que les faltan dos o tres saltos. Eso hay que acotarlo, y para eso está el parámetro MaxFetch

Diagrama de completado de cadena AIA para HotPDF: obtención de URL caIssuers, sonda PEM frente a DER, deduplicación por hash DER y tope de profundidad MaxFetch
HPDFFetchAIAIntermediates recorre URLs caIssuers por el mismo transporte, sondea el blindaje PEM y acota la cola con MaxFetch

Qué es un seed value de firma y por qué falla en silencio?

Un seed value es una restricción que el autor del documento adjunta a un campo de firma para decirle al firmante qué clase de firma es aceptable: qué SubFilter, qué algoritmo de resumen, qué motivos, qué versión mínima de PDF, si la información de revocación debe ir incrustada. Vive en un diccionario /SV del campo y se define en ISO 32000-1 §12.7.5.5. HotPDF lo escribe con AttachPAdESSeedValue y lo comprueba con CheckLoadedSignatureSeedValue, que devuelve True cuando el campo no tiene restricciones o todas las presentes pasan, y con False nombra la primera restricción que falla a través de un parámetro de salida que podéis poner directamente en un mensaje de error

El mecanismo que hace que los seed values sean fáciles de hacer mal es la entrada de flags /Ff descrita en §12.7.5.5.3. Un bit activo marca su restricción como obligatoria: una discrepancia es un error y el firmante debe negarse. Un bit apagado marca la misma restricción como preferencia: el valor filtra lo que la UI debería ofrecer y nada más. De ahí se siguen dos trampas. Primera: /Ff vive dentro del diccionario /SV, no en la anotación de widget, así que el código que lee el /Ff a nivel de campo recibe una respuesta vacía para siempre y concluye que nada es obligatorio. Segunda: la asignación de bits no es una simple racha de uno, dos, cuatro, ocho; en HotPDF el escritor emite 2 para SubFilter, 4 para MinVersion, 32 para AddRevInfo y 64 para DigestMethod. Un lector que asume bits secuenciales decodifica cada restricción como opcional y supera todas las pruebas excepto la que importa

Tabla de bits de flags de seed value para firma PAdES de HotPDF mostrando los bits Ff 2, 4, 32 y 64 y el manejo de restricciones obligatorias frente a preferidas
La entrada /Ff vive dentro de /SV, y cada posición de bit decide si una discrepancia es un rechazo duro o una preferencia de UI
var
  Violation: AnsiString;
begin
  // Preguntar al campo si el perfil con el que vamos a firmar está permitido
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // Restricción satisfecha: proceder con la pasada de firma
end;

La prueba que destapó el error de decodificación original no era una prueba positiva. Era la aserción de que una discrepancia obligatoria debe rechazarse, y es el único tipo de prueba capaz de cazar esta clase de defecto: un decodificador que lee el diccionario equivocado o las posiciones de bits equivocadas produce «ninguna restricción violada» para cada entrada, lo que se parece exactamente al comportamiento correcto hasta que violáis una deliberadamente

Dónde encaja esto en la escalera LTV

Cuatro peldaños, y cada uno necesita el de abajo. B-B es la firma desnuda. B-T añade un sello de tiempo de confianza, que fija la hora de firma para que un validador sepa contra qué momento evaluar la revocación. B-LT añade la evidencia de revocación al DSS, que es lo que automatiza PopulatePAdESLTVEvidence. B-LTA añade sellos de tiempo de documento que se renuevan antes de que el anterior se debilite, extendiendo la validez indefinidamente; HotPDF lo expone como RenewPAdESLTATimestamp, que añade un nuevo sello de tiempo como revisión incremental y conserva intactas todas las firmas, sellos de tiempo y entradas del DSS anteriores

El modelo de actualización incremental es la única forma correcta de añadir evidencia a un documento firmado, porque reescribir el fichero rompería los rangos de bytes que cubren las firmas existentes. Si necesitáis razonar qué cambió entre revisiones y si esos cambios son del tipo que una firma permite, ese análisis se cubre por separado en el análisis de revisiones DocMDP y FieldMDP. El propio pipeline de firma, incluidas las fuentes de certificados y las trampas de orden de bytes, está en el recorrido de firma PAdES, y el lado de validación está en verificar firmas en documentos cargados

Una advertencia práctica sobre el orden. Recolectad la evidencia cuanto antes después de firmar, idealmente en el mismo trabajo. Los responders capaces de responder por un certificado están en línea mientras el certificado está vigente y desaparecen años después, así que un documento que sale de vuestro pipeline como B-B puede que nunca vuelva a ser actualizable. HotPDF funciona como componente VCL nativo para Delphi y C++Builder, y toda la pasada de evidencia es en proceso salvo vuestro propio transporte; los perfiles soportados están en la página de producto de HotPDF Delphi PDF component