Odborný článok

ISO/TS 32002: politiky ECDSA a EdDSA v PDFium pre Delphi

PDFium Component pre Delphi kontroluje každý ECDSA a EdDSA podpis PDF proti algoritmickému profilu ISO/TS 32002 a verdikt hlási v TPadesSignatureValidation.AlgorithmPolicyStatus. Prejsť môžu len P-256, P-384, P-521, tri krivky Brainpool r1, Ed25519 a Ed448, každá s párujúcim digestom, a nesúlad vyhodí ppeiSignatureAlgorithmMismatch. Tá politika je dôležitejšia, než znie. Podpis na brainpoolP160r1 alebo kľúč P-256 podpisujúci digest SHA-512 sa na matematickej úrovni overí úplne v poriadku, takže Windows CryptoAPI hlási hodnotu podpisu ako dobrú, kým prísny validátor PDF 2.0 súbor odmietne. Politická kontrola tú medzeru zatvára a zámerne je oddelená od otázky, či sú bajty podpisu kryptograficky správne

Čo ISO/TS 32002 naozaj pripúšťa pre podpisy na eliptických krivkách?

ISO/TS 32002 pripúšťa presne šesť ECDSA kriviek a dve schémy EdDSA v PDF podpisoch a každú krivku viaže na šírky digestov, ktoré môže niesť. PDFium Component kóduje túto tabuľku v PadesCurveDigestAllowed, kľúčovanej OID krivky z certifikátu podpisovateľa. NIST krivky sú prísne: digest musí mať rovnakú bitovú šírku ako krivka, buď SHA-2 alebo SHA-3. Krivky Brainpool sú voľnejšie a pripúšťajú vlastnú šírku alebo čokoľvek širšie:

  • P-256 (1.2.840.10045.3.1.7): len SHA-256 alebo SHA3-256
  • P-384 (1.3.132.0.34): len SHA-384 alebo SHA3-384
  • P-521 (1.3.132.0.35): len SHA-512 alebo SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): ľubovoľný digest SHA-2 alebo SHA-3 od 256 do 512 bitov
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 s 384 alebo 512 bitmi
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): len SHA-512 alebo SHA3-512
Matrica algoritmického profilu ISO TS 32002 v PDFium Component: P-256, P-384 a P-521 pripúšťajú len zodpovedajúce šírky digestov v PadesCurveDigestAllowed, brainpoolP256r1 pripúšťa 256 až 512 bitov, brainpoolP384r1 pripúšťa 384 a 512, Ed25519 a Ed448 deklarujú SHA-512 a SHAKE256 s dĺžkou 512 a nesúahody vyhodzujú ppeiSignatureAlgorithmMismatch, kým krivky mimo profilu vracajú pcsUnsupported
Prejsť môže šesť ECDSA kriviek a dve schémy EdDSA a každá krivka je viazaná na šírky digestov, ktoré môže niesť; všetko ostatné je neplatné alebo nepodporované, nikdy poticho prijaté

EdDSA nemá žiadnu voľbu krivky ani voľbu digestu, presne preto sa jeho pravidlá týkajú kódovania, nie sily. Podľa RFC 8419 musí SignerInfo Ed25519 deklarovať SHA-512 ako svoj digestAlgorithm bez parametrov a SignerInfo Ed448, na ceste signed-attributes, ktorú PAdES vždy používa, musí deklarovať id-shake256-len (2.16.840.1.101.3.4.2.18) s INTEGER parametrom presne 512. Pre obe schémy musí AlgorithmIdentifier podpisu a AlgorithmIdentifier verejného kľúča certifikátu nesať vôbec žiadne parametre. Producent, ktorý tam zapíše NULL, zvyk, do ktorého RSA enkodéry vycvičili mnohé ASN.1 knižnice, vyrobí nekonformný podpis, aj keď kľúč aj hodnota podpisu sú v poriadku

Ako PDFium Component vyzvedá trojicu algoritmov z CMS

PDFium sám na túto otázku neodpovie, lebo jeho verejné signature API číta signature slovník, ale ani neoveruje CMS, ani neexponuje krivku certifikátu podpisovateľa. Vrstva na prehliadanie PAdES postavená na PDFium preto parsuje CMS SignedData (RFC 5652) sama. InspectPadesSignatureAlgorithm číta digestAlgorithm a signatureAlgorithm prvého SignerInfo, potom nájde certifikát podpisovateľa a prečíta jeho SubjectPublicKeyInfo, aby dostal algoritmus kľúča a krivku. Hľadanie certifikátu je zámerne ohraničené: skúma sa najviac 64 certifikátov v množine certificates v CMS, zhoda je presné bajtové porovnanie issuer a serial number z issuerAndSerialNumber a kód prepadne na "jediný certifikát, ktorý tam je", len keď množina drží presne jeden vyparsovateľný certifikát. Zobrať prvý EC certifikát z neutriedených množiny by bolo ľahké a nechalo by CA certifikát rozhodnúť, ktorú krivku podpisovateľ vraj použil

