El PDFium Component para Delphi comprueba cada firma PDF ECDSA y EdDSA contra el perfil de algoritmos de 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 le corresponda, y un desajuste levanta ppeiSignatureAlgorithmMismatch. Esa política importa más de lo que suena. Una firma sobre brainpoolP160r1, o una clave P-256 firmando un digest SHA-512, puede verificar perfectamente 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. La comprobación de política cierra ese hueco, y está deliberadamente separada de la cuestión 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 del firmante. Las curvas NIST son estrictas: el digest debe tener el mismo ancho en bits que la curva, ya sea SHA-2 o SHA-3. Las curvas Brainpool son más laxas 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
EdDSA no tiene elección de curva ni de digest, que es justo por lo que sus reglas van de codificación y no de fuerza. Según RFC 8419, un SignerInfo Ed25519 debe declarar SHA-512 como su digestAlgorithm sin parámetros, y un SignerInfo Ed448, por 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 clave pública del certificado no deben llevar parámetro alguno. Un productor que ahí escribe un NULL, el hábito que los codificadores RSA han inculcado a muchas bibliotecas ASN.1, produce una firma no conforme aunque la clave y el valor de la firma estén bien
Cómo extrae el PDFium Component el trío de algoritmos del CMS
PDFium por sí solo no puede responder a esta pregunta, porque su API pública de firmas lee el diccionario de firma pero ni verifica el CMS ni expone la curva del certificado del firmante. La capa de inspección PAdES construida sobre PDFium parsea por tanto ella misma el CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm lee el digestAlgorithm y el signatureAlgorithm del primer SignerInfo, luego encuentra el certificado del firmante y lee su SubjectPublicKeyInfo para sacar el algoritmo de clave y la curva. La búsqueda del certificado está deliberadamente acotada: se examinan como máximo 64 certificados del conjunto certificates del CMS, la coincidencia es una comparación exacta de bytes de emisor y número de serie de issuerAndSerialNumber, y el código solo cae a «el único certificado que hay» cuando el conjunto contiene exactamente un certificado parseable. Escoger el primer certificado EC de un conjunto sin orden sería fácil, y dejaría que un certificado de CA decidiera qué curva usó supuestamente el firmante
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 pase de conformidad, partiendo del certificado que localiza dentro del CMS, y TPdf.ValidatePadesTrust la vuelve a ejecutar contra el certificado del firmante que Windows CryptoAPI usó de verdad para verificar, de modo 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 record y no desde una línea de log
¿Por qué una firma P-256 con SHA3-256 suspende la política?
Una firma P-256 suspende la política del PDFium Component siempre que el digestAlgorithm del CMS y el digest que implica el signatureAlgorithm ECDSA discrepen, aunque ambos sean aceptables por separado para la curva. EvaluatePadesSignatureAlgorithm mapea primero 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 a fuego, y el resultado es un archivo que ningún verificador conforme puede interpretar con consistencia. La función es pública, así que la matriz puede fijarse en un test unitario 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 que 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 permite que ECParameters sea un OID de curva con nombre, una curva implícita (NULL) o un juego completo de parámetros explícitos, y los perfiles PKIX exigen la forma con nombre. El PDFium Component devuelve pcsInvalid cuando un certificado id-ecPublicKey llega sin parámetros, con parámetros implícitos o con parámetros explícitos, porque los parámetros explícitos dejan a un atacante describir una curva que meramente se parece a una estándar. Una curva correctamente nombrada que simplemente no está en la lista de ISO/TS 32002, como brainpoolP160r1 arriba o secp256k1, recibe pcsUnsupported en su lugar
Inválido, no soportado o indeterminado: leer el estado con honestidad
Los tres estados no válidos de AlgorithmPolicyStatus significan cosas distintas, y plegarlos en un solo cubo de «fallo» tira la información que los auditores necesitan. pcsInvalid significa que una combinación de algoritmos reconocida está malformada o no coincide; añade ppeiSignatureAlgorithmMismatch a TPadesValidationResult.Issues y arrastra el IntegrityStatus agregado a pcsInvalid, así que IsCryptographicallyValid devuelve False incluso cuando el valor de la firma del CMS verifica bien. pcsUnsupported significa que la curva o el digest quedan 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 del firmante, normalmente un CertificateSet con varios candidatos y sin coincidencia exacta de issuerAndSerialNumber, así que el código se niega a adivinar la curva; desde 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. El mismo reparto vale para EdDSA en una máquina cuya 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 faltaba el verificador. Si andas tras un rechazo de Adobe o de un validador basado en DSS, la guía sobre por qué los validadores rechazan firmas PAdES cubre las otras causas comunes
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; // sin conexión, 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 conjunto actual; no dice nada sobre si el valor de la firma es correcto. Antes de 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 conjuntos de firma de 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 de integridad global y queda marcada para revisión en vez de rechazada. Un digestAlgorithm que difiere del digest que fija el algoritmo de firma, como sha256WithRSAEncryption sobre un digest SHA-1, o un algoritmo de firma RSA sobre una clave de firmante no RSA, es pcsInvalid, se reporta como ppeiSignatureAlgorithmMismatch y suspende la integridad. Un certificado de firmante que no se encuentra da pcsIndeterminate, como ya hacía para ECDSA, y los digests no reconocidos o los OID RSA que no son de firma dan pcsUnsupported. La longitud del módulo sigue sin comprobarse, los parámetros PSS no se validan aquí (el artículo sobre RSASSA-PSS-params del RFC 4055 cubre cómo se codifican en el lado de firma), y los digests SHA-1 o MD5 levantan además 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. Trata pcsValid como «el perfil de algoritmos se sostiene», nunca como «esta clave es bastante fuerte»
Para una aplicación Delphi que acepta facturas, contratos o paquetes de archivo PDF firmados, la configuración práctica es corta: ejecuta ValidatePadesTrust, rechaza ante ppeiSignatureAlgorithmMismatch, manda pcsUnsupported y pcsIndeterminate a una persona, y aplica tu propio suelo de tamaño de clave RSA porque la política no lo hará. El PDFium Component para Delphi y Lazarus trae el validador PAdES, el constructor de informes de evidencia y el pipeline de firma, así que la misma biblioteca puede producir estas firmas y comprobarlas de extremo a extremo