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 ripercorre due trappole contro cui ogni lettore PKCS#12 deve guardarsi, entrambe nel percorso che importa una credenziale di firma, e come HotPDF le chiuda. Nessuna delle due è esotica. Entrambe nascono dalla stessa causa radice che colpisce quasi ogni parser binario scritto in un linguaggio con interi a larghezza fissa: una lunghezza o un conteggio dal file viene fidato un passo oltre il dovuto. Una porta a una lettura fuori dai limiti, l'altra a un processo che si blocca finché non lo ammazzi
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 quelle fasi corrispondono a unità distinte. La logica del contenitore PKCS#12 vive in HPDFPFX. Ogni tag, lunghezza e valore che tocca viene decodificato dal lettore ASN.1 in HPDFASN1. La derivazione della chiave e la decrittazione PBES2 siedono in HPDFCrypt accanto a PBKDF2HMACSHA256. Quando la chiave è recuperata, HPDFRSA e il costruttore CMS SignedData in HPDFCMS la trasformano nella firma detached incorporata nel PDF. Il punto di ingresso pubblico che guida l'intera catena è una chiamata sola
// Guida l'intera pipeline: carica il PDF segnaposto, analizza il PFX,
// deriva la chiave, costruisce CMS SignedData, scrive l'output firmato.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// firma incorporata
else
// la firma non è completata
;
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
Trappola uno: una lunghezza ASN.1 che fa wrap oltre il guard
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:
// La trappola: aritmetica con segno a 32 bit. Con ContentLen vicino a MaxInt,
// Pos + ContentLen va in overflow a un valore NEGATIVO, quindi il confronto
// è falso e una lunghezza contraffatta di ~2 GB passa senza ostacoli.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
Il problema è l'addizione, non il confronto. Quando ContentLen è 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'unico guard corretto allarga la somma intermedia prima del confronto, così l'addizione non può andare in overflow nel tipo in cui viene calcolata. HotPDF promuove entrambi gli operandi a Int64:
// Corretto: entrambi gli operandi allargati a Int64 prima dell'addizione, così la somma
// non può andare in overflow. Una lunghezza contraffatta di 2 GB ora fallisce il controllo dei limiti.
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 Int64 contiene la somma di due valori a 32 bit senza perdite, così il confronto vede il numero reale e rifiuta la lunghezza contraffatta. Il controllo separato di non-negatività su ContentLen chiude il caso gemello in cui un valore decodificato atterra negativo da solo. In HotPDF questo guard vive in HPDFASN1ParseNode, la funzione che produce il nodo su cui ogni altro helper si appoggia. Poiché HPDFASN1Content dimensiona il suo SetLength e la sua Move direttamente dalla lunghezza del contenuto del nodo, un nodo passato con un guard debole avvelenerebbe ogni lettura presa da esso. Far rispettare il limite nel punto di decodifica è ciò che rende sicuri gli helper a monte
Trappola due: un conteggio di iterazioni PBKDF2 usato come arma
La seconda trappola non è un errore di memoria: è il file che dice alla tua CPU quanto duramente lavorare. PKCS#12 protegge il proprio materiale chiave con PBES2, lo schema basato su password del PKCS#5, specificato nella RFC 8018. PBES2 esegue una funzione di derivazione della chiave, qui PBKDF2 con HMAC-SHA-256, poi un cifrario, qui AES-256-CBC. PBKDF2 prende un conteggio di iterazioni, e quel conteggio è un parametro trasportato nel file. Il suo intero scopo è essere lento: più iterazioni significa che ogni ipotesi di password costa di più, il che è buono contro un attaccante offline. La RFC 8018 §4.2 è esplicita: un conteggio maggiore è meglio per la sicurezza, e deliberatamente non fissa alcun tetto
Quella apertura va bene quando il file l'hai generato tu. È un'arma quando l'ha generato l'attaccante. Il conteggio di iterazioni è un fattore di lavoro controllato dall'attaccante, e un fattore di lavoro controllato dall'attaccante è un denial of service per complessità algoritmica. Un .pfx contraffatto può codificare un conteggio di iterazioni nell'ordine dei miliardi; un parser che lo legge scrupolosamente e chiama PBKDF2 per quel numero di round di HMAC-SHA-256 fa sparire il processo in un ciclo che non ritornerà per minuti od ore su un unico file fornito. Su un server di firma che gestisce una credenziale per richiesta, un singolo upload artefatto blocca un worker
Il conteggio peggiora il wraparound prima di far girare la CPU. Il valore delle iterazioni vive nel file come un INTEGER ASN.1, che non ha larghezza fissa, mentre il campo che PBKDF2 alla fine consuma è un Integer a 32 bit. Decodifica l'INTEGER direttamente in quel campo e un valore grande si tronca, e un valore costruito per cadere sul bit di segno torna negativo o come qualche numero piccolo non correlato, così perfino la dimensione del lavoro non è più ciò che il file sembrava chiedere. HotPDF legge il valore a larghezza piena e lo delimita prima di restringerlo:
// Leggi il conteggio delle iterazioni come Int64 prima, poi limitane il valore a un intervallo sensato
// PRIMA che venga ridotto al campo Iterations a 32 bit usato da PBKDF2.
LIter := HPDFASN1ToInteger(Data, Node); // restituisce 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); // sicuro: già limitato
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é entrambi i guard sono lo stesso guard
Le due trappole sembrano diverse, una un buffer overrun e una un processo bloccato, ma sono lo stesso errore. In entrambi i casi un numero da un file non fidato viene portato in un tipo a larghezza fissa un passo troppo presto, prima di essere verificato contro la realtà. La lunghezza viene sommata in 32 bit prima del test dei limiti; il conteggio di iterazioni viene ristretto a 32 bit prima del test dell'intervallo. Entrambe cedono alla stessa disciplina: decodifica a larghezza piena, verifica contro il limite reale, e solo allora restringi. L'Int64 intermedio non è una scelta di stile, è l'unica larghezza in cui il guard può vedere il valore che l'attaccante ha davvero scritto. Un limite che va in overflow non è un limite, e un conteggio senza tetto non è un parametro, è una manopola remota sulla tua stessa 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 Delphi Component per Delphi e C++Builder, insieme alle API di caricamento, modifica, crittografia e firma descritte in altre sezioni di questo blog