O PDFium Component para Delphi checa toda assinatura PDF ECDSA e EdDSA contra o perfil de algoritmos da ISO/TS 32002 e reporta o veredito em TPadesSignatureValidation.AlgorithmPolicyStatus. Só P-256, P-384, P-521, as três curvas Brainpool r1, Ed25519 e Ed448 podem passar, cada uma com um digest que bata, e uma divergência levanta ppeiSignatureAlgorithmMismatch. Essa política importa mais do que parece. Uma assinatura em brainpoolP160r1, ou uma chave P-256 assinando um digest SHA-512, verifica perfeitamente bem a nível matemático, então a Windows CryptoAPI reporta o valor da assinatura como bom enquanto um validador estrito de PDF 2.0 rejeita o arquivo. A checagem de política fecha esse vão, e ela é deliberadamente separada da questão de se os bytes da assinatura são criptograficamente corretos
O que a ISO/TS 32002 realmente permite para assinaturas de curva elíptica?
A ISO/TS 32002 permite exatamente seis curvas ECDSA e dois esquemas EdDSA em assinaturas PDF, e amarra cada curva aos tamanhos de digest que ela pode carregar. O PDFium Component codifica essa tabela no PadesCurveDigestAllowed, indexada pelo OID da curva do certificado do signatário. As curvas NIST são rígidas: o digest precisa ter a mesma largura em bits da curva, seja SHA-2 ou SHA-3. As curvas Brainpool são mais frouxas e aceitam a própria largura delas ou qualquer coisa mais larga:
- P-256 (
1.2.840.10045.3.1.7): só SHA-256 ou SHA3-256 - P-384 (
1.3.132.0.34): só SHA-384 ou SHA3-384 - P-521 (
1.3.132.0.35): só SHA-512 ou SHA3-512 - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7): qualquer digest SHA-2 ou SHA-3 de 256 a 512 bits - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 de 384 ou 512 bits - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): só SHA-512 ou SHA3-512
EdDSA não tem escolha de curva nem de digest, e é exatamente por isso que as regras dela são sobre encoding em vez de força. Conforme o RFC 8419, um SignerInfo Ed25519 precisa declarar SHA-512 como seu digestAlgorithm sem parâmetros, e um SignerInfo Ed448, no caminho de signed-attributes que o PAdES sempre usa, precisa declarar id-shake256-len (2.16.840.1.101.3.4.2.18) com um parâmetro INTEGER de exatamente 512. Para os dois esquemas o AlgorithmIdentifier da assinatura e o AlgorithmIdentifier da chave pública do certificado não podem carregar parâmetro algum. Um produtor que grava um NULL aí — o hábito que os encoders RSA treinaram em muitas bibliotecas ASN.1 — produz uma assinatura fora de conformidade mesmo que a chave e o valor da assinatura estejam bons
Como o PDFium Component extrai a tripla de algoritmos do CMS
O PDFium em si não consegue responder essa pergunta, porque a API pública de assinaturas dele lê o dicionário de assinatura mas nem verifica o CMS nem expõe a curva do certificado do signatário. A camada de inspeção PAdES construída sobre o PDFium portanto parseia o CMS SignedData (RFC 5652) por conta própria. O InspectPadesSignatureAlgorithm lê o digestAlgorithm e o signatureAlgorithm do primeiro SignerInfo, depois acha o certificado do signatário e lê o SubjectPublicKeyInfo dele para obter o algoritmo da chave e a curva. A busca do certificado é deliberadamente limitada: no máximo 64 certificados do conjunto certificates do CMS são examinados, o casamento é uma comparação exata de bytes de issuer e número de série do issuerAndSerialNumber, e o código só cai para "o único certificado que existe" quando o conjunto contém exatamente um certificado parseável. Pegar o primeiro certificado EC de um conjunto sem ordem seria fácil, e deixaria um certificado de CA decidir qual curva o signatário supostamente usou
uses
PDFium, FPdfPades;
const
StatusNames: array[TPadesCryptoStatus] of string =
('not checked', 'valid', 'invalid', 'unsupported', 'indeterminate');
var
Pdf: TPdf;
R: TPadesValidationResult;
A: TPadesSignatureAlgorithmInfo;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
for I := 0 to Length(R.Signatures) - 1 do
begin
A := R.Signatures[I].AlgorithmInfo;
Writeln('Signature ', I);
Writeln(' digestAlgorithm : ', string(A.DigestAlgorithmOid));
Writeln(' signatureAlgorithm : ', string(A.SignatureAlgorithmOid));
Writeln(' public key / curve : ', string(A.PublicKeyAlgorithmOid),
' / ', string(A.CurveOid));
Writeln(' policy : ',
StatusNames[R.Signatures[I].AlgorithmPolicyStatus]);
end;
if ppeiSignatureAlgorithmMismatch in R.Issues then
Writeln('At least one signature violates the ISO/TS 32002 profile');
finally
Pdf.Free;
end;
end;
O TPdf.ValidatePades aplica a política como parte da passada de conformidade dele, a partir do certificado que localiza dentro do CMS, e o TPdf.ValidatePadesTrust a roda de novo contra o certificado do signatário que a Windows CryptoAPI de fato usou na verificação, então o certificado reportado pela CryptoAPI tem a palavra final. Toda entrada crua vai parar no TPadesSignatureAlgorithmInfo, incluindo DigestParametersPresent, DigestParameterBits, SignatureParametersPresent e PublicKeyParametersAreNamedCurve, então uma rejeição é sempre explicável a partir do registro em vez de uma linha de log
Por que uma assinatura P-256 com SHA3-256 falha na política?
Uma assinatura P-256 falha na política do PDFium Component sempre que o digestAlgorithm do CMS e o digest implícito no signatureAlgorithm ECDSA divergem, mesmo que ambos sejam individualmente aceitáveis para a curva. O EvaluatePadesSignatureAlgorithm primeiro mapeia ecdsa-with-SHA256, ecdsa-with-SHA3-256 e seus irmãos para um digest, compara com o digestAlgorithm declarado, e devolve pcsInvalid ante qualquer diferença antes que a tabela de curvas seja consultada. O caso é real: uma ferramenta de assinatura troca o hash para SHA3-256 mas mantém um identificador ecdsa-with-SHA256 hard-coded, e o resultado é um arquivo que nenhum verificador conforme consegue interpretar de forma consistente. A função é pública, então a matriz pode ser fixada num teste unitário sem construir um PDF:
var
Info: TPadesSignatureAlgorithmInfo;
begin
Info := Default(TPadesSignatureAlgorithmInfo);
Info.Family := psafEcdsa;
Info.PublicKeyAlgorithmOid := '1.2.840.10045.2.1'; // id-ecPublicKey
Info.PublicKeyParametersPresent := True;
Info.PublicKeyParametersAreNamedCurve := True;
Info.CurveOid := '1.2.840.10045.3.1.7'; // P-256
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.8'; // SHA3-256
Info.SignatureAlgorithmOid := '2.16.840.1.101.3.4.3.10'; // ecdsa-with-SHA3-256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsValid);
Info.SignatureAlgorithmOid := '1.2.840.10045.4.3.2'; // ecdsa-with-SHA256
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsInvalid); // digest divergente
Info.CurveOid := '1.3.36.3.3.2.8.1.1.1'; // brainpoolP160r1
Info.DigestAlgorithmOid := '2.16.840.1.101.3.4.2.1'; // SHA-256, agora congruente
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curva fora do perfil
end;
A codificação da curva recebe a mesma rigidez. A RFC 5480 §2.1.1 deixa o ECParameters ser um OID de curva nomeada, uma curva implícita (NULL) ou um conjunto completo de parâmetros explícitos, e os perfis PKIX exigem a forma nomeada. O PDFium Component devolve pcsInvalid quando um certificado id-ecPublicKey carrega sem parâmetros, parâmetros implícitos ou parâmetros explícitos, porque parâmetros explícitos deixam um atacante descrever uma curva que meramente se parece com uma padrão. Uma curva corretamente nomeada que simplesmente não está na lista da ISO/TS 32002, como o brainpoolP160r1 acima ou o secp256k1, recebe pcsUnsupported em vez
Inválido, não suportado ou indeterminado: lendo o status com honestidade
Os três status não válidos do AlgorithmPolicyStatus significam coisas diferentes, e colapsá-los num único balde de "falhou" joga fora a informação de que os auditores precisam. pcsInvalid significa que uma combinação de algoritmos reconhecida está malformada ou divergente; ele adiciona ppeiSignatureAlgorithmMismatch ao TPadesValidationResult.Issues e leva o IntegrityStatus agregado para pcsInvalid, então o IsCryptographicallyValid devolve False mesmo quando o valor da assinatura CMS confere. pcsUnsupported significa que a curva ou o digest está fora do que o perfil nomeia, o que é um resultado de capacidade, não prova de adulteração. pcsIndeterminate significa que o certificado do signatário não pôde ser fixado, normalmente um CertificateSet com vários candidatos e nenhum casamento exato de issuerAndSerialNumber, então o código se recusa a adivinhar a curva; desde a v3.124.0 ele também marca uma assinatura RSA sobre SHA-1 ou um digest de 112 bits como SHA-224, que não é mais aceito para validação atual. A mesma divisão vale para EdDSA numa máquina cuja CryptoAPI não verifica Ed25519 nem Ed448: o CmsSignatureStatus fica pcsUnsupported enquanto o AlgorithmPolicyStatus ainda pode ser pcsValid, porque o encoding estava correto e só o verificador faltou. Se você está caçando uma rejeição do Adobe ou de um validador baseado em DSS, o guia de por que validadores rejeitam assinaturas PAdES cobre as outras causas comuns
var
Pdf: TPdf;
Options: TPadesTrustValidationOptions;
R: TPadesValidationResult;
S: TPadesSignatureValidation;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice-ecdsa-signed.pdf';
Pdf.Active := True;
Options := TPadesTrustValidationOptions.Default; // offline, sem revocação
R := Pdf.ValidatePadesTrust(Options);
for I := 0 to Length(R.Signatures) - 1 do
begin
S := R.Signatures[I];
if S.IsDocumentTimeStamp then
Continue;
case S.AlgorithmPolicyStatus of
pcsInvalid: Writeln(I, ': reject, algorithm combination is invalid');
pcsUnsupported: Writeln(I, ': manual review, curve or digest outside the profile');
pcsIndeterminate: Writeln(I, ': manual review, signer not identified or digest no longer agreed');
pcsValid:
if S.AlgorithmInfo.Family = psafRsa then
Writeln(I, ': RSA digest suite accepted, check key size yourself')
else
Writeln(I, ': EC/EdDSA profile satisfied');
end;
end;
Writeln('Cryptographically valid: ', R.IsCryptographicallyValid);
finally
Pdf.Free;
end;
end;
O que o pcsValid não garante?
AlgorithmPolicyStatus = pcsValid só certifica que uma assinatura ECDSA ou EdDSA usa uma curva aprovada com um digest congruente e corretamente codificado, e que uma assinatura RSA usa um digest de uma suíte atual; nada diz sobre se o valor da assinatura está correto. Antes da v3.124.0 o ramo RSA do EvaluatePadesSignatureAlgorithm era deliberadamente largo: qualquer signatureAlgorithm sob o arco PKCS #1 1.2.840.113549.1.1.* devolvia pcsValid, incluindo o legado sha1WithRSAEncryption. Desde o PDFiumPas v3.124.0 o ramo RSA aplica as suítes de assinatura da ETSI TS 119 312. Digests MD2, MD4 e MD5 são pcsInvalid. SHA-1 e digests de 112 bits como SHA-224 são pcsIndeterminate, então uma assinatura SHA-1 mantém o resultado agregado de integridade e é marcada para revisão em vez de rejeitada. Um digestAlgorithm que difere do digest fixado pelo algoritmo de assinatura, como sha256WithRSAEncryption sobre um digest SHA-1, ou um algoritmo de assinatura RSA numa chave de signatário não RSA, é pcsInvalid, reportado como ppeiSignatureAlgorithmMismatch e derruba a integridade. Um certificado de signatário que não é encontrado dá pcsIndeterminate, como já dava para ECDSA, e digests não reconhecidos ou OIDs RSA que não são de assinatura dão pcsUnsupported. O comprimento do módulo continua sem checagem, os parâmetros PSS não são validados aqui (o artigo sobre RSASSA-PSS-params da RFC 4055 cobre como eles são codificados no lado da assinatura), e digests SHA-1 ou MD5 adicionalmente levantam a questão separada ppeiBadDigestAlgorithm. Da mesma forma, a matemática da assinatura, a cadeia de certificados e a revocação continuam trabalho do CmsSignatureStatus, do CertificateTrustStatus e do RevocationStatus, que vêm da Windows CryptoAPI. Trate pcsValid como "o perfil de algoritmos se sustenta", nunca como "esta chave é forte o bastante"
Para uma aplicação Delphi que aceita faturas PDF, contratos ou pacotes de arquivamento assinados, a configuração prática é curta: rode o ValidatePadesTrust, rejeite em ppeiSignatureAlgorithmMismatch, encaminhe pcsUnsupported e pcsIndeterminate para um humano, e imponha seu próprio piso de tamanho de chave RSA porque a política não vai impor. O PDFium Component para Delphi e Lazarus traz o validador PAdES, o construtor de relatório de evidências e o pipeline de assinatura, então a mesma biblioteca pode produzir essas assinaturas e checá-las de ponta a ponta