Teknisk artikel

PDF-certifikatkryptering i Delphi: RSA-OAEP og ECDH

HotPDF krypterer en PDF til specifikke certifikatholdere gennem ISO 32000 public-key security handler: EnablePubKeyEncryption tager et tilfældigt seed på 20 bytes, og hver modtager får sin egen CMS-envelope, bygget af AddPubKeyRecipientCertificate til RSA-nøgler (RSA-OAEP key transport) eller AddPubKeyAgreementRecipientWithSecret til elliptic curve-nøgler (ECDH på P-256, P-384, P-521, X25519 eller X448). Ingen deler en adgangskode; den, der holder en matchende privat nøgle, åbner filen

Use casen er altid en eller anden version af den samme historie. En kvartals-auditpakke går til tre eksterne reviewers, legal vil have, at hver af dem læser den, kun én af dem må printe den, og ingen vil have en adgangskode liggende i en mailtråd ved siden af den vedhæftede fil. Adgangskodekryptering kan ikke udtrykke det. Certifikatkryptering kan, for hver modtager låser dokumentet op med en nøgle, de allerede holder, og hver modtager kan bære et forskelligt tilladelsessæt inde i sin egen envelope

Hvordan adskiller certifikatbaseret PDF-kryptering sig fra en adgangskode?

En public-key-krypteret PDF afleder sin filnøgle af et tilfældigt seed plus de præcise bytes af hver modtager-envelope, ikke af noget, et menneske taster. Handleren er beskrevet i ISO 32000-1 §7.6.4 (§7.6.5 i ISO 32000-2), og envelope-ne er CMS EnvelopedData-strukturer som defineret i RFC 5652. HotPDF skriver /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5; for AES-256 betyder det /V 5 og en /DefaultCryptFilter-entry under /CF med /CFM /AESV3, og /Recipients-arrayet bor inde i den crypt filter. Hver envelope krypterer 24 bytes: seeden på 20 bytes efterfulgt af den modtagers 32-bit tilladelsesord. /P-værdien i encryption-dictionaryen er kun en pladsholder, for de rigtige tilladelser rejser inde i hver envelope. Ved load pakker en reader én envelope op, gendanner seeden og hasher seeden sammen med hver envelope i /Recipients-rækkefølge (SHA-256 til AES-256, SHA-1 til de ældre ciphers) for at genopbygge filnøglen. Hvis du stadig vælger mellem denne model og almindelige adgangskoder, dækker guiden til AES-256-adgangskodekryptering og tilladelsesflag den anden side af det trade-off

HotPDF-diagram over public-key-kryptering: EnablePubKeyEncryption fastlægger et seed på 20 bytes, hver CMS EnvelopedData-envelope krypterer de 20 bytes plus ét 32-bit tilladelsesord inde i /Filter /Adobe.PubSec med /SubFilter /adbe.pkcs7.s5 og /CFM /AESV3, og readeren pakker én envelope op, gendanner seeden og hasher den med hver /Recipients-entry i array-rækkefølge for at genopbygge filnøglen
/P-værdien i encryption-dictionaryen er kun en pladsholder, for de rigtige tilladelser rejser inde i hver envelope, og intet downstream må sortere det array, digesten løber over, om eller re-encode det

At skrive RSA-modtagere med EnablePubKeyEncryption

For RSA-certifikater: kald EnablePubKeyEncryption med aes256, og kald derefter AddPubKeyRecipientCertificate én gang pr. DER-encoded certifikat før BeginDoc. Helperen bygger en RSAES-OAEP-envelope in-process med THPDFRSAOAEPHash-værdier til OAEP-digesten og MGF1-digesten (rohSHA256, rohSHA384 eller rohSHA512), og den krypterer envelope-indholdet 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);                      // præcis 20 bytes, også for AES-256
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // default key type er aes128
    // Reviewer A må printe; reviewer B må kun læse og udtrække
    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 det listing bærer vægt. For det første er seed-længden fastlåst til 20 bytes for alle nøgletyper, AES-256 inkluderet; EnablePubKeyEncryption rejser ved enhver anden længde. For det andet defaulter EnablePubKeyEncryption til aes128, og begge certifikat-helpers nægter at køre, medmindre nøgletypen er aes256, så at glemme det andet argument giver dig exceptionen "certificate envelopes require aes256". De legacy-ciphers (k40, k128, aes128) virker stadig, men kun gennem AddPubKeyRecipient med en envelope, du har bygget et andet sted. For det tredje er AES-256 public-key-kryptering en PDF 2.0-feature, så HotPDF hæver dokumentversionen til 2.0 automatisk. Med StrictVersionLock sat på en lavere version returnerer EnablePubKeyEncryption uden at aktivere noget, og fejlen viser først på næste linje som "call EnablePubKeyEncryption first". At skifte kryptering under en inkrementel opdatering rejser EInvalidOpException med det samme

