Artículo técnico

Chequeos de política ISO/TS 32002 ECDSA y EdDSA en PDFium

El PDFium Component for Delphi chequea cada firma PDF ECDSA y EdDSA contra el perfil de algoritmos ISO/TS 32002 y reporta el veredicto en TPadesSignatureValidation.AlgorithmPolicyStatus. Solo P-256, P-384, P-521, las tres curvas Brainpool r1, Ed25519 y Ed448 pueden pasar, cada una con un digest que la acompañe, y un desajuste levanta ppeiSignatureAlgorithmMismatch. Esa política importa más de lo que suena. Una firma sobre brainpoolP160r1, o una llave P-256 firmando un digest SHA-512, puede verificar perfectamente bien a nivel matemático, así que Windows CryptoAPI reporta el valor de la firma como bueno mientras un validador estricto de PDF 2.0 rechaza el archivo. El chequeo de política cierra esa brecha, y está deliberadamente separado de la pregunta de si los bytes de la firma son criptográficamente correctos

¿Qué permite realmente ISO/TS 32002 para firmas de curva elíptica?

ISO/TS 32002 permite exactamente seis curvas ECDSA y dos esquemas EdDSA en firmas PDF, y ata cada curva a los tamaños de digest que puede llevar. El PDFium Component codifica esa tabla en PadesCurveDigestAllowed, indexada por el OID de curva del certificado firmante. Las curvas NIST son estrictas: el digest debe tener el mismo ancho de bits que la curva, sea SHA-2 o SHA-3. Las curvas Brainpool son más flexibles y aceptan su propio ancho o cualquier cosa más ancha:

  • P-256 (1.2.840.10045.3.1.7): solo SHA-256 o SHA3-256
  • P-384 (1.3.132.0.34): solo SHA-384 o SHA3-384
  • P-521 (1.3.132.0.35): solo SHA-512 o SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): cualquier digest SHA-2 o SHA-3 de 256 a 512 bits
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 de 384 o 512 bits
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): solo SHA-512 o SHA3-512
Matriz del perfil de algoritmos ISO TS 32002 en PDFium Component: P-256, P-384 y P-521 aceptan solo anchos de digest coincidentes en PadesCurveDigestAllowed, brainpoolP256r1 acepta 256 a 512 bits, brainpoolP384r1 acepta 384 y 512, Ed25519 y Ed448 declaran SHA-512 y SHAKE256 con largo 512, y los desajustes levantan ppeiSignatureAlgorithmMismatch mientras las curvas fuera del perfil devuelven pcsUnsupported
Seis curvas ECDSA y dos esquemas EdDSA pueden pasar, y cada curva está atada a los anchos de digest que puede llevar; todo lo demás es inválido o no soportado, jamás aceptado en silencio

EdDSA no tiene elección de curva ni de digest, que es exactamente por qué sus reglas van de codificación y no de fuerza. Por RFC 8419, un SignerInfo Ed25519 debe declarar SHA-512 como su digestAlgorithm sin parámetros, y un SignerInfo Ed448, en el camino de signed-attributes que PAdES siempre usa, debe declarar id-shake256-len (2.16.840.1.101.3.4.2.18) con un parámetro INTEGER de exactamente 512. Para ambos esquemas el AlgorithmIdentifier de la firma y el AlgorithmIdentifier de la llave pública del certificado no deben llevar parámetro alguno. Un productor que escribe un NULL ahí, el hábito que los encoders de RSA le inculcaron a muchas librerías ASN.1, produce una firma no conforme aun cuando la llave y el valor de la firma están bien

Cómo el PDFium Component extrae la tripleta de algoritmos del CMS

PDFium por sí solo no puede responder esta pregunta, porque su API público de firmas lee el diccionario de firma pero ni verifica el CMS ni expone la curva del certificado firmante. La capa de inspección PAdES construida sobre PDFium parsea entonces por su cuenta el CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm lee el digestAlgorithm y el signatureAlgorithm del primer SignerInfo, después encuentra el certificado firmante y lee su SubjectPublicKeyInfo para obtener el algoritmo de llave y la curva. La búsqueda del certificado está deliberadamente acotada: se examinan a lo más 64 certificados del set certificates del CMS, el match es una comparación exacta de bytes de emisor y número de serie desde issuerAndSerialNumber, y el código cae a "el único certificado que hay" solo cuando el set contiene exactamente un certificado parseable. Elegir el primer certificado EC de un set sin orden sería fácil, y dejaría que un certificado de CA decidiera qué curva supuestamente usó el firmante

