Tag en fakturaen-PDF, der allerede bærer AES-256-kryptering, og bed PDFium-komponenten til Delphi og C++Builder (PDFiumPas) om at stemple den PDF/A til arkiv-opbevaring, eller signere den med PAdES, gennem en inkrementel opdatering frem for en fuld omskrivning. Biblioteket kommer ikke dertil ved at patche de krypterede bytes direkte: dens seks konformitetsmarkør-injektorer registrerer en eksisterende /Encrypt-post og lader kilden passere byte-for-byte uændret igennem, og dens PAdES-signerer kaster en undtagelse frem for at udsende en signatur, ingen validator vil acceptere
Det er et andet spørgsmål end at auditere en PDF, man ikke selv oprettede, for skjult risiko, hvilket er sin egen skrivebeskyttede øvelse. Denne artikel handler om skrive-siden af den samme tillidsgrænse: hvad ens egen kode har lov til at gøre ved en fil, hvis bytes allerede er låst bag en andens adgangskode, i det øjeblik den kode forsøger at tilføje noget til den bagefter
Hvad kræver ISO 32000-1, når man opdaterer en krypteret PDF?
ISO 32000-1 §7.5.6 kræver, at en inkrementel opdaterings trailer gentager hver post fra den forrige trailer undtagen /Prev, og Tabel 15 lister /Encrypt blandt de poster en trailer kan bære. Drop den fra den nye trailer, og en konform læser har ingen grund til at betvivle udeladelsen: den nyeste trailer er autoritativ, så en læser der ikke finder nogen /Encrypt der, afgør at hele filen er ukrypteret og forsøger at parse den ældre, stadig krypterede krop som rene bytes. Behold /Encrypt i den nye trailer, men skriv opdateringens egne objekter som ren tekst, og fejlen flytter sig bare et trin senere: læseren registrerer korrekt kryptering, kører hvert objekt den rører gennem filens cipher, inklusive de nye, der aldrig var krypteret i første omgang, og får støj tilbage for indhold, der var perfekt læsbart, før dekryptering rørte det. Begge fejl producerer en fil, der ser ud som en normal, velformet inkrementel opdatering på byte-niveau, lige indtil en konform læser åbner den
Seks markør-injektorer, én v2.14.2-krypterings-gate
PDFiumPas leverer seks byte-niveau-markør-injektorer, én for hver ISO-PDF-delmængde den kan mærke: 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), og PDF/VT-1 (ISO 16612-2). Hver tager de bytes, PDFiums egen FPDF_SaveAsCopy allerede skrev, og lægger en anden, mindre inkrementel opdatering oven på dem: en ny XMP-metadata-stream, en katalog-ordbog-redigering der peger på den, og for de trykorienterede delmængder et OutputIntent og en ICC-profil. Fra og med v2.14.2 læser hver af InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers og InjectPdfVTMarkers kildetrailer'en først, og hvis den rapporterer en eksisterende /Encrypt-post, kopierer den kilden igennem til destinations-strømmen umodificeret og returnerer straks. Ingen XMP, intet OutputIntent, ingen katalog-redigering — kalderen får den originale fil tilbage, byte for byte
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;
Tilladt at være krypteret er ikke det samme som sikkert at injicere ind i
PDF/E-1 og PDF/R-1 tillader begge eksplicit deres værtsdokument at være krypteret på specifikations-niveau, hvilket læser som en undtagelse, indtil man kigger på, hvad der rent faktisk skal ske på disken. ISO 24517-1 §6.3 tillader kryptering for PDF/E-1, og ISO 23504-1 §6.2.3 tillader det for PDF/R-1, forudsat at headeren erklærer %PDF-2.0. Ingen af klausulerne siger noget om, hvorvidt en byte-niveau-efterbehandler sikkert kan tilføje et rent-tekst-objekt til den krypterede container, og det kan den ikke, af de samme §7.5.6-grunde, der gælder for hver anden delmængde. PDFiumPas' egne konformitets-validatorer for disse to profiler, ValidatePdfECompliance og ValidatePdfRCompliance, registrerer tilstedeværelsen af /Encrypt bevidst uden at flage det som en defekt, hvilket er korrekt for en skrivebeskyttet validator, der aldrig skriver en byte. Det er også et mønster let at overse og antage, at søskende-injektoren ikke har brug for en separat vagt, når injektoren er den ene funktion i parret, der rent faktisk skal nægte
Dekrypterer SaveAsPdfX i stilhed ens dokument?
Ja, hver gang man går gennem de offentlige bekvemmeligheds-metoder frem for at kalde en injektor direkte. Hver af TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR og SaveAsPdfVT gengiver det aktuelle dokument til en midlertidig stream med SaveAs(Tmp, saRemoveSecurity), før den overdrager de bytes til dens tilhørende injektor. saRemoveSecurity mapper til PDFiums eget FPDF_REMOVE_SECURITY-flag, så den midlertidige kopi, injektoren modtager, blev aldrig krypteret i første omgang, og injektorens /Encrypt-vagt har aldrig grund til at udløses. Outputtet bærer ens PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1- eller PDF/VT-1-markører, men det er ikke længere beskyttet af hvilken som helst adgangskode, der åbnede kilden
Den afvejning er usynlig, indtil nogen nedstrøms åbner den "beskyttede" arkivkopi uden en adgangskode og bemærker, at det bare fungerer. Fixen er ikke et andet metodekald; PDFiumPas har ingen saAddSecurity-modstykke at parre med saRemoveSecurity, fordi den underliggende PDFium-motor aldrig blev bygget til at skrive ny kryptering, kun til at fjerne den. Hvis begge egenskaber betyder noget for én fil, skal kryptering være et separat trin, man selv ejer, anvendt efter konformitetsmarkørerne, ikke foldet ind i det samme SaveAsPdfA-kald
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;
Hvad sker der, når man signerer en krypteret PDF med PAdES?
PDFiumPas nægter direkte, frem for i stilhed at droppe anmodningen på den måde, en markør-injektor gør. TPdf.SignPades og SignPadesToStream render begge gennem en intern SignPadesBytes, og det første, den gør efter at have parset kildetrailer'en, er at tjekke for /Encrypt. Hvis posten er til stede, kaster den EPadesCrypto med beskeden "SignPadesBytes: the source document is encrypted; remove encryption before signing" i stedet for at fortsætte yderligere. InjectPadesDssMarkers, funktionen der indlejrer certifikater, OCSP-svar og CRL'er til langtids-validering, anvender det identiske tjek af den identiske grund, med sin egen besked: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
Ræsonnementet her er strengere end markør-injektorernes gennemløb, og bevidst sådan. Et tavst gennemløb er sikkert for et PDF/A-stempel, fordi at springe det over efterlader dig med den samme gyldige PDF, man startede med, blot umærket. Signering kan ikke fejle lige så stille: en signatur der i stilhed aldrig blev tilføjet, ser, for enhver kaldende kode der kun tjekker et boolsk resultat, nøjagtig ud som en signatur der blev tilføjet med succes. EPadesCrypto nedstammer fra den almindelige Exception-klasse, så at fange den er normal undtagelseshåndtering, ikke en speciel kontrolflow-konvention man skal lære
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;
Rækkefølge for konformitetsstempler, signaturer og kryptering
Den praktiske fix er rækkefølge, ikke et andet bibliotek. Anvend PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1- eller PDF/VT-1-markører først, tilføj enhver PAdES-signatur dernæst, og kør først derefter, hvilket trin i ens pipeline der rent faktisk ejer kryptering, hvad enten det er en dedikeret PDF-skriver, en signerings-appliance, eller ens egen AES-implementering. PDFiumPas' inkrementelle-opdaterings-lag passer naturligt ind i midten af den sekvens, tilføjer små, målrettede objekter oven på en fil, der ellers er færdig, og kryptering hører hjemme til sidst, netop fordi det er den ene operation i kæden, PDFiumPas selv ikke kan udføre eller vende om
Intet af dette ændrer, hvordan PDFiumPas læser trailer- og kryds-reference-data, som hver inkrementel opdatering afhænger af, hvilket er sin egen kilde til subtilitet, når xref-streams kommer i billedet; validering af en PDFs objekt- og xref-streams dækker, hvordan den samme trailer-læsevej håndterer PDF 1.5+-komprimerede strukturer. Og når et dokument er klar til noget stærkere end et konformitetsstempel, er signering af en PDF med en PAdES B-B-signatur i Delphi hvor SignPades tager over fra præcis det punkt, denne artikel slipper
Markør-injektorerne og SignPades-metoderne beskrevet her leveres som en del af PDFium-komponenten til Delphi og C++Builder, sammen med gengivelsen og den skrivebeskyttede inspektion, PDFium leverer nativt