Articol tehnic

Verificări de politică ECDSA și EdDSA ISO/TS 32002 în PDFium

PDFium Component pentru Delphi verifică fiecare semnătură PDF ECDSA și EdDSA contra profilului de algoritmi ISO/TS 32002 și raportează verdictul în TPadesSignatureValidation.AlgorithmPolicyStatus. Doar P-256, P-384, P-521, cele trei curbe Brainpool r1, Ed25519 și Ed448 pot trece, fiecare cu un digest potrivit, iar o nepotrivire ridică ppeiSignatureAlgorithmMismatch. Politica asta contează mai mult decât sună. O semnătură pe brainpoolP160r1, sau o cheie P-256 care semnează un digest SHA-512, se poate verifica perfect la nivel matematic, deci Windows CryptoAPI raportează valoarea semnăturii ca bună, în timp ce un validator strict PDF 2.0 respinge fișierul. Verificarea de politică închide golul acesta, și este separată în mod deliberat de întrebarea dacă byte-ii semnăturii sunt corecți criptografic

Ce permite de fapt ISO/TS 32002 pentru semnăturile pe curbe eliptice?

ISO/TS 32002 permite exact șase curbe ECDSA și două scheme EdDSA în semnăturile PDF, și leagă fiecare curbă de mărimile de digest pe care le poate purta. PDFium Component codifică tabela aceea în PadesCurveDigestAllowed, indexată după OID-ul curbei din certificatul semnatarului. Curbele NIST sunt stricte: digestul trebuie să aibă aceeași lățime în biți ca și curba, SHA-2 sau SHA-3. Curbele Brainpool sunt mai permisive și acceptă propria lățime sau orice e mai lat:

  • P-256 (1.2.840.10045.3.1.7): doar SHA-256 sau SHA3-256
  • P-384 (1.3.132.0.34): doar SHA-384 sau SHA3-384
  • P-521 (1.3.132.0.35): doar SHA-512 sau SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): orice digest SHA-2 sau SHA-3 de la 256 la 512 biți
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 de 384 sau 512 biți
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): doar SHA-512 sau SHA3-512
Matricea profilului de algoritmi ISO TS 32002 în PDFium Component: P-256, P-384 și P-521 acceptă doar lățimi de digest potrivite în PadesCurveDigestAllowed, brainpoolP256r1 acceptă 256 până la 512 biți, brainpoolP384r1 acceptă 384 și 512, Ed25519 și Ed448 declară SHA-512 și SHAKE256 cu lungimea 512, iar nepotrivirile ridică ppeiSignatureAlgorithmMismatch în timp ce curbele din afara profilului întorc pcsUnsupported
Șase curbe ECDSA și două scheme EdDSA pot trece, iar fiecare curbă e legată de lățimile de digest pe care le poate purta; tot restul este invalid sau nesuportat, niciodată acceptat în tăcere

EdDSA nu are nici alegere de curbă, nici alegere de digest, tocmai de aceea regulile lui țin de codare, nu de putere. Conform RFC 8419, un SignerInfo Ed25519 trebuie să declare SHA-512 ca digestAlgorithm fără parametri, iar un SignerInfo Ed448, pe calea atributelor semnate pe care PAdES o folosește mereu, trebuie să declare id-shake256-len (2.16.840.1.101.3.4.2.18) cu un parametru INTEGER de exact 512. Pentru ambele scheme, AlgorithmIdentifier-ul semnăturii și AlgorithmIdentifier-ul cheii publice din certificat nu trebuie să poarte niciun parametru. Un producător care scrie acolo un NULL, obiceiul pe care encodoarele RSA l-au băgat în multe biblioteci ASN.1, produce o semnătură neconformă chiar dacă cheia și valoarea semnăturii sunt în regulă

Cum extrage PDFium Component tripla de algoritmi din CMS

PDFium în sine nu poate răspunde la întrebarea aceasta, pentru că API-ul său public de semnături citește dicționarul semnăturii, dar nici nu verifică CMS-ul, nici nu expune curba certificatului semnatarului. De aceea stratul de inspecție PAdES construit peste PDFium parsează el însuși CMS SignedData (RFC 5652). InspectPadesSignatureAlgorithm citește digestAlgorithm-ul și signatureAlgorithm-ul primului SignerInfo, apoi găsește certificatul semnatarului și îi citește SubjectPublicKeyInfo ca să obțină algoritmul cheii și curba. Căutarea certificatului este limitată în mod deliberat: cel mult 64 de certificare din mulțimea certificates a CMS-ului sunt examinate, potrivirea este o comparație exactă de byte între emitent și numărul serial din issuerAndSerialNumber, iar codul cade pe „singurul certificat existent" doar când mulțimea ține exact un certificat parseabil. A alege primul certificat EC dintr-o mulțime neordonată ar fi ușor, și ar lăsa un certificat de CA să decidă ce curbă ar fi folosit semnatarul

