Műszaki cikk

Nem lehet csendben javítani egy titkosított PDF-et Delphiben

Vegyél egy számla-PDF-et, amely már AES-256 titkosítást hordoz, és kérd meg a Delphihez és C++Builderhez készült PDFium Componentet (PDFiumPas), hogy pecsételje meg PDF/A-ként archiválási megőrzéshez, vagy írja alá PAdES-szel, egy inkrementális frissítésen keresztül, teljes újraírás helyett. A könyvtár nem fog odáig jutni a titkosított bájtok közvetlen javításával: hat megfelelőség-jelölő injektora felismeri a meglévő /Encrypt bejegyzést, és bájtról bájtra változatlanul átengedi a forrást, PAdES-aláírója pedig kivételt dob ahelyett hogy egy olyan aláírást bocsátana ki, amit egyetlen validátor sem fogad el

Ez más kérdés, mint egy olyan PDF-et auditálni rejtett kockázatért, amit nem te hoztál létre, ami saját, csak-olvasható gyakorlat. Ez a cikk ugyanannak a bizalmi határnak az írási oldaláról szól: mit szabad a saját kódodnak tennie egy olyan fájllal, amelynek bájtjai már valaki más jelszava mögé vannak zárva, abban a pillanatban, amikor az a kód megpróbál bármit is hozzáadni ahhoz utólag

Mit követel meg az ISO 32000-1, amikor egy titkosított PDF-et frissítesz?

Az ISO 32000-1 §7.5.6 megköveteli, hogy egy inkrementális frissítés lezárója megismételjen minden bejegyzést az előző lezáróból a /Prev kivételével, és a 15. táblázat felsorolja a /Encrypt-et azon bejegyzések között, amelyeket egy lezáró hordozhat. Hagyd ki az új lezáróból, és egy szabványkövető olvasónak nincs oka kételkedni a kihagyásban: a legújabb lezáró az irányadó, így egy olvasó, amely nem talál ott /Encrypt-et, úgy dönt, hogy az egész fájl titkosítatlan, és megpróbálja elemezni a régebbi, még mindig titkosított testet sima bájtokként. Tartsd meg a /Encrypt-et az új lezáróban, de a frissítés saját objektumait írd sima szövegként, és a hiba csak egy lépéssel később mozdul: az olvasó helyesen felismeri a titkosítást, minden objektumot, amit érint, átfuttat a fájl titkosítóján, beleértve az újakat, amelyeket soha nem titkosítottak eleve, és zajt kap vissza olyan tartalomhoz, amely tökéletesen olvasható volt, mielőtt a visszafejtés hozzáért volna. Bármelyik hiba egy olyan fájlt eredményez, amely bájtszinten normál, jólformázott inkrementális frissítésnek néz ki, egészen addig, amíg egy szabványkövető olvasó meg nem nyitja

Egy ISO 32000-1 7.5.6 diagram Delphi inkrementális frissítésekhez titkosított PDFen: egy új trailer, amely eldobja a /Encryptet, arra készteti az olvasót, hogy rejtjeles szöveget nyers bájtként elemzzen, a nyílt szöveges frissítőobjektumok a fájlrejtjeltől kuszulódnak össze, a /Encrypt megismétlése titkosított új objektumokkal pedig az egyetlen szabálykövető kimenet
Egy /Encrypt-et elhagyó frissítőtrailer arra készteti az olvasót, hogy rejtjeles szöveget parseoljon sima bájtként, és a titkosítatlan frissítőobjektumok összekeverednek a fájl rejtjelével — csak a /Encrypt megismétlése és az új objektumok titkosítása éli túl a szabványos olvasót

Hat jelölő-injektor, egy v2.14.2-es titkosítási kapu

A PDFiumPas hat bájt-szintű jelölő-injektort szállít, egyet minden ISO PDF-részhalmazhoz, amit megjelölni tud: 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), és PDF/VT-1 (ISO 16612-2). Mindegyik veszi a bájtokat, amiket a PDFium saját FPDF_SaveAsCopy-ja már megírt, és egy második, kisebb inkrementális frissítést rétegez azok tetejére: egy új XMP metaadat-streamet, egy katalógus-szótár-szerkesztést, amely rámutat, és a nyomtatás-orientált részhalmazoknál egy OutputIntentet és ICC-profilt. v2.14.2-től kezdve mind az InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, és InjectPdfVTMarkers előbb beolvassa a forrás-lezárót, és ha az egy meglévő /Encrypt bejegyzést jelent, változatlanul másolja át a forrást a célstreamre, és azonnal visszatér. Nincs XMP, nincs OutputIntent, nincs katalógus-szerkesztés — a hívó visszakapja az eredeti fájlt, bájtról bájtra

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);
    // a pdfx-attempt.pdf bájtról bájtra azonos a forrással: még mindig titkosított,
    // nincs /GTS_PDFXVersion, nincs OutputIntent. Semmi sem lett írva, és
    // semmi sem sérült meg sem
  finally
    Dst.Free;
    Src.Free;
  end;
