Teknisk artikkel

PDF-sertifikatkryptering i Delphi: RSA-OAEP og ECDH

HotPDF krypterer en PDF for spesifikke sertifikatinnehavere gjennom ISO 32000s public-key security handler: EnablePubKeyEncryption tar et tilfeldig frø på 20 byte, og hver mottaker får sin egen CMS-konvolutt, bygget av AddPubKeyRecipientCertificate for RSA-nøkler (RSA-OAEP key transport) eller AddPubKeyAgreementRecipientWithSecret for elliptisk-kurve-nøkler (ECDH på P-256, P-384, P-521, X25519 eller X448). Ingen deler et passord; den som holder en matchende privatnøkkel, åpner filen

Bruksområdet er alltid en eller annen versjon av den samme historien. Et kvartals revisjonspakke går til tre eksterne gjennomgripere, juridisk avdeling vil at hver av dem skal lese den, bare én av dem får lov til å skrive den ut, og ingen vil ha et passord liggende i en e-posttråd ved siden av vedlegget. Passordkryptering kan ikke uttrykke det. Sertifikatkryptering kan det, fordi hver mottaker låser opp dokumentet med en nøkkel de allerede holder, og hver mottaker kan bære et annet tillatelsessett inne i sin egen konvolutt

Hvordan skiller sertifikatbasert PDF-kryptering seg fra et passord?

En public-key-kryptert PDF utleder filnøkkelen sin fra et tilfeldig frø pluss de eksakte bytene i hver mottakerkonvolutt, ikke fra noe et menneske taster inn. Handleren er beskrevet i ISO 32000-1 §7.6.4 (§7.6.5 i ISO 32000-2), og konvoluttene er CMS EnvelopedData-strukturer som definert i RFC 5652. HotPDF skriver /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5; for AES-256 betyr det /V 5 og en /DefaultCryptFilter-oppføring under /CF med /CFM /AESV3, og /Recipients-arrayen bor inne i den kryptofilteret. Hver konvolutt krypterer 24 byte: frøet på 20 byte fulgt av den mottakerens 32-bits tillatelsesord. /P-verdien i krypteringsordboken er bare en plassholder, fordi de ekte tillatelsene reiser inne i hver konvolutt. Ved lasting pakker en leser opp én konvolutt, gjenoppretter frøet, og hasher frøet sammen med hver konvolutt i /Recipients-rekkefølge (SHA-256 for AES-256, SHA-1 for de eldre siferne) for å bygge filnøkkelen på nytt. Hvis du fortsatt vurderer denne modellen mot vanlige passord, dekker veiledningen om AES-256 passordkryptering og tillatelsesflagg den andre siden av den avveiningen

Diagram over HotPDF public-key-kryptering: EnablePubKeyEncryption fester et frø på 20 byte, hver CMS EnvelopedData-konvolutt krypterer de 20 bytene pluss ett 32-bits tillatelsesord inne i /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5 og /CFM /AESV3, og leseren pakker opp én konvolutt, gjenoppretter frøet og hasher det med hver /Recipients-oppføring i array-rekkefølge for å bygge filnøkkelen på nytt
/P-verdien i krypteringsordboken er bare en plassholder fordi ekte tillatelser reiser inne i hver konvolutt, og ingenting nedstrøms kan omstokke eller re-enkode arrayen digeresten løper over

Å skrive RSA-mottakere med EnablePubKeyEncryption

For RSA-sertifikater kaller du EnablePubKeyEncryption med aes256, og deretter AddPubKeyRecipientCertificate én gang per DER-kodet sertifikat før BeginDoc. Hjelperen bygger en RSAES-OAEP-konvolutt i prosessen med THPDFRSAOAEPHash-verdier for OAEP-digesten og MGF1-digesten (rohSHA256, rohSHA384 eller rohSHA512), og den krypterer konvoluttinnholdet med AES-256-CBC

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

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // nøyaktig 20 byte, selv for AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // standard nøkkeltype er aes128
    // Gjennomgriper A får skrive ut; gjennomgriper B kan bare lese og trekke ut
    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;

Tre detaljer i den oppstillingen bærer last. For det første er frølengden fast på 20 byte for hver nøkkeltype, AES-256 inkludert; EnablePubKeyEncryption reiser på enhver annen lengde. For det andre er standarden til EnablePubKeyEncryption aes128, og begge sertifikathjelperne nekter å kjøre med mindre nøkkeltypen er aes256, så å glemme det andre argumentet gir deg unntaket «certificate envelopes require aes256». De legacysifrene (k40, k128, aes128) virker fortsatt, men bare gjennom AddPubKeyRecipient med en konvolutt du har bygget et annet sted. For det tredje er AES-256 public-key-kryptering en PDF 2.0-funksjon, så HotPDF hever dokumentversjonen til 2.0 automatisk. Med StrictVersionLock satt på en lavere versjon, returnerer EnablePubKeyEncryption uten å aktivere noe, og feilen viser seg bare på neste linje som «call EnablePubKeyEncryption first». Å bytte kryptering under en inkrementell oppdatering reiser EInvalidOpException med én gang

