Technický článek

Šifrované PDF v Delphi nelze tiše záplatovat

Vezměte fakturu PDF, která už nese šifrování AES-256, a požádejte PDFium Component pro Delphi a C++Builder (PDFiumPas), aby ji orazítkoval jako PDF/A pro archivní uchování, nebo ji podepsal PAdES, přes přírůstkovou aktualizaci místo plného přepisu. Knihovna se tam nedostane záplatováním šifrovaných bajtů přímo: jejích šest injektorů značek konformity zjistí existující záznam /Encrypt a propustí zdroj do cíle bajt po bajtu beze změny, a její signer PAdES vyvolá výjimku místo vydání podpisu, který žádný validátor nepřijme

To je jiná otázka než audit PDF, který jste nevytvořili, na skryté riziko, což je vlastní cvičení jen pro čtení. Tento článek je o zapisovací straně stejné hranice důvěry: co smí váš vlastní kód udělat se souborem, jehož bajty jsou už uzamčené za heslem někoho jiného, v okamžiku, kdy se tento kód dodatečně pokusí do něj cokoli přidat

Co ISO 32000-1 vyžaduje, když aktualizujete šifrované PDF?

ISO 32000-1 §7.5.6 vyžaduje, aby trailer přírůstkové aktualizace opakoval každý záznam z předchozího traileru kromě /Prev, a tabulka 15 uvádí /Encrypt mezi záznamy, které trailer smí nést. Vypusťte jej z nového traileru a konformní čtenář nemá důvod pochybovat o tomto vynechání: nejnovější trailer je autoritativní, takže čtenář, který v něm nenajde žádné /Encrypt, rozhodne, že celý soubor je nešifrovaný, a pokusí se parsovat starší, pořád zašifrované tělo jako obyčejné bajty. Ponechte /Encrypt v novém traileru, ale zapište vlastní objekty aktualizace jako čistý text, a selhání se jen posune o krok dál: čtenář správně detekuje šifrování, protáhne každý objekt, kterého se dotkne, přes šifru souboru, včetně nových, které nikdy zašifrované ani nebyly, a dostane zpátky šum pro obsah, který byl dokonale čitelný, ještě než se jej dešifrování dotklo. Kterákoli chyba vyprodukuje soubor, který na úrovni bajtů vypadá jako normální, dobře formovaná přírůstková aktualizace, přesně dokud jej neotevře konformní čtenář

Šest injektorů značek, jedna brána šifrování z v2.14.2

PDFiumPas dodává šest injektorů značek na úrovni bajtů, po jednom pro každou podmnožinu ISO PDF, kterou dokáže označit: 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) a PDF/VT-1 (ISO 16612-2). Každý bere bajty, které už zapsalo vlastní FPDF_SaveAsCopy PDFium, a vrství na ně druhou, menší přírůstkovou aktualizaci: nový proud metadat XMP, úpravu slovníku katalogu, která na něj ukazuje, a pro tiskově orientované podmnožiny OutputIntent a profil ICC. Od v2.14.2 každá z InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers a InjectPdfVTMarkers nejdřív přečte zdrojový trailer, a pokud hlásí existující záznam /Encrypt, zkopíruje zdroj do cílového proudu nezměněný a okamžitě se vrátí. Žádné XMP, žádný OutputIntent, žádná úprava katalogu — volající dostane originální soubor zpátky, bajt po bajtu

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;

Povoleno být šifrovaný není totéž jako bezpečné pro injektáž

PDF/E-1 i PDF/R-1 oba na úrovni specifikace výslovně dovolují, aby jejich hostitelský dokument byl šifrovaný, což čte jako výjimku, dokud se nepodíváte na to, co se skutečně musí stát na disku. ISO 24517-1 §6.3 povoluje šifrování pro PDF/E-1 a ISO 23504-1 §6.2.3 je povoluje pro PDF/R-1 za předpokladu, že hlavička deklaruje %PDF-2.0. Ani jedna klauzule neříká nic o tom, zda post-procesor na úrovni bajtů dokáže bezpečně přidat čistě textový objekt do tohoto šifrovaného kontejneru, a nedokáže, ze stejných důvodů §7.5.6, které platí pro každou jinou podmnožinu. Vlastní validátory shody PDFiumPas pro tyto dva profily, ValidatePdfECompliance a ValidatePdfRCompliance, zaznamenávají přítomnost /Encrypt záměrně, aniž by ji označily jako defekt, což je správné pro validátor jen ke čtení, který nikdy nezapíše jediný bajt. Je to také snadný vzor, který se dá přeskočit a předpokládat, že sourozenecký injektor nepotřebuje samostatnou ochranu, když je injektor tou jednou funkcí ve dvojici, která skutečně musí odmítnout

