Artículo técnico

Firmas PAdES SHA-3 con ISO/TS 32001 en PDFium Component

Cambia una rutina de firma Delphi a SHA3-512 y OpenSSL rechaza el resultado: el CMS declara SHA3-512, pero su atributo messageDigest guarda 32 bytes donde ese algoritmo produce 64. Sí, código Delphi puede firmar con SHA-3: SignPadesForDigest de PDFium Component acepta SHA3-256, SHA3-384 o SHA3-512 y escribe cada campo del CMS que depende del digest para esa única elección, como espera ISO/TS 32001

Qué añade ISO/TS 32001 a una firma PDF 2.0

ISO/TS 32001 es una extensión breve de ISO 32000-2. Ensancha el conjunto de funciones hash que una firma PDF puede declarar. La lista base está en la Tabla 260 de ISO 32000-2 §12.8.3.1, y para los subfiltros basados en CMS se queda en SHA-256, SHA-384, SHA-512 y RIPEMD-160, con SHA-1 obsoleta. La especificación técnica añade la familia SHA-3 de FIPS 202. SHA-3 es una construcción sponge que no comparte estructura interna con SHA-2, que es justo el atractivo para una política de firmas que quiere una segunda familia de hash en el estante por si SHA-2 alguna vez se debilita. En CMS cada variante es un identificador de objeto bajo el arco NIST. Los identificadores del algoritmo de firma combinado van justo al lado:

TCmsDigestAlgorithmOID del digestSalidaRSA PKCS#1 v1.5ECDSA
cdaSha2562.16.840.1.101.3.4.2.132 bytes1.2.840.113549.1.1.111.2.840.10045.4.3.2
cdaSha3_2562.16.840.1.101.3.4.2.832 bytes2.16.840.1.101.3.4.3.142.16.840.1.101.3.4.3.10
cdaSha3_3842.16.840.1.101.3.4.2.948 bytes2.16.840.1.101.3.4.3.152.16.840.1.101.3.4.3.11
cdaSha3_5122.16.840.1.101.3.4.2.1064 bytes2.16.840.1.101.3.4.3.162.16.840.1.101.3.4.3.12

En PDFium Component toda la elección es una enumeración, TCmsDigestAlgorithm de la unidad FPdfCms. Deje que la clave elija al miembro. Una curva NIST espera un digest de su propio ancho, así que P-256 se empareja con SHA3-256, P-384 con SHA3-384 y P-521 con SHA3-512, y un validador que aplica la política de curvas y digests de ISO/TS 32002 marca cualquier otro emparejamiento como inválido. RSA no tiene esa regla. El argumento de la fortaleza es corto de todos modos: una clave RSA-2048 entrega alrededor de 112 bits de seguridad y una RSA-3072 unos 128, así que SHA3-256 ya supera a la clave, mientras que SHA3-512 sobre una clave RSA-2048 añade 32 bytes al CMS y nada a la seguridad. Recomendamos SHA3-256 para RSA salvo que una política escrita nombre una variante mayor

Adiciones SHA-3 de ISO/TS 32001 para firmas PDF desde Delphi: las claves EC se emparejan con el digest del mismo ancho y RSA toma SHA3-256 por defecto en PDFium Component
La familia SHA-3 se une a la Tabla 260 como segunda familia de hash, las curvas EC se emparejan con el ancho correspondiente y SHA3-256 ya supera a una clave RSA-2048

Cinco campos del CMS que cambian con el digest

Cambiar la función hash es la parte más pequeña del trabajo. Una firma CAdES detached nombra su digest en cinco lugares, y un verificador estricto los contrasta entre sí. Si alguno todavía dice SHA-256, la firma falla aunque la aritmética RSA o ECDSA sea correcta:

  1. digestAlgorithm en el SignerInfo, espejado en el conjunto digestAlgorithms del SignedData, lleva el OID del digest, por ejemplo 2.16.840.1.101.3.4.2.9 para SHA3-384
  2. El atributo firmado messageDigest (RFC 5652) guarda el hash de los dos tramos /ByteRange, así que su OCTET STRING mide 32, 48 o 64 bytes
  3. signatureAlgorithm nombra el esquema combinado de la tabla de arriba. RSASSA-PSS es la excepción: su OID se queda en id-RSASSA-PSS (1.2.840.113549.1.1.10) y el digest pasa a los parámetros
  4. Los parámetros RSASSA-PSS repiten el digest dos veces, como hashAlgorithm y como el hash MGF1, y fijan saltLength a la longitud del digest. Si los omite, un verificador asume SHA-1, MGF1 con SHA-1 y una salt de 20 bytes, que es otro algoritmo; las reglas de tagging de RSASSA-PSS-params del RFC 4055 también importan aquí
  5. El atributo firmado CMSAlgorithmProtection (RFC 6211) copia los identificadores del digest y del algoritmo de firma dentro de los atributos firmados, donde nadie puede cambiarlos sin que se note. Debe coincidir byte por byte con los campos 1 y 3

