Paimkime sąskaitos PDF, kuris jau apsaugotas AES-256 šifravimu, ir paprašykime Delphi ir C++Builder skirto PDFium Component (PDFiumPas) pritaikyti jam PDF/A archyviniam saugojimui arba pasirašyti jį PAdES parašu, atliekant inkrementinį atnaujinimą, o ne visiškai perrašant failą. Biblioteka to nepadarys tiesiogiai taisydama užšifruotus baitus: šešios atitikties žymeklius įterpiančios funkcijos aptinka esamą /Encrypt įrašą ir perduoda šaltinį į paskirties srautą nepakeistą baitas į baitą, o PAdES pasirašyklė iškelia išimtį, užuot sukūrusi parašą, kurio nepriimtų nė vienas tikrintuvas
Tai kitoks klausimas nei PDF, kurio nesukūrėte, auditas dėl paslėptos rizikos — tai atskira tik skaitymo operacija. Šis straipsnis nagrinėja tos pačios pasitikėjimo ribos rašymo pusę: ką jūsų kodas gali daryti su failu, kurio baitai jau užrakinti kito asmens slaptažodžiu, kai tas kodas vėliau bando prie jo ką nors pridėti
Ko ISO 32000-1 reikalauja atnaujinant užšifruotą PDF?
ISO 32000-1 §7.5.6 reikalauja, kad inkrementinio atnaujinimo priekaba pakartotų kiekvieną ankstesnės priekabos įrašą, išskyrus /Prev, o 15 lentelėje /Encrypt nurodytas tarp įrašų, kuriuos priekaba gali turėti. Pašalinkite jį iš naujos priekabos, ir atitiktį užtikrinantis skaitytuvas neturės pagrindo suabejoti praleidimu: naujausia priekaba yra autoritetinga, todėl skaitytuvas, joje neradęs /Encrypt, nuspręs, kad visas failas nešifruotas, ir bandys ankstesnį, vis dar užšifruotą turinį analizuoti kaip paprastus baitus. Palikite /Encrypt naujoje priekaboje, bet atnaujinimo objektus įrašykite atviru tekstu, ir klaida pasirodys tik kitu etapu: skaitytuvas teisingai aptiks šifravimą, kiekvienam liečiamam objektui pritaikys failo šifrą, įskaitant naujus objektus, kurie nuo pradžių nebuvo užšifruoti, ir vietoje turinio, kuris prieš dešifravimą buvo visiškai įskaitomas, gaus triukšmą. Bet kuri klaida sukuria failą, kuris baitų lygmeniu atrodo kaip įprastas, taisyklingas inkrementinis atnaujinimas, kol jo neatidaro atitiktį užtikrinantis skaitytuvas
Šeši žymeklių įterpikliai ir vienas v2.14.2 šifravimo vartai
PDFiumPas pateikia šešis baitų lygmens žymeklių įterpiklius, po vieną kiekvienam ISO PDF poaibiui, kurį gali pažymėti: 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) ir PDF/VT-1 (ISO 16612-2). Kiekvienas paima baitus, kuriuos jau įrašė paties PDFium FPDF_SaveAsCopy, ir ant jų uždeda antrą, mažesnį inkrementinį atnaujinimą: naują XMP metaduomenų srautą, katalogo žodyno pakeitimą, nurodantį į jį, o spausdinimui skirtuose poaibiuose — OutputIntent ir ICC profilį. Nuo v2.14.2 kiekvienas iš InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers ir InjectPdfVTMarkers pirmiausia nuskaito šaltinio priekabą ir, jei joje pranešama apie esamą /Encrypt įrašą, nukopijuoja šaltinį į paskirties srautą nepakeistą ir iškart grįžta. Jokio XMP, jokio OutputIntent, jokio katalogo pakeitimo — iškvietėjas gauna pradinį failą baitas į baitą
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;
Tai, kad šifravimas leidžiamas, nereiškia, kad į dokumentą saugu ką nors įterpti
PDF/E-1 ir PDF/R-1 specifikacijos lygmeniu aiškiai leidžia jų pagrindiniam dokumentui būti užšifruotam, o tai skamba kaip išimtis, kol nepažiūrite, kas iš tikrųjų turi įvykti diske. ISO 24517-1 §6.3 leidžia šifravimą PDF/E-1, o ISO 23504-1 §6.2.3 leidžia jį PDF/R-1, jei antraštėje deklaruota %PDF-2.0. Nė vienoje nuostatoje nepasakyta, ar baitų lygmens vėlesnio apdorojimo funkcija gali saugiai pridėti atvirą objektą prie to užšifruoto konteinerio, ir negali, dėl tų pačių §7.5.6 priežasčių, kurios taikomos kiekvienam kitam poaibiui. PDFiumPas atitikties tikrintuvai šiems dviem profiliams, ValidatePdfECompliance ir ValidatePdfRCompliance, sąmoningai užfiksuoja /Encrypt buvimą, nelaikydami jo defektu, ir tai teisinga tik skaitymo tikrintuvui, kuris nieko neįrašo. Tačiau tokį modelį lengva prabėgomis perskaityti ir manyti, kad gretimam įterpiklio veiksmui nereikia atskiros apsaugos, nors būtent įterpiklis yra vienintelė poros funkcija, kuri privalo atsisakyti veikti
Ar SaveAsPdfX tyliai iššifruoja jūsų dokumentą?
Taip, kai naudojate viešuosius patogumo metodus, o ne tiesiogiai kviečiate įterpiklį. Kiekvienas iš TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR ir SaveAsPdfVT pateikia dabartinį dokumentą į laikiną srautą naudodamas SaveAs(Tmp, saRemoveSecurity), prieš perduodant šiuos baitus atitinkamam įterpikliui. saRemoveSecurity susiejamas su paties PDFium FPDF_REMOVE_SECURITY vėliava, todėl laikina įterpiklio gaunama kopija iš pradžių apskritai nebuvo užšifruota, o įterpiklio /Encrypt apsauga neturi kada suveikti. Išvestyje yra jūsų PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 arba PDF/VT-1 žymekliai, tačiau jos nebesaugo šaltiniui atidaryti naudotas slaptažodis
Šis kompromisas lieka nepastebimas, kol kas nors toliau grandinėje neatidaro „apsaugotos“ archyvinės kopijos be slaptažodžio ir nepamato, kad ji tiesiog atsidaro. Sprendimas nėra kitas metodo iškvietimas; PDFiumPas neturi saAddSecurity atitikmens, kurį būtų galima suporuoti su saRemoveSecurity, nes bazinis PDFium variklis niekada nebuvo sukurtas naujam šifravimui įrašyti, tik jam pašalinti. Jei vienam failui svarbios abi savybės, šifravimas turi būti atskiras jūsų valdomas žingsnis, taikomas po atitikties žymeklių, o ne įtrauktas į tą patį SaveAsPdfA iškvietimą
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;
Kas nutinka PAdES pasirašant užšifruotą PDF?
PDFiumPas iškart atsisako, o ne tyliai atmeta užklausą, kaip daro žymeklių įterpiklis. TPdf.SignPades ir SignPadesToStream abu nukreipiami per vidinę SignPadesBytes, o pirmasis jos veiksmas, išanalizavus šaltinio priekabą, yra patikrinti, ar yra /Encrypt. Jei įrašas yra, funkcija iškelia EPadesCrypto su pranešimu "SignPadesBytes: the source document is encrypted; remove encryption before signing" ir nebetęsia darbo. InjectPadesDssMarkers, funkcija, įterpianti sertifikatus, OCSP atsakus ir CRL ilgalaikiam tikrinimui, taiko identišką patikrą dėl tos pačios priežasties ir pateikia savą pranešimą: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
Čia samprotavimas griežtesnis nei žymeklių įterpiklių praleidimas, ir taip yra tyčia. Tylus praleidimas saugus PDF/A žymai, nes jos atsisakius lieka tas pats galiojantis PDF, nuo kurio pradėjote, tik be etiketės. Pasirašymas negali tyliai žlugti: parašas, kuris tyliai nebuvo pridėtas, iškvietimo kodui, tikrinančiam vien loginį rezultatą, atrodo lygiai taip pat kaip sėkmingai pridėtas parašas. EPadesCrypto paveldi įprastą Exception klasę, todėl jo gaudymas yra įprastas išimties tvarkymas, o ne speciali valdymo srauto taisyklė, kurią reikėtų išmokti
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;
Atitikties žymeklių, parašų ir šifravimo seka
Praktinis sprendimas yra tvarka, o ne kita biblioteka. Pirmiausia pritaikykite PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 arba PDF/VT-1 žymeklius, tada pridėkite PAdES parašą ir tik po to paleiskite tą savo konvejerio etapą, kuriam iš tikrųjų priklauso šifravimas — atskirą PDF rašyklę, pasirašymo įrenginį arba jūsų pačių AES realizaciją. PDFiumPas inkrementinių atnaujinimų sluoksnis natūraliai telpa sekos viduryje, pridėdamas mažus, tikslinius objektus prie kitaip užbaigto failo, o šifravimas priklauso pabaigai būtent todėl, kad tai vienintelė grandinės operacija, kurios pats PDFiumPas negali atlikti arba atšaukti
Nė vienas iš šių veiksmų nekeičia būdo, kuriuo PDFiumPas nuskaito priekabą ir kryžminės nuorodos duomenis, nuo kurių priklauso kiekvienas inkrementinis atnaujinimas — tai atskiras subtilumų šaltinis, kai atsiranda xref srautai; PDF objekto ir xref srautų tikrinimas paaiškina, kaip tas pats priekabos skaitymo kelias tvarko suspaustas PDF 1.5+ struktūras. Kai dokumentas paruoštas kažkam daugiau nei atitikties žymekliui, PDF pasirašymas su PAdES B-B parašu Delphi kalboje perima darbą ten, kur šis straipsnis baigiasi, o SignPades tęsia būtent nuo tos vietos
Čia aprašyti žymeklių įterpikliai ir SignPades metodai yra Delphi ir C++Builder skirto PDFium Component dalis, kartu su PDFium teikiamu atvaizdavimu ir tik skaitymo tikrinimu