Műszaki cikk

ISO/TS 32002 ECDSA és EdDSA házirend-ellenőrzés PDFiummal

A Delphi PDFium Component minden ECDSA és EdDSA PDF aláírást az ISO/TS 32002 algoritmusprofilra ellenőriz, és az ítéletet a TPadesSignatureValidation.AlgorithmPolicyStatus-ban jelenti. Csak a P-256, a P-384, a P-521, a három Brainpool r1 görbe, az Ed25519 és az Ed448 mehet át, mindegyik illeszkedő digesttel, az eltérés pedig ppeiSignatureAlgorithmMismatch-ot dob. Ez a házirend fontosabb, mint aminek hangzik. Egy brainpoolP160r1-en készült aláírás, vagy egy SHA-512 digestet aláíró P-256-os kulcs a matematika szintjén tökéletesen ellenőrizhető, így a Windows CryptoAPI az aláírásértéket jónak jelenti, miközben egy szigorú PDF 2.0 validátor elutasítja a fájlt. A házirend-ellenőrzés bezárja ezt a rést, és szándékosan külön áll attól a kérdéstől, hogy az alágírás bájtok kriptográfiailag helyesek-e

Mit enged valójában az ISO/TS 32002 az elliptikus görbés aláírásoknál?

Az ISO/TS 32002 pontosan hat ECDSA görbét és két EdDSA sémát enged a PDF aláírásoknál, és mindegyik görbét ahhoz a digestmérethez köti, amelyet hordozhat. A PDFium Component ezt a táblát a PadesCurveDigestAllowed-ban kódolja, az aláíró tanúsítványból vett görbe OID szerint kulcsolva. A NIST görbék szigorúak: a digestnek azonos bitszélességűnek kell lennie a görbével, SHA-2 vagy SHA-3. A Brainpool görbék engedékenyebbek, és elfogadják a saját szélességüket vagy bármi szélesebbet:

  • P-256 (1.2.840.10045.3.1.7): csak SHA-256 vagy SHA3-256
  • P-384 (1.3.132.0.34): csak SHA-384 vagy SHA3-384
  • P-521 (1.3.132.0.35): csak SHA-512 vagy SHA3-512
  • brainpoolP256r1 (1.3.36.3.3.2.8.1.1.7): bármilyen 256–512 bites SHA-2 vagy SHA-3 digest
  • brainpoolP384r1 (1.3.36.3.3.2.8.1.1.11): 384 vagy 512 bites SHA-2 / SHA-3
  • brainpoolP512r1 (1.3.36.3.3.2.8.1.1.13): csak SHA-512 vagy SHA3-512
Az ISO TS 32002 algoritmusprofil mátrixa a PDFium Componentben: a P-256, a P-384 és a P-521 a PadesCurveDigestAllowed-ban csak illeszkedő digestszélességet fogad el, a brainpoolP256r1 256–512 bitet, a brainpoolP384r1 384-et és 512-t, az Ed25519 és az Ed448 SHA-512-t, illetve 512 hosszú SHAKE256-ot deklarál, az eltérések ppeiSignatureAlgorithmMismatch-ot dobnak, a profilon kívüli görbék pedig pcsUnsupported-ot adnak
Hat ECDSA görbe és két EdDSA séma mehet át, és minden görbe ahhoz a digestszélességhez kötődik, amelyet hordozhat; minden más érvénytelen vagy nem támogatott, sosem csendben elfogadott

