Tekninen artikkeli

PDF-sertifikaattisalaus Delphissä: RSA-OAEP ja ECDH

HotPDF salaa PDF:n tietyille sertifikaatinhaltijoille ISO 32000:n public-key security handlerin kautta: EnablePubKeyEncryption ottaa 20 tavun satunnaisen siemenen, ja jokainen vastaanottaja saa oman CMS-kirjekuorensa, jonka rakentaa AddPubKeyRecipientCertificate RSA-avaimille (RSA-OAEP-avaintensiirto) tai AddPubKeyAgreementRecipientWithSecret elliptisen käyrän avaimille (ECDH kurveilla P-256, P-384, P-521, X25519 tai X448). Kukaan ei jaa salasanaa; se, jolla on vastaava yksityinen avain, avaa tiedoston

Käyttötapaus on aina jonkinlainen versio samasta tarinasta. Neljännesvuosittainen auditointipaketti menee kolmelle ulkopuoliselle tarkastajalle, lakiosasto haluaa jokaisen lukevan sen, vain yksi heistä saa tulostaa sen, eikä kukaan halua salasanan lojuvan sähköpostiketjussa liitteen vieressä. Salasanalla salaus ei osaa ilmaista tuota. Sertifikaattisalaus osaa, koska jokainen vastaanottaja avaa asiakirjan avaimella, jota hän jo pitää hallussaan, ja jokainen vastaanottaja voi kantaa erilaista käyttöoikeusjoukkoa oman kirjekuorensa sisällä

Miten sertifikaattipohjainen PDF-salaus eroaa salasanasta?

Public-key-salattu PDF johdtaa tiedostoavaimensa satunnaisesta siemenestä plus jokaisen vastaanottajakirjekuoren täsmällisistä tavuista, ei mistään siitä, mitä ihminen näppäilee. Käsittelijä on kuvattu standardissa ISO 32000-1 §7.6.4 (§7.6.5 standardissa ISO 32000-2), ja kirjekuoret ovat RFC 5652:n mukaisia CMS-EnvelopedData-rakenteita. HotPDF kirjoittaa /Filter /Adobe.PubSecin /SubFilter /adbe.pkcs7.s5llä; AES-256:lle se tarkoittaa /V 5-arvoa ja /DefaultCryptFilter-merkintää /CFin alla /CFM /AESV3llä, ja /Recipients-taulukko asuu kyseisen crypt filterin sisällä. Jokainen kirjekuori salaa 24 tavua: 20 tavun siemenen, jota seuraa kyseisen vastaanottajan 32-bittinen käyttöoikeussana. Salaussanakirjan /P-arvo on vain paikanpitäjä, sillä oikeat käyttöoikeudet matkustavat jokaisen kirjekuoren sisällä. Latausaikana lukija purkaa yhden kirjekuoren, saa siemenen takaisin ja tiivistää siemenen yhdessä jokaisen kirjekuoren kanssa /Recipients-järjestyksessä (SHA-256 AES-256:lle, SHA-1 vanhemmille salaimille) rakentaakseen tiedostoavaimen uudelleen. Jos harkitset vielä tämän mallin ja tavallisten salasanojen välillä, opas AES-256-salasanalauksesta ja käyttöoikeuslipuista kattaa kompromissin toisen puolen

HotPDF:n public-key-salauksen kaavio: EnablePubKeyEncryption lukitsee 20 tavun siemenen, jokainen CMS EnvelopedData -kirjekuori salaa ne 20 tavua plus yhden 32-bittisen käyttöoikeussanan /Filter /Adobe.PubSec -käsittelijän sisällä /SubFilter /adbe.pkcs7.s5:llä ja /CFM /AESV3:lla, ja lukija purkaa yhden kirjekuoren, saa siemenen takaisin ja tiivistää sen jokaisen /Recipients-merkinnän kanssa taulukkojärjestyksessä rakentaakseen tiedostoavaimen
Salaussanakirjan /P-arvo on vain paikanpitäjä, koska oikeat käyttöoikeudet matkustavat jokaisen kirjekuoren sisällä, eikä mikään myöhempi vaihe saa järjestää uudelleen tai uudelleenkoodata taulukkoa, jonka yli tiivistys ajetaan

RSA-vastaanottajien kirjoittaminen EnablePubKeyEncryptionilla