end;

A titkosítás megengedettsége nem ugyanaz, mint az injektálás biztonságossága

A PDF/E-1 és a PDF/R-1 mindkettő kifejezetten megengedi, hogy a gazdadokumentum titkosítva legyen a specifikáció szintjén, ami kivételnek tűnik, amíg meg nem nézed, mi is kell ténylegesen történjen lemezen. Az ISO 24517-1 §6.3 megengedi a titkosítást PDF/E-1-hez, és az ISO 23504-1 §6.2.3 megengedi PDF/R-1-hez, feltéve hogy a fejléc %PDF-2.0-t deklarál. Egyik záradék sem mond semmit arról, hogy egy bájt-szintű utófeldolgozó biztonságosan hozzáadhat-e egy sima szöveges objektumot ahhoz a titkosított konténerhez, és nem tehet, ugyanazon §7.5.6 okokból, amelyek minden más részhalmazra vonatkoznak. A PDFiumPas saját megfelelőség-érvényesítői ehhez a két profilhoz, a ValidatePdfECompliance és a ValidatePdfRCompliance, rögzítik a /Encrypt jelenlétét, szándékosan hiba nélkül jelölve, ami helyes egy csak-olvasható validátorhoz, amely soha egyetlen bájtot sem ír. Ez egyben egy olyan minta is, amit könnyű átfutni, és feltételezni, hogy a testvér-injektornak nincs szüksége külön őrre, pedig az injektor az a függvény a párban, amelynek ténylegesen meg kell tagadnia

Csendben visszafejti a SaveAsPdfX a dokumentumodat?

Igen, valahányszor a nyilvános kényelmi metódusokon keresztül mész, ahelyett hogy közvetlenül egy injektort hívnál. A TPdf.SaveAsPdfA, a SaveAsPdfX, a SaveAsPdfUa, a SaveAsPdfE, a SaveAsPdfR, és a SaveAsPdfVT mindegyike egy ideiglenes streambe rendereli az aktuális dokumentumot SaveAs(Tmp, saRemoveSecurity)-vel, mielőtt átadná azokat a bájtokat a hozzá tartozó injektornak. A saRemoveSecurity a PDFium saját FPDF_REMOVE_SECURITY jelzőjére képződik le, így az ideiglenes másolat, amit az injektor kap, soha nem is volt titkosítva eleve, és az injektor /Encrypt őre soha nem kap okot a kiváltásra. A kimenet hordozza a PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, vagy PDF/VT-1 jelölőidet, de már nem védi az a jelszó, amivel a forrás megnyílt

Ez a kompromisszum láthatatlan, amíg valaki downstream meg nem nyitja a "védett" archivális másolatot jelszó nélkül, és nem veszi észre, hogy egyszerűen működik. A javítás nem egy másik metódushívás; a PDFiumPasnak nincs saAddSecurity párja, amely a saRemoveSecurity-vel párosodna, mert az alapul szolgáló PDFium motort soha nem építették új titkosítás írására, csak annak eltávolítására. Ha mindkét tulajdonság számít egy fájlnál, a titkosításnak külön lépésnek kell lennie, amit te birtokolsz, a megfelelőség-jelölők után alkalmazva, nem pedig ugyanabba a SaveAsPdfA hívásba beolvasztva

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // kell ahhoz, hogy a forrás egyáltalán megnyíljon
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // az invoice-pdfa.pdf most PDF/A-2b-t deklarál, de a SaveAs(saRemoveSecurity)
    // előbb futott a SaveAsPdfA-n belül: a kimenet jelszó nélkül nyílik meg
  finally
    Pdf.Free;
  end;
end;

Mi történik, amikor PAdES-szel írsz alá egy titkosított PDF-et?