Az EdDSA-nak nincs görbe- és digestválasztása, pontosan ezért szólnak szabályai inkább a kódolásról, mint az erőről. Az RFC 8419 szerint egy Ed25519 SignerInfo-nak SHA-512-t kell digestAlgorithm-ként deklarálnia paraméterek nélkül, egy Ed448 SignerInfo-nak pedig — a PAdES által mindig használt signed-attributes úton — id-shake256-len-t (2.16.840.1.101.3.4.2.18) kell deklarálnia pontosan 512 értékű INTEGER paraméterrel. Mindkét sémánál az alágírás AlgorithmIdentifier-ének és a tanúsítvány nyilvános kulcsának AlgorithmIdentifier-ének egyáltalán nem szabad paramétert hordoznia. Egy ott NULL-t író gyártó — ezt a szokást az RSA kódolók nevelték sok ASN.1 könyvtárba — nem megfelelő aláírást gyárt, holott a kulcs és az alágírásérték rendben van

Hogyan nyeri ki a PDFium Component az algoritmus-hármast a CMS-ből

A PDFium önmagában nem tudja megválaszolni ezt a kérdést, mert a nyilvános alágírás API-ja az alágírás szótárát olvassa, de sem a CMS-t nem ellenőrzi, sem az aláíró tanúsítvány görbéjét nem fed fel. A PDFiumra épülő PAdES vizsgálati réteg ezért maga elemzi a CMS SignedData-t (RFC 5652). Az InspectPadesSignatureAlgorithm beolvassa az első SignerInfo digestAlgorithm-ját és signatureAlgorithm-ját, majd megkeresi az aláíró tanúsítványt, és beolvassa a SubjectPublicKeyInfo-ját, hogy megkapja a kulcs algoritmusát és a görbét. A tanúsítványkeresés szándékosan határolt: a CMS certificates halmazából legfeljebb 64 tanúsítvány vizsgálódik, az egyezés a issuerAndSerialNumber-ből vett kibocsátó és sorozatszám pontos bájt-összevetése, és a kód csak akkor tér át „az egyetlen ott lévő tanúsítványra”, ha a halmaz pontosan egy elemzhető tanúsítványt tartalmaz. Az első EC tanúsítvány felvétele egy rendezetlen halmazból könnyű lenne, és az engedné, hogy egy CA tanúsítvány döntse el, melyik görbét használta állítólag az aláíró

Hogyan vizsgálja a PDFium Component a PDF alágírás algoritmus-hármast: az InspectPadesSignatureAlgorithm beolvassa az első SignerInfo digestAlgorithm-ját és signatureAlgorithm-ját a CMS-ben, legfeljebb 64 jelölt közül pontos issuerAndSerialNumber egyezéssel rögzíti az aláíró tanúsítványt, beolvassa a SubjectPublicKeyInfo-t a görbéért, az EvaluatePadesSignatureAlgorithm pedig visszaadja az AlgorithmPolicyStatus-t
A PDFium önmagában sem a CMS-t nem ellenőrzi, sem az aláíró görbéjét nem fedja fel, ezért a PAdES réteg maga elemzi a SignedData-t, és minden nyers OID-ot a rekordban tart, hogy az elutasítás magyarázható legyen
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;

A TPdf.ValidatePades a megfelelőségi menet részeként alkalmazza a házirendet, a CMS-en belül megtalált tanúsítványból kiindulva, a TPdf.ValidatePadesTrust pedig azon az aláíró tanúsítványon futtatja újra, amelyet a Windows CryptoAPI ténylegesen használt az ellenőrzéshez, így a CryptoAPI által jelentett tanúsítványnak van végső szava. Minden nyers bemenet a TPadesSignatureAlgorithmInfo-ba kerül, a DigestParametersPresent-t, a DigestParameterBits-t, a SignatureParametersPresent-t és a PublicKeyParametersAreNamedCurve-t is beleértve, így egy elutasítás mindig a rekordból magyarázható, naplósor helyett

Miért bukik meg a házirenden egy SHA3-256-os P-256 aláírás?

