Technisch artikel

U kunt een versleutelde PDF niet stilzwijgend patchen in Delphi

Neem een factuur-PDF die al AES-256-versleuteling draagt en vraag de PDFium Component voor Delphi en C++Builder (PDFiumPas) om deze PDF/A te stempelen voor archiveringsbehoud, of om deze te ondertekenen met PAdES, via een incrementele update in plaats van een volledige herschrijving. De bibliotheek komt daar niet door de versleutelde bytes rechtstreeks te patchen: de zes conformiteitsmarkering-injectoren detecteren een bestaand /Encrypt-item en geven de bron byte-voor-byte ongewijzigd door, en de PAdES-ondertekenaar werpt liever een uitzondering op dan een handtekening af te geven die geen enkele validator zal accepteren

Dat is een andere vraag dan het auditen van een PDF die u niet zelf hebt gemaakt op verborgen risico, wat zijn eigen alleen-lezen oefening is. Dit artikel gaat over de schrijfkant van diezelfde vertrouwensgrens: wat uw eigen code mag doen met een bestand waarvan de bytes al zijn vergrendeld achter het wachtwoord van iemand anders, op het moment dat die code achteraf probeert er iets aan toe te voegen

Wat vereist ISO 32000-1 wanneer u een versleutelde PDF bijwerkt?

ISO 32000-1 §7.5.6 vereist dat de trailer van een incrementele update elk item uit de vorige trailer herhaalt behalve /Prev, en Tabel 15 vermeldt /Encrypt als een van de items die een trailer kan dragen. Laat u dit weg uit de nieuwe trailer, dan heeft een conforme lezer geen reden om aan die weglating te twijfelen: de nieuwste trailer is gezaghebbend, dus een lezer die daar geen /Encrypt aantreft, besluit dat het hele bestand onversleuteld is en probeert het oudere, nog steeds versleutelde deel als platte bytes te parsen. Behoudt u /Encrypt wel in de nieuwe trailer maar schrijft u de eigen objecten van de update als platte tekst, dan verschuift de storing gewoon één stap later: de lezer detecteert de versleuteling correct, voert elk object dat hij aanraakt door het cijfer van het bestand, inclusief de nieuwe objecten die nooit versleuteld zijn geweest, en krijgt ruis terug voor inhoud die perfect leesbaar was voordat ontsleuteling eraan raakte. Beide fouten leveren een bestand op dat op byteniveau eruitziet als een normale, goedgevormde incrementele update, tot het moment dat een conforme lezer het opent

Zes markering-injectoren, één encryptiepoort sinds v2.14.2

PDFiumPas levert zes markering-injectoren op byteniveau, één voor elk ISO-PDF-subset dat het kan labelen: 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), en PDF/VT-1 (ISO 16612-2). Elk daarvan neemt de bytes die PDFium's eigen FPDF_SaveAsCopy al heeft geschreven en legt er een tweede, kleinere incrementele update bovenop: een nieuwe XMP-metadatastream, een catalogusdictionary-bewerking die ernaar wijst, en voor de drukgeoriënteerde subsets een OutputIntent en ICC-profiel. Vanaf v2.14.2 leest elk van InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, en InjectPdfVTMarkers eerst de brontrailer, en als deze een bestaand /Encrypt-item rapporteert, kopieert het de bron ongewijzigd door naar de doelstream en keert het onmiddellijk terug. Geen XMP, geen OutputIntent, geen catalogusbewerking — de aanroeper krijgt het originele bestand terug, byte voor 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;

Toegestaan om versleuteld te zijn is niet hetzelfde als veilig om in te injecteren

PDF/E-1 en PDF/R-1 staan beide expliciet toe dat hun hostdocument op specificatieniveau versleuteld is, wat leest als een vrijstelling totdat u kijkt naar wat er daadwerkelijk op schijf moet gebeuren. ISO 24517-1 §6.3 staat versleuteling toe voor PDF/E-1, en ISO 23504-1 §6.2.3 staat het toe voor PDF/R-1 mits de header %PDF-2.0 declareert. Geen van beide clausules zegt iets over of een byteniveau-nabewerker veilig een plattetekstobject aan die versleutelde container kan toevoegen, en dat kan niet, om dezelfde §7.5.6-redenen die voor elk ander subset gelden. De eigen conformiteitsvalidators van PDFiumPas voor deze twee profielen, ValidatePdfECompliance en ValidatePdfRCompliance, registreren de aanwezigheid van /Encrypt doelbewust zonder dit als een defect te markeren, wat correct is voor een alleen-lezen validator die nooit een byte schrijft. Het is ook een gemakkelijk patroon om overheen te lezen en aan te nemen dat de zusterinjector geen aparte bewaking nodig heeft, terwijl de injector precies de ene functie in het paar is die daadwerkelijk moet weigeren