Cómo el PDFium Component inspecciona la tripleta de algoritmos de una firma PDF: InspectPadesSignatureAlgorithm lee el digestAlgorithm y el signatureAlgorithm del primer SignerInfo del CMS, fija el certificado firmante vía un match exacto de issuerAndSerialNumber entre a lo más 64 candidatos, lee el SubjectPublicKeyInfo para la curva, y EvaluatePadesSignatureAlgorithm devuelve el AlgorithmPolicyStatus
PDFium por sí solo ni verifica el CMS ni expone la curva del firmante, así que la capa PAdES parsea el SignedData y guarda cada OID crudo en el registro para un rechazo explicable
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;

TPdf.ValidatePades aplica la política como parte de su pasada de conformidad, arrancando desde el certificado que localiza dentro del CMS, y TPdf.ValidatePadesTrust la corre de nuevo contra el certificado firmante que Windows CryptoAPI realmente usó para verificar, así que el certificado reportado por CryptoAPI tiene la última palabra. Cada entrada cruda aterriza en TPadesSignatureAlgorithmInfo, incluidos DigestParametersPresent, DigestParameterBits, SignatureParametersPresent y PublicKeyParametersAreNamedCurve, así que un rechazo siempre es explicable desde el registro y no desde una línea de log

¿Por qué una firma P-256 con SHA3-256 falla la política?

Una firma P-256 falla la política del PDFium Component siempre que el digestAlgorithm del CMS y el digest que implica el signatureAlgorithm ECDSA discrepan, aun si ambos son individualmente aceptables para la curva. EvaluatePadesSignatureAlgorithm primero mapea ecdsa-with-SHA256, ecdsa-with-SHA3-256 y sus hermanos a un digest, lo compara con el digestAlgorithm declarado, y devuelve pcsInvalid ante cualquier diferencia antes de consultar la tabla de curvas. El caso es real: una herramienta de firma cambia su hash a SHA3-256 pero conserva un identificador ecdsa-with-SHA256 hard-codeado, y el resultado es un archivo que ningún verificador conforme puede interpretar de forma consistente. La función es pública, así que la matriz puede fijarse en un unit test sin construir un 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 no coincide

  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, ahora congruente
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curva fuera del perfil
end;

La codificación de la curva recibe la misma dureza. RFC 5480 §2.1.1 deja que ECParameters sea un OID de curva nombrada, una curva implícita (NULL) o un set completo de parámetros explícitos, y los perfiles PKIX exigen la forma nombrada. El PDFium Component devuelve pcsInvalid cuando un certificado id-ecPublicKey no lleva parámetros, lleva parámetros implícitos o lleva parámetros explícitos, porque los parámetros explícitos le permiten a un atacante describir una curva que meramente se parece a una estándar. Una curva correctamente nombrada que simplemente falta en la lista ISO/TS 32002, como brainpoolP160r1 arriba o secp256k1, recibe pcsUnsupported en cambio

Inválido, no soportado o indeterminado: leer el estado con honestidad

