Artículo técnico

Firma PAdES remota en PDFium VCL: claves HSM y en la nube

PDFiumPas divide la firma PAdES en dos llamadas para que la clave privada nunca tenga que estar en su proceso. PreparePadesRemoteSignature escribe una actualización incremental con un marcador de posición vacío de ancho fijo en /Contents y devuelve un registro de solicitud que lleva el digest SHA-256 del documento, el ByteRange exacto y una huella del archivo preparado. CompletePadesRemoteSignature toma el CMS separado (detached) que su servicio de firma devuelve y lo coloca en ese espacio reservado

Entre esas dos llamadas pueden pasar minutos u horas, el proceso puede reiniciarse, y el trabajo puede moverse a otra máquina. Esa brecha es toda la razón por la que la API está diseñada así

¿Por qué una clave remota no puede usar la llamada de firma ordinaria?

Porque SignPadesBytes asume que la operación de firma ocurre dentro de la llamada. Construye la actualización incremental, calcula el digest sobre el ByteRange, lo firma, y escribe el resultado, todo antes de retornar. Eso es exactamente correcto cuando la clave vive en el almacén de certificados de Windows o en un archivo PKCS#12 que usted cargó

Es imposible cuando la clave vive en un HSM de red, un dispositivo calificado de creación de firma operado por un proveedor de servicios de confianza, o una API de firma en la nube que requiere que el usuario confirme desde un teléfono. En esos casos la secuencia no es una llamada de función, es una conversación: usted envía un digest, algo más autentica a un humano, y un CMS regresa después. Una API síncrona no puede expresar "después" sin bloquear un hilo en una operación que puede necesitar un segundo factor

El protocolo de dos fases

La fase uno prepara el documento. PDFiumPas anexa el campo de firma y el diccionario de valor, reserva ContentsSize bytes de espacio hex-codificado en /Contents, calcula el ByteRange alrededor de esa reserva, y produce un TPadesRemoteSigningRequest que contiene FormatVersion, PreparedFingerprint, DocumentDigest, el ByteRange de cuatro elementos, ContentsHexOffset y ContentsSize

El único valor que su servicio de firma necesita es DocumentDigest: el SHA-256 que el SignedData CAdES devuelto debe llevar como su message digest. Todo lo demás en el registro existe para que la fase dos pueda demostrar que el archivo que está completando es el archivo a partir del cual se calculó ese digest

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // bytes hex reservados para el CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // Persista la sesión para que una ejecución posterior - o otra máquina - pueda terminarla
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

¿Qué rechaza Complete, y por qué existe cada verificación?

La finalización es donde un diseño de firma remota suele fallar, así que la validación es deliberadamente inflexible. CompletePadesRemoteSignature rechaza un PDF preparado cuya huella ya no coincide con la solicitud, un ByteRange que no coincide con las coordenadas de marcador de posición registradas, delimitadores de /Contents modificados, un marcador de posición que ya no está vacío, un CMS más grande que la reserva, un CMS que no es exactamente un valor DER, una forma de SignedData no soportada, un atributo signing-certificate-v2 faltante, y un CMS cuyo message digest no es igual al digest del documento preparado

Cada una de esas corresponde a una falla real. Las verificaciones de huella y ByteRange detectan el caso en que alguien regeneró el archivo preparado entre las dos fases, lo cual produciría una firma que valida contra bytes que nadie tiene. La verificación de marcador de posición vacío detecta la doble finalización, donde un segundo CMS se escribe sobre una firma que ya existe. La verificación de message digest detecta el caso más peligroso de todos: un CMS correctamente formado pero firmado sobre un documento distinto, que es lo que se obtiene cuando una cola mezcla dos sesiones de firma concurrentes. Sin ella usted produciría un archivo que parece firmado y falla la validación en todas partes, o peor, que lleva la aprobación de otra persona

El requisito de signing-certificate-v2 es un asunto de conformidad PAdES, no de integridad. ETSI EN 319 142 exige que el certificado de firma esté vinculado dentro de los atributos firmados, y un CMS que carezca de ese atributo no es una firma PAdES aunque verifique criptográficamente. Rechazarlo en la finalización significa que usted se entera aquí, no en un reporte de un validador de un cliente, un tema explorado más a fondo en por qué los validadores rechazan firmas PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // devuelto por el HSM o el TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // Cada rechazo lleva un motivo específico; regístrelo textualmente
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Cruzar fronteras de proceso y de máquina

SavePadesRemoteSigningRequest y LoadPadesRemoteSigningRequest serializan la sesión mediante un formato binario versionado y estable, lo cual es lo que hace que el diseño sea práctico y no solo correcto. Una aplicación web puede preparar un documento en una solicitud, almacenar el PDF preparado y el blob de sesión, devolver un digest al navegador para una firma con tarjeta inteligente, y completar el archivo en un manejador de solicitud completamente distinto

El campo FormatVersion es lo que mantiene eso seguro a través de las actualizaciones. Una sesión escrita por una compilación anterior y cargada por una más nueva se reconoce o se rechaza explícitamente, en lugar de leerse mal como un registro con una forma distinta. Si su cola puede conservar sesiones durante días, trate la versión de formato como un hecho operativo digno de registrarse, no como un detalle de implementación

Dimensionar el marcador de posición

ContentsSize es el único parámetro que usted debe pensar con cuidado, porque se fija antes de que el CMS exista. Cuenta la reserva hex-codificada, así que un CMS DER de 6 KB necesita al menos 12 KB de espacio, y la implementación limita la reserva a 64 MiB

Reservar muy poco hace que la finalización falle con un error de CMS sobredimensionado después de que su servicio de firma ya hizo su trabajo, lo cual en un servicio de firma calificada medido significa una operación desperdiciada. Reservar demasiado hace que cada documento firmado cargue el relleno para siempre. El enfoque sensato es medir: firme un documento con su cadena de certificados real, observe la longitud DER, duplíquela para hex, y agregue un margen generoso para el token de sello de tiempo si piensa actualizar a una firma de nivel T. Las cadenas con varios intermedios y una respuesta OCSP larga crecen más rápido de lo que la gente espera

Qué viene después de la firma

Una firma remota completada es PAdES B-B. La validación a largo plazo necesita un sello de tiempo y el material de validación, que es una actualización incremental separada que agrega un DSS y sus diccionarios VRI por firma, descrita en firmas de largo plazo con sellos de tiempo RFC 3161 y DSS. Ese paso es local: agrega certificados, respuestas OCSP y CRL, ninguno de los cuales necesita la clave privada

Antes de publicar, verifique lo que produjo con la misma ruta de código que usaría una parte confiante, cubierta en inspección de firmas digitales y niveles PAdES. Firmar y verificar son código distinto, y un pipeline de firma remota es exactamente el lugar donde ambos pueden divergir sin que nadie lo note hasta que un validador externo lo señale

PDFiumPas es un componente para Delphi y Lazarus construido alrededor del motor PDFium, con una pila PAdES nativa en Pascal, así que la firma, el sellado de tiempo y la validación funcionan sin herramientas externas de línea de comandos. La documentación completa de la API y una compilación de prueba están en la página del componente PDFium Delphi