Műszaki cikk

PDF tanúsítványos titkosítás Delphiben: RSA-OAEP és ECDH

A HotPDF konkrét tanúsítványtulajdonosoknak titkosítja a PDF-et az ISO 32000 public-key security handlerén keresztül: az EnablePubKeyEncryption egy 20 bájtos véletlen seedet vesz fel, minden címzett megkapja a saját CMS borítékját, amit RSA kulcsokhoz az AddPubKeyRecipientCertificate épít (RSA-OAEP key transport), elliptikus görbe kulcsokhoz pedig az AddPubKeyAgreementRecipientWithSecret (ECDH P-256-on, P-384-en, P-521-en, X25519-en vagy X448-en). Senki nem oszt meg jelszót; aki birtokolja a megfelelő privát kulcsot, megnyitja a fájlt

A használati eset mindig ugyanannak a történetnek valamilyen változata. Egy negyedéves auditcsomag három külső lektorhoz megy, a jogi osztály azt akarja, hogy mindhárman elolvassák, csak az egyikük nyomtathatja, és senki nem akar jelszót ülni egy e-mail szálban a melléklet mellett. A jelszavas titkosítás ezt nem tudja kifejezni. A tanúsítványos igen, mert minden címzett egy olyan kulccsal oldja fel a dokumentumot, amivel már rendelkezik, és minden címzett a saját borítékjában hordozhat más-más jogosultságkészletet

Miben különbözik a tanúsítványalapú PDF-titkosítás a jelszótól?

Egy public-key titkosítású PDF a fájlkulcsát egy véletlen seedből és minden címzettboríték pontos bájtjaiból származtatja, nem abból, amit egy ember begépel. A handlert az ISO 32000-1 §7.6.4-e írja le (§7.6.5 az ISO 32000-2-ben), a borítékok pedig CMS EnvelopedData struktúrák az RFC 5652 szerint. A HotPDF /Filter /Adobe.PubSec-et ír /SubFilter /adbe.pkcs7.s5-tel; AES-256-nál ez /V 5-öt jelent és egy /DefaultCryptFilter bejegyzést a /CF alatt /CFM /AESV3-mal, a /Recipients tömb pedig azon a crypt filteren belül lakik. Minden boríték 24 bájtot titkosít: a 20 bájtos seedet, majd az adott címzett 32 bites jogosultságszavát. A titkosítási szótár /P értéke csak helyőrző, mert a valódi jogosultságok minden borítékban utaznak. Betöltéskor az olvasó kibont egy borítékot, helyreállítja a seedet, és összehasheli a seedet a /Recipients sorrendjében minden borítékkal (AES-256-nál SHA-256, a régebbi ciphereknél SHA-1), hogy újraépítse a fájlkulcsot. Ha még mindig e modell és a hétköznapi jelszavak közt döntesz, az AES-256 jelszavas titkosítás és jogosultságflag-útmutató lefedi azt a mérlegelés másik oldalát

HotPDF public-key titkosítás ábra: az EnablePubKeyEncryption rögzít egy 20 bájtos seedet, minden CMS EnvelopedData boríték titkosítja azt a 20 bájtot plusz egy 32 bites jogosultságszót a /Filter /Adobe.PubSec-en belül /SubFilter /adbe.pkcs7.s5-tel és /CFM /AESV3-mal, az olvasó pedig kibont egy borítékot, helyreállítja a seedet, és tömbösorrendben minden /Recipients bejegyzéssel összehasheli, hogy újraépítse a fájlkulcsot
A titkosítási szótár /P értéke csak helyőrző, mert a valódi jogosultságok minden borítékban utaznak, és semmi a folyamat későbbi szakaszában nem rendezheti át vagy kódolhatja újra azt a tömböt, amin a digest fut

RSA címzettek írása az EnablePubKeyEncryption-nel