RSA-sertifikaateille kutsu EnablePubKeyEncryptionia arvolla aes256 ja sitten AddPubKeyRecipientCertificateia kerran per DER-koodattu sertifikaatti ennen BeginDocia. Apuri rakentaa RSAES-OAEP-kirjekuoren prosessin sisällä THPDFRSAOAEPHash-arvoilla OAEP-tiivistykselle ja MGF1-tiivistykselle (rohSHA256, rohSHA384 tai rohSHA512), ja se salaa kirjekuoren sisällön AES-256-CBC:llä

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

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // täsmälleen 20 tavua, myös AES-256:lle
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // oletusavaintyyppi on aes128
    // Tarkastaja A saa tulostaa; tarkastaja B saa vain lukea ja poimia
    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;

Kolme yksityiskohtaa kyseisessä listauksessa kantavat rakennetta. Ensinnäkin siemenen pituus on lukittu 20 tavuun kaikilla avaintyypeillä, AES-256 mukaan lukien; EnablePubKeyEncryption nostaa poikkeuksen muista pituuksista. Toiseksi EnablePubKeyEncryption olettaa aes128in, ja molemmat sertifikaattiapurit kieltäytyvät toimimasta, ellei avaintyyppi ole aes256, joten toisen argumentin unohtaminen tuottaa poikkeuksen ”certificate envelopes require aes256”. Vanhat salaimet (k40, k128, aes128) toimivat yhä, mutta vain AddPubKeyRecipientin kautta muualla rakentamallasi kirjekuorella. Kolmanneksi AES-256 public-key-salaus on PDF 2.0:n ominaisuus, joten HotPDF nostaa asiakirjan version 2.0:aan automaattisesti. Kun StrictVersionLock on asetettu matalammalle versiolle, EnablePubKeyEncryption palaa laukaisematta mitään, ja virhe ilmenee vasta seuraavalla rivillä muodossa ”call EnablePubKeyEncryption first”. Salauksen vaihtaminen inkrementaalisen päivityksen aikana nostaa EInvalidOpExceptionin suoraan

ECDH-vastaanottajien lisääminen: P-256, P-384, P-521, X25519 ja X448

Elliptisen käyrän sertifikaateille AddPubKeyAgreementRecipientWithSecret kirjoittaa CMS-avainsopimusvastaanottajan (KeyAgreeRecipientInfo, RFC 5753:n KARI-rakenne, RFC 8418:n X25519- ja X448-profiililla) ja laskee ECDH:n jaetun salaisuuden prosessin sisällä. Valitset käyrän THPDFPubKeyAgreementScheme-arvolla: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 tai pkasX448. Kaavan on täsmättävä sertifikaatin avaimeen, tai kutsu nostaa poikkeuksen ”Certificate key does not match the requested agreement scheme”. Konepellin alla jokainen kirjekuori saa tuoreen satunnaisen 32 tavun UKM:n, avainta salaavan avaimen, joka johdetaan stdDH KDF:llä (SHA-256 P-256:lle ja X25519:lle, SHA-384 P-384:lle, SHA-512 P-521:lle ja X448:lle), ja RFC 3394:n mukaisen AES-256-avainkääreen. Itse jaettu salaisuus tulee puhtaasta Pascal-käyräkoodista, ilman mukana olevaa alustan kryptopalveluntarjoajaa; artikkeli puhtaassa Pascal-koodissa toteutetusta NIST-käyräaritmetiikasta kertoo, miten kyseinen kerros rakennettiin ja varmennettiin. Montgomery-käyrille koko kertakäyttöinen avainpari voidaan tuottaa paikallisesti:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Tuore kertakäyttöinen skalaari jokaista kirjekuorta kohden; clampaus tapahtuu tikapuun sisällä
  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: merkitsevä vain NIST-käyrille
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST-käyrät vaativat kutsujalta enemmän. HotPDF toimittaa public-key-apurit vain X25519:lle ja X448:lle (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), joten P-256:lle, P-384:lle ja P-521:lle tuotat kertakäyttöisen avainparin omilla työkaluillasi ja välität big-endian-skalaarin, jonka pituus on täsmälleen kentän koko (32, 48 tai 66 tavua), plus vastaavan pakkamattoman 0x04||X||Y-pisteen OriginatorPublicKeyina. HotPDF valido vastaanottajapisteen käyräyhtälöä vasten, mutta se ei voi tarkistaa, että alkuperäisen julkinen avaimesi oikeasti kuuluu skalaarillesi. Erimittaiset puoliskot tuottavat silti täydellisen hyvin muodostuneen kirjekuoren, jota mikään vastaanottaja ei voi avata, ja siksi edestakainen lataus kuuluu testisarjaasi, eikä pelkkä tiedostokoon tarkistus riitä

