Um PDF com uma assinatura ML-DSA-65 pode passar na checagem da toolchain que o produziu e ainda assim voltar como algoritmo não suportado de um verifier que exige que o arquivo declare qual extensão define o algoritmo dele. A chave pública tem 1.952 bytes, exatamente o tamanho FIPS 204 para aquele conjunto de parâmetros. Os bytes da assinatura estão intactos e os byte ranges cobrem o arquivo inteiro. O que um verifier desses procura é uma entrada no Catalog do documento nomeando essa extensão, porque a ISO 32000-2 não lista ML-DSA de forma alguma
O lado da assinatura tem outra parede. Um token de hardware, um HSM na nuvem ou um cartão nacional de eID calcula uma assinatura sob pedido, mas nenhum deles entrega a chave privada ao seu aplicativo, então qualquer código de assinatura que espera carregar um arquivo PFX e assinar in-process simplesmente não tem nada para carregar. Os dois problemas são sobre onde uma decisão mora: a declaração de algoritmo mora no arquivo, e a chave mora em algum lugar que o seu código Delphi não alcança
Por que uma assinatura ML-DSA válida volta como não suportada?
O HotPDF verifica assinaturas CMS ML-DSA-44, ML-DSA-65 e ML-DSA-87 somente quando o documento carregado declara a extensão de desenvolvedor do HotPDF no Catalog dele. Sem essa declaração, o verifier retorna svUnsupportedAlgorithm para uma assinatura em reticulado, por mais sólida que seja a criptografia. O raciocínio é reprodutibilidade: um arquivo que usa um algoritmo que o padrão base não nomeia, e não diz de qual extensão depende, produz um veredito que outra implementação não tem base para repetir
No lado da produção, o THotPDF.EnsureMLDSAExtensions escreve /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> no Catalog e eleva o documento para PDF 2.0 se ele mirava uma versão mais antiga. No lado da leitura, o LoadedDocumentDeclaresMLDSAExtension retorna True só quando aquele dicionário está presente com /BaseVersion igual a 2.0 e /ExtensionLevel de pelo menos 1. O VerifyLoadedSignatureWithOptions combina os dois: recebe a flag AllowMLDSA do chamador e faz AND com o teste de declaração antes de qualquer verificação rodar, então ligar a flag num arquivo não declarado não muda nada
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // o padrão; deixa a chamada elevar o arquivo para PDF 2.0
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // declare antes de qualquer assinatura cobrir o Catalog
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dois detalhes nesse snippet pesam. A chamada tem que acontecer antes de o documento ser salvo e assinado, porque o Catalog fica dentro do intervalo de bytes assinado; remendá-lo depois é ou uma alteração não assinada num arquivo assinado, ou uma nova revisão incremental que qualquer validator vai listar como modificação. O segundo detalhe é o StrictVersionLock. Com o travamento ligado e o documento fixado abaixo do PDF 2.0, o EnsureMLDSAExtensions retorna sem gravar nada, já que uma extensão PDF 2.0 não pode ser declarada num arquivo 1.7. Se o seu gerador fixa versões por razões de arquivamento, confira Version = pdf20 antes de confiar na declaração
Uma chamada de verificação para EdDSA e ML-DSA
O VerifyLoadedSignatureWithOptions recebe um índice de assinatura, o stream de origem, um record THPDFCMSVerifyOptions e um out THPDFSignatureInfo. Comece o record pelo THPDFCMSVerifyOptions.Default, que deixa AllowMLDSA desligado, limita um contêiner CMS a 256 signers e restringe o aninhamento de countersignature a 8. Três campos importam para os algoritmos mais novos. O SignatureProvider deixa o seu próprio verifier responder primeiro. O OpenSSLLibraryPath nomeia o libcrypto OpenSSL 3 que executa a verificação Ed25519 e Ed448 em modo puro. O AllowMLDSA adere aos algoritmos em reticulado, sujeito ao teste de Catalog acima. O record também carrega campos de fixação de política de assinatura (RequireSignaturePolicy, ExpectedSignaturePolicyOID, ExpectedSignaturePolicyHash), que se aplicam a toda família de algoritmos
O caminho embutido se divide por algoritmo. Ed25519 e Ed448 passam pelo OpenSSL 3; sem caminho informado, o provedor de plataforma tenta libcrypto-3-x64.dll, libcrypto-3.dll e libcrypto.dll pela ordem de busca normal de DLL no Win64. ML-DSA passa pelo provedor de algoritmo ML-DSA do CNG do Windows, e num build do Windows que não o oferece o resultado é não suportado. Defina OpenSSLLibraryPath para uma cópia que você distribui junto ao executável. Deixá-lo vazio significa que o veredito depende de qual libcrypto outro instalador deixou no search path, e isso é algo difícil de explicar numa auditoria
O seu próprio SignatureProvider é consultado primeiro. Se ele retorna spsUnsupported, o provedor de plataforma embutido recebe o mesmo pedido, então um provedor que só conhece ML-DSA pode ficar na frente do tratamento EdDSA padrão sem reimplementá-lo
var
Pdf: THotPDF;
PQ: THPDFCallbackSignatureProvider;
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
PQ := THPDFCallbackSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest): THPDFSignatureProviderStatus
begin
if not (Req.Algorithm in [hsaMLDSA44, hsaMLDSA65, hsaMLDSA87]) then
Exit(spsUnsupported); // passe o EdDSA adiante para o provedor de plataforma
// MyFips204Verify é o seu wrapper em volta de uma biblioteca ML-DSA;
// Req.Input guarda os atributos assinados codificados em DER (modo puro)
Result := MyFips204Verify(Req.AlgorithmOID, Req.PublicKey,
Req.Input, Req.Signature);
end);
Pdf := THotPDF.Create(nil);
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyNone);
try
Pdf.LoadFromFile('contract-pq.pdf');
Opts := THPDFCMSVerifyOptions.Default;
Opts.SignatureProvider := PQ;
Opts.OpenSSLLibraryPath := ExtractFilePath(ParamStr(0)) + 'libcrypto-3-x64.dll';
Opts.AllowMLDSA := True; // ainda faz AND com a declaração do Catalog
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add(string(Info.SignatureAlgorithm) + ' / ' +
string(Info.SignatureAlgorithmOID));
finally
Src.Free;
Pdf.Free;
PQ.Free; // o record de opções não é dono dele
end;
end;
Registre no log o Info.SignatureAlgorithmOID em vez de qualquer algoritmo que a sua configuração esperava. Para ML-DSA-65 é 2.16.840.1.101.3.4.3.18, e uma trilha de auditoria que registra o OID encontrado no arquivo responde depois a perguntas que um valor de configuração não responde. Se você não precisa de um provedor customizado de forma alguma, o overload mais curto VerifyLoadedSignature(SigIndex, Source, Info) monta as opções padrão e define AllowMLDSA a partir da declaração do Catalog para você
Regras de encoding que derrubam CMS ML-DSA e EdDSA produzidos por terceiros
As duas famílias de algoritmos são verificadas em modo puro sobre os atributos assinados completos codificados em DER, e um SignerInfo sem atributos assinados é reportado como não suportado. Para EdDSA, os parâmetros do AlgorithmIdentifier devem estar ausentes. Um erro comum em CMS feito à mão é fazer hash dos atributos assinados com SHA-512 ou SHAKE256 primeiro e assinar esse digest, o que transforma silenciosamente PureEdDSA numa variante prehash que nada no CMS declara. Para ML-DSA o verifier também confere o pareamento entre conjunto de parâmetros e message digest: ML-DSA-44 aceita SHA-256, SHA-384, SHA-512 ou SHAKE256, ML-DSA-65 precisa de SHA-384 ou mais forte, e ML-DSA-87 precisa de SHA-512 ou SHAKE256. Um pareamento errado aparece no Info.DigestConsistencyIssue, que é o primeiro campo a ler quando um arquivo de outra toolchain falha
O EdDSA tem uma história de registro própria. A ISO/TS 32002 é a extensão que registra Ed25519 e Ed448 para assinaturas PDF 2.0, e o EnsureISO32002Extensions adiciona a entrada ISO_ correspondente com ExtensionLevel 32002 ao Catalog. A verificação aceita EdDSA sem essa entrada, mas um arquivo que você produz ainda deve carregá-la, pelo mesmo motivo de reprodutibilidade da declaração ML-DSA
O que se pede a um provedor de assinatura
Um provedor de assinatura recebe um THPDFSignatureProviderRequest e retorna um THPDFSignatureProviderStatus mais, em caso de sucesso, os bytes da assinatura. O pedido carrega o algoritmo e o OID dele, o OID do algoritmo de digest, o comprimento do salt PSS, se o Input é uma mensagem (sikMessage) ou um digest pré-calculado (sikDigest), os bytes de entrada, o certificado do signer, um identificador de chave e um identificador de operação único da chamada. Esse é o vocabulário que um driver de token ou um serviço de assinatura já fala, e nada disso exige que a chave atravesse para o seu processo
Cada provedor também anuncia um conjunto de capacidades como spcSign, spcCancel, spcHardwareKey e spcRemoteKey. Várias implementações vêm na biblioteca:
THPDFCallbackSignatureProviderenvolve funções anônimas para verify, sign e cancel, que é a rota mais curta de uma rotina de assinatura interna existente para uma assinatura PDFTHPDFRemoteSignatureProviderenvolve uma função de transporte com limite de retentativas, um registro de cancelamento e tetos de tamanho de entrada e de assinatura, então um serviço remoto mal comportado não consegue empurrar uma resposta gigante para dentro do codificador CMSTHPDFPKCS11SignatureProvidertoma emprestados uma sessão e um handle de chave privada que o seu código já abriu e autenticou. O HotPDF nunca vê o PIN e não gerencia a sessão do token; ele serializa chamadasC_Sign, antepõe o DigestInfo para RSA PKCS#1 v1.5, passa o comprimento real de salt para RSA-PSS por parâmetros de mecanismo, e converte um resultado cru de ECDSAr||sna forma DER que o CMS esperaTHPDFWindowsKeyStorageSignatureProviderassina pelo armazenamento de chaves CNG do Windows, que cobre smart cards e chaves com respaldo de TPM, e pode exigir uma chave com respaldo de hardware
O provedor PKCS#11 recebe digest como entrada, então cobre RSA PKCS#1 v1.5, RSA-PSS e ECDSA. Ed25519 e Ed448 assinam a mensagem completa de atributos assinados, e esses passam pelo provedor callback ou remoto, em que o seu código entrega Req.Input ao token ou serviço como mensagem
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils; certificado DER da chave remota
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService é o seu cliente HTTP próprio: mapeie timeouts e 5xx para
// spsProviderError, uma aprovação recusada para spsCancelled, uma chave desconhecida para spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit: até quatro tentativas no total
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf foi salvo com um placeholder do AddSignedSignatureField
InStream := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
OutStream := TFileStream.Create('contract-signed.pdf', fmCreate);
try
HPDFCMSSignPDFStreamWithProvider(InStream, OutStream, CertDER,
Provider, 'contracts-2026', Opts);
finally
OutStream.Free;
InStream.Free;
Provider.Free;
end;
end;
Seis status, e o que cada um deveria disparar
O THPDFSignatureProviderStatus tem seis valores porque a reação certa difere para cada um. spsInvalid é uma falha criptográfica e pertence a um log de segurança. spsUnsupported significa que o provedor não implementa o algoritmo, o que é um problema de implantação. spsMalformed significa que o pedido ou a resposta tinha a forma errada. spsProviderError é uma falha de transporte ou de dispositivo que pode dar certo na retentativa. spsCancelled significa que uma pessoa recusou o prompt do token, e repetir só a irrita
O provedor remoto embute essa política. Ele só repete em spsProviderError, confere o registro de cancelamento antes de cada tentativa, rejeita entrada vazia ou gigante como spsMalformed antes de chamar o transporte, e rebaixa uma resposta "válida" com assinatura vazia ou gigante para spsMalformed. No lado da verificação, a resposta de um provedor mapeia para o status de verificação: spsInvalid vira svSignatureInvalid, spsMalformed vira svMalformed, spsProviderError e spsCancelled viram ambos svCryptoProviderError, e spsUnsupported vira svUnsupportedAlgorithm depois que o provedor de plataforma também recusou o pedido
Se você escreve um provedor, resista a mapear toda condição inesperada para spsInvalid. Isso transforma uma DLL ausente ou um token de serviço expirado num relato de que a assinatura do cliente é forjada, e alguém vai ter que explicar isso ao cliente. Retorne spsInvalid só quando a matemática de fato falhou. Para assinar, retorne spsValid só junto de uma assinatura não vazia; os dois pontos de entrada levantam EHPDFCMSError quando um provedor retorna sucesso sem bytes
Onde a assinatura do provedor pousa no arquivo
Duas funções conectam um provedor a bytes PDF reais. O HPDFCMSBuildSignedDataWithProvider constrói CMS desanexado a partir de um digest de documento que você calculou em outro lugar; o comprimento do digest tem que bater com Options.DigestAlgorithm, então uma configuração SHA-384 espera 48 bytes e rejeita um digest SHA-256 de 32 bytes com exceção. O HPDFCMSSignPDFStreamWithProvider assina o placeholder que o AddSignedSignatureField dispôs, faz hash das duas janelas do /ByteRange, pede ao provedor a assinatura sobre os atributos assinados, e remenda o resultado hexadecimal no /Contents. Se o CMS cresce além do placeholder, a chamada levanta exceção e restaura o placeholder não assinado no stream de saída em vez de truncar qualquer coisa
O layout é o mesmo que o caminho de assinatura baseado em PFX grava: duas faixas que pulam a janela hexadecimal da assinatura. O verifier cobra essa forma de todo arquivo, conferindo que o vão entre as faixas contém exatamente a string /Contents desta assinatura e nada mais, e reportando svInvalidByteRange caso contrário. Um documento assinado por um token PKCS#11 por isso verifica com o mesmo código de verificação de assinatura que um assinado a partir de um arquivo PFX. As regras de perfil que ficam acima da escolha de algoritmo são cobertas em assinaturas baseline PAdES em Delphi, e as armadilhas de encoding ECDSA com que a conversão PKCS#11 lida estão em verificação CMS ECDSA e formatos de assinatura P1363
Implantando assinaturas pós-quânticas sem abandonar leitores
Um leitor de PDF que não implementa ML-DSA reporta tal assinatura como inverificável, e a pessoa olhando o documento vê um aviso em vez de um visto verde. Isso torna a migração um problema de cronograma. Mantenha RSA ou ECDSA como a assinatura que os validators de hoje vão julgar. Onde uma política exige evidência resistente a quânticos, declare a extensão com o EnsureMLDSAExtensions e adicione uma segunda assinatura produzida por um signer capaz de ML-DSA ao lado da clássica. Essa assinatura vem do seu próprio signer FIPS 204, já que o construtor CMS do HotPDF não cria assinaturas ML-DSA; a parte da biblioteca é verificá-la. Mude a assinatura primária só quando os sistemas que consomem seus documentos puderem verificá-la
Nesse plano o HotPDF cobre as duas pontas que você controla a partir do Delphi. Os construtores CMS dele assinam com RSA PKCS#1 v1.5, RSA-PSS, ECDSA, Ed25519 e Ed448 por qualquer dos provedores acima, e o verifier dele confere ML-DSA, EdDSA e os algoritmos clássicos numa única chamada, com o OID de fato encontrado registrado no resultado. O HotPDF é um componente VCL de PDF nativo para Delphi e C++Builder que não precisa de runtime PDF externo, então o código de assinatura e verificação compila dentro do seu executável. Edições, o conjunto de recursos de assinatura e um download de avaliação estão na página do componente HotPDF para Delphi