Artículo técnico

Firma PDF postcuántica y EdDSA con HotPDF en Delphi

HotPDF verifica firmas CMS ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519 y Ed448 en documentos PDF cargados, y firma a través de proveedores enchufables de modo que la clave privada nunca tenga que vivir dentro de tu proceso Delphi. Esa segunda mitad es la parte que la mayoría de equipos necesita primero. Un token hardware, un servicio de firma remoto y una tarjeta eID nacional se niegan a entregar la clave, y mientras el flujo de firma no esté separado del almacén de claves ninguno de ellos se puede usar en absoluto

La separación es justamente la finalidad de THPDFSignatureProvider. HotPDF conserva las partes que debe dominar —interpretar CMS, construir el SignedData, disponer el /ByteRange— y delega la única operación que no puede dominar, que es convertir un resumen en una firma con una clave que no tiene permitido ver. Todo lo que sigue se deriva de esa división

¿Por qué una firma ML-DSA válida no verifica?

Porque HotPDF rechaza ML-DSA en un documento cargado que no declare la extensión para ello. ML-DSA —el esquema de firma basado en retículas normalizado como FIPS 204, y el motivo por el que se habla de «PDF postcuántico»— todavía no tiene registro en ISO 32000-2. Un PDF que porta una firma así usa un algoritmo que la norma base no nombra, y un archivo que usa en silencio un algoritmo innominado es un archivo cuyo veredicto nadie más puede reproducir

Así que HotPDF hace explícita la reclamación. EnsureMLDSAExtensions eleva el documento a PDF 2.0 donde procede y escribe /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> en el catálogo. En el lado de lectura, LoadedDocumentDeclaresMLDSAExtension informa de si esa declaración ha sobrevivido, y VerifyLoadedSignatureWithOptions aplica la misma comprobación antes de honrar Options.AllowMLDSA. Activa la marca en un documento no declarado y sigue desactivada —la opción puede aflojar la política, nunca el requisito estructural—

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'contract-pq.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
    Pdf.EnsureMLDSAExtensions;   // declare before the signature is written
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Llámala antes de guardar, no después. La declaración forma parte del rango de bytes firmado, y un catálogo parcheado después es o bien un cambio no firmado en un archivo firmado o bien una segunda revisión que un validador reportará como modificación

Tres familias de algoritmos, un único punto de entrada de verificación

Las tres familias entran por VerifyLoadedSignatureWithOptions, que toma un índice de firma, el flujo de origen, un registro THPDFCMSVerifyOptions y un parámetro de salida para los detalles de la firma. El registro tiene exactamente tres campos, y cada uno responde a una pregunta que antes exigía una recompilación

SignatureProvider sustituye tu propio proveedor por el integrado de plataforma. OpenSSLLibraryPath selecciona una librería OpenSSL 3, que es lo que aporta la verificación Ed25519 y Ed448 en modo puro que Windows CNG no ofrece en todas partes. AllowMLDSA da entrada a los algoritmos basados en retículas, condicionado a la comprobación de extensión anterior. El OID exacto del algoritmo que se ha reconocido vuelve en THPDFSignatureInfo.SignatureAlgorithmOID, de modo que un registro de auditoría puede dejar constancia de lo verificado en lugar de lo solicitado

var
  Opts: THPDFCMSVerifyOptions;
  Info: THPDFSignatureInfo;
  Status: THPDFSignatureVerifyStatus;
  Src: TFileStream;
begin
  Opts := THPDFCMSVerifyOptions.Default;
  Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
  Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
  Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
    if Status = svValid then
      Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
  finally
    Src.Free;
  end;
end;

Ed25519 y Ed448 no necesitan declaración de extensión, porque ISO 32000-2 ya los admite. Sí necesitan un proveedor que los implemente, lo que en la mayoría de despliegues Windows significa apuntar OpenSSLLibraryPath a una librería que tú distribuyes y controlas, no a la que por casualidad haya en la máquina

¿Qué promete realmente un proveedor de firma?

Un proveedor promete una sola cosa: dada una petición, devolver un estado y, al firmar, unos bytes. THPDFSignatureProviderRequest porta el algoritmo y su OID, el OID del resumen, la longitud de sal PSS, si la entrada es un mensaje o un resumen ya calculado, la propia entrada, la clave pública o el certificado, un identificador de clave y un identificador de operación. Nada en ese registro es específico de HotPDF —es el vocabulario que ya hablan un controlador de token o un servicio de firma—

