Візьміть PDF рахунку-фактури, що вже несе шифрування AES-256, і попросіть компонент PDFium для Delphi та C++Builder (PDFiumPas) проштампувати його PDF/A для архівного зберігання чи підписати його PAdES через інкрементальне оновлення, а не повний перепис. Бібліотека не досягне цього, латаючи зашифровані байти напряму: її шість ін'єкторів маркерів відповідності виявляють наявний запис /Encrypt і пропускають джерело до призначення побайтово незмінним, а її підписувач PAdES піднімає виняток замість того, щоб видати підпис, який жоден валідатор не прийме
Це інше питання, ніж аудит PDF, який ви не створювали, на приховані ризики, що є власною вправою лише для читання. Ця стаття про бік запису тієї самої межі довіри: що вашому власному коду дозволено робити з файлом, чиї байти вже заблоковані за чиїмось паролем, тієї миті, коли цей код намагається щось додати до нього постфактум
Що вимагає ISO 32000-1, коли ви оновлюєте зашифрований PDF?
ISO 32000-1 §7.5.6 вимагає, щоб трейлер інкрементального оновлення повторював кожен запис із попереднього трейлера, крім /Prev, а таблиця 15 перелічує /Encrypt серед записів, які трейлер може нести. Приберіть його з нового трейлера, і читач, що відповідає специфікації, не має підстав засумніватися в цьому пропуску: найновіший трейлер авторитетний, тож читач, що не знаходить там /Encrypt, вирішує, що весь файл нешифрований, і намагається розібрати старіше, усе ще зашифроване тіло як звичайні байти. Залиште /Encrypt у новому трейлері, але запишіть власні об'єкти оновлення відкритим текстом, і збій просто зсувається на крок пізніше: читач правильно виявляє шифрування, проганяє кожен об'єкт, якого торкається, через шифр файлу, включно з новими, які взагалі ніколи не шифрувалися, і отримує шум назад для вмісту, що був цілком читабельним, поки дешифрування його не торкнулося. Будь-яка з цих помилок виробляє файл, що на рівні байтів виглядає як нормальне, коректно сформоване інкрементальне оновлення, аж доки його не відкриє читач, що відповідає специфікації
Шість ін'єкторів маркерів, один шлюз шифрування v2.14.2
PDFiumPas постачає шість ін'єкторів маркерів на рівні байтів, по одному на кожен підмножину PDF ISO, яку може позначити: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1) та PDF/VT-1 (ISO 16612-2). Кожен бере байти, які вже записав власний FPDF_SaveAsCopy у PDFium, і накладає на них другий, менший інкрементальний оновлення: новий потік метаданих XMP, редагування словника каталогу, що на нього вказує, а для орієнтованих на друк підмножин — OutputIntent та профіль ICC. Станом на v2.14.2, кожен з InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers та InjectPdfVTMarkers спочатку читає вихідний трейлер, і якщо той повідомляє про наявний запис /Encrypt, копіює джерело до потоку призначення немодифікованим і негайно повертається. Ні XMP, ні OutputIntent, ні редагування каталогу — викликач отримує назад оригінальний файл, байт у байт
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Дозволено бути зашифрованим — не те саме, що безпечно вводити всередину
PDF/E-1 та PDF/R-1 обидва явно дозволяють своєму документу-хосту бути зашифрованим на рівні специфікації, що читається як звільнення, доки не подивишся, що насправді має відбутися на диску. ISO 24517-1 §6.3 дозволяє шифрування для PDF/E-1, а ISO 23504-1 §6.2.3 дозволяє його для PDF/R-1 за умови, що заголовок оголошує %PDF-2.0. Жоден пункт нічого не каже про те, чи може постпроцесор на рівні байтів безпечно додати об'єкт відкритого тексту в цей зашифрований контейнер, а він не може, з тих самих причин §7.5.6, що застосовуються до кожної іншої підмножини. Власні валідатори відповідності PDFiumPas для цих двох профілів, ValidatePdfECompliance та ValidatePdfRCompliance, реєструють присутність /Encrypt навмисно, не позначаючи це як дефект, що правильно для валідатора лише для читання, який ніколи не записує жодного байта. Це також легкий для побіжного пропуску шаблон, що змушує припустити, ніби суміжному ін'єктору не потрібна окрема охорона, тоді як ін'єктор — та єдина функція в парі, яка справді мусить відмовити
Чи мовчки дешифрує SaveAsPdfX ваш документ?
Так, щоразу коли ви йдете через публічні зручні методи замість прямого виклику ін'єктора. Кожен з TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR та SaveAsPdfVT рендерить поточний документ у тимчасовий потік через SaveAs(Tmp, saRemoveSecurity), перш ніж передати ці байти відповідному ін'єктору. saRemoveSecurity відображається на власний прапорець FPDF_REMOVE_SECURITY PDFium, тож тимчасова копія, яку отримує ін'єктор, взагалі ніколи не була зашифрованою, і охорона /Encrypt ін'єктора ніколи не має причин спрацювати. Вихід несе ваші маркери PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 чи PDF/VT-1, але вже не захищений тим паролем, що відкривав джерело
Цей компроміс невидимий, доки хтось вниз за течією не відкриє «захищену» архівну копію без пароля й не помітить, що вона просто працює. Виправлення — не інший виклик методу; PDFiumPas не має аналога saAddSecurity, щоб парюватися з saRemoveSecurity, бо базовий рушій PDFium ніколи не будувався для запису нового шифрування, лише для його видалення. Якщо обидві властивості важать для одного файлу, шифрування має бути окремим кроком, яким ви володієте, застосованим після маркерів відповідності, а не згорнутим у той самий виклик SaveAsPdfA
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
Що відбувається, коли ви підписуєте зашифрований PDF за допомогою PAdES?
PDFiumPas відверто відмовляє, а не мовчки скидає запит так, як це робить ін'єктор маркерів. TPdf.SignPades та SignPadesToStream обидва проходять через внутрішній SignPadesBytes, і перше, що він робить після розбору вихідного трейлера, — перевіряє на /Encrypt. Якщо запис присутній, він піднімає EPadesCrypto з повідомленням "SignPadesBytes: the source document is encrypted; remove encryption before signing" замість того, щоб продовжувати далі. InjectPadesDssMarkers, функція, що вбудовує сертифікати, відповіді OCSP та CRL для довгострокової валідації, застосовує ідентичну перевірку з тієї самої причини, з власним повідомленням: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
Міркування тут суворіше за прохідний пропуск ін'єкторів маркерів, і навмисно так. Тихий прохідний пропуск безпечний для штампу PDF/A, бо його пропуск залишає вас із тим самим дійсним PDF, з яким ви почали, просто непозначеним. Підписування не може провалитися так само тихо: підпис, що мовчки ніколи не був доданий, виглядає для будь-якого коду, що викликає й перевіряє лише булевий результат, точно так само, як успішно доданий підпис. EPadesCrypto походить від звичайного класу Exception, тож його перехоплення — звичайна обробка винятків, а не спеціальна конвенція керування потоком виконання, яку доведеться вивчати
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Послідовність штампів відповідності, підписів та шифрування
Практичне виправлення — порядок, не інша бібліотека. Спочатку застосуйте маркери PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 чи PDF/VT-1, далі додайте будь-який підпис PAdES, і лише тоді запустіть той крок вашого конвеєра, що справді володіє шифруванням, чи то виділений записувач PDF, підписувальний пристрій, чи ваша власна реалізація AES. Шар інкрементальних оновлень PDFiumPas природно вписується в середину цієї послідовності, додаючи невеликі, цільові об'єкти до файлу, що в іншому випадку завершений, а шифрування належить у кінець саме тому, що це та одна операція в ланцюжку, яку сам PDFiumPas не може виконати чи скасувати
Ніщо з цього не змінює, як PDFiumPas читає дані трейлера та перехресних посилань, від яких залежить кожне інкрементальне оновлення, а це власне джерело тонкощів, щойно в кадр потрапляють потоки xref; перевірка потоків об'єктів та xref PDF розглядає, як той самий шлях читання трейлера обробляє стиснуті структури PDF 1.5+. А щойно документ готовий до чогось сильнішого за штамп відповідності, підписування PDF підписом PAdES B-B у Delphi — місце, де SignPades переймає естафету рівно з тієї точки, де ця стаття зупиняється
Ін'єктори маркерів та методи SignPades, описані тут, постачаються як частина компонента PDFium для Delphi та C++Builder, поряд з рендерингом та інспекцією лише для читання, які PDFium надає нативно