RSA tanúsítványokhoz hívd meg az EnablePubKeyEncryption-t aes256-fal, majd hívd meg az AddPubKeyRecipientCertificate-et DER-kódolt tanúsítványonként egyszer a BeginDoc előtt. A helper folyamaton belül épít RSAES-OAEP borítékot THPDFRSAOAEPHash értékekkel az OAEP digestre és az MGF1 digestre (rohSHA256, rohSHA384 vagy rohSHA512), és a borítéktartalmat AES-256-CBC-vel titkosítja

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // pontosan 20 bájt, AES-256 esetén is
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // az alapértelmezett kulcstípus az aes128
    // A lektor A nyomtathat; a lektor B csak olvashat és kinyerhet
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
      [prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
      [prExtractContent]);
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A felsorolásban három részlet tartószerkezet. Először, a seed hossza minden kulcstípusnál 20 bájtra van rögzítve, az AES-256-ot is beleértve; az EnablePubKeyEncryption bármilyen más hossznál kivételt dob. Másodszor, az EnablePubKeyEncryption alapból aes128, és mindkét tanúsítványhelper megtagadja a futást, hacsak a kulcstípus nem aes256, így a második argumentum elfelejtése a „certificate envelopes require aes256" kivételt hozza. Az örökölt cipherek (k40, k128, aes128) továbbra is működnek, de csak az AddPubKeyRecipient-ön át, máshol épített borítékkal. Harmadszor, az AES-256 public-key titkosítás PDF 2.0-s funkció, így a HotPDF automatikusan 2.0-ra emeli a dokumentum verzióját. StrictVersionLock beállítva egy alacsonyabb verzión az EnablePubKeyEncryption úgy tér vissza, hogy semmit nem kapcsol be, és a hiba csak a következő sorban bukkan fel, mint „call EnablePubKeyEncryption first". Titkosításváltás inkrementális frissítés közben rögtön EInvalidOpException-t dob

ECDH címzettek hozzáadása: P-256, P-384, P-521, X25519 és X448

Elliptikus görbe tanúsítványokhoz az AddPubKeyAgreementRecipientWithSecret CMS key-agreement címzettet ír (KeyAgreeRecipientInfo, a KARI struktúra az RFC 5753-ból, az RFC 8418 szerinti X25519 és X448 profillal), és folyamaton belül számolja ki az ECDH megosztott titkot. A görbét egy THPDFPubKeyAgreementScheme értékkel választod: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 vagy pkasX448. A sémának egyeznie kell a tanúsítvány kulcsával, vagy a hívás „Certificate key does not match the requested agreement scheme" kivételt dob. A motorháztető alatt minden boríték kap egy friss véletlen 32 bájtos UKM-et, egy stdDH KDF-fel származtatott kulcstitkosító kulcsot (SHA-256 P-256-hoz és X25519-hez, SHA-384 P-384-hez, SHA-512 P-521-hez és X448-hoz), és egy RFC 3394 szerinti AES-256 key wrap-et. Maga a megosztott titok tiszta Pascal görbekódból jön, platform crypto provider nélkül; a tiszta Pascal NIST görbearitmetika cikk elmagyarázza, hogyan épült és hogyan hitelesült az a réteg. A Montgomery-görbéknél a teljes efemer kulcspár lokálisan generálható:

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
  HPDFKeyAgreement;

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Friss efemer skalár borítékonként; a clamping a létrán belül történik
  SetLength(Scalar, 32);
  AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
  try
    OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
    Pdf.AddPubKeyAgreementRecipientWithSecret(
      TFile.ReadAllBytes('legal-x25519.cer'),
      [prPrint, prExtractContent], pkasX25519,
      OriginatorPublic, Scalar,
      []);   // OwnPublicPoint: csak a NIST görbéknél értelmes
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

A NIST görbékre több kell a hívótól. A HotPDF public-key helpereket csak X25519-hez és X448-hoz szállít (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), így P-256-hoz, P-384-hez és P-521-hez te generálod az efemer kulcspárt a saját eszközeiddel, és pontosan a testméretű big-endian skalárt adsz át (32, 48 vagy 66 bájt), plusz a hozzáillő tömörítetlen 0x04||X||Y pontot OriginatorPublicKey-ként. A HotPDF a címzett pontját validálja a görbeegyenletre, de nem tudja ellenőrizni, hogy a te originátor publikus kulcsod valóban hozzátartozik-e a skalárodhoz. Az eltérő felek tökéletesen szabályos borítékot gyártanak, amit egyetlen címzett sem tud megnyitni, ezért kell az oda-vissza betöltés a tesztsuite-odba, nem csak egy fájlméret-ellenőrzés

