Artículo técnico

Firma PDF post-cuá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 conectables, así que la clave privada nunca tiene que vivir dentro de tu proceso Delphi. Esa segunda mitad es la parte que la mayoría de los equipos necesita primero. Un token de hardware, un servicio de firma remoto y una tarjeta de identidad electrónica nacional se rehúsan a entregar una clave, y hasta que el flujo de firma no se separe del almacén de claves, ninguno se puede usar

Esa separación es el punto de THPDFSignatureProvider. HotPDF conserva las partes que le corresponden — analizar CMS, construir el SignedData, disponer el /ByteRange — y delega la única operación que no puede asumir, que es convertir un digest 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 falla al verificar?

Porque HotPDF rechaza ML-DSA en un documento cargado que no declara la extensión para ello. ML-DSA — el esquema de firma de retícula estandarizado como FIPS 204, y la razón por la que se habla de «PDF post-cuántico» — todavía no tiene registro ISO 32000-2. Un PDF que lleva uno está usando un algoritmo que la norma base no nombra, y un archivo que usa en silencio un algoritmo sin nombre es un archivo cuyo veredicto no puede reproducir nadie más

Así que HotPDF hace explícita la afirmación. EnsureMLDSAExtensions eleva el documento a PDF 2.0 donde está permitido y escribe /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> en el catálogo. Del lado de lectura, LoadedDocumentDeclaresMLDSAExtension reporta si esa declaración sobrevivió, y VerifyLoadedSignatureWithOptions aplica la misma prueba antes de honrar Options.AllowMLDSA. Activa la bandera en un documento no declarado y se mantiene apagada — 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 es parte del rango de bytes firmado, y un catálogo parcheado después es o bien un cambio no firmado a 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 llegan a través de 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 una pregunta que antes requería una reconstrucción

SignatureProvider sustituye tu propio proveedor por el integrado de plataforma. OpenSSLLibraryPath selecciona una biblioteca OpenSSL 3, que es lo que suministra la verificación Ed25519 y Ed448 en modo puro que Windows CNG no ofrece en todas partes. AllowMLDSA opta por los algoritmos de retícula, sujeto a la verificación de extensión anterior. El OID exacto del algoritmo reconocido regresa en THPDFSignatureInfo.SignatureAlgorithmOID, así que un registro de auditoría puede asentar lo que se verificó en vez de lo que se pidió

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 cual en la mayoría de los despliegues Windows significa apuntar OpenSSLLibraryPath a una biblioteca que tú distribuyes y controlas, en vez de a lo que sea que esté en la máquina

¿Qué promete realmente un proveedor de firma?

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

Tres implementaciones se incluyen con la biblioteca. THPDFCallbackSignatureProvider envuelve métodos anónimos, que es el camino más corto de una rutina de firma interna existente a una firma PDF funcional. THPDFRemoteSignatureProvider envuelve un callback de transporte con un límite de reintentos, un registro de cancelación y cotas sobre el tamaño de entrada y de firma, así que un HSM colgado no puede convertirse en una aplicación colgada. THPDFPKCS11SignatureProvider serializa operaciones RSA contra una sesión PKCS#11 propiedad de quien llama y ya autenticada, con un identificador 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 vez 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 equivocada (spsInvalid) es un evento de seguridad. Un algoritmo que el proveedor no implementa (spsUnsupported) es un hueco de despliegue. Una falla de transporte (spsProviderError) vale la pena reintentar, y un aviso de token cancelado por el usuario (spsCancelled) no vale la pena reintentar en absoluto

La regla para firmar es estricta: un proveedor de firma devuelve spsValid solo con una firma no vacía. Los proveedores de verificación devuelven spsValid o spsInvalid, y los otros cuatro se mantienen distintos en ambas rutas. Si escribes un proveedor, resiste la tentación de mapear todo lo que no reconoces a spsInvalid — eso convierte una DLL faltante en un reporte 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 separado a partir de un digest SHA-256 del documento, que es el punto de entrada correcto cuando tu flujo calcula el digest en otro lado. HPDFCMSSignPDFStreamWithProvider firma un marcador de firma existente en un flujo PDF y preserva el flujo estándar de /ByteRange, que es el punto de entrada correcto cuando HotPDF disponía el marcador mismo

Preservar ese flujo importa más de lo que parece. La convención /ByteRange — dos rangos que saltan la ventana de firma en hexadecimal — es lo primero que verifica todo validador, y una ruta basada en proveedores que la reescribiera rompería la conformidad PAdES por más correcta que fuera la criptografía. HotPDF mantiene la disposición idéntica a la ruta de firma integrada, así que un documento firmado a través de un token PKCS#11 se verifica con el mismo código de verificación de firmas que uno firmado desde un archivo PFX. Para las reglas de perfil que están por encima de 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 proveedores, las notas sobre la verificación CMS de ECDSA y los formatos de firma P1363

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

La preparación post-cuántica es un problema de calendario, no un interruptor. Casi ningún visor PDF desplegado valida ML-DSA hoy, así que un documento firmado solo con ese algoritmo es, desde el punto de vista del lector, un documento con una firma no verificable. El orden que sobrevive el contacto con archivos reales es: conservar RSA o ECDSA como la firma que un validador juzgará, agregar la declaración de extensión y una segunda firma ML-DSA donde una política exija evidencia resistente a ataques cuánticos, y mover la firma primaria solo cuando los sistemas consumidores hayan alcanzado el ritmo

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