Un campo no cambia en absoluto. El ESSCertIDv2 dentro del atributo signing-certificate-v2 (RFC 5035) hashea el certificado del firmante con su propio hashAlgorithm, que por defecto es SHA-256 y es independiente del digest del SignedData, así que PDFium Component lo deja en SHA-256 en la codificación por defecto. El error que más aparece en código CMS escrito a mano es la longitud del messageDigest. El código escrito para SHA-256 tiende a guardar el hash en un registro fijo de 32 bytes y copiar ese registro en el atributo. Empuje un resultado SHA3-512 por ahí y el atributo recibe los primeros 32 bytes mientras digestAlgorithm dice .10: el rechazo de OpenSSL de la apertura. El mismo error también vive una capa más abajo, donde el EVP_PKEY_sign de OpenSSL toma la longitud del digest como tbslen y la contrasta contra el message digest fijado en el contexto, así que un firmante que siempre pasa 32 recibe un error de longitud que nunca menciona SHA-3. PDFium Component calcula el digest del documento con el algoritmo que declare el firmante resuelto y deriva de esa misma declaración los atributos, los parámetros PSS y el atributo de protección de algoritmos, así que ningún campo puede desviarse de los demás

Los cinco campos del CMS que cambian con un digest SHA-3 en una firma PAdES desde Delphi, contrastados por verificadores estrictos tal como PDFium Component los escribe
digestAlgorithm, messageDigest, signatureAlgorithm, los parámetros PSS y CMSAlgorithmProtection deben nombrar un solo digest, mientras que ESSCertIDv2 conserva su propio SHA-256

Firmar un PDF local con SignPadesForDigest

En el componente TPdf los puntos de entrada SHA-3 son SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean y su gemelo de flujo SignPadesToStreamForDigest. Ambos toman el mismo registro TPadesSignOptions que SignPades, y cdaSha256 produce la misma firma que la sobrecarga existente, así que un solo camino de código sirve a las dos familias. En Windows un thumbprint SHA-1 selecciona un certificado en el almacén MY del Current User. El backend OpenSSL en otras plataformas firma con la clave y el certificado dados a ConfigureSslSigningCredentials y rechaza un thumbprint. Revise también la versión del documento. ISO/TS 32001 extiende PDF 2.0, y la firma entra como una actualización incremental que conserva el encabezado original, así que una relying party que valida contra las reglas de PDF 2.0 necesita un archivo 2.0 antes de que usted firme

uses
  System.SysUtils, System.Classes, System.IOUtils,
  PDFium, FPdfCms, FPdfCrypto, FPdfPades
  {$IFDEF MSWINDOWS}, FPdfCryptoWin{$ELSE}, FPdfCryptoSsl{$ENDIF};

function Sha3SigningAvailable: Boolean;
begin
{$IFDEF MSWINDOWS}
  Result := WinSha3DigestAvailable;   // proveedores SHA-3 de CNG, Windows 11 24H2+
{$ELSE}
  Result := SslSha3SigningAvailable;  // False hasta configurar las credenciales
{$ENDIF}
end;

procedure SignWithSha3(const SourceFile, SignedFile, Thumbprint: string;
  const KeyPassword: AnsiString; const Log: TProc<string>);
var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
  Signed: TMemoryStream;
begin
{$IFNDEF MSWINDOWS}
  // OpenSSL firma con material configurado explícitamente; sin cadena en este bosquejo
  ConfigureSslSigningCredentials(
    TFile.ReadAllBytes('/etc/signing/signer-key.pem'),
    TFile.ReadAllBytes('/etc/signing/signer.der'), nil, KeyPassword);
{$ENDIF}
  if not Sha3SigningAvailable then
    raise Exception.Create('No SHA-3 capable signing backend on this machine');

  Opts := TPadesSignOptions.Default;          // B-B, RSASSA-PSS
{$IFDEF MSWINDOWS}
  Opts.CertificateThumbprint := Thumbprint;   // certificado en el MY del Current User
{$ENDIF}
  Opts.Reason := 'Approved for release';

  Pdf := TPdf.Create(nil);
  Signed := TMemoryStream.Create;
  try
    Pdf.FileName := SourceFile;
    Pdf.Active := True;
    if Pdf.PdfVersion <> pv20 then
      Log(SourceFile + ' is not a PDF 2.0 file');
    try
      if Pdf.SignPadesToStreamForDigest(Signed, Opts, cdaSha3_256) then
        Signed.SaveToFile(SignedFile);
    except
      on E: EPadesCryptoUnavailable do
        Log('SHA-3 signing unavailable: ' + E.Message);
    end;
  finally
    Signed.Free;
    Pdf.Free;
  end;
