Technický článek

Kontroly politiky ECDSA a EdDSA podle ISO/TS 32002 v PDFium

PDFium Component pro Delphi kontroluje každý podpis PDF typu ECDSA a EdDSA podle profilu algoritmů ISO/TS 32002 a verdikt hlásí v TPadesSignatureValidation.AlgorithmPolicyStatus. Projít můžou jen P-256, P-384, P-521, tři křivky Brainpool r1, Ed25519 a Ed448, každá s odpovídajícím digestem, a neshoda vyvolá ppeiSignatureAlgorithmMismatch. Tahle politika je důležitější, než zní. Podpis na brainpoolP160r1, nebo P-256 klíč podepsaný přes digest SHA-512, se na matematické úrovni ověří naprosto v pořádku, takže Windows CryptoAPI ohlásí hodnotu podpisu jako dobrou, zatímco přísný validátor PDF 2.0 soubor odmítne. Kontrola politiky tuhle mezeru zavírá a záměrně je oddělená od otázky, zda jsou bajty podpisu kryptograficky správné

Co doopravdy ISO/TS 32002 dovoluje u podpisů na eliptických křivkách?

ISO/TS 32002 dovoluje v podpisech PDF přesně šest křivek ECDSA a dvě schémata EdDSA a ke každé křivce přiváže velikosti digestů, které smí nést. PDFium Component zakóduje tuhle tabulku v PadesCurveDigestAllowed, klíčované podle OID křivky z certifikátu podepisujícího. NIST křivky jsou přísné: digest musí mít stejnou bitovou šířku jako křivka, buď SHA-2, nebo SHA-3. Křivky Brainpool jsou shovívavější a přijímají vlastní šířku nebo cokoliv širšího:

  • P-256 (1.2.840.10045.3.1.7): jen SHA-256 nebo SHA3-256
  • P-384 (1.3.132.0.34): jen SHA-384 nebo SHA3-384
  • P-521 (1.3.132.0.35): jen SHA-512 nebo SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): jakýkoli digest SHA-2 nebo SHA-3 od 256 do 512 bitů
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): SHA-2 / SHA-3 o 384 nebo 512 bitech
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): jen SHA-512 nebo SHA3-512
Matice profilu algoritmů ISO TS 32002 v PDFium Component: P-256, P-384 a P-521 přijímají v PadesCurveDigestAllowed jen odpovídající šířky digestů, brainpoolP256r1 přijímá 256 až 512 bitů, brainpoolP384r1 384 a 512, Ed25519 a Ed448 deklarují SHA-512 a SHAKE256 s délkou 512 a neshody vyvolávají ppeiSignatureAlgorithmMismatch, zatímco křivky mimo profil vracejí pcsUnsupported
Projít můžou šest křivek ECDSA a dvě schémata EdDSA a každá křivka je přivázaná k šířkám digestů, které smí nést; všechno ostatní je invalid nebo unsupported, nikdy se to potichu nepřijme

EdDSA nemá na výběr křivku ani digest, a přesně proto se jeho pravidla točí kolem kódování, ne síly. Podle RFC 8419 musí SignerInfo pro Ed25519 deklarovat SHA-512 jako svůj digestAlgorithm bez parametrů a SignerInfo pro Ed448, na cestě signed-attributes, kterou PAdES vždy používá, musí deklarovat id-shake256-len (2.16.840.1.101.3.4.2.18) s INTEGER parametrem přesně 512. U obou schémat nesmí AlgorithmIdentifier podpisu ani AlgorithmIdentifier veřejného klíče certifikátu mít žádné parametry. Producent, který tam zapíše NULL — zvyk, který RSA enkodéry vycvičily do spousty ASN.1 knihoven — vyprodukuje nekonformní podpis, i když je klíč i hodnota podpisu v pořádku

Jak PDFium Component vytahuje trojici algoritmů z CMS