Ontsleutelt SaveAsPdfX uw document stilzwijgend?

Ja, wanneer u via de publieke gemaksmethoden gaat in plaats van rechtstreeks een injector aan te roepen. Elk van TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, en SaveAsPdfVT rendert het huidige document naar een tijdelijke stream met SaveAs(Tmp, saRemoveSecurity) voordat het die bytes aan de bijbehorende injector overhandigt. saRemoveSecurity wordt gemapt naar PDFium's eigen vlag FPDF_REMOVE_SECURITY, dus de tijdelijke kopie die de injector ontvangt was om te beginnen nooit versleuteld, en de /Encrypt-bewaking van de injector krijgt nooit reden om te triggeren. De uitvoer draagt uw PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1-, of PDF/VT-1-markeringen, maar wordt niet langer beschermd door welk wachtwoord de bron ook opende

Die afweging is onzichtbaar totdat iemand stroomafwaarts de "beschermde" archiefkopie zonder wachtwoord opent en merkt dat het gewoon werkt. De oplossing is geen andere methodeaanroep; PDFiumPas heeft geen saAddSecurity-tegenhanger om te paren met saRemoveSecurity, omdat de onderliggende PDFium-engine nooit is gebouwd om nieuwe versleuteling te schrijven, alleen om deze te verwijderen. Als beide eigenschappen voor één bestand ertoe doen, moet versleuteling een aparte stap zijn die u zelf bezit, toegepast na de conformiteitsmarkeringen, niet gevouwen in dezelfde SaveAsPdfA-aanroep

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;

Wat gebeurt er wanneer u een versleutelde PDF ondertekent met PAdES?

PDFiumPas weigert regelrecht, in plaats van het verzoek stilzwijgend te laten vallen zoals een markering-injector doet. TPdf.SignPades en SignPadesToStream routeren beide via een interne SignPadesBytes, en het eerste wat deze doet na het parsen van de brontrailer, is controleren op /Encrypt. Is het item aanwezig, dan werpt het EPadesCrypto op met de melding "SignPadesBytes: the source document is encrypted; remove encryption before signing" in plaats van verder te gaan. InjectPadesDssMarkers, de functie die certificaten, OCSP-antwoorden, en CRL's insluit voor langetermijnvalidatie, past dezelfde controle toe om dezelfde reden, met zijn eigen melding: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

De redenering hier is strenger dan de pass-through van de markering-injectoren, en dat is opzettelijk. Een stille pass-through is veilig voor een PDF/A-stempel omdat het overslaan ervan u achterlaat met dezelfde geldige PDF waarmee u begon, alleen ongelabeld. Ondertekenen kan niet zo stil falen: een handtekening die stilzwijgend nooit is toegevoegd, ziet er voor elke aanroepende code die alleen een booleaans resultaat controleert, precies uit als een handtekening die succesvol is toegevoegd. EPadesCrypto is afgeleid van de gewone klasse Exception, dus het afvangen ervan is normale foutafhandeling, geen speciale controlestroom-conventie die u apart moet leren

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;

Volgorde bepalen voor conformiteitsstempels, handtekeningen, en versleuteling

De praktische oplossing is volgorde, geen andere bibliotheek. Pas eerst PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1-, of PDF/VT-1-markeringen toe, voeg vervolgens een eventuele PAdES-handtekening toe, en voer pas daarna de stap in uw pijplijn uit die daadwerkelijk de versleuteling bezit, of dat nu een toegewijde PDF-schrijver is, een ondertekeningsapparaat, of uw eigen AES-implementatie. De incrementele-update-laag van PDFiumPas past van nature in het midden van die volgorde, door kleine, gerichte objecten toe te voegen aan een bestand dat verder klaar is, en versleuteling hoort precies aan het einde thuis omdat het de ene bewerking in de keten is die PDFiumPas zelf niet kan uitvoeren of terugdraaien

Niets hiervan verandert hoe PDFiumPas de trailer- en cross-reference-data leest waar elke incrementele update van afhangt, wat een eigen bron van subtiliteit is zodra xref-streams in beeld komen; het valideren van de object- en xref-streams van een PDF behandelt hoe datzelfde trailer-leespad PDF 1.5+-gecomprimeerde structuren afhandelt. En zodra een document klaar is voor iets sterkers dan een conformiteitsstempel, is een PDF ondertekenen met een PAdES B-B-handtekening in Delphi waar SignPades het overneemt precies vanaf het punt waar dit artikel ophoudt

De hier beschreven markering-injectoren en SignPades-methoden worden geleverd als onderdeel van de PDFium Component voor Delphi en C++Builder, naast de rendering en alleen-lezen inspectie die PDFium van nature biedt