end;

Cuando el backend no puede producir SHA-3, la llamada lanza EPadesCryptoUnavailable en lugar de caer a SHA-256. Ese es el comportamiento que se quiere. Alguien pidió SHA3-256 por una razón, normalmente un documento de política de la relying party, y una firma SHA-256 producida en su lugar pasaría todas las comprobaciones de su lado y sería rechazada del de ellos, semanas después, cuando nadie en su equipo recuerda qué máquina firmó. Capture la excepción donde todavía puede decirle al operador qué host carece del algoritmo

Dónde hay SHA-3 disponible: CNG de Windows y OpenSSL

La disponibilidad es asunto de la máquina. En Windows el backend por defecto es CNG, y CNG trae proveedores de algoritmos SHA-3 desde Windows 11 24H2 en adelante; en builds anteriores BCryptOpenAlgorithmProvider para "SHA3-256" falla con STATUS_NOT_FOUND. WinSha3DigestAvailable abre una sola vez los proveedores SHA3-256, SHA3-384 y SHA3-512 y guarda la respuesta en caché, ya que no puede cambiar mientras el proceso corre. En todas las demás partes firma el backend OpenSSL, y lo que puede hacer depende del libcrypto que encuentre:

  • OpenSSL 3.x siempre exporta EVP_sha3_256, EVP_sha3_384 y EVP_sha3_512, así que la firma SHA-3 funciona en toda plataforma donde el backend carga
  • El loader también acepta una biblioteca de la serie 1.1 (libcrypto-1_1.dll, libcrypto.so.1.1). SHA-3 llegó en OpenSSL 1.1.1 y no existe en 1.1.0, así que en imágenes Linux antiguas decide SslSha3SigningAvailable
  • SslSha3SigningAvailable también exige credenciales de firma configuradas. Si la pregunta va antes de ConfigureSslSigningCredentials, responde False incluso cuando SslLibraryVersion reporta una biblioteca 3.x en perfecto estado. Eso parece un backend faltante. Es solo un problema de orden
  • Un ejecutable de 32 bits no puede cargar libcrypto-3-x64.dll. Cuando se trajo en marcha este backend, un build Win32 apuntando a la DLL x64 reportaba SslAvailable = False mientras SslMissingSymbols no devolvía nada más que la ruta de la biblioteca, un síntoma que parece un enlace de símbolos roto; la causa era el bitness, y una DLL que coincidiera con el ejecutable lo arregló

Una flota Windows que todavía incluya máquinas anteriores a 24H2 puede conservar CNG donde hay SHA-3 y enrutar el resto por OpenSSL con un signer provider. IPadesSignerProvider tiene dos métodos. ResolveSslSignerForDigest en FPdfCryptoSsl devuelve un firmante cuyo digest declarado, algoritmo de firma, longitud de digest y salt PSS siguen todos el algoritmo pedido, y con un provider configurado, ResolvePadesSignerForDigest verifica que el firmante devuelto declare el digest que la llamada pidió, lanzando EPadesCrypto si no lo hace. Un provider construido para SHA3-256 no puede estampar una petición SHA3-384. El provider es de todo el proceso, así que un servidor que firma en concurrencia debería configurarlo una vez al arrancar en lugar de por llamada como hace este bosquejo; ConfigureSslLibraryPath apunta el loader al libcrypto cuando la DLL no está en la ruta de búsqueda

Flujo de disponibilidad de firma SHA-3 en Delphi: proveedores CNG de Windows desde 24H2, guardianes de versión y bitness de OpenSSL, y el camino de fallo EPadesCryptoUnavailable en PDFium Component
La disponibilidad es de la máquina, así que la comprobación pertenece al arranque: CNG donde hay SHA-3, OpenSSL en el resto, y sin caída silenciosa a SHA-256
uses
  System.SysUtils, System.Classes, System.IOUtils,
  PDFium, FPdfCms, FPdfCrypto, FPdfCryptoWin, FPdfCryptoSsl, FPdfPades;

type
  TOpenSslSha3Provider = class(TInterfacedObject, IPadesSignerProvider)
  private
    FDigest: TCmsDigestAlgorithm;
  public
    constructor Create(ADigest: TCmsDigestAlgorithm);
    function BackendName: string;
    function Resolve(const Options: TPadesSignerOptions): TPadesResolvedSigner;
  end;

constructor TOpenSslSha3Provider.Create(ADigest: TCmsDigestAlgorithm);
begin
  inherited Create;
  FDigest := ADigest;