HotPDF:n ECDH-sopimuksen kaavio: AddPubKeyAgreementRecipientWithSecret johdtaa jaetun salaisuuden puhtaalla Pascal-käyräkoodilla, sekoittaa tuoreen 32 tavun UKM:n stdDH KDF:n läpi SHA-256:lla P-256:lle ja X25519:lle, SHA-384:lla P-384:lle, SHA-512:lla P-521:lle ja X448:lle ja käärii sitten sisältöavaimen RFC 3394:n AES-256-avainkääreeseen rakentaakseen KeyAgreeRecipientInfo-kirjekuoren
Kaava-arvon pkasECDHP256:sta pkasX448:in on täsmättävä sertifikaatin avaimeen, ja erimittaiset skalaarin ja julkisen pisteen puoliskot tuottavat silti hyvin muodostuneen kirjekuoren, jota mikään vastaanottaja ei voi avata

Miksi /Recipients-järjestyksellä on väliä?

/Recipients-taulukon järjestyksellä on väliä, koska tiedostoavain on tiivistelmä siemenestä ja jokaisesta kirjekuoresta taulukkojärjestyksessä, joten kirjoittajan ja lukijan on tiivistettävä samat tavut samassa järjestyksessä. HotPDF pitää kirjekuoret siinä järjestyksessä kuin lisäät ne ja kirjoittaa ne muuttumattomina, eli voit lisätä vastaanottajia missä järjestyksessä tahansa, mutta mikään myöhempi vaihe ei saa järjestää, uudelleenkoodata tai siistiä kyseistä taulukkoa. Useimmat alueen oikeista bugeista olivat variatio tuosta teemasta, jossa kaksi puolta tiivistivät hieman eri tavut:

  • Dynaamisten taulukoiden tallentaminen TListiin Addilla pitää kiinni vain raakaa osoitinta, kun taas viitemäärä jää paikalliselle muuttujalle. Seuraava SetLength vapauttaa puskurin ja voi käyttää sen uudelleen, joten jokainen lokero päätyi aliastamaan viimeistä kirjekuorta ja monivastaanottajaiset tiedostot johdattivat väärän avaimen. Korjaus on tallentaa omistettu kopio muodossa List.Add(Pointer(System.Copy(Bytes)))
  • Kirjekuoren purku jäsentää DER:n paikallaan, ja avainten palautusvaihe tiisti alun perin samat elossa olevat taulukot. Lukija ottaa nyt koskemattomat kopiot jokaisesta kirjekuoresta ennen kuin mikään purku koskee niitä, ja tiivistelmä ajetaan kopioiden yli
  • Binäärinen DER, joka kulkee Unicode-TStringListin läpi, saa tavunsa $80sta ylöspäin uudelleenkoodatuksi koodisivun toimesta, joten HotPDF tallentaa kirjekuoret heksatekstinä sisäisesti
  • Salatut ja binääriset merkkijonot on kirjoitettava heksamerkkijonoina. Literaalimerkkijonoon sovelletaan rivinlopun normalisointia, jossa CR, LF ja CRLF muuttuvat kaikki yhdeksi LF:ksi (ISO 32000-1 §7.3.4.2), ja se kirjoittaa salatekstin uudelleen hiljaa. HotPDF tuottaa jokaisen /Recipients-merkinnän heksamerkkijonona ja vapauttaa sen merkkijonosalaamiselta, sillä jokainen lukija tarvitsee kirjekuoret ennen kuin sillä on mitään avainta
  • DER-BIT STRINGin ensimmäinen tavu laskee käyttämättömiä bittejä ja sen on oltava nolla tavutasattujen avainten kohdalla. Alustamatta jättäminen SetLengthin jälkeen kirjoitti pinossa sattuneen, ja tiukka purkaja hylkäsi alkuperäisen avaimen, joten tiedosto saattoi satunnaisesti epäonnistua avautumaan täsmälleen sillä avaimella, jolle se oli kirjoitettu
  • Kun sama avain ei silti pysty purkamaan, vertaa kerros kerrokselta: tiedostoavain, sitten salatekstin etuliite (IV), sitten objektiavain, sitten selväkielinen teksti. Bugi asuu heti ensimmäisen eriävän kerroksen jälkeen

Miten avaat sertifikaatilla salatun PDF:n yksityisellä avaimella?