Egy P-256 aláírás akkor bukik meg a PDFium Component házirendjén, ha a CMS digestAlgorithm-ja és az ECDSA signatureAlgorithm által sejtetett digest eltér, akkor is, ha mindkettő önmagában elfogadható a görbére. Az EvaluatePadesSignatureAlgorithm először az ecdsa-with-SHA256-ot, az ecdsa-with-SHA3-256-ot és testvéreiket digestre képezi le, összeveti a deklarált digestAlgorithm-mal, és bármilyen eltérésnél pcsInvalid-ot ad, mielőtt a görbetáblához érne. Az eset valós: egy aláíró eszköz SHA3-256-ra váltja a hashét, de megtartja a bedrótozott ecdsa-with-SHA256 azonosítót, és az eredmény olyan fájl, amelyet megfelelő ellenőrző következetesen nem tud értelmezni. A függvény nyilvános, így a mátrix unit tesztben szögezhető le PDF építése nélkül:

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); // a digest nem egyezik

  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, immár kongruens
  Assert(EvaluatePadesSignatureAlgorithm(Info) = pcsUnsupported); // a görbe nincs a profilban
end;

A görbe kódolása ugyanolyan szigorúságot kap. Az RFC 5480 §2.1.1 engedi, hogy az ECParameters nevesített görbe OID, implicit görbe (NULL) vagy teljes explicit paraméterkészlet legyen, a PKIX profilok pedig a nevesített formát követelik meg. A PDFium Component pcsInvalid-ot ad, ha egy id-ecPublicKey tanúsítvány nem hordoz paramétert, implicit paramétert vagy explicit paramétert hordoz, mert az explicit paraméterek lehetővé teszik, hogy egy támadó olyan görbét írjon le, amely csak hasonlít egy standardhoz. A helyesen nevesített, az ISO/TS 32002 listájáról egyszerűen lemaradt görbe — a fenti brainpoolP160r1 vagy a secp256k1 — pcsUnsupported-ot kap helyette

Érvénytelen, nem támogatott vagy meghatározhatatlan: az állapot becsületes olvasása

A AlgorithmPolicyStatus három nem érvényes állapota különböző dolgokat jelent, és egyetlen „elbukott” vödörbe gyűrve elveszik az az információ, amelyre az auditoroknak szükségük van. A pcsInvalid azt jelenti, hogy egy felismert algoritmus-kombináció rosszul formált vagy eltérő; felveszi a ppeiSignatureAlgorithmMismatch-ot a TPadesValidationResult.Issues-ba, és az összesített IntegrityStatus-t pcsInvalid-ra viszi, így az IsCryptographicallyValid False-t ad akkor is, ha a CMS alágírásértéke rendben van. A pcsUnsupported azt jelenti, hogy a görbe vagy a digest a profil által nevesítetten kívül esik — ez képesség-eredmény, nem bizonyíték manipulációra. A pcsIndeterminate azt jelenti, hogy az aláíró tanúsítványt nem sikerült rögzíteni, jellemzően több jelölttel bíró CertificateSet és pontos issuerAndSerialNumber egyezés nélkül, így a kód megtagadja a görbe találgatását; a v3.124.0 óta SHA-1 feletti vagy SHA-224 jellegű, 112 bites digestet hordozó RSA aláírást is így jelöl, mert a jelenlegi ellenőrzéshez már nem egyeztetett. Ugyanez a felosztás él az EdDSA-ra olyan gépen, amelynek CryptoAPI-ja nem tudja ellenőrizni az Ed25519-et vagy az Ed448-at: a CmsSignatureStatus pcsUnsupported marad, miközben az AlgorithmPolicyStatus lehet még pcsValid, mert a kódolás helyes volt, csak az ellenőrző hiányzott. Ha Adobe-tól vagy DSS alapú validátortól üldözi az elutasítás, a validátorok PAdES aláírás-elutasításait ismertető útikalauz a többi gyakori okot sorolja fel

