HotPDF шифрова PDF за конкретни притежатели на сертификати чрез ISO 32000 public-key security handler-а: EnablePubKeyEncryption взима случайно seed от 20 байта, а всеки получател получава собствен CMS плик, строен от AddPubKeyRecipientCertificate за RSA ключове (key transport по RSA-OAEP) или AddPubKeyAgreementRecipientWithSecret за елиптични ключове (ECDH на P-256, P-384, P-521, X25519 или X448). Никой не споделя парола; който държи съответстващия частен ключ, отваря файла
Use case-ът е винаги някаква версия на същата история. Тримесечно одитно досие отива при трима външни ревюори, legal иска всеки от тях да го прочете, само един може да го отпечата, а никой не иска парола да седима в имейл нишка до attachment-а. Паролното шифроване не може да изрази това. Сертификатното може, защото всеки получател отключва документа с ключ, който вече държи, а всеки получател може да носи различен набор права в собствения си плик
Как шифроването на PDF със сертификати се различава от парола?
PDF, шифрован с публичен ключ, извлича файловия си ключ от случайно seed плюс точните байтове на всеки плик на получател, а не от нещо, което човек пише. Handler-ът е описан в ISO 32000-1 §7.6.4 (§7.6.5 в ISO 32000-2), а пликовете са CMS структури EnvelopedData по RFC 5652. HotPDF записва /Filter /Adobe.PubSec с /SubFilter /adbe.pkcs7.s5; за AES-256 това значи /V 5 и запис /DefaultCryptFilter под /CF с /CFM /AESV3, а масивът /Recipients живее в този crypt filter. Всеки плик шифрова 24 байта: seed-ът от 20 байта, следван от 32-битовата дума права на съответния получател. Стойността /P в encryption речника е само placeholder, защото истинските права пътуват във всеки плик. При зареждане четец разгръща един плик, възстановява seed-а и хешира seed-а заедно с всеки плик в реда на /Recipients (SHA-256 за AES-256, SHA-1 за по-старите шифри), за да преизгради файловия ключ. Ако все още решавате между този модел и обикновени пароли, ръководството за AES-256 паролно шифроване и permission флагове покрива другата страна на този компромис
Писане на RSA получатели с EnablePubKeyEncryption
За RSA сертификати викнете EnablePubKeyEncryption с aes256, после викайте AddPubKeyRecipientCertificate по веднъж на DER-кодиран сертификат преди BeginDoc. Helper-ът строи RSAES-OAEP плик в процеса със стойности THPDFRSAOAEPHash за OAEP дайджеста и MGF1 дайджеста (rohSHA256, rohSHA384 или rohSHA512), а съдържанието на плика шифрова с 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); // точно 20 байта, дори за AES-256
AESGenerateRandomBytes(@Seed[1], Length(Seed));
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := OutFile;
Pdf.EnablePubKeyEncryption(Seed, aes256, True); // типът ключ по подразбиране е aes128
// Ревюор А може да печата; ревюор Б само да чете и извлича
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;
Три подробности в този листинг са товароносещи. Първо, дължината на seed-а е фиксирана на 20 байта за всеки тип ключ, включително AES-256; EnablePubKeyEncryption вдига грешка при всяка друга дължина. Второ, EnablePubKeyEncryption подразбира aes128, а двата certificate helper-а отказват да работят, освен ако типът ключ не е aes256, така че забравеният втори аргумент ви връща изключението „certificate envelopes require aes256". Legacy шифрите (k40, k128, aes128) пак работят, но само през AddPubKeyRecipient с плик, изграден другаде. Трето, public-key шифроването с AES-256 е feature на PDF 2.0, затова HotPDF вдига версията на документа на 2.0 автоматично. С StrictVersionLock зададен на по-ниска версия, EnablePubKeyEncryption се връща, без да е включила нещо, а провалът се появява чак на следващия ред като „call EnablePubKeyEncryption first". Смяната на шифроването по време на инкрементална актуализация вдига EInvalidOpException веднага
Добавяне на ECDH получатели: P-256, P-384, P-521, X25519 и X448
За елиптични сертификати AddPubKeyAgreementRecipientWithSecret записва CMS получател по key agreement (KeyAgreeRecipientInfo — KARI структурата от RFC 5753, с профила X25519 и X448 от RFC 8418) и смята ECDH споделената тайна в процеса. Кривата я избирате със стойност THPDFPubKeyAgreementScheme: pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519 или pkasX448. Схемата трябва да пасва на ключа в сертификата, иначе извикването вдига „Certificate key does not match the requested agreement scheme". Под капака всеки плик получава свеж случаен UKM от 32 байта, key-encryption ключ, извлечен със stdDH KDF (SHA-256 за P-256 и X25519, SHA-384 за P-384, SHA-512 за P-521 и X448), и AES-256 key wrap по RFC 3394. Самата споделена тайна идва от чист Pascal код за криви, без платформен crypto provider; статията за чист Pascal NIST крива аритметика обяснява как този слой е изграден и проверен. За Montgomery кривите целият ефемерен ключов чифт може да се генерира локално:
uses
System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
HPDFKeyAgreement;
procedure AddLegalRecipient(Pdf: THotPDF);
var
Scalar, OriginatorPublic: TBytes;
begin
// Свеж ефемерен scalar за всеки плик; clamping-ът става вътре в 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: има значение само за NIST кривите
finally
HPDFSecureClearBytes(Scalar);
end;
end;
NIST кривите искат повече от извикващия. HotPDF доставя public-key helper-и само за X25519 и X448 (HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar), така че за P-256, P-384 и P-521 генерирате ефемерния ключов чифт с собствените си инструменти и подавате big-endian scalar с точно размера на полето (32, 48 или 66 байта) плюс съответстващата некомпресирана точка 0x04||X||Y като OriginatorPublicKey. HotPDF валидира точката на получателя срещу уравнението на кривата, но не може да провери, че вашият originator public key наистина принадлежи на вашия scalar. Разминати половини пак дават напълно коректно оформен плик, който никой получател не може да отвори — затова round trip зареждането си е част от тестовия ви набор, а не само проверка за размер на файла
Защо редът на /Recipients има значение?
Редът на /Recipients има значение, защото файловият ключ е дайджест над seed-а и всеки плик в реда на масива, така че writer и reader трябва да хешират едни и същи байтове в една и съща последователност. HotPDF пази пликовете в реда, в който ги добавяте, и ги записва непроменени, което значи, че може да добавяте получатели в какъвто ред искате, но нищо надолу по веригата няма право да пренарежда, прекодира или „разчиства" този масив. Повечето истински бъгове в тази зона бяха вариации на тази тема, при които две страни хешираха малко по-различни байтове:
- Съхранение на dynamic масиви в
TListпрезAddпази само суров указател, докато reference броилката остава при локалната променлива. СледващотоSetLengthосвобождава буфера и може да го преизползва, така че всеки слот свършваше aliasing на последния плик, а файлове с много получатели извличаха грешен ключ. Поправката е да се съхранява собствено копие сList.Add(Pointer(System.Copy(Bytes))) - Разгръщането на пликове парсва DER на място, а пасът за възстановяване на ключа първоначално хешеше същите живи масиви. Четецът вече прави снимки на чисти копия на всеки плик, преди някое разгръщане да ги пипне, а дайджестът минава върху снимките
- Бинарен DER, преминал през Unicode
TStringList, получава байтове на$80или над него прекодирани от code page-а, затова HotPDF пази пликовете вътрешно като hex текст - Шифрованите и бинарните низове трябва да се записват като hex низове. Литерален низ е подложен на end-of-line нормализация, при която CR, LF и CRLF стават един LF (ISO 32000-1 §7.3.4.2), а това тихо пренаписва ciphertext-а. HotPDF излъчва всеки запис в
/Recipientsкато hex низ и го освобождава от string шифроване, защото всеки четец трябва да има пликовете, преди да държи какъвто и да е ключ - Първият байт на DER
BIT STRINGброи неизползваните битове и трябва да е нула за ключове, подравнени по байт. Оставен неинициализиран следSetLength, записваше каквото има на стека, а строг разгръщащ отхвърляше ключа на originator-а, така че файл понякога не можеше да се отвори с точно ключа, за който е бил написан - Когато същият ключ пак не може да дешифрира, сравнявайте слой по слой: файловия ключ, после ciphertext префикса (IV-то), после обектния ключ, после plaintext-а. Бъгът живее точно след първия слой, който не съвпада
Как отваряте шифрован със сертификат PDF с частен ключ?
За да отворите шифрован със сертификат PDF, регистрирайте материала с частния ключ, преди да викнете LoadFromFile, защото HotPDF възстановява файловия ключ по време на структурния проход. Задайте RSA или EC ключ, парснат с HPDFParsePFX, на PubSecKeyMaterial, добавяйте още RSA ключове с AddPubSecKeyMaterial, а сурови ECDH scalars регистрирайте с AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint), ползвайки константите HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384 или HPDFOIDECP521. NIST кривите изискват собствения некомпресиран public point на получателя; Montgomery кривите го игнорират
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);
// По желание: изберете плика направо, вместо да пробвате всички
Reader.PubSecRecipientQuery :=
function(Context: Pointer; RecipientCount: Integer): Integer
begin
Result := -1; // -1 = пробвай всеки плик по ред
end;
Reader.LoadFromFile('audit-pack.pdf', '');
Writeln('Pages: ', Reader.GetLoadedPageCount);
finally
Reader.Free;
end;
end;
Без callback HotPDF пробва всеки плик срещу всеки регистриран ключ: първичния ключ, после всеки допълнителен RSA ключ, после EC материала. PubSecRecipientQuery получава броя пликове и връща индекс от нулата или -1, а индекс извън масива вдига изключение, вместо да се отреже. Обърнете внимание, че AddPubSecKeyMaterial приема само RSA материал (държи на модул и частен експонент), така че EC ключовете принадлежат на PubSecKeyMaterial или AddPubSecAgreementKeyMaterial. Когато никой ключ не разгръща нито един плик, стъпката за възстановяване се връща без файлов ключ, вместо да вдига грешка, така че проверете, че очакваното съдържание наистина се е дешифрирало, вместо да вярвате, че зареждащото извикване се е върнало
Какво HotPDF не гарантира
HotPDF гарантира, че собствените му writer и reader се съгласуват байт по байт, и строи пликове, следващи цитираните по-горе CMS структури. Не гарантира, че всеки PDF viewer отваря всяка комбинация. Поддръжката на key transport по RSA-OAEP и на получатели X25519 или X448 варира между четци и версии, а ние не сме публикували резултати за съвместимост за тези комбинации. Ако документът трябва да се отваря в конкретен viewer, шифровайте тестов файл за тестов сертификат от същия тип ключ и го отворете там, преди да се ангажирате със схема. Правата, носени в плика, остават политика, която конформен софтуер уважава — точно както при паролното шифроване. Качеството на seed-а също е ваша отговорност: AESGenerateRandomBytes е точно за тази работа, а HotPDF изтрива своето копие на seed-а, щом файловият ключ е извлечен. Ако освен това ви трябва string, stream или attachment да ползва друг crypt filter, ръководството за crypt filter политики за StmF, StrF и EFF показва кои имена на филтри приема public-key handler-ът
Сертификатното шифроване, пликовете за получатели RSA-OAEP и ECDH и зареждането на частни ключове излизат в HotPDF Delphi PDF компонента, редом с паролното шифроване, цифровите подписи и останалата част от ISO 32000 toolset-а за Delphi и C++Builder