O HotPDF verifica assinaturas CMS ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519 e Ed448 em documentos PDF carregados, e assina através de providers plugáveis para que a chave privada nunca tenha de viver dentro do seu processo Delphi. Essa segunda metade é a parte que a maioria das equipas precisa primeiro. Um token de hardware, um serviço de assinatura remoto e um cartão de eID nacional recusam-se todos a entregar uma chave, e até o pipeline de assinatura estar separado do arquivo de chaves, nenhum deles pode ser usado de todo
A separação é o ponto de THPDFSignatureProvider. O HotPDF fica com as partes que lhe devem pertencer — analisar CMS, construir SignedData, dispor o /ByteRange — e delega a única operação que não pode ter, que é transformar um digest numa assinatura com uma chave que não lhe é permitido ver. Tudo o que se segue decorre dessa divisão
Porque razão é que uma assinatura ML-DSA válida falha ao verificar?
Porque o HotPDF recusa ML-DSA num documento carregado que não declare a extensão para tal. ML-DSA — o esquema de assinatura em reticulado normalizado como FIPS 204, e a razão pela qual se diz "PDF pós-quântico" — ainda não tem registo ISO 32000-2. Um PDF que transporta uma está a usar um algoritmo que a norma base não nomeia, e um ficheiro que silenciosamente usa um algoritmo não nomeado é um ficheiro cujo veredito não pode ser reproduzido por mais ninguém
Pelo que o HotPDF torna a reclamação explícita. EnsureMLDSAExtensions eleva o documento para PDF 2.0 quando permitido e escreve /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> no Catálogo. No lado da leitura, LoadedDocumentDeclaresMLDSAExtension reporta se essa declaração sobreviveu, e VerifyLoadedSignatureWithOptions aplica o mesmo teste antes de honrar Options.AllowMLDSA. Defina o sinalizador num documento não declarado e ele fica desligado — a opção pode aliviar política, nunca o requisito estrutural
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;
Chame antes de guardar, não depois. A declaração faz parte do intervalo de bytes assinado, e um Catálogo remendado depois é ou uma alteração não assinada a um ficheiro assinado ou uma segunda revisão que um validador reportará como modificação
Três famílias de algoritmos, um ponto de entrada de verificação
Todas as três famílias chegam através de VerifyLoadedSignatureWithOptions, que recebe um índice de assinatura, o stream de origem, um registo THPDFCMSVerifyOptions e um parâmetro de saída para os detalhes da assinatura. O registo tem exatamente três campos, e cada um responde a uma pergunta que costumava exigir uma recompilação
SignatureProvider substitui o seu próprio provider pelo da plataforma embutido. OpenSSLLibraryPath seleciona uma biblioteca OpenSSL 3, que é o que fornece a verificação Ed25519 e Ed448 em modo puro que o Windows CNG não oferece em todo o lado. AllowMLDSA opta pelos algoritmos em reticulado, sujeito à verificação de extensão acima. O OID exato do algoritmo que foi reconhecido volta em THPDFSignatureInfo.SignatureAlgorithmOID, pelo que um registo de auditoria consegue registar o que foi verificado em vez do que foi pedido
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 e Ed448 não precisam de declaração de extensão, porque o ISO 32000-2 já os admite. Precisam de um provider que os implemente, o que na maioria das implementações Windows significa apontar OpenSSLLibraryPath para uma biblioteca que o programador distribui e controla em vez de para a que por acaso estiver na máquina
O que promete efetivamente um provider de assinatura?
Um provider promete uma coisa: dado um pedido, devolver um estado e, ao assinar, bytes. THPDFSignatureProviderRequest transporta o algoritmo e o seu OID, o OID do digest, o comprimento do salt PSS, se a entrada é uma mensagem ou um digest já calculado, a própria entrada, a chave pública ou certificado, um identificador de chave e um identificador de operação. Nada neste registo é específico do HotPDF — é o vocabulário que um driver de token ou um serviço de assinatura já fala
Três implementações vêm com a biblioteca. THPDFCallbackSignatureProvider envolve métodos anónimos, que é o caminho mais curto de uma rotina de assinatura interna existente para uma assinatura PDF funcional. THPDFRemoteSignatureProvider envolve um callback de transporte com um limite de tentativas, um registo de cancelamento e limites no tamanho de entrada e assinatura, pelo que um HSM bloqueado não se torna numa aplicação bloqueada. THPDFPKCS11SignatureProvider serializa operações RSA contra uma sessão PKCS#11 detida pelo chamador e já autenticada e um manípulo de chave privada — o HotPDF nunca inicia sessão, nunca vê um PIN, e nunca fecha uma sessão que não abriu
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;
Porque razão o enumerado de estado tem seis valores em vez de um booleano
THPDFSignatureProviderStatus distingue spsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError e spsCancelled, e colapsá-los custa-lhe a capacidade de agir corretamente. Uma assinatura criptograficamente errada (spsInvalid) é um evento de segurança. Um algoritmo que o provider não implementa (spsUnsupported) é uma lacuna de implantação. Uma falha de transporte (spsProviderError) vale a pena tentar novamente, e um pedido de token cancelado pelo utilizador (spsCancelled) não vale a pena tentar de todo
A regra para assinar é estreita: um provider de assinatura só devolve spsValid com uma assinatura não vazia. Os providers de verificação devolvem spsValid ou spsInvalid, e os outros quatro mantêm-se distintos em ambos os caminhos. Se escrever um provider, resista à tentação de mapear tudo o que não reconhece para spsInvalid — isso transforma uma DLL em falta num relatório de que a assinatura do cliente é forjada
Onde a assinatura vai realmente parar no ficheiro
Duas funções ligam providers a bytes PDF reais. HPDFCMSBuildSignedDataWithProvider constrói CMS destacado de um digest SHA-256 do documento, que é o ponto de entrada certo quando o seu fluxo de trabalho calcula o digest noutro lado. HPDFCMSSignPDFStreamWithProvider assina um marcador de assinatura existente num stream PDF e preserva o pipeline /ByteRange padrão, que é o ponto de entrada certo quando o HotPDF dispôs o marcador ele próprio
Preservar esse pipeline importa mais do que parece. A convenção /ByteRange — dois intervalos que saltam a janela de assinatura em hexadecimal — é o que todos os validadores verificam primeiro, e um caminho baseado em provider que a reescrevesse quebraria a conformidade PAdES por mais sólida que fosse a criptografia. O HotPDF mantém a disposição idêntica ao caminho de assinatura embutido, pelo que um documento assinado através de um token PKCS#11 verifica com o mesmo código de verificação de assinaturas que um assinado a partir de um ficheiro PFX. Para as regras de perfil que estão acima da escolha de algoritmo, veja o guia de assinaturas de base PAdES em Delphi, e para as armadilhas de codificação específicas de ECDSA que antecedem este modelo de provider, as notas sobre verificação CMS ECDSA e formatos de assinatura P1363
Uma ordem de migração que não deixa os seus documentos encalhados
A prontidão pós-quântica é um problema de calendário, não um interruptor. Quase nenhum visualizador PDF implementado valida ML-DSA hoje, pelo que um documento assinado apenas com ele é, do ponto de vista do leitor, um documento com uma assinatura impossível de verificar. A ordem que sobrevive ao contacto com arquivos reais é: manter RSA ou ECDSA como a assinatura que um validador vai julgar, adicionar a declaração de extensão e uma segunda assinatura ML-DSA onde uma política exija evidência resistente à quântica, e mudar a assinatura primária apenas quando os sistemas consumidores tiverem acompanhado
O que o HotPDF lhe dá hoje é a capacidade de escrever e verificar ambas, a partir do mesmo código, com o algoritmo registado honestamente no ficheiro e no resultado da verificação. O HotPDF é um componente PDF VCL nativo para Delphi e C++Builder sem qualquer runtime PDF externo, pelo que os caminhos de assinatura e verificação são distribuídos dentro do seu executável em vez de ao lado dele — veja a página do componente HotPDF para Delphi para a lista completa de funcionalidades e descarregamento de avaliação