O PDFium Component para Delphi verifica cada assinatura PDF ECDSA e EdDSA contra o perfil de algoritmos do ISO/TS 32002 e comunica o veredicto 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 correspondente, e uma discrepância dispara ppeiSignatureAlgorithmMismatch. Essa política importa mais do que parece. Uma assinatura em brainpoolP160r1, ou uma chave P-256 a assinar um digest SHA-512, pode verificar perfeitamente bem ao nível da matemática, por isso o Windows CryptoAPI reporta o valor da assinatura como bom enquanto um validador estrito de PDF 2.0 rejeita o ficheiro. A verificação de política fecha esse vão, e é deliberadamente separada da questão de saber se os bytes da assinatura estão criptograficamente corretos
O que permite realmente o ISO/TS 32002 para assinaturas de curvas elípticas?
O ISO/TS 32002 permite exatamente seis curvas ECDSA e dois esquemas EdDSA em assinaturas PDF, e amarra cada curva aos tamanhos de digest que pode transportar. O PDFium Component codifica essa tabela no PadesCurveDigestAllowed, indexada pelo OID da curva do certificado do assinante. As curvas NIST são estritas: o digest tem de ter a mesma largura em bits que a curva, SHA-2 ou SHA-3. As curvas Brainpool são mais folgadas e aceitam a sua própria largura 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
O EdDSA não tem escolha de curva nem de digest, e é precisamente por isso que as suas regras são sobre codificação e não sobre força. Segundo o RFC 8419, um SignerInfo Ed25519 tem de declarar SHA-512 como o seu digestAlgorithm sem parâmetros, e um SignerInfo Ed448, no caminho de signed attributes que o PAdES usa sempre, tem de declarar id-shake256-len (2.16.840.1.101.3.4.2.18) com um parâmetro INTEGER de exatamente 512. Para ambos os esquemas o AlgorithmIdentifier da assinatura e o AlgorithmIdentifier da chave pública do certificado não podem transportar parâmetro nenhum. Um produtor que escreva aí um NULL, o hábito que os codificadores RSA treinaram em muitas bibliotecas ASN.1, produz uma assinatura não conforme mesmo que a chave e o valor da assinatura estejam bem
Como o PDFium Component extrai o triplo de algoritmos do CMS
O PDFium em si não consegue responder a esta pergunta, porque a sua API pública de assinaturas lê o dicionário de assinatura mas nem verifica o CMS nem expõe a curva do certificado do assinante. A camada de inspeção PAdES construída sobre o PDFium analisa portanto o CMS SignedData (RFC 5652) ela própria. O InspectPadesSignatureAlgorithm lê o digestAlgorithm e o signatureAlgorithm do primeiro SignerInfo, depois encontra o certificado do assinante e lê o SubjectPublicKeyInfo dele para obter o algoritmo da chave e a curva. A procura do certificado é deliberadamente limitada: no máximo 64 certificados do conjunto certificates do CMS são examinados, a correspondência é uma comparação exata de bytes de issuer e número de série do issuerAndSerialNumber, e o código recua para "o único certificado que lá está" só quando o conjunto tem exatamente um certificado analisável. Escolher o primeiro certificado EC de um conjunto sem ordem seria fácil, e deixaria um certificado de CA decidir que curva o assinante 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 sua passagem de conformidade, a partir do certificado que localiza dentro do CMS, e o TPdf.ValidatePadesTrust corre-a de novo contra o certificado do assinante que o Windows CryptoAPI efetivamente usou para verificar, por isso o certificado reportado pelo CryptoAPI tem a última palavra. Todos os dados brutos aterraram em TPadesSignatureAlgorithmInfo, incluindo DigestParametersPresent, DigestParameterBits, SignatureParametersPresent e PublicKeyParametersAreNamedCurve, por isso uma rejeição é sempre explicável a partir do registo e não de uma linha de log
Porque falha a política uma assinatura P-256 com SHA3-256?
Uma assinatura P-256 falha a política do PDFium Component sempre que o digestAlgorithm do CMS e o digest implícito no signatureAlgorithm ECDSA discordam, mesmo que ambos sejam individualmente aceitáveis para a curva. O EvaluatePadesSignatureAlgorithm primeiro mapeia ecdsa-with-SHA256, ecdsa-with-SHA3-256 e os seus irmãos para um digest, compara isso com o digestAlgorithm declarado, e devolve pcsInvalid a qualquer diferença antes de a tabela de curvas ser consultada. O caso é real: uma ferramenta de assinatura muda o seu hash para SHA3-256 mas mantém um identificador ecdsa-with-SHA256 codificado a ferro, e o resultado é um ficheiro que nenhum verificador conforme consegue interpretar de forma consistente. A função é pública, por isso 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 em discrepância
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 estriteza. O RFC 5480 §2.1.1 deixa o ECParameters ser um OID de curva nomeada, uma curva implícita (NULL) ou um conjunto explícito completo de parâmetros, e os perfis PKIX exigem a forma nomeada. O PDFium Component devolve pcsInvalid quando um certificado id-ecPublicKey transporta parâmetros nenhum, parâmetros implícitos ou parâmetros explícitos, porque parâmetros explícitos deixam um atacante descrever uma curva que apenas se assemelhe a uma padrão. Uma curva corretamente nomeada que simplesmente falhe da lista do ISO/TS 32002, como o brainpoolP160r1 acima ou o secp256k1, recebe antes pcsUnsupported
Inválido, não suportado ou indeterminado: ler o estado com honestidade
Os três estados não válidos do AlgorithmPolicyStatus significam coisas diferentes, e colapsá-los num único balde "falhou" deita fora a informação de que os auditores precisam. pcsInvalid significa que uma combinação de algoritmos reconhecida está malformada ou em discrepância; acrescenta ppeiSignatureAlgorithmMismatch a TPadesValidationResult.Issues e leva o IntegrityStatus agregado a pcsInvalid, por isso 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 assinante não pôde ser fixado, normalmente um CertificateSet com vários candidatos e nenhuma correspondência exata de issuerAndSerialNumber, por isso o código recusa-se a adivinhar a curva; desde a v3.124.0 também marca uma assinatura RSA sobre SHA-1 ou um digest de 112 bits como SHA-224, que já não é acordado para validação atual. A mesma divisão aplica-se ao EdDSA numa máquina cujo CryptoAPI não consegue verificar Ed25519 nem Ed448: o CmsSignatureStatus fica pcsUnsupported enquanto o AlgorithmPolicyStatus pode continuar pcsValid, porque a codificação estava correta e só faltava o verificador. Se está a caçar uma rejeição do Adobe ou de um validador baseado em DSS, o guia de porque é que os 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; não diz nada 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, sha1WithRSAEncryption incluído. Desde o PDFiumPas v3.124.0 que o ramo RSA aplica as suítes de assinatura do 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, por isso uma assinatura SHA-1 mantém o seu resultado global de integridade e é sinalizada para revisão em vez de rejeitada. Um digestAlgorithm que difira do digest fixado pelo algoritmo de assinatura, como sha256WithRSAEncryption sobre um digest SHA-1, ou um algoritmo de assinatura RSA numa chave de assinante não RSA, é pcsInvalid, reportado como ppeiSignatureAlgorithmMismatch e falha a integridade. Um certificado de assinante que não se encontre dá pcsIndeterminate, como já dava para ECDSA, e digests não reconhecidos ou OIDs RSA que não sejam de assinatura dão pcsUnsupported. O comprimento do módulo continua por verificar, os parâmetros PSS não são validados aqui (o artigo sobre RSASSA-PSS-params do RFC 4055 cobre como são codificados no lado da assinatura), e digests SHA-1 ou MD5 levantam adicionalmente o problema separado ppeiBadDigestAlgorithm. Do mesmo modo, a matemática da assinatura, a cadeia de certificados e a revocação continuam a ser trabalho do CmsSignatureStatus, do CertificateTrustStatus e do RevocationStatus, que vêm do Windows CryptoAPI. Trate o pcsValid como "o perfil de algoritmos aguenta-se", nunca como "esta chave é forte o suficiente"
Para uma aplicação Delphi que aceita faturas PDF assinadas, contratos ou pacotes de arquivo, a configuração prática é curta: corra o ValidatePadesTrust, rejeite perante ppeiSignatureAlgorithmMismatch, encaminhe pcsUnsupported e pcsIndeterminate para um humano, e imponha o seu próprio piso de tamanho de chave RSA porque a política não o fará. O PDFium Component para Delphi e Lazarus traz o validador PAdES, o construtor de relatórios de evidência e o pipeline de assinatura, por isso a mesma biblioteca pode produzir estas assinaturas e verificá-las de ponta a ponta