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