Ako skúma PDFium Component trojicu algoritmov PDF podpisu: InspectPadesSignatureAlgorithm číta digestAlgorithm a signatureAlgorithm prvého SignerInfo v CMS, pripichne certifikát podpisovateľa cez presnú zhodu issuerAndSerialNumber medzi najviac 64 kandidátmi, číta SubjectPublicKeyInfo kvôli krivke a EvaluatePadesSignatureAlgorithm vracia AlgorithmPolicyStatus
PDFium sám ani neoveruje CMS, ani neexponuje krivku podpisovateľa, takže vrstva PAdES parsuje SignedData a drží každý surový OID v recorde kvôli vysvetliteľnému zamietnutiu
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 aplikuje politiku ako súčasť svojho prechodu zhodou, štartujúc od certifikátu, ktorý nájde vnútri CMS, a TPdf.ValidatePadesTrust ju znova spustí proti certifikátu podpisovateľa, ktorý Windows CryptoAPI naozaj použil na overenie, takže certifikát hlásený CryptoAPI má posledné slovo. Každý surový vstup pristane v TPadesSignatureAlgorithmInfo, vrátane DigestParametersPresent, DigestParameterBits, SignatureParametersPresent a PublicKeyParametersAreNamedCurve, takže zamietnutie je vždy vysvetliteľné z recordu, nie z log riadku

Prečo padne podpis P-256 s SHA3-256 na politike?

Podpis P-256 padne na politike PDFium Component vždy, keď sa digestAlgorithm v CMS a digest implikovaný ECDSA signatureAlgorithm rozchádzajú, aj keď sú oba jednotlivo pre krivku prijateľné. EvaluatePadesSignatureAlgorithm najprv mapuje ecdsa-with-SHA256, ecdsa-with-SHA3-256 a ich súrodencov na digest, porovná to s deklarovaným digestAlgorithm a pri akomkoľvek rozdiele vráti pcsInvalid, skôr než sa vôbec pozrie do tabuľky kriviek. Prípad je reálny: podpisovací nástroj prepne hash na SHA3-256, ale nechá natvrdo zakódovaný identifikátor ecdsa-with-SHA256 a výsledkom je súbor, ktorý žiaden konformný verifikátor nedokáže interpretovať konzistentne. Funkcia je verejná, takže matricu si možno pripichnúť v unit teste bez stavby 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 nesedí

  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, teraz kongruentný
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // krivka nie je v profile
end;

Kódovanie krivky dostáva rovnakú prísnosť. RFC 5480 §2.1.1 pripúšťa, aby ECParameters boli OID menovanej krivky, implicitná krivka (NULL) alebo plná explicitná množina parametrov, a profily PKIX vyžadujú menovanú formu. PDFium Component vracia pcsInvalid, keď certifikát id-ecPublicKey nesie žiadne parametre, implicitné parametre ani explicitné parametre, lebo explicitné parametre dovoľujú útočníkovi opísať krivku, ktorá štandardnú len pripomína. Správne menovaná krivka, ktorá v zozname ISO/TS 32002 jednoducho chýba, ako brainpoolP160r1 vyššie alebo secp256k1, dostane namiesto toho pcsUnsupported

Invalid, unsupported alebo indeterminate: poctivé čítanie statusu