end;

function TOpenSslSha3Provider.BackendName: string;
begin
  Result := 'OpenSSL';
end;

function TOpenSslSha3Provider.Resolve(
  const Options: TPadesSignerOptions): TPadesResolvedSigner;
begin
  Result := ResolveSslSignerForDigest(Options, FDigest);
end;

procedure SignSha3OnAnyWindows(Pdf: TPdf; Dest: TStream;
  const Opts: TPadesSignOptions; const KeyPassword: AnsiString);
var
  Provider: IPadesSignerProvider;
  Chain: TPadesChainCertArray;
  SslOpts: TPadesSignOptions;
begin
  if WinSha3DigestAvailable then
  begin
    Pdf.SignPadesToStreamForDigest(Dest, Opts, cdaSha3_256);   // camino CNG
    Exit;
  end;

  Chain := nil;
  ConfigureSslSigningCredentials(
    TFile.ReadAllBytes('C:\Signing\signer-key.pem'),
    TFile.ReadAllBytes('C:\Signing\signer.der'), Chain, KeyPassword);
  if not SslSha3SigningAvailable then
    raise Exception.Create('No SHA-3 in loaded OpenSSL ' + SslLibraryVersion);

  SslOpts := Opts;
  SslOpts.CertificateThumbprint := '';   // OpenSSL firma con el certificado configurado
  Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
  ConfigurePadesSignerProvider(Provider);
  try
    Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
  finally
    ConfigurePadesSignerProvider(nil);
  end;
end;

¿Cómo se entera un firmante remoto o HSM del digest?

La firma remota divide el trabajo en dos. PreparePadesRemoteSignature escribe un PDF con una ranura /Contents vacía más un TPadesRemoteSigningRequest, y el guardián de la clave devuelve un CMS detached más tarde, a menudo desde otro proceso o máquina, a través de la sesión de preparación y completado usada para claves HSM y en la nube. Del lado de la preparación usted fija TPadesRemoteSignOptions.DigestAlgorithm, digamos a cdaSha3_384, y la petición registra la elección en TPadesRemoteSigningRequest.DigestAlgorithm junto a DocumentDigest, el valor que el messageDigest del CMS debe llevar. Desde ahí la petición es la autoridad. CompletePadesRemoteSignature recalcula el digest del /ByteRange con el algoritmo guardado en la petición y nunca vuelve a consultar las opciones; de lo contrario, un cambio de configuración entre preparar y completar juzgaría una petición vieja con un algoritmo nuevo. Un servicio de firma construido sobre el componente debe tomar el algoritmo del mismo lugar:

uses
  System.SysUtils, System.Classes,
  FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
  FPdfPadesRemote;   // enlaza la implementación Enterprise

function SignRemoteRequest(const RequestFile: string;
  Padding: TPadesPaddingScheme): TBytes;
var
  Stream: TFileStream;
  Request: TPadesRemoteSigningRequest;
  SignerOpts: TPadesSignerOptions;
  Resolved: TPadesResolvedSigner;
begin
  Stream := TFileStream.Create(RequestFile, fmOpenRead or fmShareDenyWrite);
  try
    Request := LoadPadesRemoteSigningRequest(Stream);
  finally
    Stream.Free;
  end;

  SignerOpts := TPadesSignerOptions.Default;   // thumbprint vacío para OpenSSL
  SignerOpts.PaddingScheme := Padding;
  // El digest lo decide la petición; no la configuración de este servicio
  Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
  Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
    Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;

Dentro de la petición, la longitud del digest es la única evidencia del algoritmo. Hace también de huella digital. Antes de leer cualquier flujo, CompletePadesRemoteSignature compara Length(DocumentDigest) con CmsDigestByteLength(DigestAlgorithm), así que un digest truncado o un algoritmo reetiquetado falla en ese punto y el flujo de destino jamás se toca. La longitud por sí sola no puede distinguir SHA3-256 de SHA-256, ambos de 32 bytes, razón por la cual el valor del digest también se compara con el messageDigest dentro del CMS devuelto. El PDF preparado, en cambio, es idéntico byte a byte para SHA-256 y SHA3-384: el placeholder guarda solo números /ByteRange y ningún digest. Los archivos idénticos se leen fácilmente como si la configuración se estuviera ignorando. La firma remota se envía en la edición Enterprise a través de la unidad FPdfPadesRemote

La variante rara vez es la decisión difícil; para una clave RSA suele ser cdaSha3_256, y lo que falla en producción es un host cuyo CNG o libcrypto no puede producirla, razón por la que la comprobación de disponibilidad pertenece al arranque de la aplicación en cualquier despliegue del PDFium Component para Delphi