Nelle build di PDFium Component precedenti alla v3.126.2, TPdf.AnalyzeSignatureRevisions poteva classificare una vera modifica del contenuto di pagina come cambiamento di annotazione permesso sotto DocMDP P=3, perché il suo grafo dei ruoli delle revisioni trattava il back-reference /P del widget firma alla sua pagina come proprietà. Dalla v3.126.2, PDFium Component separa gli archi di navigazione dagli archi di payload posseduto, così il contenuto di pagina resta contenuto di pagina. La segnalazione dietro a questa correzione sembra innocua sulla carta. Un contratto certificato consente le annotazioni, la controparte aggiunge un salvataggio incrementale, e l'analizzatore dice che ogni cambiamento successivo è permesso. Poi qualcuno fa il diff delle pagine renderizzate e l'importo del pagamento a pagina 2 è diverso
Questo articolo è il seguito dal punto di vista dell'attaccante della panoramica sull'analisi dei cambiamenti delle revisioni post-firma, quindi salta le basi della ricostruzione delle revisioni e della classificazione DocMDP e va dritto al grafo degli oggetti: come era modellata la proprietà, perché la direzione di un arco decide un verdetto di sicurezza, che cosa è cambiato nella v3.126.2, e come auditare la tua logica di accettazione
Perché una modifica di pagina passava come modifica di annotazione sotto DocMDP P=3?
La modifica di pagina passava perché il vecchio grafo dei ruoli seguiva ogni riferimento indiretto in un dizionario come se l'oggetto referenziato appartenesse a quello referenziante, e il widget firma punta alla sua pagina. Un dizionario di annotazione porta /P, un riferimento indiretto all'oggetto pagina su cui sta (ISO 32000-1 §12.5.2). Quell'entry è un indizio di navigazione. Il widget non possiede la pagina; la pagina possiede il widget attraverso il suo array /Annots
L'analizzatore assegna a ogni oggetto un insieme di bit di ruolo prima di classificare i cambiamenti successivi: pagina, annotazione, form e materiale di validazione. Gli oggetti radice prendono il ruolo dal proprio dizionario, e il ruolo poi si sparga su tutto ciò che referenziano. Nella vecchia propagazione, la catena andava così:
- Il widget firma è un
/Subtype /Widgetcon/FT /Sig, quindi prende il ruolo di annotazione - La
/Pdel widget spinge il ruolo di annotazione sul dizionario di pagina, che ha già il ruolo di pagina - La pagina spinge entrambi i ruoli in
/Contents,/Resourcese, tramite/Parent, su nell'albero Pages e attraverso verso ogni pagina gemella - Un dizionario di content stream come
<< /Length 812 >>non ha/Type, quindi il classificatore ricadeva sui bit di ruolo e controllava il ruolo di annotazione prima di quello di pagina
Il content stream modificato quindi usciva come prckAnnotation. Sotto ISO 32000-1 §12.8.2.2, DocMDP P=3 consente i cambiamenti di annotazione, quindi la decisione era prdAllowed e il report si arrotolava su prasAllowed. Lo stesso file sotto P=2 veniva rifiutato, ma solo per coincidenza: P=2 vieta i cambiamenti di annotazione, così lo stream etichettato male veniva respinto per la ragione sbagliata. Un loop di propagazione a quattro passate fisse aggiungeva una seconda debolezza. Il payload arrivato attraverso un array indiretto, o attraverso una lunga catena i cui numeri di oggetto girano al contrario, poteva non ricevere alcun ruolo
Perché un validatore di firme deve chiedersi chi possiede un oggetto?
Un validatore di firme deve chiedersi chi possiede un oggetto perché gli aggiornamenti incrementali PDF (ISO 32000-1 §7.5.6) lasciano che chiunque appenda una revisione che ridefinisce un numero di oggetto esistente, e il corpo ridefinito non annuncia che cosa è. La firma verifica comunque, dato che copre solo i byte della propria revisione. Ogni difesa contro la manipolazione post-firma dipende quindi dal mappare ogni oggetto cambiato sulla struttura che lo usa, e poi dal chiedersi se il firmatario abbia permesso a quella struttura di cambiare
Parecchie classi di attacco pubblicate lavorano esattamente in quel varco. Gli incremental saving attack appendono una revisione che sostituisce il contenuto di pagina e contano sul verificatore che controlla solo il byte range firmato. Gli shadow attack piantano contenuto nascosto prima della firma e lo attivano dopo con un piccolo cambiamento dall'aria innocente. Gli attacchi ai documenti certificati abusano del fatto che P=2 e P=3 consentono esplicitamente alcune modifiche successive, poi travestono una modifica vietata da permessa. Un verificatore che classifica gli oggetti per etichette come /Type /Annot, o per qualsiasi percorso di riferimento che capita di raggiungerli, è esposto alla terza classe: all'attaccante basta una struttura permessa che possa raggiungere quella vietata
Ecco perché la domanda non è quali oggetti sono cambiati ma chi li possiede. Un content stream raggiunto da una pagina tramite /Contents è contenuto di pagina non importa cos'altro punti a lui. Un'annotazione che punta alla pagina tramite /P dice dove vive l'annotazione, non che cosa possiede
Come modella la proprietà PDFium Component v3.126.2?
PDFium Component v3.126.2 tratta i back-reference come navigazione, li tiene fuori dalla propagazione dei ruoli, e decide quali chiavi contano come navigazione dal ruolo strutturale del dizionario che le contiene, non dal solo nome della chiave. La tabella riassume le chiavi di navigazione che non trasportano più proprietà
| Dizionario proprietario | Chiavi trattate come navigazione | Riferimento alla specifica |
|---|---|---|
| Nodo Page o Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotazione o widget | /P | ISO 32000-1 §12.5.2 |
| Dizionario widget o campo | /Parent | ISO 32000-1 §12.7.3 |
Filtrare per nome chiave a livello globale avrebbe creato un buco nuovo. Una risorsa font o XObject può legittimamente chiamarsi /P, /Parent o /Annots, e un dizionario /Resources che togliesse la sua entry /P dalla propagazione lascerebbe un attaccante nascondere un XObject di proprietà della pagina dietro un nome di risorsa innocente. Nella v3.126.2 il filtro di navigazione si applica solo quando il dizionario proprietario è davvero una pagina, un nodo Pages, un'annotazione, un widget o un campo. Se uno di quei dizionari porta una chiave di navigazione duplicata, come due entry /P in un widget, l'analizzatore non indovina quale copia userebbe un viewer; la costruzione dei ruoli fallisce e la firma diventa Indeterminate
Altre regole chiudono le restanti vie di rietichettatura:
- I nodi Pages sono radici di ruolo pagina a pieno titolo, quindi le risorse ereditate dall'albero Pages (ISO 32000-1 §7.7.3.4) entrano nel contesto pagina attraverso vera proprietà, non attraverso una camminata
/Parentda una pagina figlia - Un ruolo di annotazione o form che arriva su un catalogo, nodo Pages, pagina, annotazione o dizionario di campo si ferma lì, perché quegli oggetti strutturali stabiliscono i propri ruoli e un ruolo payload in arrivo non deve sovrascriverli
- Il ruolo pagina è autorevole durante la classificazione: un oggetto di proprietà della pagina è
prckPageContentanche se una revisione successiva lo riscrive con una/FTcontraffatta, un'etichetta/Type /Annot, o lo condivide con un appearance stream - Un Form XObject usato solo come appearance di campo o annotazione conserva la sua categoria form o annotazione, così la rigenerazione ordinaria degli appearance dopo una compilazione di form viene classificata sotto le normali regole di permesso
- Un widget senza una
/FTpropria risolve il tipo di campo ereditato attraverso la catena/Parent, e una catena irrisolvibile fa fallire la costruzione dei ruoli invece di ripiegare su annotazione - I bit di ruolo di ogni revisione successiva vengono fusi nei ruoli della revisione coperta, così un aggiornamento posteriore non può cancellare una precedente relazione di proprietà della pagina staccando prima uno stream e modificandolo dopo
Punto fisso invece di un numero fisso di passate
La raggiungibilità dei ruoli nella v3.126.2 gira come una coda di lavoro che itera finché nessun oggetto guadagna un nuovo bit di ruolo, il che è un vero punto fisso a prescindere dalla profondità delle catene e dalla numerazione degli oggetti. Anche gli array indiretti come un array /Contents memorizzato come oggetto proprio vengono percorsi. Ogni oggetto può guadagnare al massimo quattro bit di ruolo distinti, quindi la coda è limitata a quattro entry per numero di oggetto; superare quel budget solleva prrResourceLimitExceeded. Un riferimento a un oggetto libero, una mancata combacianza di generazione o un header di oggetto rotto solleva prrMalformedRevisionChain, e payload dentro un object stream compresso solleva prrCompressedObjectUnresolved. Ognuno di questi fallimenti finisce con prasIndeterminate, mai con un verdetto di permesso, e quando il fallimento avviene durante la costruzione dei ruoli della revisione coperta la firma non riporta proprio nessun Changes
La routine seguente elenca le modifiche di contenuto di pagina che sopravvivono a questa analisi. Una modifica prckPageContent non viene mai classificata prdAllowed: DocMDP P=1, 2 o 3 la rende prdDisallowed, e una firma senza DocMDP la classifica prdSuspicious:
uses
SysUtils, TypInfo, PDFium, FPdfPades;
procedure ListPageContentEdits(const FileName: string);
const
ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
Sig: TPadesSignatureRevisionAnalysis;
Change: TPadesRevisionObjectChange;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions; // record, niente da liberare
for i := 0 to High(Report.Signatures) do
begin
Sig := Report.Signatures[i];
if not (prrPageContentChanged in Sig.Risks) then
Continue;
Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
[Sig.SignatureIndex, Sig.DocMdpPermission]));
for j := 0 to High(Sig.Changes) do
begin
Change := Sig.Changes[j];
if Change.Kind <> prckPageContent then
Continue;
Writeln(Format(' revision %d object %d %d R %s%s',
[Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
ShadowTag[Change.IsAuthoritative]]));
end;
end;
finally
Pdf.Free;
end;
end;
Che cosa succede quando FieldMDP e un'annotazione condividono un oggetto?
Quando la /V di un campo e la /Contents di un'annotazione puntano allo stesso oggetto indiretto, la v3.126.2 mantiene il lucchetto FieldMDP in forza anche se il cambiamento viene classificato come modifica di annotazione. Lo scenario è facile da costruire a mano: un firmatario blocca il campo Total con FieldMDP (ISO 32000-1 §12.8.2.4), e l'attaccante fa sì che la /Contents di un'annotazione testuale referenzi lo stesso oggetto stringa che contiene il valore del campo. Sotto P=3 la modifica di annotazione è permessa, quindi prima della correzione riscrivere quella stringa condivisa cambiava un valore di campo bloccato con un verdetto di permesso
L'oggetto ora porta sia il ruolo di annotazione sia quello di form, e la decisione di annotazione ricontrolla il lato form ogni volta che la firma ha una trasformazione FieldMDP:
- Sotto P=2 il cambiamento di annotazione è vietato a prescindere, esattamente come prima
- Con FieldMDP
All, ogni campo è bloccato, quindi il cambiamento condiviso èprdDisallowed - Con FieldMDP
IncludeoExclude, l'analizzatore non può risalire da uno scalare condiviso a un nome di campo, quindi la decisione èprdIndeterminateanziché un'ipotesi - Senza FieldMDP, vale la regola di annotazione P=3 e il cambiamento resta permesso
Un dettaglio di reporting conta per il codice gate. Il caso condiviso viene riportato come Kind = prckAnnotation con Decision = prdIndeterminate, e prrFieldMdpUnresolved viene aggiunto all'insieme dei rischi solo per i cambiamenti classificati come campi form. Un gate che cerca prrFieldMdpUnresolved e ignora Status si perde questo caso completamente
Come deve fallire in modo chiuso il codice Delphi sull'analisi delle revisioni?
Il codice Delphi dovrebbe accettare un documento firmato solo quando lo stato dell'analisi è prasNoLaterChanges o prasAllowed e nessun rischio strutturale è presente, e dovrebbe trattare prasIndeterminate e prasSuspicious come non affidabili, non come avvisi da registrare e far passare. Indeterminate significa che l'analizzatore non è riuscito a dimostrare che le revisioni successive erano permesse; per un attaccante, un input che produce in modo affidabile Indeterminate è utile quanto uno che produce Allowed se il tuo codice lo lascia passare. La AnalyzePadesSignatureRevisions globale prende qualsiasi TStream e lo legge dalla posizione 0, il che la adatta agli handler di upload che non hanno mai bisogno di renderizzare il documento
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Le definizioni duplicate vengono registrate senza degradare Status
BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
prrResourceLimitExceeded];
function SignedRevisionsAcceptable(const FileName: string;
out Reason: string): Boolean;
var
Source: TFileStream;
Report: TPadesRevisionAnalysisReport;
begin
Result := False;
Reason := '';
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Report := AnalyzePadesSignatureRevisions(Source);
finally
Source.Free;
end;
if Report.SignatureCount = 0 then
begin
Reason := 'no signature anchors the analysis';
Exit;
end;
if Report.Risks * BlockingRisks <> [] then
begin
Reason := 'structural risk in the revision chain';
Exit;
end;
case Report.Status of
prasNoLaterChanges, prasAllowed:
Result := True;
else
// prasIndeterminate e prasSuspicious sono rifiuti, non avvisi
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Due confini valgono la pena di essere dichiarati senza giri di parole. TPadesRevisionAnalysisReport non dice nulla sull'integrità CMS o sulla fiducia nei certificati, quindi questo gate sta accanto alla validazione crittografica e di fiducia, non al loro posto. E un grafo di proprietà corretto non rende P=3 sicuro per ogni flusso di lavoro. P=3 consente genuinamente le annotazioni, e un'annotazione con un appearance opaco può coprire testo firmato senza toccare nemmeno un content stream. Se i tuoi documenti certificati sono contratti anziché copie di revisione, o certifica con P=2 o instrada i cambiamenti di annotazione permessi a un essere umano, come in questo helper:
function AllowedAnnotationEditsUnderP3(
const Report: TPadesRevisionAnalysisReport): Integer;
var
i, j: Integer;
begin
Result := 0;
for i := 0 to High(Report.Signatures) do
if Report.Signatures[i].DocMdpPermission = 3 then
for j := 0 to High(Report.Signatures[i].Changes) do
if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
(Report.Signatures[i].Changes[j].Decision = prdAllowed) then
Inc(Result);
end;
Checklist di audit delle revisioni di firma
Usa questa lista per controllare se la tua pipeline di verifica era esposta e se ora fallisce in modo chiuso:
- Le build di PDFium Component precedenti alla v3.126.2 potevano riportare
prasAllowedper modifiche di contenuto di pagina in documenti DocMDP P=3; rifai girareTPdf.AnalyzeSignatureRevisionssui file P=3 certificati accettati da build più vecchie - Ricontrolla i documenti P=3 con lucchetti FieldMDP dove un valore di campo e un'annotazione potrebbero condividere un oggetto indiretto
- Accetta solo
prasNoLaterChangeseprasAllowed; trattaprasIndeterminateeprasSuspiciouscome non affidabili - Testa
Report.Risksoltre aReport.Status, perchéprrDuplicateObjectDefinitionda solo non cambia lo status - Non leggere un array
Changesvuoto come risultato pulito quando lo status della firma è Indeterminate; una costruzione dei ruoli fallita non riporta cambiamenti - Non contare solo su
prrFieldMdpUnresolvedper beccare i problemi FieldMDP, visto che il caso dell'annotazione condivisa emerge solo dalla decisione e dallo status - Decidi se i cambiamenti di annotazione permessi sotto P=3 richiedono revisione umana nel tuo flusso di lavoro
- Analizza i byte originali del file; un documento riscritto da
SaveAsnon contiene più la catena delle revisioni
L'analisi delle revisioni è uno strato di un controllo di firma. Accoppiala con l'ispezione delle firme digitali PDF e dei livelli PAdES per il dizionario e il livello baseline, e con un più ampio audit dei rischi di sicurezza PDF per JavaScript, azioni launch e file incorporati. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions e il grafo dei ruoli consapevole della proprietà descritto qui viaggiano nel PDFium Component per Delphi, C++Builder e Lazarus