Tri neplatné statusy AlgorithmPolicyStatus znamenajú rôzne veci a ich zrútenie do jedného koša "zlé" zahodí informácie, ktoré audítori potrebujú. pcsInvalid znamená, že rozpoznaná kombinácia algoritmov je deformovaná alebo nesúladná; pridáva ppeiSignatureAlgorithmMismatch do TPadesValidationResult.Issues a stiahne agregovaný IntegrityStatus na pcsInvalid, takže IsCryptographicallyValid vráti False, aj keď hodnota CMS podpisu obstojí. pcsUnsupported znamená, že krivka alebo digest je mimo toho, čo profil menuje, čo je výsledok schopností, nie dôkaz manipulácie. pcsIndeterminate znamená, že certifikát podpisovateľa sa nepodarilo pripichnúť, obvykle CertificateSet s viacerými kandidátmi a bez presnej zhody issuerAndSerialNumber, takže kód odmietne krivku hádať; od v3.124.0 tiež označí RSA podpis nad SHA-1 alebo digestom 112 bitov, ako SHA-224, ktorý sa už pre aktuálnu validáciu nezhoduje. To isté rozdelenie platí pre EdDSA na stroji, ktorého CryptoAPI nedokáže overiť Ed25519 ani Ed448: CmsSignatureStatus zostáva pcsUnsupported, kým AlgorithmPolicyStatus môže byť stále pcsValid, lebo kódovanie bolo správne a chýbal len verifikátor. Ak prenasledujete zamietnutie od Adobe alebo validátora založeného na DSS, príručka o tom, prečo validátory odmietajú PAdES podpisy pokrýva ďalšie bežné príčiny

Rozhodovacia cesta EvaluatePadesSignatureAlgorithm v PDFium Component: nesúlad digestov nastaví pcsInvalid a ppeiSignatureAlgorithmMismatch, menovaná krivka mimo profilu ISO TS 32002 ako brainpoolP160r1 nastaví pcsUnsupported, certifikát podpisovateľa, ktorý sa nepodarí pripichnúť, nastaví pcsIndeterminate a od v3.124.0 aplikuje RSA vetva digestové sady ETSI TS 119 312, s veľkosťou kľúča stále ponechanou na aplikácii
Tri neplatné statusy znamenajú rôzne veci: invalid je dôkaz pokorenej kombinácie, unsupported je výsledok schopností a indeterminate znamená, že kód odmietol hádať
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, bez 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;

Čo pcsValid negarantuje?

AlgorithmPolicyStatus = pcsValid certifikuje len to, že ECDSA alebo EdDSA podpis používa schválenú krivku s kongruentným, správne zakódovaným digestom a že RSA podpis používa digest z aktuálnej sady; o správnosti hodnoty podpisu nepovie nič. Pred v3.124.0 bola RSA vetva EvaluatePadesSignatureAlgorithm zámerne široká: akýkoľvek signatureAlgorithm pod oblúkom PKCS #1 1.2.840.113549.1.1.* vracal pcsValid, legacy sha1WithRSAEncryption nevynímajúc. Od PDFiumPas v3.124.0 aplikuje RSA vetva signature sady ETSI TS 119 312. Digesty MD2, MD4 a MD5 sú pcsInvalid. SHA-1 a 112-bitové digesty, ako SHA-224, sú pcsIndeterminate, takže podpis SHA-1 si drží svoj celkový integrity výsledok a označí sa na revíziu namiesto zamietnutia. digestAlgorithm, ktorý sa líši od digestu fixovaného signature algoritmom, ako sha256WithRSAEncryption nad digestom SHA-1, alebo RSA signature algoritmus na kľúči podpisovateľa, ktorý RSA nie je, je pcsInvalid, hlásený ako ppeiSignatureAlgorithmMismatch a padá na integrity. Certifikát podpisovateľa, ktorý sa nenájde, dáva pcsIndeterminate, ako už dával pri ECDSA, a nerozpoznané digesty alebo RSA OID, ktoré nie sú signature, dávajú pcsUnsupported. Dĺžka modulu sa stále nekontroluje, parametre PSS sa tu nevalidujú (článok o RSASSA-PSS-params podľa RFC 4055 popisuje, ako sa kódujú na strane podpisovania) a digesty SHA-1 alebo MD5 navyše vyhodia samostatný issue ppeiBadDigestAlgorithm. Podpisová matematika, reťaz certifikátov a revokácia podobne ostávajú prácou CmsSignatureStatus, CertificateTrustStatus a RevocationStatus, ktoré prichádzajú z Windows CryptoAPI. Berte pcsValid ako "algoritmický profil drží", nikdy ako "tento kľúč je dosť silný"

Pre Delphi aplikáciu, ktorá prijíma podpísané PDF faktúry, zmluvy alebo archívne balíky, je praktické nastavenie krátke: spustite ValidatePadesTrust, zamietajte pri ppeiSignatureAlgorithmMismatch, smerujte pcsUnsupported a pcsIndeterminate na človeka a vynúťte si vlastný strop pod dĺžkou RSA kľúča, lebo politika to neurobí. PDFium Component pre Delphi a Lazarus dodáva PAdES validátor, builder evidence reportu aj podpisovú pipeline, takže tá istá knižnica dokáže tieto podpisy vyrábať aj kontrolovať end to end