Los tres estados no válidos de AlgorithmPolicyStatus significan cosas distintas, y colapsarlos en un solo balde de "falló" tira la información que los auditores necesitan. pcsInvalid significa que una combinación de algoritmos reconocida está malformada o desajustada; suma ppeiSignatureAlgorithmMismatch a TPadesValidationResult.Issues y lleva el IntegrityStatus agregado a pcsInvalid, así que IsCryptographicallyValid devuelve False incluso cuando el valor de la firma CMS cuadra. pcsUnsupported significa que la curva o el digest está fuera de lo que el perfil nombra, que es un resultado de capacidad, no prueba de manipulación. pcsIndeterminate significa que no se pudo fijar el certificado firmante, usualmente un CertificateSet con varios candidatos y sin match exacto de issuerAndSerialNumber, así que el código se niega a adivinar la curva; desde la v3.124.0 también marca una firma RSA sobre SHA-1 o un digest de 112 bits como SHA-224, que ya no está acordado para validación actual. La misma división aplica a EdDSA en una máquina cuyo CryptoAPI no puede verificar Ed25519 ni Ed448: CmsSignatureStatus se queda en pcsUnsupported mientras AlgorithmPolicyStatus puede seguir siendo pcsValid, porque la codificación era correcta y solo faltó el verificador. Si anda tras un rechazo de Adobe o de un validador basado en DSS, la guía de por qué los validadores rechazan firmas PAdES cubre las demás causas comunes

Camino de decisión de EvaluatePadesSignatureAlgorithm en PDFium Component: un desajuste de digest fija pcsInvalid y ppeiSignatureAlgorithmMismatch, una curva nombrada fuera del perfil ISO TS 32002 como brainpoolP160r1 fija pcsUnsupported, un certificado firmante que no se puede fijar fija pcsIndeterminate, y desde la v3.124.0 la rama RSA aplica los suites de digest ETSI TS 119 312, dejando el tamaño de llave todavía en manos de la aplicación
Los tres estados no válidos significan cosas distintas: inválido es prueba de una combinación rota, no soportado es un resultado de capacidad, e indeterminado significa que el código se negó a adivinar
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, sin revocación
    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;

¿Qué no garantiza pcsValid?

AlgorithmPolicyStatus = pcsValid solo certifica que una firma ECDSA o EdDSA usa una curva aprobada con un digest congruente y correctamente codificado, y que una firma RSA usa un digest de un suite actual; no dice nada sobre si el valor de la firma es correcto. Antes de la v3.124.0 la rama RSA de EvaluatePadesSignatureAlgorithm era deliberadamente ancha: cualquier signatureAlgorithm bajo el arco PKCS #1 1.2.840.113549.1.1.* devolvía pcsValid, incluido el legado sha1WithRSAEncryption. Desde PDFiumPas v3.124.0 la rama RSA aplica los suites de firma ETSI TS 119 312. Los digests MD2, MD4 y MD5 son pcsInvalid. SHA-1 y los digests de 112 bits como SHA-224 son pcsIndeterminate, así que una firma SHA-1 conserva su resultado global de integridad y queda marcada para revisión en lugar de rechazada. Un digestAlgorithm que difiere del digest fijado por el algoritmo de firma, como sha256WithRSAEncryption sobre un digest SHA-1, o un algoritmo de firma RSA sobre una llave de firmante no RSA, es pcsInvalid, se reporta como ppeiSignatureAlgorithmMismatch y falla la integridad. Un certificado firmante que no se encuentra da pcsIndeterminate, como ya pasaba para ECDSA, y los digests no reconocidos u los OIDs RSA que no son de firma dan pcsUnsupported. El largo del módulo sigue sin chequearse, los parámetros PSS no se validan aquí (el artículo de RSASSA-PSS-params de RFC 4055 cubre cómo se codifican del lado de firma), y los digests SHA-1 o MD5 adicionalmente levantan el issue aparte ppeiBadDigestAlgorithm. Igualmente, la matemática de la firma, la cadena de certificados y la revocación siguen siendo trabajo de CmsSignatureStatus, CertificateTrustStatus y RevocationStatus, que vienen de Windows CryptoAPI. Trate pcsValid como "el perfil de algoritmos se cumple", jamás como "esta llave es suficientemente fuerte"

Para una aplicación Delphi que acepta facturas, contratos o paquetes de archivo PDF firmados, la configuración práctica es corta: corra ValidatePadesTrust, rechace ante ppeiSignatureAlgorithmMismatch, derive pcsUnsupported y pcsIndeterminate a un humano, y imponga su propio piso de tamaño de llave RSA porque la política no lo hará. El PDFium Component for Delphi and Lazarus trae el validador PAdES, el constructor de reportes de evidencia y el pipeline de firma, así que la misma librería puede producir estas firmas y chequearlas de punta a punta