Cum inspectează PDFium Component tripla de algoritmi a unei semnături PDF: InspectPadesSignatureAlgorithm citește digestAlgorithm și signatureAlgorithm ale primului SignerInfo din CMS, fixează certificatul semnatar printr-o potrivire exactă issuerAndSerialNumber printre cel mult 64 de candidați, citește SubjectPublicKeyInfo pentru curbă, iar EvaluatePadesSignatureAlgorithm întoarce AlgorithmPolicyStatus
PDFium în sine nici nu verifică CMS-ul, nici nu expune curba semnatarului, deci stratul PAdES parsează SignedData și păstrează fiecare OID brut în înregistrare pentru o respingere explicabilă
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 aplică politica ca parte a trecerii sale de conformitate, pornind de la certificatul pe care îl localizează în interiorul CMS-ului, iar TPdf.ValidatePadesTrust o rulează din nou contra certificatului semnatar pe care Windows CryptoAPI l-a folosit efectiv la verificare, deci certificatul raportat de CryptoAPI are cuvântul final. Fiecare intrare brută ajunge în TPadesSignatureAlgorithmInfo, inclusiv DigestParametersPresent, DigestParameterBits, SignatureParametersPresent și PublicKeyParametersAreNamedCurve, deci o respingere este mereu explicabilă din înregistrare, nu dintr-o linie de log

De ce o semnătură P-256 cu SHA3-256 pică la politică?

O semnătură P-256 pică la politica PDFium Component ori de câte ori digestAlgorithm-ul din CMS și digestul implicit al signatureAlgorithm-ului ECDSA se contrazic, chiar dacă ambele sunt pe separat acceptabile pentru curbă. EvaluatePadesSignatureAlgorithm mapează întâi ecdsa-with-SHA256, ecdsa-with-SHA3-256 și frații lor la un digest, compară cu digestAlgorithm-ul declarat și întoarce pcsInvalid la orice diferență înainte ca tabela de curbe să fie consultată. Cazul e real: o unealtă de semnare își schimbă hash-ul în SHA3-256, dar păstrează un identificator ecdsa-with-SHA256 hard-codat, iar rezultatul e un fișier pe care niciun verificator conform nu-l poate interpreta consistent. Funcția e publică, deci matricea poate fi fixată într-un test unitar fără să construiești 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); // nepotrivire de 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, acum congruent
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // curbă lipsă din profil
end;

Codarea curbei primește aceeași strictețe. RFC 5480 §2.1.1 lasă ECParameters să fie un OID de curbă numită, o curbă implicită (NULL) sau un set complet de parametri expliciți, iar profilurile PKIX cer forma numită. PDFium Component întoarce pcsInvalid când un certificat id-ecPublicKey nu poartă parametri, poartă parametri impliciti sau parametri expliciți, pentru că parametrii expliciți lasă un atacator să descrie o curbă care doar seamănă cu una standard. O curbă corect numită, dar pur și simplu lipsă din lista ISO/TS 32002, precum brainpoolP160r1 de mai sus sau secp256k1, primește în schimb pcsUnsupported

Invalid, nesuportat sau indeterminat: citirea onestă a statusului