Å legge til ECDH-mottakere: P-256, P-384, P-521, X25519 og X448

For elliptisk-kurve-sertifikater skriver AddPubKeyAgreementRecipientWithSecret en CMS key-agreement-mottaker (KeyAgreeRecipientInfo, KARI-strukturen fra RFC 5753, med X25519- og X448-profilen fra RFC 8418) og beregner den ECDH-delte hemmeligheten i prosessen. Du velger kurven med en THPDFPubKeyAgreementScheme-verdi: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 eller pkasX448. Skjemaet må matche nøkkelen i sertifikatet, ellers reiser kallet «Certificate key does not match the requested agreement scheme». Under panseret får hver konvolutt en fersk tilfeldig 32-byte UKM, en nøkkelkrypteringsnøkkel utledet med stdDH KDF (SHA-256 for P-256 og X25519, SHA-384 for P-384, SHA-512 for P-521 og X448), og en AES-256 key wrap som definert i RFC 3394. Den delte hemmeligheten i seg selv kommer fra ren Pascal-kurvekode, uten noen plattform-kryptoleverandør involvert; artikkelen om ren Pascal NIST-kurve-aritmetikk forklarer hvordan det laget ble bygget og verifisert. For Montgomery-kurvene kan hele det efemere nøkkelparet genereres lokalt:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Fersk efemer skalar per konvolutt; clamping skjer inne i stigen
  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: bare meningsfullt for NIST-kurvene
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST-kurvene krever mer av kalleren. HotPDF leverer public-key-hjelpere bare for X25519 og X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), så for P-256, P-384 og P-521 genererer du det efemere nøkkelparet med eget verktøy og sender en big-endian skalar på nøyaktig feltstørrelsen (32, 48 eller 66 byte) pluss det matchende ukomprimerte 0x04||X||Y-punktet som OriginatorPublicKey. HotPDF validerer mottakerpunktet mot kurveligningen, men den kan ikke sjekke at originatorens offentlige nøkkel faktisk tilhører skalaren din. Umatchede halvdeler produserer fortsatt en fullt velformet konvolutt som ingen mottaker kan åpne, noe som er grunnen til at en rundturs-lasting hører hjemme i testsuiten din, ikke bare en filstørrelsessjekk

Diagram over HotPDF ECDH-avtale: AddPubKeyAgreementRecipientWithSecret utleder den delte hemmeligheten med ren Pascal-kurvekode, mikser en fersk 32-byte UKM gjennom stdDH KDF med SHA-256 for P-256 og X25519, SHA-384 for P-384, SHA-512 for P-521 og X448, og pakker så innholdsnøkkelen med RFC 3394 AES-256 key wrap for å bygge KeyAgreeRecipientInfo-konvolutten
Skjemaverdien fra pkasECDHP256 til pkasX448 må matche sertifikatnøkkelen, og umatchede skalar- og offentlig-punkt-halvdeler produserer fortsatt en velformet konvolutt ingen mottaker kan åpne

Hvorfor betyr rekkefølgen på /Recipients noe?

Rekkefølgen på /Recipients betyr noe fordi filnøkkelen er en digest over frøet og hver konvolutt i array-rekkefølge, så skriver og leser må hashe de samme bytene i samme rekkefølge. HotPDF beholder konvoluttene i den rekkefølgen du legger dem til og skriver dem uendret, noe som betyr at du kan legge til mottakere i vilkårlig rekkefølge, men ingenting nedstrøms kan omstokke, re-enkode eller «rydde opp i» den arrayen. De fleste ekte feilene på dette området var en variant av det temaet, der de to sidene hashet litt forskjellige byte:

  • Å lagre dynamiske arrayer i en TList via Add beholder bare en rå peker mens referansetellingen blir igjen hos den lokale variabelen. Neste SetLength frigjør bufferen og kan gjenbruke den, så hver plass endte opp med å aliasere siste konvolutt, og fler-mottaker-filer utledet feil nøkkel. Fiksen er å lagre en eiet kopi med List.Add(Pointer(System.Copy(Bytes)))
  • Konvoluttoppakking parser DER-en på stedet, og nøkkelgjenopprettingspasset hashet opprinnelig de samme live-arrayene. Leseren tar nå øyeblikksbilder av rene kopier av hver konvolutt før noen opppakking rører dem, og digeresten løper over øyeblikksbildene
  • Binær DER sendt gjennom en Unicode TStringList får byte på eller over $80 re-enkodet av kodesiden, så HotPDF lagrer konvolutter som hekstekst internt
  • Krypterte og binære strenger må skrives som heksstrenger. En literal streng er underlagt linjeslutt-normalisering, der CR, LF og CRLF alle blir en enkelt LF (ISO 32000-1 §7.3.4.2), og det omskriver stille chifferteksten. HotPDF sender hver /Recipients-oppføring som en heksstreng og fritar den fra strengkryptering, siden hver leser trenger konvoluttene før den holder noen nøkkel
  • Den første byten i en DER BIT STRING teller ubrukte biter og må være null for byte-justerte nøkler. Å la den stå uinitialisert etter SetLength skrev hva som helst som lå på stakken, og en streng opppakker avviste originatornøkkelen, så en fil kunne innimellom nekte å åpne med nøyaktig nøkkelen den var skrevet for
  • Når samme nøkkel fortsatt ikke kan dekryptere, sammenlign lag for lag: filnøkkelen, så chiffertekst-prefikset (IV-en), så objektnøkkelen, så klarteksten. Feilen bor rett etter det første laget som er uenig

