Você troca uma rotina de assinatura em Delphi para SHA3-512 e o OpenSSL rejeita o resultado: o CMS declara SHA3-512, mas o atributo messageDigest dele guarda 32 bytes onde esse algoritmo produz 64. Sim, código Delphi pode assinar com SHA-3: o SignPadesForDigest do PDFium Component aceita SHA3-256, SHA3-384 ou SHA3-512 e escreve todo campo do CMS dependente do digest para essa única escolha, como a ISO/TS 32001 espera
O que a ISO/TS 32001 acrescenta a uma assinatura PDF 2.0
A ISO/TS 32001 é uma extensão curta da ISO 32000-2. Ela amplia o conjunto de funções hash que uma assinatura PDF pode declarar. A lista base fica na Tabela 260 da ISO 32000-2 §12.8.3.1, e para os subfilters baseados em CMS ela para em SHA-256, SHA-384, SHA-512 e RIPEMD-160, com SHA-1 deprecated. A especificação técnica acrescenta a família SHA-3 do FIPS 202. SHA-3 é uma sponge construction que não divide estrutura interna nenhuma com o SHA-2, e esse é todo o apelo para uma política de assinatura que quer uma segunda família de hash na prateleira caso o SHA-2 enfraqueça algum dia. No CMS cada variante é um object identifier sob o arco NIST. Os identificadores de algoritmo de assinatura combinados ficam bem ao lado deles:
TCmsDigestAlgorithm | OID do digest | Saída | 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 |
No PDFium Component a escolha inteira é uma enumeração, a TCmsDigestAlgorithm da unit FPdfCms. Deixe a chave escolher o membro. Uma curva NIST espera um digest da própria largura dela, então P-256 combina com SHA3-256, P-384 com SHA3-384 e P-521 com SHA3-512, e um validador que aplica a política de curvas e digests da ISO/TS 32002 marca qualquer outro par como inválido. RSA não tem regra assim. O argumento de força é curto de qualquer jeito: uma chave RSA-2048 entrega cerca de 112 bits de segurança e a RSA-3072 uns 128, então o SHA3-256 já ultrapassa a chave, enquanto SHA3-512 em uma chave RSA-2048 soma 32 bytes ao CMS e nada à segurança. Recomendamos SHA3-256 para RSA a menos que uma política escrita nomeie uma variante maior
Cinco campos do CMS que mudam com o digest
Trocar a função hash é a menor parte do trabalho. Uma assinatura CAdES detached nomeia o seu digest em cinco lugares, e um verificador estrito confere um contra o outro. Se qualquer um deles ainda diz SHA-256, a assinatura falha mesmo que a aritmética RSA ou ECDSA esteja correta:
- O
digestAlgorithmno SignerInfo, espelhado no conjuntodigestAlgorithmsdo SignedData, carrega o OID do digest, por exemplo 2.16.840.1.101.3.4.2.9 para SHA3-384 - O atributo assinado
messageDigest(RFC 5652) guarda o hash dos dois trechos do/ByteRange, então o OCTET STRING dele tem 32, 48 ou 64 bytes - O
signatureAlgorithmnomeia o esquema combinado da tabela acima. RSASSA-PSS é a exceção: o OID dele continua id-RSASSA-PSS (1.2.840.113549.1.1.10) e o digest vai para os parâmetros - Os parâmetros RSASSA-PSS repetem o digest duas vezes, como
hashAlgorithme como o hash do MGF1, e definemsaltLengthpara o comprimento do digest. Deixe-os de fora e um verificador assume SHA-1, MGF1 com SHA-1 e um salt de 20 bytes, que é um algoritmo diferente; as regras de tagging do RSASSA-PSS-params do RFC 4055 importam aqui também - O atributo assinado CMSAlgorithmProtection (RFC 6211) copia os identificadores de algoritmo de digest e de assinatura para os atributos assinados, onde ninguém pode trocá-los sem ser notado. Ele precisa casar com os campos 1 e 3 byte a byte
Um campo não muda de forma alguma. O ESSCertIDv2 dentro do atributo signing-certificate-v2 (RFC 5035) faz o hash do certificado do signatário com o hashAlgorithm próprio dele, que tem SHA-256 como padrão e é independente do digest do SignedData, então o PDFium Component o mantém em SHA-256 na codificação padrão. O erro que mais aparece em código CMS escrito à mão é o comprimento do messageDigest. Código escrito para SHA-256 tende a guardar o hash em um record fixo de 32 bytes e copiar esse record para o atributo. Empurre um resultado SHA3-512 por ele e o atributo recebe os primeiros 32 bytes enquanto o digestAlgorithm diz .10: a rejeição do OpenSSL da abertura. O mesmo erro também vive uma camada abaixo, onde o EVP_PKEY_sign do OpenSSL recebe o comprimento do digest como tbslen e o confere contra o message digest definido no contexto, então um signatário que sempre passa 32 recebe um erro de comprimento que nunca menciona SHA-3. O PDFium Component calcula o digest do documento a partir do algoritmo que o signatário resolvido declara e deriva os atributos, os parâmetros PSS e o atributo de proteção de algoritmo dessa mesma declaração, então nenhum campo pode se afastar dos outros
Assinando um PDF local com SignPadesForDigest
No componente TPdf os pontos de entrada SHA-3 são SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean e o seu gêmeo de stream SignPadesToStreamForDigest. Ambos recebem o mesmo record TPadesSignOptions que o SignPades, e o cdaSha256 produz a mesma assinatura que a sobrecarga existente, então um único caminho de código serve as duas famílias. No Windows um thumbprint SHA-1 seleciona um certificado no repositório MY do Current User. O backend OpenSSL nas outras plataformas assina com a chave e o certificado dados ao ConfigureSslSigningCredentials e recusa um thumbprint. Confira também a versão do documento. A ISO/TS 32001 estende o PDF 2.0, e a assinatura entra como uma atualização incremental que mantém o header original, então uma relying party que valida contra as regras do PDF 2.0 precisa de um arquivo 2.0 antes de você assinar
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; // provedores SHA-3 do CNG, Windows 11 24H2+
{$ELSE}
Result := SslSha3SigningAvailable; // False até as credenciais serem configuradas
{$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}
// O OpenSSL assina com material configurado explicitamente; sem chain neste esboço
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 no MY do 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;
Quando o backend não consegue produzir SHA-3, a chamada levanta EPadesCryptoUnavailable em vez de recuar para SHA-256. Esse é o comportamento desejável. Alguém pediu SHA3-256 por um motivo, geralmente um documento de política da relying party, e uma assinatura SHA-256 produzida no lugar passaria em toda checagem do seu lado e seria barrada no deles, semanas depois, quando ninguém do seu time lembra mais qual máquina assinou. Capture a exceção onde você ainda consegue dizer ao operador qual host não tem o algoritmo
Onde o SHA-3 está disponível: CNG do Windows e OpenSSL
A disponibilidade pertence à máquina. No Windows o backend padrão é o CNG, e o CNG traz provedores de algoritmo SHA-3 a partir do Windows 11 24H2; em builds anteriores o BCryptOpenAlgorithmProvider para "SHA3-256" falha com STATUS_NOT_FOUND. O WinSha3DigestAvailable abre os provedores SHA3-256, SHA3-384 e SHA3-512 uma vez e guarda a resposta em cache, já que ela não pode mudar enquanto o processo roda. Em todo o resto assina o backend OpenSSL, e o que ele consegue fazer depende do libcrypto que encontrar:
- O OpenSSL 3.x sempre exporta
EVP_sha3_256,EVP_sha3_384eEVP_sha3_512, então assinar com SHA-3 funciona em toda plataforma em que o backend carrega - O loader também aceita uma biblioteca da série 1.1 (
libcrypto-1_1.dll,libcrypto.so.1.1). SHA-3 chegou no OpenSSL 1.1.1 e está ausente do 1.1.0, então em imagens Linux mais antigas oSslSha3SigningAvailabledecide - O
SslSha3SigningAvailabletambém exige credenciais de assinatura configuradas. Perguntado antes doConfigureSslSigningCredentials, ele responde False mesmo quando oSslLibraryVersionreporta uma biblioteca 3.x perfeitamente boa. Isso parece um backend faltando. É só um problema de ordem - Um executável de 32 bits não consegue carregar
libcrypto-3-x64.dll. Quando este backend foi trazido para cima, um build Win32 apontado para a DLL x64 reportavaSslAvailable = Falseenquanto oSslMissingSymbolsecoava nada além do caminho da biblioteca, um sintoma que parece binding de símbolos quebrado; a causa era o bitness, e uma DLL correspondente ao executável resolveu
Uma frota Windows que ainda inclui máquinas pré-24H2 pode manter o CNG onde há SHA-3 e rotear o resto pelo OpenSSL com um signer provider. O IPadesSignerProvider tem dois métodos. O ResolveSslSignerForDigest na FPdfCryptoSsl retorna um signer cujo digest declarado, algoritmo de assinatura, comprimento de digest e salt PSS seguem todos o algoritmo pedido, e com um provider configurado o ResolvePadesSignerForDigest confere que o signer retornado declara o digest que a chamada pediu, levantando EPadesCrypto se não declarar. Um provider construído para SHA3-256 não pode carimbar um pedido SHA3-384. O provider é global ao processo, porém, então um servidor que assina em concorrência deve configurá-lo uma vez na inicialização em vez de por chamada como este esboço faz; o ConfigureSslLibraryPath aponta o loader para o libcrypto quando a DLL não está no path de busca
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); // caminho 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 := ''; // o OpenSSL assina com o certificado configurado
Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
ConfigurePadesSignerProvider(Provider);
try
Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
finally
ConfigurePadesSignerProvider(nil);
end;
end;
Como um signer remoto ou HSM fica sabendo do digest?
Assinatura remota divide o trabalho em dois. O PreparePadesRemoteSignature escreve um PDF com um slot /Contents vazio mais um TPadesRemoteSigningRequest, e o guardião da chave devolve um CMS detached mais tarde, muitas vezes de outro processo ou máquina, pela sessão de preparo e conclusão usada para chaves HSM e de nuvem. No lado do preparo você define o TPadesRemoteSignOptions.DigestAlgorithm, digamos para cdaSha3_384, e o request registra a escolha no TPadesRemoteSigningRequest.DigestAlgorithm ao lado do DocumentDigest, o valor que o messageDigest do CMS precisa carregar. A partir daí o request é a autoridade. O CompletePadesRemoteSignature recalcula o digest do /ByteRange com o algoritmo guardado no request e nunca mais consulta as opções; de outro modo, uma mudança de configuração entre preparo e conclusão julgaria um request antigo por um algoritmo novo. Um serviço de assinatura construído sobre o componente deve pegar o algoritmo do mesmo lugar:
uses
System.SysUtils, System.Classes,
FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
FPdfPadesRemote; // liga a implementação 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 vazio para o OpenSSL
SignerOpts.PaddingScheme := Padding;
// O request decide o digest; a configuração deste serviço não
Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;
Dentro do request, o comprimento do digest é a única evidência do algoritmo. Ele faz as vezes de fingerprint. Antes de ler qualquer stream, o CompletePadesRemoteSignature compara Length(DocumentDigest) com CmsDigestByteLength(DigestAlgorithm), então um digest truncado ou um algoritmo renomeado falha nesse ponto e o stream de destino nunca é tocado. O comprimento sozinho não distingue SHA3-256 de SHA-256, ambos 32 bytes, e é por isso que o valor do digest também é comparado com o messageDigest dentro do CMS devolvido. O PDF preparado, em contraste, é byte-idêntico para SHA-256 e SHA3-384: o placeholder guarda só números do /ByteRange e nenhum digest. Arquivos idênticos são fáceis de serem lidos como se a configuração estivesse sendo ignorada. Assinatura remota vem na edição Enterprise pela unit FPdfPadesRemote
A variante raramente é a decisão difícil; para uma chave RSA em geral é cdaSha3_256, e o que falha em produção é um host cujo CNG ou libcrypto não consegue produzi-la, e é por isso que a checagem de disponibilidade pertence à inicialização da aplicação em qualquer deployment do PDFium Component para Delphi