PDFium samo na tuhle otázku neodpoví, protože jeho veřejné signature API čte signature slovník, ale CMS neověřuje ani neexponuje křivku certifikátu podepisujícího. Inspekční vrstva PAdES postavená na PDFium proto parsuje CMS SignedData (RFC 5652) sama. InspectPadesSignatureAlgorithm přečte digestAlgorithm a signatureAlgorithm prvního SignerInfo, pak najde certifikát podepisujícího a přečte jeho SubjectPublicKeyInfo, aby získal algoritmus klíče a křivku. Vyhledávání certifikátu je záměrně ohraničené: zkoumá se nejvýše 64 certifikátů z množiny certificates v CMS, shoda je exaktní bajtové porovnání issueru a sériového čísla z issuerAndSerialNumber a kód sahá k „jedinému certifikátu, který tam je", jen když množina drží přesně jeden parsovatelný certifikát. Vybrat první EC certifikát z neuspořádané množiny by bylo snadné a nechalo by CA certifikát rozhodovat, jakou křivku podepisující údajně použil

Jak PDFium Component prohlíží trojici algoritmů podpisu PDF: InspectPadesSignatureAlgorithm přečte digestAlgorithm a signatureAlgorithm prvního SignerInfo v CMS, připíchne certifikát podepisujícího exaktní shodou issuerAndSerialNumber mezi nejvýše 64 kandidáty, přečte SubjectPublicKeyInfo kvůli křivce a EvaluatePadesSignatureAlgorithm vrací AlgorithmPolicyStatus
PDFium samo CMS neověřuje ani neexponuje křivku podepisujícího, takže vrstva PAdES parsuje SignedData a drží každý syrový OID v záznamu, aby odmítnutí bylo vysvětlitelné
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 jako součást compliance průchodu, od certifikátu, který najde uvnitř CMS, a TPdf.ValidatePadesTrust ji znovu spustí proti certifikátu podepisujícího, který Windows CryptoAPI doopravdy použil k ověření, takže certifikát hlášený CryptoAPI má poslední slovo. Každý syrový vstup dopadne do TPadesSignatureAlgorithmInfo, včetně DigestParametersPresent, DigestParameterBits, SignatureParametersPresent a PublicKeyParametersAreNamedCurve, takže odmítnutí je vždy vysvětlitelné ze záznamu, ne z log řádku

Proč podpis P-256 se SHA3-256 selhává v politice?

Podpis P-256 selhává v politice PDFium Component vždy, když se CMS digestAlgorithm a digest implikovaný ECDSA signatureAlgorithm rozcházejí, i když oba jsou pro křivku jednotlivě přijatelné. EvaluatePadesSignatureAlgorithm nejdřív zmapuje ecdsa-with-SHA256, ecdsa-with-SHA3-256 a jejich sourozence na digest, porovná ho s deklarovaným digestAlgorithm a při jakémkoli rozdílu vrátí pcsInvalid, ještě než se vůbec sáhne po tabulce křivek. Tenhle případ je skutečný: podepisovací nástroj přepne hash na SHA3-256, ale nechá natvrdo zakódovaný identifikátor ecdsa-with-SHA256 a výsledkem je soubor, který žádný konformní verifikátor nedokáže interpretovat konzistentně. Funkce je veřejná, takže se matice dá připíchnout v unit testu 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); // neshoda 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, teď kongruentní
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // křivka není v profilu
end;

Kódování křivky dostává stejnou přísnost. RFC 5480 §2.1.1 dovoluje, aby ECParameters byla pojmenovaná křivka OID, implicitní křivka (NULL) nebo kompletní explicitní sada parametrů, a profily PKIX vyžadují pojmenovanou formu. PDFium Component vrací pcsInvalid, když certifikát id-ecPublicKey nese žádné parametry, implicitní parametry nebo explicitní parametry, protože explicitní parametry dovolí útočníkovi popsat křivku, která standardní jen připomíná. Správně pojmenovaná křivka, která prostě chybí ze seznamu ISO/TS 32002, jako brainpoolP160r1 výše nebo secp256k1, dostane místo toho pcsUnsupported

Invalid, unsupported nebo indeterminate: čtěte stav poctivě