Tilføjelse af ECDH-modtagere: P-256, P-384, P-521, X25519 og X448

Til elliptic curve-certifikater skriver AddPubKeyAgreementRecipientWithSecret en CMS key-agreement-modtager (KeyAgreeRecipientInfo, KARI-strukturen fra RFC 5753, med X25519- og X448-profilen fra RFC 8418) og beregner den ECDH-delte secret in-process. Du vælger kurven med en THPDFPubKeyAgreementScheme-værdi: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 eller pkasX448. Schemat skal matche nøglen i certifikatet, ellers rejser kaldet "Certificate key does not match the requested agreement scheme". Under motorhjelmen får hver envelope et frisk tilfældigt UKM på 32 bytes, en key-encryption-nøgle afledt med stdDH KDF (SHA-256 til P-256 og X25519, SHA-384 til P-384, SHA-512 til P-521 og X448) og en AES-256 key wrap som defineret i RFC 3394. Den delte secret kommer selv fra ren Pascal-kurvekode uden nogen platform crypto provider involveret; artiklen om ren Pascal NIST curve-aritmetik forklarer, hvordan det lag blev bygget og verificeret. Til Montgomery-kurverne kan hele det efemære nøglepar genereres lokalt:

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

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // Frisk efemær scalar pr. envelope; clamping sker inde i ladderen
  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: kun meningsfuldt for NIST-kurverne
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST-kurverne kræver mere af calleren. HotPDF leverer kun public-key-helpers til X25519 og X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), så til P-256, P-384 og P-521 genererer du det efemære nøglepar med dit eget værktøj og giver en big-endian scalar af præcis feltstørrelsen (32, 48 eller 66 bytes) plus det matchende ukomprimerede 0x04||X||Y-punkt som OriginatorPublicKey. HotPDF validerer modtager-punktet mod kurveligningen, men den kan ikke tjekke, at din originator public key faktisk hører til din scalar. Mispassende halvdele producerer stadig en helt velformet envelope, som ingen modtager kan åbne, og det er derfor, en round-trip-load hører hjemme i din test-suite, ikke kun et filstørrelse-tjek

HotPDF-diagram over ECDH-aftale: AddPubKeyAgreementRecipientWithSecret afleder den delte secret med ren Pascal-kurvekode, blander et friskt UKM på 32 bytes gennem stdDH KDF med SHA-256 til P-256 og X25519, SHA-384 til P-384, SHA-512 til P-521 og X448 og pakker derefter content-nøglen med RFC 3394 AES-256 key wrap for at bygge KeyAgreeRecipientInfo-envelope
Scheme-værdien fra pkasECDHP256 til pkasX448 skal matche certifikatnøglen, og mispassende scalar- og public point-halvdele producerer stadig en velformet envelope, som ingen modtager kan åbne

Hvorfor betyder rækkefølgen af /Recipients noget?

Rækkefølgen af /Recipients betyder noget, fordi filnøglen er en digest over seeden og hver envelope i array-rækkefølge, så writer og reader skal hashe de samme bytes i samme sekvens. HotPDF beholder envelopes i den rækkefølge, du tilføjer dem, og skriver dem uændret, hvilket betyder, at du kan tilføje modtagere i vilkårlig rækkefølge, men intet downstream må sortere det array om, re-encode det eller "rydde op" i det. De fleste af de rigtige bugs på dette område var en variation over det tema, hvor to sider hashe let forskellige bytes:

  • At gemme dynamic arrays i en TList via Add beholder kun en rå pointer, mens reference countet bliver hos den lokale variabel. Næste SetLength frigør bufferen og kan genbruge den, så hver slot endte med at alias'e den sidste envelope, og multi-modtager-filer afledte den forkerte nøgle. Fixet er at gemme en ejet kopi med List.Add(Pointer(System.Copy(Bytes)))
  • Envelope-udpakning parser DER in place, og key-recovery-passet hashe oprindeligt de samme levende arrays. Readeren tager nu snapshots af upåvirkede kopier af hver envelope, før nogen udpakning rører dem, og digesten løber over snapshotene
  • Binær DER, der sendes gennem en Unicode TStringList, får bytes på eller over $80 re-encodet af code siden, så HotPDF gemmer envelopes som hex-tekst internt
  • Krypterede og binære strenge skal skrives som hex-strenge. En literal streng er underlagt end-of-line-normalisering, hvor CR, LF og CRLF alle bliver til én enkelt LF (ISO 32000-1 §7.3.4.2), og det omskriver ciphertext i stilhed. HotPDF udsender hver /Recipients-entry som en hex-streng og fritager den for string-kryptering, da enhver reader skal bruge envelopes, før den holder nogen nøgle
  • Den første byte af en DER BIT STRING tæller ubrugte bits og skal være nul for byte-justerede nøgler. At lade den stå uinitialiseret efter SetLength skrev, hvad der nu lå på stakken, og en strict unwrapper afviste originator-nøglen, så en fil af og til kunne nægte at åbne med netop den nøgle, den var skrevet til
  • Når samme nøgle stadig ikke kan dekryptere, så sammenlign lag for lag: filnøglen, derefter ciphertext-præfikset (IV), derefter objektnøglen, derefter plaintext. Buggen bor lige efter det første lag, der ikke stemmer

