Artigo Técnico

Política ECDSA e EdDSA do ISO/TS 32002 em Delphi com PDFium

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
Matriz do perfil de algoritmos do ISO TS 32002 no PDFium Component: P-256, P-384 e P-521 só aceitam larguras de digest correspondentes no PadesCurveDigestAllowed, brainpoolP256r1 aceita 256 a 512 bits, brainpoolP384r1 aceita 384 e 512, Ed25519 e Ed448 declaram SHA-512 e SHAKE256 com comprimento 512, e discrepâncias disparam ppeiSignatureAlgorithmMismatch enquanto curvas fora do perfil devolvem pcsUnsupported
Seis curvas ECDSA e dois esquemas EdDSA podem passar, e cada curva está amarrada às larguras de digest que pode transportar; todo o resto é inválido ou não suportado, nunca aceite em silêncio

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

Como o PDFium Component inspeciona um triplo de algoritmos de assinatura PDF: o InspectPadesSignatureAlgorithm lê o digestAlgorithm e o signatureAlgorithm do primeiro SignerInfo no CMS, fixa o certificado do assinante através de uma correspondência exata de issuerAndSerialNumber entre no máximo 64 candidatos, lê o SubjectPublicKeyInfo para a curva, e o EvaluatePadesSignatureAlgorithm devolve o AlgorithmPolicyStatus
O PDFium em si nem verifica o CMS nem expõe a curva do assinante, por isso a camada PAdES analisa o SignedData e guarda todos os OIDs brutos no registo para uma rejeição explicável
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

Caminho de decisão do EvaluatePadesSignatureAlgorithm no PDFium Component: uma discrepância de digest fixa pcsInvalid e ppeiSignatureAlgorithmMismatch, uma curva nomeada fora do perfil ISO TS 32002 como brainpoolP160r1 fixa pcsUnsupported, um certificado de assinante que não se consegue fixar fixa pcsIndeterminate, e desde a v3.124.0 o ramo RSA aplica as suítes de digest do ETSI TS 119 312, com o tamanho da chave ainda deixado para a aplicação
Os três estados não válidos significam coisas diferentes: inválido é prova de uma combinação partida, não suportado é um resultado de capacidade, e indeterminado significa que o código recusou-se a adivinhar
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