PDFium Component за Delphi проверява всеки ECDSA и EdDSA PDF подпис срещу алгоритмичния профил ISO/TS 32002 и докладва присъдата в TPadesSignatureValidation.AlgorithmPolicyStatus. Само P-256, P-384, P-521, трите Brainpool r1 криви, Ed25519 и Ed448 могат да минат, всеки със съответстващ digest, а разминаване вдига ppeiSignatureAlgorithmMismatch. Тази политика има повече значение, отколкото звучи. Подпис върху brainpoolP160r1 или P-256 ключ, подписващ SHA-512 digest, може да се верифицира съвършено добре на математическо ниво, така че Windows CryptoAPI докладва стойността на подписа като добра, докато строг PDF 2.0 валидатор отхвърля файла. Проверката по политиката затваря тази дупка и е нарочно отделена от въпроса дали байтовете на подписа са криптографски коректни
Какво всъщност допуска ISO/TS 32002 за подписи с елиптични криви?
ISO/TS 32002 допуска точно шест ECDSA криви и две EdDSA схеми в PDF подписите и завързва всяка крива за размерите на 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): всеки SHA-2 или SHA-3 digest от 256 до 512 бита - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): 384- или 512-битов SHA-2 / SHA-3 - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): само SHA-512 или SHA3-512
EdDSA няма избор на крива и няма избор на digest — точно затова правилата му са за кодирането, а не за силата. По RFC 8419 Ed25519 SignerInfo трябва да декларира SHA-512 като свой digestAlgorithm без параметри, а Ed448 SignerInfo — по пътя със 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 прилага политиката като част от своя compliance pass, тръгвайки от сертификата, който намира вътре в 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 при каквото и да е разминаване, преди да се консултира таблицата с криви. Случаят е реален: инструмент за подписване сменя hash-а си на SHA3-256, но запазва закодиран идентификатор ecdsa-with-SHA256 и резултатът е файл, който никой съответстващ верификатор не може да тълкува последователно. Функцията е публична, така че матрицата може да бъде закована в unit тест, без да се строи 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
Невалидно, неподдържано или неопределено: да четеш статуса честно
Трите невалидни статуса на 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; // offline, no 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 от текущ suite; то не казва нищо дали стойността на подписа е коректна. Преди v3.124.0 RSA клонът на EvaluatePadesSignatureAlgorithm беше нарочно широк: всеки signatureAlgorithm под арка PKCS #1 1.2.840.113549.1.1.* връщаше pcsValid, включително legacy sha1WithRSAEncryption. От PDFiumPas v3.124.0 RSA клонът прилага signature suit-овете на ETSI TS 119 312. MD2, MD4 и MD5 digest-и са pcsInvalid. SHA-1 и 112-битови digest-и като SHA-224 са pcsIndeterminate, така че подпис със SHA-1 запазва общия си integrity резултат и се отбелязва за преглед, вместо да бъде отхвърлян. digestAlgorithm, различен от digest-а, фиксиран от signature алгоритъма — като sha256WithRSAEncryption върху SHA-1 digest — или RSA signature алгоритъм върху не-RSA ключ на подписващия, е pcsInvalid, докладван като ppeiSignatureAlgorithmMismatch и проваля integrity. Сертификат на подписващия, който не може да бъде намерен, дава pcsIndeterminate, както вече беше за ECDSA, а неразпознати digest-и или не-signature RSA OID-и дават pcsUnsupported. Дължината на модула пак не се проверява, PSS параметрите не се валидират тук (статията за RFC 4055 RSASSA-PSS-params обяснява как са кодирани от страната на подписването), а SHA-1 или MD5 digest-и допълнително вдигат отделния issue ppeiBadDigestAlgorithm. По същия начин математиката на подписа, веригата от сертификати и revocation остават работа на CmsSignatureStatus, CertificateTrustStatus и RevocationStatus, които идват от Windows CryptoAPI. Отнасяйте се към pcsValid като към „алгоритмичният профил спазва“, никога като към „този ключ е достатъчно силен“
За Delphi приложение, което приема подписани PDF фактури, договори или архивни пакети, практичната настройка е кратка: пуснете ValidatePadesTrust, отхвърляйте при ppeiSignatureAlgorithmMismatch, насочвайте pcsUnsupported и pcsIndeterminate към човек и налагайте собствен праг за размера на RSA ключа, защото политиката няма да го направи. PDFium Component за Delphi и Lazarus доставя PAdES валидатора, строителя на evidence доклади и signing pipeline-а, така че същата библиотека може да произвежда тези подписи и да ги проверява от край до край