Articolo tecnico

Non Puoi Applicare Patch Silenziose a un PDF Crittografato in Delphi

Prendi una fattura PDF che già porta crittografia AES-256 e chiedi al componente PDFium per Delphi e C++Builder (PDFiumPas) di stampigliarla PDF/A per la conservazione archivistica, o di firmarla con PAdES, tramite un aggiornamento incrementale invece di una riscrittura completa. La libreria non ci arriverà applicando una patch diretta ai byte crittografati: i suoi sei iniettori di marcatori di conformità rilevano una voce /Encrypt esistente e fanno passare la sorgente verso la destinazione byte per byte invariata, e il suo firmatario PAdES solleva un'eccezione invece di emettere una firma che nessun validatore accetterà

Questa è una domanda diversa dall'auditare un PDF che non hai creato tu alla ricerca di rischi nascosti, che è un esercizio a sé di sola lettura. Questo articolo riguarda il lato scrittura dello stesso confine di fiducia: cosa il tuo stesso codice è autorizzato a fare a un file i cui byte sono già bloccati dietro la password di qualcun altro, nel momento in cui quel codice cerca di aggiungervi qualcosa a posteriori

Cosa richiede ISO 32000-1 quando aggiorni un PDF crittografato?

ISO 32000-1 §7.5.6 richiede che il trailer di un aggiornamento incrementale ripeta ogni voce del trailer precedente eccetto /Prev, e la Tabella 15 elenca /Encrypt tra le voci che un trailer può portare. Eliminala dal nuovo trailer e un lettore conforme non ha motivo di dubitare dell'omissione: il trailer più recente è autorevole, quindi un lettore che non vi trova /Encrypt decide che l'intero file non è crittografato e tenta di analizzare il corpo più vecchio, ancora cifrato, come byte semplici. Mantieni /Encrypt nel nuovo trailer ma scrivi gli oggetti propri dell'aggiornamento come testo in chiaro, e il fallimento si sposta semplicemente di un passo più avanti: il lettore rileva correttamente la crittografia, esegue ogni oggetto che tocca attraverso il cifrario del file, inclusi quelli nuovi che non erano mai stati crittografati in primo luogo, e ottiene rumore al posto di contenuto che era perfettamente leggibile prima che la decrittazione lo toccasse. Entrambi gli errori producono un file che sembra un normale, ben formato aggiornamento incrementale a livello di byte, fino al momento in cui un lettore conforme lo apre

Sei iniettori di marcatori, un unico cancello di crittografia v2.14.2

PDFiumPas distribuisce sei iniettori di marcatori a livello di byte, uno per ogni sottoinsieme PDF ISO che può etichettare: 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) e PDF/VT-1 (ISO 16612-2). Ognuno prende i byte che l'FPDF_SaveAsCopy proprio di PDFium ha già scritto e vi stratifica sopra un secondo, più piccolo aggiornamento incrementale: un nuovo stream di metadati XMP, una modifica al dizionario catalogo che vi punta, e per i sottoinsiemi orientati alla stampa un OutputIntent e un profilo ICC. A partire dalla v2.14.2, ognuno tra InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers e InjectPdfVTMarkers legge prima il trailer sorgente, e se questo segnala una voce /Encrypt esistente, copia la sorgente sullo stream di destinazione senza modifiche e ritorna immediatamente. Nessun XMP, nessun OutputIntent, nessuna modifica al catalogo — il chiamante riceve indietro il file originale, byte per 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;

Consentito essere crittografato non è lo stesso che sicuro da iniettare

PDF/E-1 e PDF/R-1 permettono entrambi esplicitamente al proprio documento ospitante di essere crittografato a livello di specifica, il che si legge come un'esenzione finché non guardi cosa deve realmente accadere su disco. ISO 24517-1 §6.3 permette la crittografia per PDF/E-1, e ISO 23504-1 §6.2.3 la permette per PDF/R-1 a condizione che l'intestazione dichiari %PDF-2.0. Nessuna delle due clausole dice nulla su se un post-processore a livello di byte possa aggiungere in sicurezza un oggetto in chiaro a quel contenitore crittografato, e non può, per le stesse ragioni della §7.5.6 che si applicano a ogni altro sottoinsieme. I validatori di conformità propri di PDFiumPas per questi due profili, ValidatePdfECompliance e ValidatePdfRCompliance, registrano la presenza di /Encrypt deliberatamente senza segnalarla come difetto, il che è corretto per un validatore di sola lettura che non scrive mai un byte. È anche uno schema facile da sfiorare presupponendo che l'iniettore gemello non abbia bisogno di una protezione separata, quando l'iniettore è l'unica funzione della coppia che deve realmente rifiutare

