Technical Article

Protezione di un firmatario PDF Delphi contro file PKCS#12 dannosi

Quando si firma un PDF, di solito si pensa alla chiave di firma come a qualcosa sotto il proprio controllo. Essa risiede in un file .pfx generato personalmente e protetto da una password scelta da noi. Il codice che legge quel file sembra solo una connessione tecnica, non un confine di sicurezza. Questa intuizione si rivela errata nel momento in cui il certificato non è più nostro. Uno strumento desktop che consente all'utente di scegliere qualsiasi .pfx, un server che accetta credenziali caricate o un firmatario batch che riceve certificati tramite la rete, passano tutti byte potenzialmente modificati da un utente malintenzionato a un parser prima ancora che venga generato un singolo byte di firma. Un lettore PKCS#12 è una superficie di attacco, allo stesso modo in cui lo sono un decodificatore di immagini o un caricatore di font.

Questo articolo esamina due difetti reali presenti in quel lettore, entrambi nel percorso che importa le credenziali di firma. Nessuno dei due è insolito. Entrambi derivano dalla stessa causa principale che colpisce quasi tutti i parser binari scritti in linguaggi con interi a larghezza fissa: una lunghezza o un conteggio provenienti dal file vengono considerati attendibili più del dovuto. Uno porta a una lettura fuori dai limiti, l'altro a un processo che si blocca fino all'arresto forzato.

Dove viaggiano i byte

L'importazione di un file .pfx per firmare un documento non è una singola operazione, ma una breve pipeline, e ogni fase analizza elementi che potrebbero essere stati scritti da un utente malintenzionato. Il contenitore è una struttura PKCS#12 definita nella specifica RFC 7292, un insieme di blocchi AuthenticatedSafe avvolti in una protezione crittografata che contiene la chiave privata. Leggerlo significa analizzare la struttura ASN.1, derivare una chiave dalla password, decrittografare e infine passare la chiave RSA recuperata al codice che crea la firma.

In HotPDF queste fasi sono mappate in unità distinte. La logica del contenitore PKCS#12 risiede in HPDFPFX. Ogni tag, lunghezza e valore esaminati vengono decodificati dal lettore ASN.1 in HPDFASN1. La derivazione della chiave e la decrittografia PBES2 risiedono in HPDFCrypt insieme a PBKDF2HMACSHA256. Una volta recuperata la chiave, HPDFRSA e il costruttore CMS SignedData in HPDFCMS la trasformano nella firma separata incorporata nel PDF. Il punto di ingresso pubblico che guida l'intera catena è rappresentato da una singola chiamata.

// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // signature embedded
else
  // signing did not complete
;

Ogni byte di signer.pfx passa attraverso HPDFASN1 e HPDFPFX prima che avvenga qualsiasi operazione crittografica. Se queste due unità non verificano con attenzione quanto dichiarato nel file, la crittografia a valle non avrà mai la possibilità di agire.

Difetto uno: una lunghezza ASN.1 che supera la protezione tramite overflow

Le codifiche ASN.1 in DER e BER rappresentano ogni elemento come un tag, una lunghezza e i rispettivi byte di contenuto. La lunghezza è il campo che è necessario verificare pur fidandosi, poiché indica al parser fino a che punto leggere, ed è scritta da chi ha generato il file. La specifica X.690 §8.1.3 definisce due codifiche. La forma breve racchiude una lunghezza da 0 a 127 in un singolo byte. La forma lunga, utilizzata per valori superiori, usa un byte iniziale i cui sette bit inferiori indicano il numero di byte di lunghezza che seguono, dopodiché tale quantità di byte in formato big-endian esprime il valore effettivo. Quattro byte di lunghezza possono quindi dichiarare una dimensione del contenuto vicina ai quattro gigabyte.

Dopo aver decodificato tale valore, il parser deve verificare che il contenuto rientri effettivamente nel buffer prima di considerarlo attendibile. Il controllo naturale consiste nel confermare che la posizione corrente sommata alla lunghezza del contenuto non superi la fine dei dati. Se scritta nel modo classico, con la posizione, la lunghezza del contenuto e il totale memorizzati in interi con segno a 32 bit, tale protezione risulta inefficace:

// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Il problema è l'addizione, non il confronto. Quando ContentLen is vicino a MaxInt (2147483647), l'operazione Pos + ContentLen supera il limite del tipo a 32 bit con segno e produce un valore negativo per overflow. Una somma negativa non è mai superiore a Total, quindi la protezione segnala che tutto è corretto e consente al parser di procedere con una lunghezza del contenuto di circa due gigabyte che il buffer non contiene. Il danno avviene subito dopo: il lettore alloca un buffer per la lunghezza dichiarata ed esegue una copia al suo interno, tramite SetLength seguita da una Move che legge dalla sorgente. La sorgente contiene solo poche centinaia di byte, quindi la copia legge molto oltre la fine dell'input, causando una lettura fuori dai limiti che nel migliore dei casi provoca un crash e nel peggiore espone la memoria di processo adiacente durante l'analisi.

L'unica protezione corretta converte la somma intermedia a un tipo più ampio prima del confronto, in modo che l'addizione non possa generare un overflow nel tipo in cui viene calcolata. La correzione promuove entrambi gli operandi a Int64:

// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
  raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
  raise EHPDFASN1Error.Create('content overruns buffer');

Un tipo Int64 mantiene la somma di due valori a 32 bit senza perdita di dati, quindi il confronto rileva il valore reale e rifiuta la lunghezza contraffatta. Il controllo separato di non negatività su ContentLen copre il caso in cui un valore decodificato risulti negativo. In HotPDF questa protezione risiede in HPDFASN1ParseNode, la funzione che genera il nodo su cui si basano tutti gli altri componenti di supporto. Poiché HPDFASN1Content determina la dimensione di SetLength e Move direttamente dalla lunghezza del contenuto del nodo, un nodo che avesse superato un controllo errato avrebbe compromesso ogni lettura successiva. Correggere il limite al momento della decodifica è ciò che rende sicuri i componenti sovrastanti.

Difetto due: un conteggio delle iterazioni PBKDF2 usato come arma

La seconda vulnerabilità non è un errore di memoria, ma riguarda il file che impone alla CPU il carico di lavoro. Lo standard PKCS#12 protegge le sue chiavi con lo schema PBES2 basato su password definito in PKCS#5 e specificato in RFC 8018. PBES2 esegue una funzione di derivazione della chiave, in questo caso PBKDF2 con HMAC-SHA-256, e quindi una cifratura, qui AES-256-CBC. PBKDF2 richiede un conteggio delle iterazioni, fornito come parametro all'interno del file. Il suo scopo principale è rallentare il processo: più iterazioni significano un costo maggiore per ogni tentativo di indovinare la password, utile contro attacchi offline. La specifica RFC 8018 §4.2 chiarisce che un numero maggiore di iterazioni aumenta la sicurezza e non stabilisce deliberatamente alcun limite massimo.

Questa flessibilità è corretta quando si genera il file. Diventa invece un'arma quando il file proviene da un utente malintenzionato. Il conteggio delle iterazioni è un fattore di lavoro controllato dall'attaccante, e questo rappresenta una vulnerabilità di Denial of Service basata sulla complessità algoritmica. Un file .pfx contraffatto può dichiarare miliardi di iterazioni; il parser le legge ed esegue PBKDF2 per quel numero di cicli di HMAC-SHA-256, bloccando il processo in un ciclo che non terminerà per minuti o ore. Su un server di firma che gestisce una credenziale per richiesta, un singolo caricamento creato ad hoc blocca l'elaborazione.

Il conteggio aggrava l'overflow prima ancora di saturare la CPU. Il valore dell'iterazione risiede nel file come tipo INTEGER ASN.1, che non ha una larghezza fissa, mentre il campo utilizzato da PBKDF2 è un Integer a 32 bit. Se si decodifica l'INTEGER direttamente in quel campo, un valore elevato viene troncato, e un valore strutturato per attivare il bit di segno restituisce un valore negativo o un numero casuale piccolo, rendendo il carico di lavoro diverso da quanto richiesto nel file. La soluzione legge il valore a larghezza intera e ne verifica i limiti prima della conversione a un tipo più piccolo:

// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node);          // returns Int64
if (LIter < 1) or (LIter > 100000000) then
  raise EHPDFPFXError.CreateFmt(
    'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
    [LIter]);
Iterations := Integer(LIter);                    // safe: already bounded

Leggere il dato in un Int64 garantisce che il valore decodificato sia quello effettivo e non una rappresentazione troncata. Il limite inferiore rifiuta valori pari a zero o negativi, privi di significato per la derivazione della chiave. Il limite superiore, impostato a cento milioni, supera ampiamente qualsiasi file PKCS#12 legittimo (che oggi utilizza da decine a poche centinaia di migliaia di iterazioni), limitando il caso peggiore a un carico di lavoro gestibile. Solo dopo aver verificato che il valore rientri in questo intervallo, questo viene convertito al campo a 32 bit, impedendo che il troncamento causi anomalie. In HotPDF questo controllo risiede in ParsePBES2Params, dove i parametri PBKDF2 vengono decodificati prima del passaggio a PBKDF2HMACSHA256.

Perché entrambe le correzioni condividono lo stesso principio

I due difetti appaiono diversi, uno è un buffer overrun e l'altro un blocco del processo, ma derivano dallo stesso errore. In entrambi i casi, un valore proveniente da un file non attendibile è stato inserito in un tipo a larghezza fissa in anticipo, prima di essere verificato rispetto ai limiti reali. La lunghezza è stata sommata a 32 bit prima del controllo dei limiti; il conteggio delle iterazioni è stato convertito a 32 bit prima del controllo dell'intervallo. Entrambi si risolvono con la stessa regola: decodificare a larghezza intera, verificare rispetto al limite reale e solo allora ridurre il tipo. L'uso di un tipo intermedio Int64 non è una scelta estetica, è l'unico modo per consentire al controllo di sicurezza di rilevare il valore effettivo scritto nel file. Un limite soggetto a overflow non esclude il rischio, e un conteggio senza tetto massimo non è un parametro, bensì un controllo remoto sul consumo della propria CPU.

Consigli pratici per una pipeline di firma

La lezione immediata è convalidare l'input del certificato non attendibile come si farebbe per qualsiasi caricamento esterno. Limita la dimensione del file .pfx accettato, poiché un file legittimo è dell'ordine dei kilobyte, non dei megabyte. Gestisci un errore di analisi come un rifiuto standard dell'input, non come un'anomalia che richiede l'invio di uno stack trace all'utente. Se la firma avviene su un server, esegui l'importazione in un ambiente in cui un thread bloccato non comprometta l'intero servizio, e imposta un timeout sull'operazione in modo che un file insolitamente pesante sia limitato sia dal tempo reale di esecuzione sia dal limite di iterazioni.

La lezione più ampia va oltre i certificati. La protezione dei parser non è una verifica da eseguire una sola volta su una singola unità, ma è un requisito per ogni parte della libreria che legge byte non scritti internamente. Una libreria PDF analizza molti dati da fonti non attendibili: font incorporati nel documento, immagini in svariati codec, filtri di stream e, nel percorso di firma, i certificati. Ognuno di essi rappresenta una superficie di attacco e merita lo stesso livello di controllo su ogni lunghezza e conteggio. HotPDF basa il percorso di importazione e firma sulle unità protette HPDFASN1, HPDFPFX, HPDFCrypt e HPDFCMS descritte qui, garantendo che le credenziali fornite, da qualsiasi fonte provengano, siano analizzate in modo difensivo prima di essere considerate attendibili.

Il flusso di lavoro di firma protetto da questi controlli è trattato in dettaglio nella nostra guida alle firme digitali PAdES in Delphi, e la stessa impostazione difensiva applicata alla crittografia dei documenti, compreso il percorso della chiave AES-256 condiviso in questa base di codice, è descritta nell'articolo sulla crittografia AES-256 e sulla sicurezza. Tutto questo è incluso all'interno di HotPDF Component per Delphi e C++Builder, insieme alle API di caricamento, modifica, crittografia e firma descritte in altre sezioni di questo blog.