Hvordan åbner du en certifikatkrypteret PDF med en privat nøgle?

For at åbne en certifikatkrypteret PDF registrerer du det private nøglemateriale, før du kalder LoadFromFile, for HotPDF gendanner filnøglen under det strukturelle pass. Tildel en RSA- eller EC-nøgle parset med HPDFParsePFX til PubSecKeyMaterial, tilføj flere RSA-nøgler med AddPubSecKeyMaterial, og registrer rå ECDH-scalars med AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), med konstanterne HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 eller HPDFOIDECP521. NIST-kurverne kræver modtagerens eget ukomprimerede public point; Montgomery-kurverne 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);
    // Valgfrit: vælg envelope direkte i stedet for at prøve dem alle
    Reader.PubSecRecipientQuery :=
      function(Context: Pointer; RecipientCount: Integer): Integer
      begin
        Result := -1;   // -1 = prøv hver envelope i rækkefølge
      end;
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

Uden en callback prøver HotPDF hver envelope mod hver registreret nøgle: primærnøglen først, derefter hver ekstra RSA-nøgle, derefter EC-materialet. PubSecRecipientQuery modtager envelope-antallet og returnerer et 0-baseret indeks eller -1, og et indeks uden for arrayet rejser en exception i stedet for at blive clamped. Bemærk, at AddPubSecKeyMaterial kun accepterer RSA-materiale (den insisterer på en modulus og en privat eksponent), så EC-nøgler hører hjemme i PubSecKeyMaterial eller AddPubSecAgreementKeyMaterial. Når ingen nøgle pakker nogen envelope op, returnerer recovery-trinnet uden en filnøgle i stedet for at rejse, så verificér, at det indhold, du forventer, faktisk blev dekrypteret, i stedet for at stole på, at load-kaldet returnerede

HotPDF-diagram over indlæsning af private nøgler: PubSecKeyMaterial bærer den primære RSA- eller EC-nøgle fra HPDFParsePFX, AddPubSecKeyMaterial tilføjer kun RSA-nøgler, AddPubSecAgreementKeyMaterial registrerer rå ECDH-scalars under curve-OID'erne HPDFOIDX25519 til HPDFOIDP521, og ved LoadFromFile prøver provideren primærnøglen, derefter hver ekstra RSA-nøgle, derefter EC-materialet mod hver envelope
Når ingen nøgle pakker nogen envelope op, returnerer recovery-trinnet uden en filnøgle i stedet for at rejse, så verificér, at indholdet faktisk blev dekrypteret, eller fastlås envelope gennem PubSecRecipientQuery

Hvad HotPDF ikke garanterer

HotPDF garanterer, at dens egen writer og reader er enige byte for byte, og den bygger envelopes, der følger de CMS-strukturer, der er citeret ovenfor. Den garanterer ikke, at enhver PDF-viewer åbner enhver kombination. Support for RSA-OAEP key transport og for X25519- eller X448-modtagere varierer mellem readers og versioner, og vi har ikke publiceret kompatibilitetsresultater for de kombinationer. Skal et dokument åbne i en bestemt viewer, så kryptér en testfil til et testcertifikat af samme nøgletype og åbn den dér, før du beslutter dig for et scheme. Tilladelser, der bæres i envelope, forbliver policy, som conforming software ærer, præcis som under adgangskodekryptering. Seed-kvaliteten er også dit ansvar: AESGenerateRandomBytes er der til det job, og HotPDF tørrer sin kopi af seeden, så snart filnøglen er afledt. Skal du også bruge en streng, stream eller vedhæftet fil til at bruge et andet crypt filter, viser crypt filter-policy-guiden for StmF, StrF og EFF, hvilke filternavne public-key-handleren accepterer

Certifikatkryptering, RSA-OAEP- og ECDH-modtager-envelope og indlæsning af private nøgler følger alle med i HotPDF Delphi PDF-komponenten, sammen med adgangskodekryptering, digitale signaturer og resten af ISO 32000-værktøjskassen til Delphi og C++Builder