PDFium Component for Delphi kiekvieną ECDSA ir EdDSA PDF parašą tikrina prieš ISO/TS 32002 algoritmų profilį ir verdiktą praneša TPadesSignatureValidation.AlgorithmPolicyStatus. Praeiti gali tik P-256, P-384, P-521, trys Brainpool r1 kreivės, Ed25519 ir Ed448, kiekviena su derančia santrauka, o nesutapimas pakelia ppeiSignatureAlgorithmMismatch. Ta politika svarbiau, nei skamba. Parašas ant brainpoolP160r1 arba P-256 raktas, pasirašantis SHA-512 santrauką, matematikos lygiu gali patikrintis puikiai, tad Windows CryptoAPI praneša parašo reikšmę kaip gerą, o griežtas PDF 2.0 validatorius failą atmeta. Politikos tikra tą plyšį uždaro, ir ji sąmoningai atskirta nuo klausimo, ar parašo baitai kriptografiškai teisingi
Ką ISO/TS 32002 iš tikrųjų leidžia eliptinių kreivių parašams?
ISO/TS 32002 PDF parašams leidžia lygiai šešias ECDSA kreives ir dvi EdDSA schemas, ir kiekvieną kreivę suriša su santraukų dydžiais, kuriuos ji gali nešti. PDFium Component tą lentelę užkoduoja PadesCurveDigestAllowed, surištoje su kreivės OID iš pasirašytojo sertifikato. NIST kreivės griežtos: santrauka turi būti to paties bitų pločio kaip kreivė, SHA-2 arba SHA-3. Brainpool kreivės laisvesnės ir priima savąjį plotį ar bet ką platesnį:
- P-256 (
1.2.840.10045.3.1.7): tik SHA-256 arba SHA3-256 - P-384 (
1.3.132.0.34): tik SHA-384 arba SHA3-384 - P-521 (
1.3.132.0.35): tik SHA-512 arba SHA3-512 - brainpoolP256r1 (
1.3.36.3.3.2.8.1.1.7): bet kuri SHA-2 arba SHA-3 santrauka nuo 256 iki 512 bitų - brainpoolP384r1 (
1.3.36.3.3.2.8.1.1.11): 384 ar 512 bitų SHA-2 / SHA-3 - brainpoolP512r1 (
1.3.36.3.3.2.8.1.1.13): tik SHA-512 arba SHA3-512
EdDSA neturi nei kreivės pasirinkimo, nei santraukos pasirinkimo, ir būtent todėl jos taisyklės — apie koduotę, o ne stiprumą. Pagal RFC 8419 Ed25519 SignerInfo turi deklaruoti SHA-512 kaip savą digestAlgorithm be parametrų, o Ed448 SignerInfo, pasirašytųjų atributų kelyje, kurį visada naudoja PAdES, turi deklaruoti id-shake256-len (2.16.840.1.101.3.4.2.18) su INTEGER parametru lygiai 512. Abiems schemoms parašo AlgorithmIdentifier ir sertifikato viešojo rakto AlgorithmIdentifier turi nešti apskritai jokių parametrų. Gamintojas, ten parašantis NULL — įprotį, į kurį RSA koduotojai išmokino daugelį ASN.1 bibliotekų, — pagamina nederantį standartui parašą, nors raktas ir parašo reikšmė geros
Kaip PDFium Component ištraukia algoritmų trejetą iš CMS
Pats PDFium šio klausimo atsakyti negali, nes jo viešasis parašų API skaito parašų žodyną, bet nei CMS patikrina, nei atveria pasirašytojo sertifikato kreivę. PAdES patikros sluoksnis, pastatytas ant PDFium, todėl CMS SignedData (RFC 5652) išskaido pats. InspectPadesSignatureAlgorithm skaito pirmojo SignerInfo digestAlgorithm ir signatureAlgorithm, tada randa pasirašytojo sertifikatą ir skaito jo SubjectPublicKeyInfo, kad gautų rakto algoritmą ir kreivę. Sertifikato paieška sąmoningai ribota: CMS certificates rinkinyje apžiūrima ne daugiau nei 64 sertifikatai, atitikimas — tikslus baitų palyginimas iš issuerAndSerialNumber paimto leidėjo ir serijos numerio, o kodas į „vienintelį ten esantį sertifikatą“ grįžta tik tada, kai rinkinyje lygiai vienas išskaidomas sertifikatas. Paimti pirmą EC sertifikatą iš nesurikiuoto rinkinio būtų paprasta, ir CA sertifikatas tada nuspręstų, kurią kreivę pasirašytojas, kaip teigiama, naudojo
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 politiką taiko kaip savo atitikties pravažiavimo dalį, pradėdamas nuo CMS viduje rasto sertifikato, o TPdf.ValidatePadesTrust ją pakartoja prieš pasirašytojo sertifikatą, kurį Windows CryptoAPI realiai naudojo patikrinimui, tad CryptoAPI praneštas sertifikatas turi paskutinį žodį. Kiekviena žalia įvestis atsiduria TPadesSignatureAlgorithmInfo, įskaitant DigestParametersPresent, DigestParameterBits, SignatureParametersPresent ir PublicKeyParametersAreNamedCurve, tad atmetimas visada paaiškinamas iš įrašo, o ne iš žurnalo eilutės
Kodėl P-256 parašas su SHA3-256 nepraeina politikos?
P-256 parašas nepraeina PDFium Component politikos, kai tik CMS digestAlgorithm ir ECDSA signatureAlgorithm numanoma santrauka nesutampa, net jei abi atskiros kreivei priimtinos. EvaluatePadesSignatureAlgorithm pirmiausia ecdsa-with-SHA256, ecdsa-with-SHA3-256 ir jų brolius susieja su santrauka, palygina su deklaruotąja digestAlgorithm ir grąžina pcsInvalid bet kokiu skirtumu, dar prieš sprendžiant iš kreivių lentelės. Atvejis realus: pasirašymo įrankis persijungia į SHA3-256, bet palieka įkaltą ecdsa-with-SHA256 identifikatorių, ir rezultatas — failas, kurio nė vienas derantis patikrintuvas nesugebės aiškinti nuosekliai. Funkcija vieša, tad matricą galima prisegti unit teste nepastatant 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); // santraukos nesutapimas
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, dabar suderinta
Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // kreivės profilyje nėra
end;
Kreivės koduotė gauna tą pačią griežtumą. RFC 5480 §2.1.1 leidžia ECParameters būti vardinės kreivės OID, netiesiogine kreive (NULL) ar visu aiškiu parametrų rinkiniu, o PKIX profiliai reikalauja vardinės formos. PDFium Component grąžina pcsInvalid, kai id-ecPublicKey sertifikatas neša jokių parametrų, netiesioginius parametrus ar aiškius parametrus, nes aiškūs parametrai leidžia puolėjui apibūdinti kreivę, kuri tik panaši į standartinę. Teisingai pavadinta kreivė, tiesiog nesanti ISO/TS 32002 sąraše, pavyzdžiui aukščiau minėtas brainpoolP160r1 ar secp256k1, vietoj to gauna pcsUnsupported
Invalid, unsupported ar indeterminate: statusą skaitome sąžiningai
Trys ne validūs AlgorithmPolicyStatus statusai reiškia skirtingus dalykus, ir jų suglaudimas į vieną „nepavyko“ kibirą išmeta informaciją, kurios reikia auditoriams. pcsInvalid reiškia, jog atpažintas algoritmų derinys yra kreivas arba nesutampa; jis prideda ppeiSignatureAlgorithmMismatch prie TPadesValidationResult.Issues ir varo bendrąjį IntegrityStatus iki pcsInvalid, tad IsCryptographicallyValid grąžina False net tada, kai CMS parašo reikšmė sutvarkyta. pcsUnsupported reiškia, jog kreivė ar santrauka yra už to, ką profilis įvardija — tai gebėjimų rezultatas, ne įrodymas apie kišimąsi. pcsIndeterminate reiškia, jog pasirašytojo sertifikato nepavyko prisegti — dažniausiai CertificateSet su keliais kandidatais ir be tikraus issuerAndSerialNumber atitikimo, tad kodas atsisako spėti kreivę; nuo v3.124.0 jis taip pat žymi RSA parašą virš SHA-1 ar 112 bitų santraukos, tokios kaip SHA-224, kurios dabartinei validacijai jau nesutariama. Tas pats skirstymas tinka EdDSA mašinoje, kurios CryptoAPI nesugeba patikrinti Ed25519 ar Ed448: CmsSignatureStatus lieka pcsUnsupported, o AlgorithmPolicyStatus vis dar gali būti pcsValid, nes koduotė buvo teisinga, o nebuvęs buvo tik patikrintuvas. Jei medžiojate atmetimą iš Adobe ar DSS pagrindo validatoriaus, gidas, kodėl validatoriai atmeta PAdES parašus dengia kitas dažnas priežastis
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; // be tinklo, be atšaukimo patikros
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;
Ką pcsValid negarantuoja?
AlgorithmPolicyStatus = pcsValid liudija tik tai, jog ECDSA ar EdDSA parašas naudoja patvirtintą kreivę su suderinta, teisingai užkoduota santrauka, o RSA parašas — santrauką iš dabartinio rinkinio; apie tai, ar parašo reikšmė teisinga, jis nesako nieko. Iki v3.124.0 EvaluatePadesSignatureAlgorithm RSA šaka buvo sąmoningai plati: bet koks signatureAlgorithm po PKCS #1 šaka 1.2.840.113549.1.1.* grąžindavo pcsValid, įskaitant paveldėtąjį sha1WithRSAEncryption. Nuo PDFiumPas v3.124.0 RSA šaka taiko ETSI TS 119 312 parašų rinkinius. MD2, MD4 ir MD5 santraukos — pcsInvalid. SHA-1 ir 112 bitų santraukos, tokios kaip SHA-224, — pcsIndeterminate, tad SHA-1 parašas išlaiko bendrąjį vientisumo rezultatą ir pažymimas peržiūrai, vietoj to, kad būtų atmetamas. digestAlgorithm, skiriantis nuo parašo algoritmo fiksuotos santraukos, pavyzdžiui sha256WithRSAEncryption virš SHA-1 santraukos, ar RSA parašo algoritmas ant ne RSA pasirašytojo rakto — pcsInvalid, pranešamas kaip ppeiSignatureAlgorithmMismatch ir krintantis su vientisumu. Nerandamas pasirašytojo sertifikatas duoda pcsIndeterminate, kaip ir anksčiau ECDSA atveju, o neatpažintos santraukos ar ne parašo RSA OID — pcsUnsupported. Modulio ilgis vis dar netikrinamas, PSS parametrai čia nevaliduojami (RFC 4055 RSASSA-PSS-params straipsnis pasakoja, kaip jie užkoduoti pasirašymo pusėje), ir SHA-1 ar MD5 santraukos papildomai kelia atskirą ppeiBadDigestAlgorithm problemą. Panašiai parašo matematika, sertifikatų grandinė ir atšaukimas lieka CmsSignatureStatus, CertificateTrustStatus ir RevocationStatus darbas, kurie atkeliauja iš Windows CryptoAPI. Elkitės su pcsValid kaip su „algoritmų profilis laikosi“, niekada kaip su „šis raktas pakankamai stiprus“
Delphi programai, priimančiai pasirašytas PDF sąskaitas, sutartis ar archyvo paketus, praktinis nustatymas trumpas: paleiskite ValidatePadesTrust, atmeskite ties ppeiSignatureAlgorithmMismatch, pcsUnsupported ir pcsIndeterminate nukreipkite žmogui ir patys užveskite savą RSA rakto dydžio lubas, nes politika to nedaros. PDFium Component for Delphi and Lazarus atveža PAdES validatorių, įrodymų ataskaitos statytoją ir pasirašymo konvejerį, tad ta pati biblioteka gali šiuos parašus ir pagaminti, ir patikrinti nuo galo iki galo