A PDFiumPas egyenesen megtagadja, ahelyett hogy csendben eldobná a kérést úgy, ahogyan egy jelölő-injektor teszi. A TPdf.SignPades és a SignPadesToStream mindkettő egy belső SignPadesBytes-en keresztül halad, és az első dolog, amit tesz a forrás-lezáró elemzése után, hogy ellenőrzi a /Encrypt-et. Ha a bejegyzés jelen van, EPadesCrypto-t dob "SignPadesBytes: the source document is encrypted; remove encryption before signing" üzenettel, ahelyett hogy tovább folytatná. Az InjectPadesDssMarkers, a függvény, amely tanúsítványokat, OCSP-válaszokat, és CRL-eket ágyaz be hosszú távú érvényesítéshez, ugyanazt az ellenőrzést alkalmazza ugyanazon okból, saját üzenetével: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

A PDFium Component titkosítási kapuja Delphi-ben: mind a hat markerinjektor elolvassa a forrástrailert, /Encrypt bejegyzés esetén a bájtok változatlanul átmennek, titkosítatlan fájl XMP, katalógus és OutputIntent markereket kap, a PAdES aláíró pedig EPadesCryptot emel átengedés helyett
Mind a hat injektor és a PAdES aláíró előbb a forrástrailert olvassa — a titkosított bemenet érintetlenül átmegy, a titkosítatlan megkapja a jelölőit, az aláírás pedig EPadesCryptoval utasít el a csendes átengedés helyett

Az itteni indoklás szigorúbb, mint a jelölő-injektorok átengedése, és ez szándékos. Egy csendes átengedés biztonságos egy PDF/A-pecsétnél, mert annak kihagyása ugyanazzal az érvényes PDF-fel hagy téged, amivel kezdtél, csak felirat nélkül. Az aláírás nem bukhat el ilyen csendesen: egy aláírás, amelyet csendben soha nem adtak hozzá, pontosan úgy néz ki bármely hívó kód számára, amely csak egy logikai eredményt ellenőriz, mint egy aláírás, amelyet sikeresen hozzáadtak. Az EPadesCrypto a közönséges Exception osztályból származik, így elkapása normál kivételkezelés, nem egy speciális vezérlésfolyam-konvenció, amit meg kell tanulnod

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;

Megfelelőségi pecsétek, aláírások, és titkosítás sorrendezése

A gyakorlati javítás sorrendezés, nem egy másik könyvtár. Alkalmazd a PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, vagy PDF/VT-1 jelölőket először, add hozzá bármely PAdES-aláírást ezután, és csak azután futtasd, bármi is az a lépés a csővezetékedben, amely ténylegesen birtokolja a titkosítást, legyen az egy dedikált PDF-író, egy aláíró berendezés, vagy a saját AES-megvalósításod. A PDFiumPas inkrementális-frissítési rétege természetesen illeszkedik e sorozat közepébe, kicsi, célzott objektumokat fűzve hozzá egy egyébként kész fájlhoz, és a titkosítás a végén tartozik, pontosan azért, mert ez az egyetlen művelet a láncban, amelyet maga a PDFiumPas nem tud elvégezni vagy visszafordítani

Egy Delphi folyamat sorrend PDFiumPashoz titkosított PDFeken: a megfelelőségi markerek először kerülnek be, amíg a fájl titkosítatlan, ezt követi a PAdES aláírás hozzáadása, a titkosítás pedig utolsóként, külön lépésként, amelyet a folyamat birtokol, mert a PDFiumPas nem tudja alkalmazni vagy eltávolítani
A PDFiumPas megfelelőségi jelölőket és PAdES aláírásokat fűz hozzá, amíg a fájl még olvasható, a titkosítás pedig utolsóként jön, a folyamathoz tartozó külön lépésként

Ebből semmi nem változtatja meg, hogyan olvassa a PDFiumPas a lezáró- és kereszthivatkozás-adatokat, amelyektől minden inkrementális frissítés függ, ami a maga finomságainak forrása, amint az xref-streamek képbe kerülnek; egy PDF objektum- és xref-streamjeinek érvényesítése tárgyalja, hogyan kezeli ugyanaz a lezáró-olvasó útvonal a PDF 1.5+ tömörített struktúrákat. És amint egy dokumentum kész valami erősebbre egy megfelelőségi pecsétnél, egy PDF aláírása PAdES B-B aláírással Delphiben az, ahol a SignPades átveszi pontosan onnan, ahol ez a cikk abbahagyja

Az itt leírt jelölő-injektorok és SignPades metódusok a Delphihez és C++Builderhez készült PDFium Komponens részeként érkeznek, a renderelés és a csak-olvasható vizsgálat mellett, amit a PDFium natívan biztosít