Artículo técnico

Firma PAdES remota en PDFium VCL: HSM y claves 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 /Contents vacío y de ancho fijo, y devuelve un registro de petición con el resumen SHA-256 del documento, el ByteRange exacto y una huella del fichero preparado. CompletePadesRemoteSignature toma el CMS desasociado que devuelve su servicio de firma y lo coloca en ese hueco reservado

Entre esas dos llamadas pueden pasar minutos u horas, el proceso puede reiniciarse, y el trabajo puede pasar a otra máquina. Ese hueco 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 resumen sobre el ByteRange, firma y escribe el resultado, todo antes de devolver el control. Eso es exactamente correcto cuando la clave vive en el almacén de certificados de Windows o en un fichero PKCS#12 que usted cargó

Es imposible cuando la clave vive en un HSM de red, un dispositivo cualificado 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 resumen, algo más autentica a un humano, y un CMS llega más tarde. Una API síncrona no puede expresar "más tarde" 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 añade el campo de firma y el diccionario de valor, reserva ContentsSize bytes de espacio codificado en hexadecimal 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 necesita su servicio de firma es DocumentDigest: el SHA-256 que el SignedData CAdES devuelto debe llevar como su resumen de mensaje. Todo lo demás en el registro existe para que la fase dos pueda demostrar que el fichero que está completando es el fichero a partir del cual se calculó ese resumen

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

  // Guarde la sesión para que una ejecución posterior, u otra máquina, pueda completarla
  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 comprobación?

La finalización es donde un diseño de firma remota suele salir mal, así que la validación es deliberadamente estricta. CompletePadesRemoteSignature rechaza un PDF preparado cuya huella ya no coincide con la petición, 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 ausente y un CMS cuyo resumen de mensaje no coincide con el resumen del documento preparado

Cada uno de esos casos corresponde a un fallo real. Las comprobaciones de huella y ByteRange detectan el caso en que alguien regeneró el fichero preparado entre las dos fases, lo que produciría una firma que se verifica contra bytes que nadie tiene. La comprobación de marcador de posición vacío detecta la doble finalización, cuando se escribe un segundo CMS sobre una firma que ya existe. La comprobación del resumen de mensaje 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 produciría un fichero 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 una cuestión de conformidad PAdES más que de integridad. ETSI EN 319 142 exige que el certificado de firma quede vinculado dentro de los atributos firmados, y un CMS sin ese atributo no es una firma PAdES aunque se verifique criptográficamente. Rechazarlo en la finalización significa descubrirlo aquí, no en un informe de validador de un cliente, un tema que se explora más 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 literalmente
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

Cruzando límites de proceso y de máquina

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

El campo FormatVersion es lo que mantiene eso seguro entre actualizaciones. Una sesión escrita por una compilación anterior y cargada por una más reciente se reconoce o se rechaza de forma explícita, en lugar de leerse mal como un registro con otra forma. Si su cola puede mantener sesiones durante días, trate la versión de formato como un dato operativo que merece registrarse, no como un detalle de implementación

Dimensionar el marcador de posición

ContentsSize es el único parámetro que debe pensar bien, porque se fija antes de que exista el CMS. Cuenta la reserva codificada en hexadecimal, 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

Reserve poco y la finalización falla con un error de CMS sobredimensionado después de que su servicio de firma ya haya hecho su trabajo, lo que en un servicio de firma cualificada tarifado por operación es una operación desperdiciada. Reserve de más y cada documento firmado carga con ese relleno para siempre. El enfoque sensato es medir: firme un documento con su cadena de certificados real, observe la longitud DER, duplíquela para el hexadecimal, y añada un margen generoso para el token de sellado de tiempo si tiene previsto pasar a una firma de nivel T. Las cadenas con varios intermedios y una respuesta OCSP larga crecen más deprisa 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 aparte que añade un DSS y sus diccionarios VRI por firma, descrita en las firmas a largo plazo con sellos de tiempo RFC 3161 y DSS. Ese paso es local: añade certificados, respuestas OCSP y CRL, ninguno de los cuales necesita la clave privada

Antes de distribuir, verifique lo que produjo con la misma ruta de código que usaría un tercero que confía en la firma, cubierta en la inspección de firmas digitales y niveles PAdES. Firmar y verificar son código distinto, y una canalización de firma remota es exactamente el sitio donde ambos pueden desalinearse sin que nadie lo note hasta que lo diga un validador externo

PDFiumPas es un componente para Delphi y Lazarus alrededor del motor PDFium con una pila PAdES nativa en Pascal, así que firmar, sellar el tiempo y validar 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 de PDFium para Delphi