Avataksesi sertifikaatilla salatun PDF:n rekisteröi yksityisen avaimen materiaali ennen LoadFromFileia, koska HotPDF palauttaa tiedostoavaimen rakenteellisen kävelyn aikana. Sijoita HPDFParsePFXilla jäsennetty RSA- tai EC-avain PubSecKeyMaterialiin, lisää lisää RSA-avaimia AddPubSecKeyMaterialilla ja rekisteröi raakat ECDH-skalaarit kutsulla AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint) käyttäen vakioita HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 tai HPDFOIDECP521. NIST-käyrät vaativat vastaanottajan oman pakkamattoman julkisen pisteen; Montgomery-käyrät ohittavat sen

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);
    // Valinnainen: poimi kirjekuori suoraan sen sijaan että kokeilet niitä kaikkia
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = kokeile jokaista kirjekuorta järjestyksessä
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Ilman takaisinkutsua HotPDF kokeilee jokaista kirjekuorta jokaista rekisteröityä avainta vastaan: ensin ensisijainen avain, sitten jokainen lisätty RSA-avain, sitten EC-materiaali. PubSecRecipientQuery vastaanottaa kirjekuorten määrän ja palauttaa nollapohjaisen indeksin tai -1:n, ja taulukon ulkopuolinen indeksi nostaa poikkeuksen sen sijaan että se katkaistaisiin rajoille. Huomaa, että AddPubSecKeyMaterial hyväksyy vain RSA-materiaalin (se vaatii moduluksen ja yksityisen eksponentin), joten EC-avaimet kuuluvat PubSecKeyMaterialiin tai AddPubSecAgreementKeyMaterialiin. Kun mikään avain ei pysty purkamaan mitään kirjekuorta, palautusvaihe palaa ilman tiedostoavainta poikkeuksen sijaan, joten varmista, että odottamasi sisältö oikeasti purkautui sen sijaan että luottaisit siihen, että latauskutsu palasi

HotPDF:n yksityisen avaimen lataamisen kaavio: PubSecKeyMaterial kantaa ensisijaisen RSA- tai EC-avaimen HPDFParsePFX:stä, AddPubSecKeyMaterial lisää vain RSA-avaimia, AddPubSecAgreementKeyMaterial rekisteröi raakat ECDH-skalaarit HPDFOIDX25519:stä HPDFOIDP521:een ulottuvien käyrä-OIDien alle, ja LoadFromFilessa palveluntarjoaja kokeilee ensisijaista avainta, sitten jokaista lisättyä RSA-avainta, sitten EC-materiaalia jokaista kirjekuorta vastaan
Kun mikään avain ei pysty purkamaan mitään kirjekuorta, palautusvaihe palaa ilman tiedostoavainta poikkeuksen sijaan, joten varmista että sisältö oikeasti purkautui tai kiinnitä kirjekuori PubSecRecipientQueryn kautta

Mitä HotPDF ei takaa

HotPDF takaa, että sen oma kirjoittaja ja lukija ovat yhtä mieltä tavu tavulta, ja se rakentaa kirjekuoret, jotka noudattavat yllä siteerattuja CMS-rakenteita. Se ei takaa, että jokainen PDF-katselin avaa jokaisen yhdistelmän. Tuki RSA-OAEP-avaintensiirrolle ja X25519- tai X448-vastaanottajille vaihtelee lukijoiden ja versioiden välillä, emmekä ole julkaisseet yhteensopivuustuloksia niille yhdistelmille. Jos asiakirjan on avauduttava tietyssä katselimessa, salaa testitiedosto saman avaintyypin testisertifikaatille ja avaa se siellä ennen kuin sitoudut kaavaan. Kirjekuorissa kulkevat käyttöoikeudet pysyvät politiikkana, jota standardinmukainen ohjelmisto kunnioittaa, täsmälleen kuten salasanalauksessakin. Siemenen laatu on myös sinun vastuullasi: AESGenerateRandomBytes on olemassa juuri tuohon työhön, ja HotPDF pyyhkii oman siemenkopionsa heti, kun tiedostoavain on johdettu. Jos tarvitset lisäksi merkkijonon, streamin tai liitteen käyttävän toista crypt filteriä, opas crypt filter -politiikasta StmF, StrF ja EFF näyttää, mitkä suodattimien nimet public-key-käsittelijä hyväksyy

Sertifikaattisalaus, RSA-OAEP- ja ECDH-vastaanottajakirjekuoret sekä yksityisen avaimen lataaminen toimitetaan kaikki HotPDF Delphi PDF -komponentissa, yhdessä salasanalauksen, digitaalisten allekirjoitusten ja muun ISO 32000 -työkaluvalikoiman kanssa Delphille ja C++Builderille