HotPDF ECDH megállapodási ábra: az AddPubKeyAgreementRecipientWithSecret tiszta Pascal görbekóddal származtatja a megosztott titkot, egy friss 32 bájtos UKM-et kever át a stdDH KDF-en SHA-256-tal P-256-hoz és X25519-hez, SHA-384-gyel P-384-hez, SHA-512-vel P-521-hez és X448-hoz, majd az RFC 3394-es AES-256 key wrap-pel csomagolja a tartalomkulcsot, hogy felépítse a KeyAgreeRecipientInfo borítékot
A sémaértéknek a pkasECDHP256-től a pkasX448-ig egyeznie kell a tanúsítvány kulcsával, és az eltérő skalár- és publikuspont-felek is szabályos borítékot gyártanak, amit egyetlen címzett sem tud megnyitni

Miért számít a /Recipients sorrendje?

A /Recipients sorrendje azért számít, mert a fájlkulcs a seed és minden boríték digestje tömbösorrendben, így az írónak és az olvasónak ugyanazokat a bájtokat kell hashelnie ugyanabban a sorrendben. A HotPDF a borítékokat abban a sorrendben tartja, ahogy hozzáadod, és változatlanul írja ki őket, ami azt jelenti, hogy a címzetteket tetszőleges sorrendben adhatod hozzá, de semmi a folyamat későbbi szakaszában nem rendezheti át, kódolhatja újra vagy „takaríthatja ki" azt a tömböt. A valódi hibák többsége ezen a területen ennek a témának a variációja volt, ahol a két oldal kicsit más bájtokat hashelt:

  • Dinamikus tömbök TList-ben való tárolása Add-del csak nyers pointert őriz, miközben a referenciaszám a lokális változónál marad. A következő SetLength felszabadítja a buffert, és újrahasznosíthatja, így minden slot az utolsó borítékot aliasolta, és a többcímzettes fájlok rossz kulcsot származtattak. A javítás, hogy birtokolt másolatot tárolsz: List.Add(Pointer(System.Copy(Bytes)))
  • A borítékkibontás helyben elemzi a DER-t, és a kulcs-helyreállítási menet eredetileg ugyanazokat az élő tömböket hashelte. Az olvasó mostantól minden boríték tiszta másolatáról pillanatképet készít, mielőtt bármilyen kibontás hozzányúlna, és a digest a pillanatképeken fut
  • Egy Unicode TStringList-en átengedett bináris DER $80-os vagy afölötti bájtjai a kódlap szerint újrakódolódnak, ezért a HotPDF a borítékokat hex szövegként tárolja belül
  • A titkosított és bináris stringeket hex stringként kell írni. Egy literál string end-of-line normalizáción esik át, ahol CR, LF és CRLF egyaránt egyetlen LF lesz (ISO 32000-1 §7.3.4.2), és az csendben átírja a ciphertextet. A HotPDF minden /Recipients bejegyzést hex stringként bocsát ki, és kivonja a stringtitkosítás alól, mert minden olvasónak szüksége van a borítékokra, mielőtt bármilyen kulcsa lenne
  • Egy DER BIT STRING első bájtja a fel nem használt biteket számolja, és bájt-igazított kulcsnál nullának kell lennie. SetLength után inicializálatlanul hagyva azt írta be, ami épp a stacken volt, és egy szigorú kibontó elutasította az originátor kulcsot, így egy fájl alkalmanként éppen azzal a kulccsal nem tudott megnyílni, amire írták
  • Amikor ugyanaz a kulcs továbbra sem tud dekódolni, hasonlíts rétegről rétegre: a fájlkulcsot, aztán a ciphertext prefixet (az IV-et), aztán az objektumkulcsot, végül a plaintextet. A hiba ott lakik, közvetlenül az első, eltérést mutató réteg után

Hogyan nyitsz meg tanúsítvánnyal titkosított PDF-et privát kulccsal?

Egy tanúsítvánnyal titkosított PDF megnyitásához regisztráld a privát kulcsanyagot, mielőtt meghívnád a LoadFromFile-t, mert a HotPDF a szerkezeti menet alatt állítja helyre a fájlkulcsot. Egy HPDFParsePFX-szel feldolgozott RSA vagy EC kulcsot rendeld a PubSecKeyMaterial-hoz, további RSA kulcsokat vegyél fel az AddPubSecKeyMaterial-lal, nyers ECDH skálárokat pedig regisztrálj az AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint)-tal, a HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 vagy HPDFOIDECP521 konstansokkal. A NIST görbék megkövetelik a címzett saját tömörítetlen publikus pontját; a Montgomery-görbék figyelmen kívül hagyják

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;