Cele trei statusuri non-valide ale AlgorithmPolicyStatus înseamnă lucruri diferite, iar prăbușirea lor într-o singură găletuță „picat" aruncă informația de care au nevoie auditorii. pcsInvalid înseamnă că o combinație de algoritmi recunoscută e malformată sau nepotrivită; adaugă ppeiSignatureAlgorithmMismatch în TPadesValidationResult.Issues și împinge IntegrityStatus-ul agregat la pcsInvalid, deci IsCryptographicallyValid întoarce False chiar când valoarea CMS a semnăturii se verifică. pcsUnsupported înseamnă că curba sau digestul e în afara a ceea ce numește profilul, ceea ce e un rezultat de capabilitate, nu o dovadă de falsificare. pcsIndeterminate înseamnă că certificatul semnatarului nu a putut fi fixat, de obicei un CertificateSet cu mai mulți candidați și fără potrivire exactă de issuerAndSerialNumber, deci codul refuză să ghicească curba; din v3.124.0 marchează și o semnătură RSA peste SHA-1 sau peste un digest de 112 de biți precum SHA-224, care nu mai este agreată pentru validarea curentă. Aceeași împărțire se aplică EdDSA pe o mașină a cărei CryptoAPI nu poate verifica Ed25519 sau Ed448: CmsSignatureStatus rămâne pcsUnsupported în timp ce AlgorithmPolicyStatus poate fi tot pcsValid, pentru că codarea era corectă și lipsea doar verificatorul. Dacă vânați o respingere de la Adobe sau de la un validator bazat pe DSS, ghidul despre de ce resping validatorii semnăturile PAdES acoperă celelalte cauze obișnuite

Calea de decizie a lui EvaluatePadesSignatureAlgorithm în PDFium Component: o nepotrivire de digest setează pcsInvalid și ppeiSignatureAlgorithmMismatch, o curbă numită din afara profilului ISO TS 32002 precum brainpoolP160r1 setează pcsUnsupported, un certificat semnatar care nu poate fi fixat setează pcsIndeterminate, iar din v3.124.0 ramura RSA aplică suitele de digest ETSI TS 119 312, mărimea cheii rămânând tot la aplicație
Cele trei statusuri non-valide înseamnă lucruri diferite: invalid e dovada unei combinații stricate, nesuportat e un rezultat de capabilitate, iar indeterminat înseamnă că codul a refuzat să ghicească
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;

Ce nu garantează pcsValid?

AlgorithmPolicyStatus = pcsValid certifică doar că o semnătură ECDSA sau EdDSA folosește o curbă aprobată cu un digest congruent și corect codat, și că o semnătură RSA folosește un digest dintr-o suită curentă; nu spune nimic despre dacă valoarea semnăturii e corectă. Înainte de v3.124.0, ramura RSA a lui EvaluatePadesSignatureAlgorithm era intenționat largă: orice signatureAlgorithm sub arcul PKCS #1 1.2.840.113549.1.1.* întorcea pcsValid, sha1WithRSAEncryption-ul din moștenire inclus. Din PDFiumPas v3.124.0, ramura RSA aplică suitele de semnătură ETSI TS 119 312. Digesturile MD2, MD4 și MD5 sunt pcsInvalid. SHA-1 și digesturile de 112 de biți precum SHA-224 sunt pcsIndeterminate, deci o semnătură SHA-1 își păstrează rezultatul general de integritate și e marcată pentru revizuire, nu respinsă. Un digestAlgorithm care diferă de digestul fixat de algoritmul de semnătură, precum sha256WithRSAEncryption peste un digest SHA-1, sau un algoritm de semnătură RSA pe o cheie de semnatar non-RSA, este pcsInvalid, raportat ca ppeiSignatureAlgorithmMismatch, și pică la integritate. Un certificat de semnatar care nu poate fi găsit dă pcsIndeterminate, așa cum dădea deja la ECDSA, iar digesturile nerecunoscute sau OID-urile RSA non-semnătură dau pcsUnsupported. Lungimea modulului nu e totuși verificată, parametrii PSS nu sunt validați aici (articolul despre parametrii RSASSA-PSS din RFC 4055 acoperă cum sunt codați pe partea de semnare), iar digesturile SHA-1 sau MD5 ridică în plus și problema separată ppeiBadDigestAlgorithm. La fel, matematica semnăturii, lanțul de certificare și revocarea rămân treaba lui CmsSignatureStatus, CertificateTrustStatus și RevocationStatus, care vin din Windows CryptoAPI. Tratați pcsValid ca „profilul de algoritmi se susține", niciodată ca „cheia asta e suficient de puternică"

Pentru o aplicație Delphi care acceptă facturi PDF semnate, contracte sau pachete de arhivă, aranjamentul practic e scurt: rulați ValidatePadesTrust, respingeți la ppeiSignatureAlgorithmMismatch, trimiteți pcsUnsupported și pcsIndeterminate către un om și impuneți-vă singuri pragul de mărime a cheii RSA, pentru că politica nu va face-o. PDFium Component pentru Delphi și Lazarus livrează validatorul PAdES, generatorul de raport de dovezi și pipeline-ul de semnare, deci aceeași bibliotecă poate produce aceste semnături și le poate verifica cap-coadă