SaveAsPdfX decrittografa silenziosamente il tuo documento?

Sì, ogni volta che passi attraverso i metodi di comodità pubblici invece di chiamare direttamente un iniettore. Ognuno tra TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR e SaveAsPdfVT renderizza il documento corrente su uno stream temporaneo con SaveAs(Tmp, saRemoveSecurity) prima di passare quei byte al proprio iniettore corrispondente. saRemoveSecurity mappa sul flag FPDF_REMOVE_SECURITY proprio di PDFium, quindi la copia temporanea che l'iniettore riceve non era mai stata crittografata in primo luogo, e la protezione /Encrypt dell'iniettore non ha mai motivo di scattare. L'output porta i tuoi marcatori PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 o PDF/VT-1, ma non è più protetto da qualunque password abbia aperto la sorgente

Quel compromesso è invisibile finché qualcuno a valle non apre la copia archivistica "protetta" senza password e nota che funziona e basta. La correzione non è una chiamata di metodo diversa; PDFiumPas non ha una controparte saAddSecurity da abbinare a saRemoveSecurity, perché il motore PDFium sottostante non è mai stato costruito per scrivere nuova crittografia, solo per rimuoverla. Se entrambe le proprietà contano per un file, la crittografia deve essere un passaggio separato che possiedi tu, applicato dopo i marcatori di conformità, non ripiegato nella stessa chiamata 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;

Cosa succede quando firmi con PAdES un PDF crittografato?

PDFiumPas rifiuta apertamente, invece di abbandonare silenziosamente la richiesta nel modo in cui fa un iniettore di marcatori. TPdf.SignPades e SignPadesToStream passano entrambi attraverso un SignPadesBytes interno, e la prima cosa che fa dopo aver analizzato il trailer sorgente è controllare /Encrypt. Se la voce è presente, solleva EPadesCrypto con il messaggio "SignPadesBytes: the source document is encrypted; remove encryption before signing" invece di procedere oltre. InjectPadesDssMarkers, la funzione che incorpora certificati, risposte OCSP e CRL per la validazione a lungo termine, applica il controllo identico per il motivo identico, con il proprio messaggio: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Il ragionamento qui è più severo del pass-through degli iniettori di marcatori, e deliberatamente così. Un pass-through silenzioso è sicuro per uno stampiglio PDF/A perché saltarlo lascia lo stesso PDF valido da cui si è partiti, semplicemente non etichettato. La firma non può fallire altrettanto silenziosamente: una firma silenziosamente mai aggiunta appare, a qualsiasi codice chiamante che controlli solo un risultato booleano, esattamente come una firma aggiunta con successo. EPadesCrypto discende dalla comune classe Exception, quindi catturarla è normale gestione delle eccezioni, non una convenzione speciale di controllo del flusso da imparare

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;

Sequenziare stampigli di conformità, firme e crittografia

La correzione pratica è l'ordinamento, non una libreria diversa. Applica prima i marcatori PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 o PDF/VT-1, aggiungi poi qualsiasi firma PAdES, e solo allora esegui qualunque passaggio nella tua pipeline possieda realmente la crittografia, che sia uno scrittore PDF dedicato, un'appliance di firma, o la tua stessa implementazione AES. Il livello di aggiornamento incrementale di PDFiumPas si inserisce naturalmente nel mezzo di questa sequenza, aggiungendo oggetti piccoli e mirati sopra a un file altrimenti finito, e la crittografia appartiene alla fine proprio perché è l'unica operazione della catena che PDFiumPas stesso non può eseguire né invertire

Nulla di tutto ciò cambia come PDFiumPas legge il trailer e i dati cross-reference da cui dipende ogni aggiornamento incrementale, che è una fonte di sottigliezza a sé una volta che entrano in gioco gli xref stream; validare gli object e xref stream di un PDF tratta come quello stesso percorso di lettura del trailer gestisce le strutture compresse PDF 1.5+. E una volta che un documento è pronto per qualcosa di più forte di uno stampiglio di conformità, firmare un PDF con una firma PAdES B-B in Delphi è dove SignPades subentra esattamente dal punto in cui questo articolo si ferma

Gli iniettori di marcatori e i metodi SignPades descritti qui fanno parte del componente PDFium per Delphi e C++Builder, insieme al rendering e all'ispezione di sola lettura che PDFium fornisce nativamente