La librería se entrega con tres implementaciones. THPDFCallbackSignatureProvider envuelve métodos anónimos, que es el camino más corto desde una rutina de firma interna existente hasta una firma PDF funcional. THPDFRemoteSignatureProvider envuelve un callback de transporte con un límite de reintentos, un registro de cancelación y cotas de tamaño de entrada y de firma, de modo que un HSM colgado no se convierta en una aplicación colgada. THPDFPKCS11SignatureProvider serializa operaciones RSA contra una sesión PKCS#11 propiedad del llamador y ya autenticada y un handle de clave privada —HotPDF nunca inicia sesión, nunca ve un PIN y nunca cierra una sesión que no abrió—

var
  Provider: THPDFRemoteSignatureProvider;
begin
  Provider := THPDFRemoteSignatureProvider.Create(
    function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
      out Signature: TBytes): THPDFSignatureProviderStatus
    begin
      // POST Req.Input to the signing service; Req.KeyIdentifier selects the key
      if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
        Result := spsValid
      else
        Result := spsProviderError;
    end,
    3,          // RetryLimit
    1048576,    // MaxInputBytes
    65536);     // MaxSignatureBytes
  try
    // hand Provider to the signing call
  finally
    Provider.Free;
  end;
end;

Por qué la enumeración de estado tiene seis valores en lugar de un booleano

THPDFSignatureProviderStatus distingue spsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError y spsCancelled, y colapsarlos te cuesta la capacidad de actuar correctamente. Una firma criptográficamente errónea (spsInvalid) es un incidente de seguridad. Un algoritmo que el proveedor no implementa (spsUnsupported) es un hueco de despliegue. Un fallo de transporte (spsProviderError) merece un reintento, y un prompt de token cancelado por el usuario (spsCancelled) no merece reintento alguno

La regla de firma es estricta: un proveedor de firma devuelve spsValid únicamente con una firma no vacía. Los proveedores de verificación devuelven spsValid o spsInvalid, y los otros cuatro siguen diferenciados en ambas rutas. Si escribes un proveedor, resiste la tentación de mapear todo lo que no reconozcas a spsInvalid —eso convierte una DLL ausente en un informe de que la firma del cliente está falsificada—

Dónde aterriza realmente la firma en el archivo

Dos funciones conectan los proveedores con bytes PDF reales. HPDFCMSBuildSignedDataWithProvider construye CMS desprendido a partir de un resumen SHA-256 del documento, que es el punto de entrada adecuado cuando tu flujo calcula el resumen en otra parte. HPDFCMSSignPDFStreamWithProvider firma un marcador de firma existente en un flujo PDF y preserva el cauce estándar del /ByteRange, que es el punto de entrada adecuado cuando el propio HotPDF ha dispuesto el marcador

Preservar ese cauce importa más de lo que parece. La convención del /ByteRange —dos rangos que saltan la ventana hexadecimal de la firma— es lo primero que comprueba cualquier validador, y un cauce basado en proveedores que la reescribiera rompería la conformidad PAdES por muy sólida que fuera la criptografía. HotPDF mantiene la disposición idéntica al cauce de firma integrado, de modo que un documento firmado a través de un token PKCS#11 verifica con el mismo código de verificación de firmas que uno firmado desde un archivo PFX. Para las reglas de perfil que se sientan sobre la elección de algoritmo, consulta el recorrido por las firmas baseline PAdES en Delphi, y para las trampas de codificación específicas de ECDSA anteriores a este modelo de proveedor, las notas sobre la verificación CMS de ECDSA y los formatos de firma P1363

Un orden de migración que no deje varados tus documentos

La preparación postcuántica es un problema de calendario, no un conmutador. Prácticamente ningún visor PDF desplegado valida ML-DSA hoy, así que un documento firmado solo con él es, desde el punto de vista del lector, un documento con una firma no verificable. El orden que sobrevive al contacto con archivos reales es: conservar RSA o ECDSA como la firma que juzgará un validador, añadir la declaración de extensión y una segunda firma ML-DSA donde una política exija evidencia resistente a lo cuántico, y trasladar la firma principal solo cuando los sistemas consumidores hayan alcanzado ese nivel

Lo que HotPDF te ofrece hoy es la capacidad de escribir y verificar ambas, desde el mismo código, con el algoritmo registrado de forma honesta en el archivo y en el resultado de verificación. HotPDF es un componente PDF VCL nativo para Delphi y C++Builder sin runtime PDF externo, así que los cauces de firma y verificación viajan dentro de tu ejecutable en lugar de a su lado; consulta la página del componente PDF Delphi de HotPDF para la lista completa de características y la descarga de prueba