Een certificaatversleutelde PDF draagt één enkele /P-waarde in zijn encryption-dictionary, voor wie het bestand ook geschreven is; in de bestanden die verderop in dit artikel worden gebouwd is dat -64. Drie ontvangers met drie verschillende permissionsets delen nog steeds dat ene getal. Code die "mag deze gebruiker printen?" uit /P beantwoordt leest een placeholder, want onder de public-key security handler reist het echte permissionword binnenin de versleutelde envelope van elke ontvanger en verschijnt hij pas zodra de privésleutel van die ontvanger hem heeft geopend
Dat ontwerp is de reden dat certificaatversleuteling de moeite waard is. Een kwartaalauditpakket gaat naar drie externe reviewers; legal wil dat ze het alle drie lezen, slechts één van hen mag het printen, en niemand wil een wachtwoord in de e-mailthread laten slingeren naast de bijlage. Een wachtwoord kan dat niet uitdrukken. Certificaten wel: elke ontvanger ontgrendelt het bestand met een sleutel die hij al bezit, en elke envelope draagt zijn eigen permissionword
Hoe verschilt certificaatgebaseerde PDF-versleuteling van een wachtwoord?
Een met public keys versleutelde PDF leidt zijn file key af uit een random seed plus de exacte bytes van elke ontvanger-envelope, en niets wat een mens intikt komt in de berekening. De handler is gespecificeerd in ISO 32000-1 §7.6.4 (§7.6.5 in ISO 32000-2), en de envelopes zijn CMS EnvelopedData-structuren uit RFC 5652. HotPDF schrijft /Filter /Adobe.PubSec met /SubFilter /adbe.pkcs7.s5. Bij AES-256 betekent dat /V 5, een /DefaultCryptFilter-entry onder /CF met /CFM /AESV3, en de /Recipients-array geplaatst binnen die crypt filter in plaats van in het toplevel-dictionary
Elke envelope versleutelt 24 bytes: de seed van 20 bytes gevolgd door het 32-bit permissionword van die ontvanger. Bij het laden pakt een reader één envelope uit, herstelt de seed en hasht de seed samen met elke envelope in /Recipients-volgorde (SHA-256 voor AES-256, SHA-1 voor de oudere ciphers) om de file key opnieuw op te bouwen. Reviewer B komt nooit te weten wat reviewer A mag doen, maar de reader van B hasht toch de envelopebytes van A, dus de hele array moet byte voor byte overleven. Twijfel je nog tussen dit model en gewone wachtwoorden, dan behandelt de gids over AES-256-wachtwoordversleuteling en permissionflags de andere kant van die afweging
RSA-ontvangers schrijven met EnablePubKeyEncryption
Voor RSA-certificaten is de reeks kort: EnablePubKeyEncryption met aes256, dan één AddPubKeyRecipientCertificate-aanroep per DER-gecodeerd certificaat, allemaal vóór BeginDoc. De helper bouwt in-process een RSAES-OAEP-envelope. Zijn laatste twee parameters kiezen de OAEP-digest en de MGF1-digest uit THPDFRSAOAEPHash (rohSHA256, rohSHA384 of rohSHA512), beide met default rohSHA256, en de envelope-inhoud wordt versleuteld met 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); // precies 20 bytes, ook voor AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // standaard keytype is aes128
// Reviewer A mag printen; reviewer B mag alleen lezen en extraheren
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;
Drie details in die listing dragen gewicht. De seed is 20 bytes voor elk keytype, AES-256 incluis, en EnablePubKeyEncryption gooit bij elke andere lengte. Het keytype staat standaard op aes128 terwijl beide certificaathelpers op aes256 staan, dus het weglaten van het tweede argument laat de eerste AddPubKeyRecipientCertificate-aanroep "certificate envelopes require aes256" geven. De legacy-ciphers (k40, k128, aes128) blijven beschikbaar via AddPubKeyRecipient, die een complete DER-envelope neemt die door andere tooling is gebouwd
Het derde detail is de versie. AES-256 onder de public-key handler is een PDF 2.0-feature, en HotPDF tilt de documentversie zelf naar 2.0. Met StrictVersionLock gezet op een lagere versie is die bump niet toegestaan: EnablePubKeyEncryption keert terug zonder iets aan te zetten, en de fout duikt één regel later op wanneer de recipienthelper "call EnablePubKeyEncryption first" meldt. Die melding stuurt mensen op jacht naar een aanroepvolgorde-fout. Controleer in plaats daarvan de versielock. Versleuteling wisselen tijdens een incrementele update wordt ronduit geweigerd met EInvalidOpException
ECDH-ontvangers toevoegen op P-256, P-384, P-521, X25519 en X448
Elliptische-curve-certificaten lopen via AddPubKeyAgreementRecipientWithSecret, die een CMS key-agreement-ontvanger schrijft (KeyAgreeRecipientInfo uit RFC 5753, met het X25519- en X448-profiel uit RFC 8418) en het ECDH shared secret in-process berekent. Een THPDFPubKeyAgreementScheme-waarde benoemt de curve: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 of pkasX448. Het scheme moet matchen met de sleutelalgoritme, curve en sleutellengte van het certificaat, anders geeft de aanroep "Certificate key does not match the requested agreement scheme"
Elke envelope krijgt een verse random UKM van 32 bytes, een key-encryption key uit de stdDH-KDF (SHA-256 voor P-256 en X25519, SHA-384 voor P-384, SHA-512 voor P-521 en X448), en een AES-256 key wrap zoals gedefinieerd in RFC 3394. De curve-aritmetiek is kale Pascal zonder platform-cryptoprovider eronder; het artikel over pure Pascal NIST-curve-aritmetiek loopt door hoe die laag in elkaar zit. Op de Montgomery-curves kan het hele ephemere sleutelpaar lokaal worden gegenereerd:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// verse ephemere scalar per envelope; clamping gebeurt binnen de ladder
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: alleen betekenisvol voor de NIST-curves
finally
HPDFSecureClearBytes(Scalar);
end;
end;
De NIST-curves vragen meer van de aanroeper. HotPDF leidt originator public keys alleen voor X25519 en X448 af (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), dus voor P-256, P-384 en P-521 komt het ephemere paar uit je eigen tooling. Geef een big-endian scalar van precies de veldgrootte (32, 48 of 66 bytes) mee en het bijpassende ongecomprimeerde 0x04||X||Y-punt (65, 97 of 133 bytes) als OriginatorPublicKey, en geef hetzelfde punt door aan OwnPublicPoint, de parameter gereserveerd voor de NIST-schemes. Verkeerde lengtes gooien onmiddellijk. Het ontvangerpunt dat uit het certificaat komt wordt tegen de curvevergelijking gecontroleerd voordat er ook maar één vermenigvuldiging draait
Wat geen enkele library kan controleren is of je originator public key bij je scalar hoort. Niet-passende helften leveren nog steeds een keurig gevormde envelope op. De writer leidt zijn key af uit scalar maal ontvangerpunt, de ontvanger leidt de zijne af uit zijn privésleutel maal het verkeerde originatorpunt, en de RFC 3394-integriteitscontrole aan de kant van de ontvanger is het eerste dat het merkt, tegen de tijd dat het bestand al verzonnen is. Een round-trip-load met een testsleutel op dezelfde curve hoort thuis in je build; een geslaagde EndDoc en een plausibele bestandsgrootte bewijzen hier niets
Die koppelingsstap is waarom de curvekeuze in de praktijk ertoe doet. Heb je de certificaatuitgifte in de hand, dan is X25519 de makkelijkere curve om vanuit Delphi goed te doen: één random scalar van 32 bytes, één helperaanroep, geen punt dat je met de hand gesynchroniseerd moet houden. De NIST-curves verdienen hun plek wanneer de certificaten van de ontvangers uit een PKI komen dat alleen P-256 of P-384 uitgeeft, en in dat geval houdt de round-trip-load op met optioneel zijn
Waarom maakt de volgorde van /Recipients uit?
De volgorde van /Recipients telt omdat de file key een digest is over de seed gevolgd door elke envelope in arrayvolgorde, dus writer en reader moeten identieke bytes in een identieke volgorde hashen. HotPDF houdt envelopes in de volgorde waarin je ze toevoegt en schrijft ze onveranderd weg, wat je vrij laat in welke volgorde dan ook te kiezen. Wat er daarna met de array gebeurt is het risicovolle deel. Postprocessingtools, signing-pipelines en zelfgebouwde envelopebuilders zijn waar het breekt, en de mislukkingen delen één vorm — twee kanten die licht verschillende bytes hashen:
- De untyped
TListvan Delphi bewaart een kale pointer. Envelopes verzamelen metList.Add(Pointer(Bytes))laat de reference count bij de lokale variabele, dus de volgendeSetLengthop die variabele geeft de buffer vrij of hergebruikt hem, en elk slot kan eindigen met aliassen naar de laatste envelope.TList<TBytes>of een kaleTArray<TBytes>houdt de referenties geteld - DER-parsers die in place werken modificeren de buffer die ze parsen. Hash een pristine kopie van elke envelope, genomen voordat een uitpoging hem aanraakt
- Een Unicode
TStringListis geen plek voor binaire DER: bytes op of boven$80worden onderweg naar binnen door de codepage hergecodeerd. Houd envelopes inTBytes, of hex-codeer ze eerst - Ciphertext hoort in hexstrings. Binnen een literal string worden CR, LF en CRLF allebei teruggelezen als één LF (ISO 32000-1 §7.3.4.2), dus een envelope die toevallig
$0D $0Abevat verliest een byte. HotPDF schrijft elke/Recipients-entry als hexstring en houdt hem buiten stringversleuteling, want een reader heeft de envelopes nodig voordat hij enige key bezit - De eerste contentbyte van een DER
BIT STRINGtelt ongebruikte bits en moet$00zijn voor byte-uitgelijnde sleutels. Handgeschreven DER die deze byte laat vallen en het0x04||X||Y-punt rechtstreeks in de content zet schuift de hele sleutel één byte op, en een strikte parser weigert de originator key ronduit - Als een sleutel die zou moeten werken nog steeds niet kan decoderen, vergelijk dan laag voor laag, in deze volgorde: file key, de IV aan de voorkant van de ciphertext, per-object key, plaintext. De fout zit vlak na de eerste laag die afwijkt
Een certificaatversleutelde PDF laden met privésleutels
Privésleutels moeten worden geregistreerd voordat LoadFromFile draait, want HotPDF herstelt de file key tijdens de structurele pas. RSA-identiteiten geparsed met HPDFParsePFX gaan naar PubSecKeyMaterial, en verdere RSA-sleutels naar AddPubSecKeyMaterial, die materiaal zonder modulus en private exponent weigert. Beide sleutelslots worden gematcht op de RSA-modulus en private exponent, dus EC-sleutels, PFX-gebaseerde incluis, nemen een derde route: AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), met een van de constanten HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 of HPDFOIDECP521. De NIST-curves vereisen daar het eigen ongecomprimeerde public point van de ontvanger; de Montgomery-curves negeren het
Een EC-identiteit die als PFX-bestand binnenkomt past in dezelfde aanroep. HPDFParsePFX meldt hem met KeyAlgorithm = pfxkaECDSA (X25519- en X448-identiteiten incluis) en vult CurveOID, de private scalar in PrivateKey en het certificaatpunt in PublicKey, die rechtstreeks op de drie parameters mappen
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;
procedure OpenAuditPack(const LegalScalar: TBytes);
var
Reader: THotPDF;
EcKey: THPDFPFXKeyMaterial;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
// RSA-identiteit wordt de primaire sleutel
Reader.PubSecKeyMaterial :=
HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
// EC-identiteit uit een PFX: curve, scalar en punt gaan naar de agreementlijst
EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
if EcKey.KeyAlgorithm = pfxkaECDSA then
Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
EcKey.PrivateKey, EcKey.PublicKey);
// kale X25519-scalar in handen van legal; geen punt nodig
Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Met rust gelaten probeert HotPDF elke envelope tegen elke geregistreerde sleutel: de primaire RSA-sleutel, dan elke extra RSA-sleutel, dan het EC-materiaal. PubSecRecipientQuery beperkt dat tot één enkele envelope. De callback ontvangt het aantal envelopes en moet een nulgebaseerde index binnen de array teruggeven; elke andere waarde, -1 incluis, geeft een exception tijdens het laden. Om elke envelope te proberen laat je de eigenschap niet toegewezen. Onder Delphi is het type een reference to a function, dus een anonieme methode werkt, terwijl het onder FPC een kale functiepointer is waarvan het Context-argument uit PubSecRecipientQueryContext komt
Eén gedrag verdient een eigen test. Wanneer geen geregistreerde sleutel een envelope uitpakt, eindigt key recovery zonder file key en gooit hij niet om die reden. Assert op content waarvan je weet dat het document haar bevat, zoals een tekststring of een metadatawaarde; een LoadFromFile-aanroep die normaal terugkeert laat alleen zien dat de bestandsstructuur is geparsed
Wat controleer je in de doelviewer?
De writer en reader van HotPDF zijn het byte voor byte met elkaar eens, en de envelopes volgen de hierboven geciteerde CMS-structuren. Third-party viewers zijn een andere vraag: ondersteuning voor RSA-OAEP key transport, en nog meer voor X25519- of X448-ontvangers, verschilt tussen readers en tussen versies van dezelfde reader. Moet het document in een specifieke viewer opengaan, versleutel dan een testbestand voor een testcertificaat van hetzelfde keytype en open het daar voordat je aan een scheme vasthoudt. Ligt de viewer buiten je bereik, dan kiest zijn ondersteuningslijst het scheme, en dat rangschikt hoger dan het X25519-gemaksargument van eerder
Envelopemachtigingen blijven policy die conformerende software eert, precies zoals onder wachtwoordversleuteling. De seedkwaliteit is aan jou: AESGenerateRandomBytes bestaat voor die klus, en HotPDF wist zijn eigen kopie van de seed zodra de file key is afgeleid. Heeft een string, stream of bijlage een andere crypt filter nodig, dan laat de crypt filter policy-gids voor StmF, StrF en EFF zien hoe je er een toewijst; de public-key handler accepteert DefaultCryptFilter of Identity
Per-ontvanger-envelopes voor RSA- en elliptische-curve-certificaten, samen met het sleutelladen op de terugweg, maken deel uit van de HotPDF PDF component voor Delphi en C++Builder, naast wachtwoordversleuteling en digitale handtekeningen