Az EvaluatePadesSignatureAlgorithm döntési útja a PDFium Componentben: a digesteltérés pcsInvalid-ot és ppeiSignatureAlgorithmMismatch-ot állít, az ISO TS 32002 profilon kívüli nevesített görbe, mint a brainpoolP160r1, pcsUnsupported-ot, a rögzíthetetlen aláíró tanúsítvány pcsIndeterminate-et, a v3.124.0 óta pedig az RSA ág az ETSI TS 119 312 digestcsomagokat alkalmazza, a kulcsméret továbbra is az alkalmazásra hagyatkozva
A három nem érvényes állapot különbözőt jelent: az érvénytelen egy elromlott kombináció bizonyítéka, a nem támogatott képesség-eredmény, a meghatározhatatlan pedig azt jelenti, hogy a kód megtagadta a találgatást
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, visszavonás-ellenőrzés nélkül
    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;

Mit nem garantál a pcsValid?

A AlgorithmPolicyStatus = pcsValid csak azt tanúsítja, hogy egy ECDSA vagy EdDSA alágírás jóváhagyott görbét használ illeszkedő, helyesen kódolt digesttel, egy RSA alágírás pedig egy aktuális csomagból vett digestet; arról nem szól, hogy az alágírásérték helyes-e. A v3.124.0 előtt az EvaluatePadesSignatureAlgorithm RSA ága szándékosan széles volt: bármely signatureAlgorithm a PKCS #1 arc alatt, az 1.2.840.113549.1.1.* alatt, pcsValid-ot adott, a legacy sha1WithRSAEncryption-t is beleértve. A PDFiumPas v3.124.0 óta az RSA ág az ETSI TS 119 312 alágírási csomagokat alkalmazza. Az MD2, MD4 és MD5 digestek pcsInvalid-ot kapnak. A SHA-1 és az olyan 112 bites digestek, mint a SHA-224, pcsIndeterminate-et kapnak, így egy SHA-1 alágírás megtartja az összesített integritási eredményét, és felülvizsgálatra jelölődik, nem pedig elutasításra. Olyan digestAlgorithm, amely eltér az alágírás algoritmus által rögzített digesttől — például sha256WithRSAEncryption SHA-1 digest felett —, vagy RSA alágírásalgoritmus nem RSA aláírónkulcson, pcsInvalid, ppeiSignatureAlgorithmMismatch-ként jelentve megbuktatja az integritást. A nem megtalálható aláíró tanúsítvány pcsIndeterminate-et ad, ahogy az ECDSA-nál is tette, a felismerhetetlen digestek vagy nem alágírás RSA OID-ok pedig pcsUnsupported-ot. A modulus hossza továbbra sem ellenőrzött, a PSS paraméterek itt nem validálódnak (az RFC 4055 RSASSA-PSS-params cikk tárgyalja, hogyan kódolódnak az aláíró oldalon), a SHA-1 vagy MD5 digest ráadásul a külön ppeiBadDigestAlgorithm problémát is felveti. Hasonlóan: az alágírás matematikája, a tanúsítványlánc és a visszavonás továbbra is a CmsSignatureStatus, a CertificateTrustStatus és a RevocationStatus dolga, amelyek a Windows CryptoAPI-ból jönnek. A pcsValid-t úgy kezelje, mint „az algoritmusprofil áll”, sosem mint „ez a kulcs elég erős”

Egy aláírt PDF számlákat, szerződéseket vagy archív csomagokat elfogadó Delphi alkalmazásnak a gyakorlati beállítása rövid: futtassa a ValidatePadesTrust-ot, utasítsa el ppeiSignatureAlgorithmMismatch-ra, irányítsa az pcsUnsupported-ot és az pcsIndeterminate-et emberhez, és érvényesítse a saját RSA kulcsméret-alapját, mert a házirend ezt nem teszi meg. A Delphihez és Lazarushoz készült PDFium Component szállítja a PAdES validátort, a bizonyítékriport-építőt és az aláírási futószalagot, így ugyanaz a könyvtár elő tudja állítani ezeket az aláírásokat, és végről végre ellenőrizni is tudja őket