Dešifruje SaveAsPdfX tiše váš dokument?

Ano, kdykoli projdete přes veřejné pohodlné metody místo přímého volání injektoru. Každá z TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR a SaveAsPdfVT vykreslí aktuální dokument do dočasného proudu přes SaveAs(Tmp, saRemoveSecurity) ještě předtím, než tyto bajty předá svému odpovídajícímu injektoru. saRemoveSecurity se mapuje na vlastní příznak PDFium FPDF_REMOVE_SECURITY, takže dočasná kopie, kterou injektor dostane, nikdy nebyla šifrovaná, a ochrana /Encrypt injektoru nikdy nemá důvod se spustit. Výstup nese vaše značky PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 nebo PDF/VT-1, ale už není chráněný jakýmkoli heslem, které otevřelo zdroj

Tento kompromis je neviditelný, dokud jej někdo po směru neotevře „chráněnou" archivní kopii bez hesla a nevšimne si, že to prostě funguje. Oprava není jiné volání metody; PDFiumPas nemá žádný protějšek saAddSecurity, který by se pároval s saRemoveSecurity, protože podkladový engine PDFium nikdy nebyl postaven na to, aby zapisoval nové šifrování, jen aby jej odstraňoval. Pokud na obou vlastnostech pro jeden soubor záleží, musí být šifrování samostatný krok, který vlastníte vy, aplikovaný po značkách konformity, ne sloučený do stejného volání 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;

Co se stane, když podepíšete šifrované PDF pomocí PAdES?

PDFiumPas rovnou odmítne, místo aby požadavek tiše zahodil tak, jako to dělá injektor značek. TPdf.SignPades i SignPadesToStream obě vedou přes interní SignPadesBytes, a první, co udělá po naparsování zdrojového traileru, je kontrola na /Encrypt. Pokud je záznam přítomen, vyvolá EPadesCrypto se zprávou „SignPadesBytes: the source document is encrypted; remove encryption before signing" místo toho, aby pokračovala dál. InjectPadesDssMarkers, funkce, která vkládá certifikáty, odpovědi OCSP a CRL pro dlouhodobé ověřování, aplikuje identickou kontrolu ze stejného důvodu, s vlastní zprávou: „InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Úvaha zde je přísnější než průchod injektorů značek, a záměrně. Tichý průchod je bezpečný pro razítko PDF/A, protože jeho přeskočení vás nechá se stejným platným PDF, jaké jste měli na začátku, jen bez štítku. Podepisování takhle tiše selhat nemůže: podpis, který se tiše nikdy nepřidal, vypadá pro jakýkoli volající kód, který kontroluje jen booleovský výsledek, přesně jako podpis, který byl úspěšně přidán. EPadesCrypto je potomek obyčejné třídy Exception, takže jeho zachycení je normální zpracování výjimek, ne speciální konvence řízení toku, kterou byste se museli učit

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;

Sekvenování razítek konformity, podpisů a šifrování

Praktická oprava je pořadí, ne jiná knihovna. Aplikujte značky PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, nebo PDF/VT-1 nejdřív, přidejte jakýkoli podpis PAdES další, a teprve pak spusťte jakýkoli krok ve vaší pipeline, který skutečně vlastní šifrování, ať už je to vyhrazený zapisovač PDF, podpisové zařízení, nebo vaše vlastní implementace AES. Vrstva přírůstkových aktualizací PDFiumPas přirozeně zapadá doprostřed této sekvence, připojuje malé, cílené objekty na soubor, který je jinak hotový, a šifrování patří na konec přesně proto, že je to jedna operace v řetězu, kterou samotný PDFiumPas nedokáže provést ani zvrátit

Nic z tohoto nemění, jak PDFiumPas čte data traileru a křížového odkazu, na kterých závisí každá přírůstková aktualizace, což je vlastní zdroj jemností, jakmile do hry vstoupí proudy xref; ověřování proudů objektů a xref v PDF popisuje, jak stejná cesta čtení traileru zpracovává komprimované struktury PDF 1.5+. A jakmile je dokument připraven na něco silnějšího než razítko konformity, podepsání PDF podpisem PAdES B-B v Delphi je místo, kde SignPades přebírá přesně tam, kde tento článek končí

Injektory značek a metody SignPades popsané zde se dodávají jako součást komponenty PDFium pro Delphi a C++Builder, spolu s vykreslováním a inspekcí jen pro čtení, kterou PDFium poskytuje nativně