Cambias una rutina de firma Delphi a SHA3-512 y OpenSSL rechaza el resultado: el CMS declara SHA3-512, pero su atributo messageDigest contiene 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 dependiente del resumen para esa única elección, como espera la ISO/TS 32001
Qué añade la ISO/TS 32001 a una firma PDF 2.0
La ISO/TS 32001 es una extensión corta de la ISO 32000-2. Ensancha el conjunto de funciones hash que una firma PDF puede declarar. La lista base vive en la Tabla 260 de la ISO 32000-2 §12.8.3.1, y para los subfiltros basados en CMS se detiene en SHA-256, SHA-384, SHA-512 y RIPEMD-160, con SHA-1 desaconsejado. La especificación técnica añade la familia SHA-3 del FIPS 202. SHA-3 es una construcción esponja que no comparte estructura interna alguna con SHA-2, y ahí está todo su atractivo para una política de firmas que quiere una segunda familia hash en la estantería por si SHA-2 se debilita algún día. En CMS cada variante es un identificador de objeto bajo el arco NIST. Los identificadores de algoritmo de firma combinado se sientan justo a su lado:
TCmsDigestAlgorithm | OID del resumen | Salida | RSA PKCS#1 v1.5 | ECDSA |
|---|---|---|---|---|
cdaSha256 | 2.16.840.1.101.3.4.2.1 | 32 bytes | 1.2.840.113549.1.1.11 | 1.2.840.10045.4.3.2 |
cdaSha3_256 | 2.16.840.1.101.3.4.2.8 | 32 bytes | 2.16.840.1.101.3.4.3.14 | 2.16.840.1.101.3.4.3.10 |
cdaSha3_384 | 2.16.840.1.101.3.4.2.9 | 48 bytes | 2.16.840.1.101.3.4.3.15 | 2.16.840.1.101.3.4.3.11 |
cdaSha3_512 | 2.16.840.1.101.3.4.2.10 | 64 bytes | 2.16.840.1.101.3.4.3.16 | 2.16.840.1.101.3.4.3.12 |
En PDFium Component toda la elección es una enumeración, TCmsDigestAlgorithm de la unidad FPdfCms. Deja que la clave elija al miembro. Una curva NIST espera un resumen 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 aplique la política de curvas y resúmenes de la ISO/TS 32002 marca como inválido cualquier otro emparejamiento. RSA no tiene regla semejante. El argumento de fortaleza es corto de todos modos: una clave RSA-2048 aporta unos 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
Cinco campos del CMS que cambian con el resumen
Cambiar la función hash es la parte más pequeña del trabajo. Una firma CAdES detached nombra su resumen en cinco sitios, y un verificador estricto los cruza. Si cualquiera sigue diciendo SHA-256, la firma falla aunque la aritmética RSA o ECDSA sea correcta:
digestAlgorithmen el SignerInfo, reflejado en el conjuntodigestAlgorithmsdel SignedData, transporta el OID del resumen, por ejemplo 2.16.840.1.101.3.4.2.9 para SHA3-384- El atributo firmado
messageDigest(RFC 5652) contiene el hash de los dos tramos del/ByteRange, de modo que su OCTET STRING mide 32, 48 o 64 bytes signatureAlgorithmnombra 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 resumen pasa a los parámetros- Los parámetros RSASSA-PSS repiten el resumen dos veces, como
hashAlgorithmy como hash del MGF1, y fijansaltLengtha la longitud del resumen. Déjalos fuera y un verificador asume SHA-1, MGF1 con SHA-1 y un salt de 20 bytes, que es otro algoritmo; las reglas de etiquetado de RSASSA-PSS-params del RFC 4055 también importan aquí - El atributo firmado CMSAlgorithmProtection (RFC 6211) copia los identificadores del algoritmo de resumen y de firma dentro de los atributos firmados, donde nadie puede cambiarlos sin que se note. Debe coincidir byte a byte con los campos 1 y 3
Un campo no cambia en absoluto. El ESSCertIDv2 dentro del atributo signing-certificate-v2 (RFC 5035) aplica el hash al certificado firmante con su propio hashAlgorithm, que por defecto es SHA-256 y es independiente del resumen del SignedData, así que PDFium Component lo mantiene 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 dentro del atributo. Pasa 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 vive también una capa más abajo, donde el EVP_PKEY_sign de OpenSSL toma la longitud del resumen como tbslen y la comprueba contra el resumen de mensaje puesto en el contexto, de modo que un firmante que siempre pasa 32 recibe un error de longitud que nunca menciona SHA-3. PDFium Component calcula el resumen del documento a partir del algoritmo que declare el firmante resuelto y deduce los atributos, los parámetros PSS y el atributo de protección de algoritmos de esa misma declaración, así que ningún campo puede desviarse de los demás
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, de modo que un solo camino de código sirve a las dos familias. En Windows una huella digital SHA-1 selecciona un certificado en el almacén MY del usuario actual. El backend OpenSSL en otras plataformas firma con la clave y el certificado dados a ConfigureSslSigningCredentials y rechaza una huella digital. Comprueba también la versión del documento. La ISO/TS 32001 extiende PDF 2.0, y la firma entra como actualización incremental que conserva la cabecera original, de modo que una parte que valide contra las reglas de PDF 2.0 necesita un fichero 2.0 antes de que lo firmes
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 esbozo
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 usuario actual
{$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 vez de caer en SHA-256. Ese es el comportamiento que se quiere. Alguien pidió SHA3-256 por un motivo, normalmente un documento de políticas de la parte que va a validar, y una firma SHA-256 producida en su lugar pasaría todas las comprobaciones de tu lado y la rechazarían en el suyo, semanas después, cuando ya nadie de tu equipo recuerda qué máquina la firmó. Captura la excepción donde todavía puedas decirle al operador qué host carece del algoritmo
Dónde hay SHA-3 disponible: CNG de Windows y OpenSSL
La disponibilidad es cosa de la máquina. En Windows el backend por defecto es CNG, y CNG trae proveedores de algoritmo SHA-3 desde Windows 11 24H2 en adelante; en compilaciones anteriores BCryptOpenAlgorithmProvider para "SHA3-256" falla con STATUS_NOT_FOUND. WinSha3DigestAvailable abre los proveedores SHA3-256, SHA3-384 y SHA3-512 una vez y guarda la respuesta en caché, porque no puede cambiar mientras el proceso vive. 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_384yEVP_sha3_512, así que la firma SHA-3 funciona en toda plataforma donde el backend cargue - El cargador también acepta una librería 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 decideSslSha3SigningAvailable SslSha3SigningAvailableexige además credenciales de firma configuradas. Si la preguntas antes deConfigureSslSigningCredentials, responde False incluso cuandoSslLibraryVersioninforma de una librería 3.x perfectamente válida. Eso parece un backend ausente. Es solo un problema de orden- Un ejecutable de 32 bits no puede cargar
libcrypto-3-x64.dll. Cuando se levantó este backend, una compilación Win32 apuntando a la DLL x64 informabaSslAvailable = FalsemientrasSslMissingSymbolsno devolvía más que la ruta de la librería, síntoma que se lee como un enlace de símbolos roto; la causa era la arquitectura de bits, y una DLL que casara con el ejecutable lo arregló
Una flota Windows que todavía incluya máquinas anteriores a 24H2 puede conservar CNG donde haya SHA-3 y encaminar el resto por OpenSSL con un proveedor de firmantes. IPadesSignerProvider tiene dos métodos. ResolveSslSignerForDigest en FPdfCryptoSsl devuelve un firmante cuyo resumen declarado, algoritmo de firma, longitud de resumen y salt PSS siguen todos el algoritmo pedido, y con un proveedor configurado, ResolvePadesSignerForDigest comprueba que el firmante devuelto declare el resumen que la llamada pidió, lanzando EPadesCrypto si no. Un proveedor construido para SHA3-256 no puede atender una petición SHA3-384. El proveedor es de proceso entero, así que un servidor que firme concurrentemente debería configurarlo una vez al arrancar en vez de por llamada como hace este esbozo; ConfigureSslLibraryPath apunta el cargador al libcrypto cuando la DLL no está en la ruta de búsqueda
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 resumen?
La firma remota parte el trabajo en dos. PreparePadesRemoteSignature escribe un PDF con un hueco /Contents vacío más una TPadesRemoteSigningRequest, y el poseedor de la clave devuelve más tarde un CMS detached, a menudo desde otro proceso o máquina, mediante la sesión de prepare y complete usada para claves HSM y en la nube. En el lado de preparación estableces TPadesRemoteSignOptions.DigestAlgorithm, digamos 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 ese momento la petición es la autoridad. CompletePadesRemoteSignature recalcula el resumen 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 debería tomar el algoritmo del mismo sitio:
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; // huella digital vacía para OpenSSL
SignerOpts.PaddingScheme := Padding;
// El resumen lo decide la petición; la configuración de este servicio no
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 resumen es la única evidencia del algoritmo. Hace a la vez de huella digital. Antes de leer ningún flujo, CompletePadesRemoteSignature compara Length(DocumentDigest) con CmsDigestByteLength(DigestAlgorithm), de modo que un resumen truncado o un algoritmo reetiquetado falla en ese punto y el flujo de destino jamás se toca. La longitud sola no distingue SHA3-256 de SHA-256, ambos 32 bytes, y por eso el valor del resumen se compara también con el messageDigest dentro del CMS devuelto. El PDF preparado, en cambio, es byte a byte idéntico para SHA-256 y SHA3-384: el marcador solo contiene números del /ByteRange y ningún resumen. Los ficheros idénticos se leen fácilmente como si se estuviera ignorando la configuración. La firma remota viene en la edición Enterprise mediante 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