Tři nevalidní stavy AlgorithmPolicyStatus znamenají různé věci a jejich srovnání do jednoho koše „selhalo" vyhází informace, které auditoři potřebují. pcsInvalid znamená, že rozpoznaná kombinace algoritmů je znetvořená nebo se neshoduje; přidává ppeiSignatureAlgorithmMismatch do TPadesValidationResult.Issues a žene agregátní IntegrityStatus na pcsInvalid, takže IsCryptographicallyValid vrací False, i když hodnota CMS podpisu vyjde. pcsUnsupported znamená, že křivka nebo digest jsou mimo to, co profil jmenuje — výsledek o schopnostech, ne důkaz manipulace. pcsIndeterminate znamená, že se certifikát podepisujícího nepodařilo připíchnout, obvykle CertificateSet s několika kandidáty a bez exaktní shody issuerAndSerialNumber, takže kód odmítá křivku hádat; od v3.124.0 to označuje i RSA podpis přes SHA-1 nebo 112bitový digest jako SHA-224, které už pro současnou validaci nejsou dohodnuté. Stejné dělení platí pro EdDSA na stroji, jehož CryptoAPI neumí ověřit Ed25519 ani Ed448: CmsSignatureStatus zůstává pcsUnsupported, zatímco AlgorithmPolicyStatus může být pořád pcsValid, protože kódování bylo správné a chyběl jen verifikátor. Pokud honíte odmítnutí od Adobe nebo validátoru postaveného na DSS, průvodce tím, proč validátory odmítají PAdES podpisy pokrývá další běžné příčiny

Rozhodovací cesta EvaluatePadesSignatureAlgorithm v PDFium Component: neshoda digestů nastaví pcsInvalid a ppeiSignatureAlgorithmMismatch, pojmenovaná křivka mimo profil ISO TS 32002 jako brainpoolP160r1 nastaví pcsUnsupported, certifikát podepisujícího, který se nedá připíchnout, nastaví pcsIndeterminate a od v3.124.0 aplikuje RSA větev digestové sady ETSI TS 119 312, přičemž velikost klíče zůstává na aplikaci
Tři nevalidní stavy znamenají různé věci: invalid je důkaz rozbité kombinace, unsupported je výsledek o schopnostech a indeterminate znamená, že kód odmítl hádat
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 revokace
    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;

Co pcsValid negarantuje?

AlgorithmPolicyStatus = pcsValid certifikuje jen to, že ECDSA nebo EdDSA podpis používá schválenou křivku s kongruentním, správně enkódovaným digestem, a že RSA podpis používá digest z aktuální sady; o správnosti hodnoty podpisu neříká nic. Před v3.124.0 byla RSA větev EvaluatePadesSignatureAlgorithm záměrně široká: jakýkoli signatureAlgorithm pod obloukem PKCS #1 1.2.840.113549.1.1.* vracel pcsValid, legacy sha1WithRSAEncryption včetně. Od PDFiumPas v3.124.0 aplikuje RSA větev signaturové sady ETSI TS 119 312. Digesty MD2, MD4 a MD5 jsou pcsInvalid. SHA-1 a 112bitové digesty jako SHA-224 jsou pcsIndeterminate, takže SHA-1 podpis si udrží celkový integrity výsledek a označí se k revizi místo odmítnutí. digestAlgorithm, který se liší od digestu fixovaného signaturovým algoritmem, jako sha256WithRSAEncryption přes digest SHA-1, nebo RSA signaturový algoritmus na klíči podepisujícího, který RSA není, je pcsInvalid, hlášený jako ppeiSignatureAlgorithmMismatch a selhává integritu. Certifikát podepisujícího, který se nenajde, dává pcsIndeterminate, jak už to dělal u ECDSA, a nerozpoznané digesty nebo RSA OID, které nejsou signaturové, dávají pcsUnsupported. Délka modulu se pořád nekontroluje, PSS parametry se tady nevalidují (článek o RFC 4055 RSASSA-PSS-params rozebírá, jak se enkódují na straně podepisování) a digesty SHA-1 nebo MD5 navíc vyvolávají samostatný issue ppeiBadDigestAlgorithm. Podpisová matematika, řetěz certifikátů a revokace zůstávají prací CmsSignatureStatus, CertificateTrustStatus a RevocationStatus, které přicházejí z Windows CryptoAPI. Berte pcsValid jako „profil algoritmů drží", nikdy jako „tenhle klíč je dost silný"

Pro aplikaci Delphi, která přijímá podepsané faktury, smlouvy nebo archivní balíčky, je praktické nastavení krátké: pusťte ValidatePadesTrust, odmítněte při ppeiSignatureAlgorithmMismatch, přesměrujte pcsUnsupported a pcsIndeterminate na člověka a vynucujte vlastní spodek velikosti RSA klíče, protože politika to za vás neudělá. PDFium Component pro Delphi a Lazarus shipuje PAdES validátor, builder evidence reportu i podepisovací pipeline, takže tatáž knihovna umí tyhle podpisy produkovat i zkontrolovat od začátku do konce