procedure OpenAuditPack(const LegalScalar: TBytes);
var
  Reader: THotPDF;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    Reader.PubSecKeyMaterial :=
      HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
    Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
    // Opcionális: válaszd ki közvetlenül a borítékot az összes kipróbálása helyett
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = próbáld végig minden borítékot sorrendben
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Callback nélkül a HotPDF minden borítékot kipróbál minden regisztrált kulccsal: először az elsődleges kulcsot, majd minden további RSA kulcsot, végül az EC anyagot. A PubSecRecipientQuery megkapja a borítékok számát, és nullától induló indexet vagy -1-et ad vissza, a tömbön kívüli index pedig kivételt dob ahelyett, hogy beszorulna. Ne feledd, hogy az AddPubSecKeyMaterial csak RSA anyagot fogad el ( modulusra és privát exponensre ragaszkodik), az EC kulcsok tehát a PubSecKeyMaterial-ba vagy az AddPubSecAgreementKeyMaterial-ba tartoznak. Amikor egyetlen kulcs sem bont fel borítékot, a helyreállítási lépés fájlkulcs nélkül tér vissza kivétel helyett, ezért ellenőrizd, hogy a várt tartalom tényleg dekódolódott-e, ahelyett hogy megbíznál abban, hogy a betöltési hívás visszatért

HotPDF privátkulcs-betöltési ábra: a PubSecKeyMaterial az elsődleges RSA vagy EC kulcsot hordozza a HPDFParsePFX-ből, az AddPubSecKeyMaterial csak RSA kulcsokat vesz fel, az AddPubSecAgreementKeyMaterial nyers ECDH skálárokat regisztrál a HPDFOIDX25519-től a HPDFOIDP521-ig terjedő görbe OID-ek alatt, és a LoadFromFile-nál a provider először az elsődleges kulcsot, majd minden további RSA kulcsot, végül az EC anyagot próbálja minden borítékra
Amikor egyetlen kulcs sem bont fel borítékot, a helyreállítási lépés fájlkulcs nélkül tér vissza kivétel helyett, ezért ellenőrizd, hogy a tartalom tényleg dekódolódott-e, vagy rögzítsd a borítékot a PubSecRecipientQuery-n keresztül

Mit nem garantál a HotPDF

A HotPDF garantálja, hogy a saját írója és olvasója bájtról bájtra egyetértenek, és olyan borítékokat épít, amik követik a fent hivatkozott CMS struktúrákat. Nem garantálja viszont, hogy minden PDF-megjelenítő minden kombinációt megnyit. Az RSA-OAEP key transport és az X25519 vagy X448 címzettek támogatása olvasónként és verziónként változik, és azokhoz a kombinációkhoz nem publikáltunk kompatibilitási eredményeket. Ha egy dokumentumnak egy konkrét megjelenítőben kell megnyílnia, titkosíts egy tesztfájlt azonos kulcstípusú teszttanúsítványra, és ott nyisd meg, mielőtt elkötelezed magad egy séma mellett. A borítékban utazó jogosultságok szabályzatot jelentenek, amit a szabványmegfelelő szoftver tiszteletben tart, pontosanúgy, mint a jelszavas titkosítás alatt. A seed minősége is a te felelősséged: az AESGenerateRandomBytes pont erre való, és a HotPDF kitörli a seed saját másolatát, amint a fájlkulcs származtatódott. Ha egy stringhez, streamhez vagy melléklethez is más crypt filter kell, a crypt filter szabályzat-útmutató az StmF-hez, StrF-hez és EFF-hez megmutatja, melyik szűrneveket fogadja el a public-key handler

A tanúsítványos titkosítás, az RSA-OAEP és ECDH címzettborítékok és a privátkulcs-betöltés mind a HotPDF Delphi PDF komponensben érkezik, a jelszavas titkosítás, a digitális aláírások és az ISO 32000 eszközkészlet többi része mellett Delphihez és C++Builderhez