Hvordan åpner du en sertifikatkryptert PDF med en privatnøkkel?

For å åpne en sertifikatkryptert PDF, registrer det private nøkkelmaterialet før du kaller LoadFromFile, for HotPDF gjenoppretter filnøkkelen under den strukturelle passeringen. Tildel en RSA- eller EC-nøkkel parsert med HPDFParsePFX til PubSecKeyMaterial, legg til flere RSA-nøkler med AddPubSecKeyMaterial, og registrer rå ECDH-skalarer med AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), med konstantene HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 eller HPDFOIDECP521. NIST-kurvene krever mottakerens eget ukomprimerte offentlige punkt; Montgomery-kurvene ignorerer det

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);
    // Valgfritt: plukk konvolutten direkte i stedet for å prøve alle
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = prøv hver konvolutt i rekkefølge
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Uten en callback prøver HotPDF hver konvolutt mot hver registrerte nøkkel: primærnøkkelen først, så hver ekstra RSA-nøkkel, så EC-materialet. PubSecRecipientQuery mottar konvoluttantallet og returnerer en nullbasert indeks eller -1, og en indeks utenfor arrayen reiser et unntak i stedet for å klemmes inn. Merk at AddPubSecKeyMaterial bare godtar RSA-materiale (den insisterer på en modul og en privat eksponent), så EC-nøkler hører hjemme i PubSecKeyMaterial eller AddPubSecAgreementKeyMaterial. Når ingen nøkkel pakker opp noen konvolutt, returnerer gjenopprettingstrinnet uten en filnøkkel i stedet for å reise, så verifiser at innholdet du forventer, faktisk ble dekryptert i stedet for å stole på at lastekallet returnerte

Diagram over HotPDF lasting av privatnøkkel: PubSecKeyMaterial bærer den primære RSA- eller EC-nøkkelen fra HPDFParsePFX, AddPubSecKeyMaterial legger bare til RSA-nøkler, AddPubSecAgreementKeyMaterial registrerer rå ECDH-skalarer under kurve-OID-ene HPDFOIDX25519 til HPDFOIDP521, og ved LoadFromFile prøver leverandøren primærnøkkelen, så hver ekstra RSA-nøkkel, så EC-materialet mot hver konvolutt
Når ingen nøkkel pakker opp noen konvolutt, returnerer gjenopprettingstrinnet uten en filnøkkel i stedet for å reise, så verifiser at innholdet faktisk ble dekryptert eller fest konvolutten gjennom PubSecRecipientQuery

Hva HotPDF ikke garanterer

HotPDF garanterer at dens egen skriver og leser er enige byte for byte, og den bygger konvolutter som følger CMS-strukturene sitert over. Den garanterer ikke at ethvert PDF-visningsprogram åpner enhver kombinasjon. Støtten for RSA-OAEP key transport og for X25519- eller X448-mottakere varierer mellom lesere og versjoner, og vi har ikke publisert kompatibilitetsresultater for de kombinasjonene. Hvis et dokument må åpnes i et spesifikt visningsprogram, krypter en testfil for et testsertifikat av samme nøkkeltype og åpne den der før du forplikter deg til et skjema. Tillatelser båret i konvolutten forblir policy som konform programvare hedrer, nøyaktig som de gjør under passordkryptering. Frøkvaliteten er også ditt ansvar: AESGenerateRandomBytes er der for den jobben, og HotPDF visker ut sin kopi av frøet så snart filnøkkelen er utledet. Hvis du også trenger en streng, strøm eller et vedlegg til å bruke et annet kryptofilter, viser veiledningen om kryptofilter-policy for StmF, StrF og EFF hvilke filternavn public-key-handleren godtar

Sertifikatkryptering, RSA-OAEP- og ECDH-mottakerkonvolutter og lasting av privatnøkkel leveres alle i HotPDF Delphi PDF-komponenten, ved siden av passordkryptering, digitale signaturer og resten av ISO 32000-verktøysettet for Delphi og C++Builder