PDFium Component для Delphi сверяет каждую подпись PDF на ECDSA и EdDSA с профилем алгоритмов ISO/TS 32002 и отчитывается вердиктом в TPadesSignatureValidation.AlgorithmPolicyStatus. Пройти могут только P-256, P-384, P-521, три кривые Brainpool r1, Ed25519 и Ed448, каждая с парным digest, а несовпадение поднимает ppeiSignatureAlgorithmMismatch. Эта политика важнее, чем звучит. Подпись на brainpoolP160r1 или ключ P-256, подписывающий digest SHA-512, прекрасно верифицируются на уровне математики, поэтому Windows CryptoAPI рапортует значение подписи как хорошее, в то время как строгий валидатор PDF 2.0 отвергает файл. Проверка политики закрывает ту брешь, и она нарочно отделена от вопроса, корректны ли байты подписи криптографически
Что ISO/TS 32002 на самом деле допускает для подписей на эллиптических кривых?
ISO/TS 32002 допускает в подписях PDF ровно шесть кривых ECDSA и две схемы EdDSA, и каждую кривую привязывает к ширинам digest, которые она может нести. PDFium Component кодирует ту таблицу в PadesCurveDigestAllowed, с ключом по OID кривой из сертификата подписанта. Кривые NIST строги: digest обязан иметь ту же битовую ширину, что и кривая, SHA-2 или SHA-3. Кривые Brainpool снисходительнее и принимают собственную ширину или что угодно шире:
- P-256 (
1.2.840.10045.3.1.7): только SHA-256 или SHA3-256 - P-384 (
1.3.132.0.34): только SHA-384 или SHA3-384 - P-521 (
1.3.132.0.35): только SHA-512 или SHA3-512 - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7): любой digest SHA-2 или SHA-3 от 256 до 512 бит - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 на 384 или 512 бит - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): только SHA-512 или SHA3-512
У EdDSA нет ни выбора кривой, ни выбора digest, и именно поэтому его правила — про кодирование, а не про стойкость. По RFC 8419 SignerInfo на Ed25519 обязан объявить SHA-512 своим digestAlgorithm без параметров, а SignerInfo на Ed448, на пути signed attributes, который PAdES использует всегда, обязан объявить id-shake256-len (2.16.840.1.101.3.4.2.18) с параметром INTEGER ровно 512. Для обеих схем AlgorithmIdentifier подписи и AlgorithmIdentifier публичного ключа сертификата не должны нести параметров вовсе. Производитель, пишущий там NULL — привычка, которую RSA-энкодеры вбили во многие ASN.1-библиотеки, — выдаёт несоответствующую стандарту подпись, хотя ключ и значение подписи в порядке
Как PDFium Component извлекает тройку алгоритмов из CMS
Сам PDFium ответить на этот вопрос не может: его публичный signature API читает словарь подписи, но ни CMS не верифицирует, ни кривую сертификата подписанта не выдаёт. Поэтому слой инспекции PAdES, построенный поверх PDFium, парсит CMS SignedData (RFC 5652) сам. InspectPadesSignatureAlgorithm читает digestAlgorithm и signatureAlgorithm первого SignerInfo, затем находит сертификат подписанта и читает его SubjectPublicKeyInfo, чтобы получить алгоритм ключа и кривую. Поиск сертификата нарочно ограничен: рассматривается максимум 64 сертификата из набора certificates CMS, матч — точное байтовое сравнение issuer и serial number из issuerAndSerialNumber, а код откатывается к «единственному сертификату, который там есть», только когда набор держит ровно один парсящийся сертификат. Взять первый попавшийся EC-сертификат из неупорядоченного набора легко — и тогда CA-сертификат решал бы, какой кривой «пользовался» подписант
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 применяет политику в составе своего прохода на соответствие, стартуя от сертификата, который он находит внутри CMS, а TPdf.ValidatePadesTrust перегоняет её против сертификата подписанта, которым Windows CryptoAPI реально верифицировала, так что последнее слово за сертификатом, отрапортованным CryptoAPI. Каждый сырой вход попадает в TPadesSignatureAlgorithmInfo, включая DigestParametersPresent, DigestParameterBits, SignatureParametersPresent и PublicKeyParametersAreNamedCurve, поэтому отказ всегда объясним из записи, а не из строки лога
Почему подпись P-256 с SHA3-256 проваливает политику?
Подпись P-256 проваливает политику PDFium Component всякий раз, когда CMS-поле digestAlgorithm и digest, подразумеваемый ECDSA-полем signatureAlgorithm, расходятся, даже если каждый по отдельности допустим для кривой. EvaluatePadesSignatureAlgorithm сперва мапит ecdsa-with-SHA256, ecdsa-with-SHA3-256 и их собратьев в digest, сверяет это с объявленным digestAlgorithm и возвращает pcsInvalid при любом расхождении ещё до обращения к таблице кривых. Случай настоящий: подписывающий инструмент переключает хеш на SHA3-256, но оставляет зашитый идентификатор ecdsa-with-SHA256, и результат — файл, который ни один соответствующий стандарту верификатор не сможет трактовать согласованно. Функция публичная, поэтому матрицу можно пришпилить юнит-тестом, не собирая 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 не совпал
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, теперь согласован
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // кривой нет в профиле
end;
Кодирование кривой получает ту же строгость. RFC 5480 §2.1.1 позволяет ECParameters быть OID именованной кривой, неявной кривой (NULL) или полным явным набором параметров, а профили PKIX требуют именованной формы. PDFium Component возвращает pcsInvalid, когда сертификат id-ecPublicKey несёт ни параметров, ни неявные параметры, ни явные, — явные параметры позволяют атакующему описать кривую, которая лишь притворяется стандартной. Корректно именованная кривая, просто отсутствующая в списке ISO/TS 32002, скажем brainpoolP160r1 выше или secp256k1, получает вместо этого pcsUnsupported
Invalid, unsupported или indeterminate: честное чтение статуса
Три невалидных статуса AlgorithmPolicyStatus означают разное, и слипание их в одно ведро «провалено» выбрасывает информацию, которая нужна аудиторам. pcsInvalid означает, что распознанная комбинация алгоритмов искажена или не согласована; он добавляет ppeiSignatureAlgorithmMismatch в TPadesValidationResult.Issues и гонит агрегатный IntegrityStatus в pcsInvalid, так что IsCryptographicallyValid возвращает False, даже когда значение CMS-подписи сходится. pcsUnsupported означает, что кривая или digest вне того, что называет профиль, — это результат о возможностях, а не доказательство подделки. pcsIndeterminate означает, что сертификат подписанта не удалось пришпилить, обычно CertificateSet с несколькими кандидатами и без точного матча issuerAndSerialNumber, поэтому код отказывается угадывать кривую; с v3.124.0 он помечает так же и RSA-подпись поверх SHA-1 или 112-битного digest вроде SHA-224, которые более не согласованы для актуальной валидации. Тот же расклад относится к EdDSA на машине, чья CryptoAPI не умеет верифицировать Ed25519 или Ed448: CmsSignatureStatus остаётся pcsUnsupported, тогда как AlgorithmPolicyStatus может быть pcsValid — кодирование было верным, не хватало лишь верификатора. Если вы гоняетесь за отказом от Adobe или валидатора на базе DSS, гид о том, почему валидаторы отвергают подписи PAdES, покрывает остальные частые причины
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; // офлайн, без revocation
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;
Чего pcsValid не гарантирует?
AlgorithmPolicyStatus = pcsValid удостоверяет лишь, что подпись ECDSA или EdDSA использует одобренную кривую с согласованным и корректно закодированным digest, а подпись RSA — digest из актуального набора; о том, верно ли значение подписи, она не говорит ничего. До v3.124.0 RSA-ветка EvaluatePadesSignatureAlgorithm была нарочно широкой: любой signatureAlgorithm под аркой PKCS #1 1.2.840.113549.1.1.* возвращал pcsValid, включая legacy-схему sha1WithRSAEncryption. С PDFiumPas v3.124.0 RSA-ветка применяет наборы подписей ETSI TS 119 312. Digest MD2, MD4 и MD5 — pcsInvalid. SHA-1 и 112-битные digest вроде SHA-224 — pcsIndeterminate, так что SHA-1-подпись сохраняет свой общий результат целостности и помечается на ревью, а не отвергается. digestAlgorithm, отличный от digest, фиксированного алгоритмом подписи, скажем sha256WithRSAEncryption поверх digest SHA-1, или алгоритм RSA-подписи на ключе подписанта не-RSA — это pcsInvalid, репортится как ppeiSignatureAlgorithmMismatch и проваливает целостность. Ненайденный сертификат подписанта даёт pcsIndeterminate, как и раньше для ECDSA, а нераспознанные digest или не-подписные RSA OID дают pcsUnsupported. Длина модуля по-прежнему не проверяется, параметры PSS здесь не валидируются (статья о RSASSA-PSS-params по RFC 4055 рассказывает, как они кодируются на подписывающей стороне), а digest SHA-1 или MD5 вдобавок поднимают отдельный вопрос ppeiBadDigestAlgorithm. Равным образом математика подписи, цепочка сертификатов и revocation остаются за CmsSignatureStatus, CertificateTrustStatus и RevocationStatus, приходящими из Windows CryptoAPI. Считайте pcsValid «профиль алгоритмов сходится» и никогда — «этот ключ достаточно стоек»
Для Delphi-приложения, принимающего подписанные PDF-счёта, контракты или архивные пакеты, практическая настройка коротка: гоняйте ValidatePadesTrust, отвергайте по ppeiSignatureAlgorithmMismatch, маршрутизируйте pcsUnsupported и pcsIndeterminate человеку и enforcing'ьте собственный RSA-пол по размеру ключа, потому что политика этого не сделает. PDFium Component для Delphi и Lazarus везёт с собой валидатор PAdES, сборщик отчёта-доказательства и подписывающий конвейер, так что та же библиотека может и производить такие подписи, и проверять их end to end